@gisatcz/deckgl-geolib 2.4.1-dev.1 → 2.5.0-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -4876,7 +4876,7 @@ const DefaultGeoImageOptions = {
4876
4876
  planarConfig: undefined,
4877
4877
  // --- Mesh generation (terrain only) ---
4878
4878
  tesselator: 'martini',
4879
- terrainColor: [133, 133, 133, 255],
4879
+ terrainColor: [200, 200, 200, 255],
4880
4880
  terrainSkirtHeight: 100,
4881
4881
  // Default fallback for invalid/nodata elevations. Should be configured based on the dataset's actual range.
4882
4882
  terrainMinValue: 0,
@@ -4893,9 +4893,23 @@ const DefaultGeoImageOptions = {
4893
4893
  color: [255, 0, 255, 255],
4894
4894
  colorScale: chroma.brewer.YlOrRd,
4895
4895
  colorScaleValueRange: [0, 255],
4896
+ // colorScale: [
4897
+ // [75, 120, 90], // Brightened forest green
4898
+ // [100, 145, 100], // Soft meadow green
4899
+ // [130, 170, 110], // Bright moss
4900
+ // [185, 210, 145], // Sunny sage
4901
+ // [235, 235, 185], // Pale primrose (transitional)
4902
+ // [225, 195, 160], // Sand / light terracotta (matches slope)
4903
+ // [195, 160, 130], // Warm clay brown
4904
+ // [170, 155, 150], // Warm slate grey
4905
+ // [245, 245, 240], // Bright mist
4906
+ // [255, 255, 255], // Pure peak white
4907
+ // ],
4908
+ // colorScaleValueRange: [0, 6500],
4896
4909
  colorsBasedOnValues: undefined,
4897
4910
  colorClasses: undefined,
4898
4911
  alpha: 100,
4912
+ maxGlazeAlpha: 128,
4899
4913
  nullColor: [0, 0, 0, 0],
4900
4914
  unidentifiedColor: [0, 0, 0, 0],
4901
4915
  clippedColor: [0, 0, 0, 0],
@@ -4905,6 +4919,11 @@ const DefaultGeoImageOptions = {
4905
4919
  hillshadeAzimuth: 315,
4906
4920
  hillshadeAltitude: 45,
4907
4921
  zFactor: 1,
4922
+ useSwissRelief: false,
4923
+ swissSlopeWeight: 0.5,
4924
+ useReliefGlaze: false,
4925
+ // --- Lighting control ---
4926
+ disableLighting: false,
4908
4927
  };
4909
4928
 
4910
4929
  class Martini {
@@ -5591,6 +5610,27 @@ function scale(num, inMin, inMax, outMin, outMax) {
5591
5610
  }
5592
5611
 
5593
5612
  class BitmapGenerator {
5613
+ /**
5614
+ * Cache for Swiss relief color LUTs to avoid regenerating on every tile.
5615
+ * Key: colorScale config + range, Value: pre-computed RGBA LUT
5616
+ */
5617
+ static _swissColorLUTCache = new Map();
5618
+ /**
5619
+ * Cache for 8-bit (256-entry) color LUTs.
5620
+ * Shared process-wide across all tiles and datasets when options are fixed (i.e. !useAutoRange).
5621
+ * Key: serialised coloring options, Value: pre-computed 256×RGBA LUT
5622
+ */
5623
+ static _8bitLUTCache = new Map();
5624
+ /**
5625
+ * Cache for float/16-bit (1024-entry) heatmap LUTs.
5626
+ * Shared process-wide across all tiles and datasets when !useAutoRange.
5627
+ * Key: serialised coloring options + range, Value: pre-computed 1024×RGBA LUT
5628
+ */
5629
+ static _floatLUTCache = new Map();
5630
+ /** Build a cache key that captures all options affecting LUT colour output. */
5631
+ static getLUTCacheKey(options, rangeMin, rangeMax, optAlpha) {
5632
+ return `${rangeMin}_${rangeMax}_${optAlpha}_${JSON.stringify(options.colorScale)}_${options.useSingleColor}_${JSON.stringify(options.color)}_${options.useColorClasses}_${JSON.stringify(options.colorClasses)}_${options.useColorsBasedOnValues}_${JSON.stringify(options.colorsBasedOnValues)}_${options.useHeatMap}_${options.clipLow ?? ''}_${options.clipHigh ?? ''}_${JSON.stringify(options.clippedColor)}_${JSON.stringify(options.nullColor)}_${JSON.stringify(options.unidentifiedColor)}`;
5633
+ }
5594
5634
  /**
5595
5635
  * Main entry point: Generates an ImageBitmap from raw raster data.
5596
5636
  */
@@ -5617,7 +5657,31 @@ class BitmapGenerator {
5617
5657
  // If planar support is added, this logic must be updated to handle both layouts correctly.
5618
5658
  const numAvailableChannels = optionsLocal.numOfChannels ??
5619
5659
  (rasters.length === 1 ? rasters[0].length / (width * height) : rasters.length);
5620
- if (optionsLocal.useChannelIndex == null) {
5660
+ if (optionsLocal.useReliefGlaze) {
5661
+ if (rasters.length >= 1) {
5662
+ // Relief glaze: pure black/white overlay with variable alpha
5663
+ imageData.data.set(this.getReliefGlazeRGBA(rasters, optionsLocal, size));
5664
+ }
5665
+ else {
5666
+ // Missing relief mask: fill with transparent
5667
+ const transparentData = new Uint8ClampedArray(size);
5668
+ transparentData.fill(0);
5669
+ imageData.data.set(transparentData);
5670
+ }
5671
+ }
5672
+ else if (optionsLocal.useSwissRelief) {
5673
+ if (rasters.length === 2) {
5674
+ // Normal Swiss relief rendering: hypsometric color × relief mask
5675
+ imageData.data.set(this.getColorValue(rasters, optionsLocal, size));
5676
+ }
5677
+ else { // Missing mask: fill with null color (fully transparent or a fallback)
5678
+ const defaultColorData = this.getDefaultColor(size, optionsLocal.nullColor);
5679
+ defaultColorData.forEach((value, index) => {
5680
+ imageData.data[index] = value;
5681
+ });
5682
+ }
5683
+ }
5684
+ else if (optionsLocal.useChannelIndex == null) {
5621
5685
  if (isInterleaved) {
5622
5686
  const ratio = rasters[0].length / (width * height);
5623
5687
  if (ratio === 1) {
@@ -5685,23 +5749,90 @@ class BitmapGenerator {
5685
5749
  const optAlpha = Math.floor((options.alpha ?? 100) * 2.55);
5686
5750
  const rangeMin = options.colorScaleValueRange?.[0] ?? 0;
5687
5751
  const rangeMax = options.colorScaleValueRange?.[1] ?? 255;
5688
- const is8Bit = dataArray instanceof Uint8Array || dataArray instanceof Uint8ClampedArray;
5689
- const isFloatOrWide = !is8Bit && (dataArray instanceof Float32Array || dataArray instanceof Uint16Array || dataArray instanceof Int16Array);
5690
- // 1. 8-BIT COMPREHENSIVE LUT
5691
- // Single-band 8-bit (grayscale or indexed): use LUT for fast mapping
5692
- if (is8Bit && !options.useDataForOpacity) {
5693
- const lut = new Uint8ClampedArray(256 * 4);
5694
- for (let i = 0; i < 256; i++) {
5695
- if ((options.clipLow != null && i <= options.clipLow) ||
5696
- (options.clipHigh != null && i >= options.clipHigh)) {
5697
- lut.set(options.clippedColor, i * 4);
5752
+ const isMultiRaster = Array.isArray(dataArray);
5753
+ const primaryBuffer = isMultiRaster ? dataArray[0] : dataArray;
5754
+ const isSwiss = options.useSwissRelief && isMultiRaster && dataArray.length >= 2;
5755
+ const is8Bit = primaryBuffer instanceof Uint8Array || primaryBuffer instanceof Uint8ClampedArray;
5756
+ const isFloatOrWide = !is8Bit && (primaryBuffer instanceof Float32Array || primaryBuffer instanceof Uint16Array || primaryBuffer instanceof Int16Array);
5757
+ // 1. SWISS MODE BRANCH
5758
+ if (isSwiss) {
5759
+ const reliefMask = dataArray[1];
5760
+ const rangeSpan = (rangeMax - rangeMin) || 1;
5761
+ // Only use LUT optimization for useHeatMap mode; other modes use calculateSingleColor per-pixel
5762
+ let lut = null;
5763
+ if (options.useHeatMap) {
5764
+ const LUT_SIZE = 1024;
5765
+ // Cache LUT: generate key from colorScale config + range + alpha
5766
+ const cacheKey = `${rangeMin}_${rangeMax}_${optAlpha}_${JSON.stringify(options.colorScale)}`;
5767
+ lut = this._swissColorLUTCache.get(cacheKey) || null;
5768
+ if (!lut) {
5769
+ // LUT not cached, generate it
5770
+ lut = new Uint8ClampedArray(LUT_SIZE * 4);
5771
+ for (let i = 0; i < LUT_SIZE; i++) {
5772
+ const domainVal = rangeMin + (i / (LUT_SIZE - 1)) * rangeSpan;
5773
+ const rgb = colorScale(domainVal).rgb();
5774
+ lut[i * 4] = rgb[0];
5775
+ lut[i * 4 + 1] = rgb[1];
5776
+ lut[i * 4 + 2] = rgb[2];
5777
+ lut[i * 4 + 3] = optAlpha;
5778
+ }
5779
+ this._swissColorLUTCache.set(cacheKey, lut);
5780
+ }
5781
+ }
5782
+ for (let i = 0, sampleIndex = (options.useChannelIndex ?? 0); i < arrayLength; i += 4, sampleIndex += samplesPerPixel) {
5783
+ const elevationVal = primaryBuffer[sampleIndex];
5784
+ // NaN-aware noData check for Swiss relief
5785
+ const isNoData = options.noDataValue !== undefined && (Number.isNaN(options.noDataValue)
5786
+ ? Number.isNaN(elevationVal)
5787
+ : elevationVal === options.noDataValue);
5788
+ if (Number.isNaN(elevationVal) || isNoData) {
5789
+ colorsArray.set(options.nullColor, i);
5790
+ continue;
5791
+ }
5792
+ let baseColor;
5793
+ if (lut) {
5794
+ // LUT-optimized path for useHeatMap
5795
+ const t = (elevationVal - rangeMin) / rangeSpan;
5796
+ const lutIdx = Math.min(1023, Math.max(0, Math.floor(t * 1023))) * 4;
5797
+ baseColor = [lut[lutIdx], lut[lutIdx + 1], lut[lutIdx + 2], lut[lutIdx + 3]];
5698
5798
  }
5699
5799
  else {
5700
- lut.set(this.calculateSingleColor(i, colorScale, options, optAlpha), i * 4);
5800
+ // Per-pixel calculation for useSingleColor, useColorClasses, useColorsBasedOnValues
5801
+ baseColor = this.calculateSingleColor(elevationVal, colorScale, options, optAlpha);
5802
+ }
5803
+ // Apply relief mask as multiplier (Ambient Fill approach)
5804
+ const maskVal = reliefMask[sampleIndex];
5805
+ const multiplier = 0.4 + 0.6 * (maskVal / 255);
5806
+ colorsArray[i] = Math.floor(baseColor[0] * multiplier);
5807
+ colorsArray[i + 1] = Math.floor(baseColor[1] * multiplier);
5808
+ colorsArray[i + 2] = Math.floor(baseColor[2] * multiplier);
5809
+ colorsArray[i + 3] = baseColor[3];
5810
+ }
5811
+ return colorsArray;
5812
+ }
5813
+ // 2. 8-BIT COMPREHENSIVE LUT
5814
+ // Single-band 8-bit (grayscale or indexed): use LUT for fast mapping.
5815
+ // The LUT covers all 256 possible values and is fixed for a given set of coloring options,
5816
+ // so cache it across tiles (skip cache only when useAutoRange recomputes the range per tile).
5817
+ if (is8Bit && !options.useDataForOpacity) {
5818
+ const cacheKey = !options.useAutoRange ? this.getLUTCacheKey(options, rangeMin, rangeMax, optAlpha) : null;
5819
+ let lut = cacheKey ? (this._8bitLUTCache.get(cacheKey) ?? null) : null;
5820
+ if (!lut) {
5821
+ lut = new Uint8ClampedArray(256 * 4);
5822
+ for (let i = 0; i < 256; i++) {
5823
+ if ((options.clipLow != null && i <= options.clipLow) ||
5824
+ (options.clipHigh != null && i >= options.clipHigh)) {
5825
+ lut.set(options.clippedColor, i * 4);
5826
+ }
5827
+ else {
5828
+ lut.set(this.calculateSingleColor(i, colorScale, options, optAlpha), i * 4);
5829
+ }
5701
5830
  }
5831
+ if (cacheKey)
5832
+ this._8bitLUTCache.set(cacheKey, lut);
5702
5833
  }
5703
5834
  for (let i = 0, sampleIndex = (options.useChannelIndex ?? 0); i < arrayLength; i += 4, sampleIndex += samplesPerPixel) {
5704
- const lutIdx = dataArray[sampleIndex] * 4;
5835
+ const lutIdx = primaryBuffer[sampleIndex] * 4;
5705
5836
  colorsArray[i] = lut[lutIdx];
5706
5837
  colorsArray[i + 1] = lut[lutIdx + 1];
5707
5838
  colorsArray[i + 2] = lut[lutIdx + 2];
@@ -5709,27 +5840,36 @@ class BitmapGenerator {
5709
5840
  }
5710
5841
  return colorsArray;
5711
5842
  }
5712
- // 2. FLOAT / 16-BIT LUT (HEATMAP ONLY)
5713
- if (isFloatOrWide && options.useHeatMap && !options.useDataForOpacity) {
5843
+ // 3. FLOAT / 16-BIT LUT (HEATMAP ONLY)
5844
+ // Guard: only activate when heatmap is the highest-priority active mode.
5845
+ // If a more specific mode (useSingleColor, useColorClasses, useColorsBasedOnValues) is set,
5846
+ // fall through to the general loop so calculateSingleColor can honour the priority chain.
5847
+ if (isFloatOrWide && options.useHeatMap && !options.useSingleColor && !options.useColorClasses && !options.useColorsBasedOnValues && !options.useDataForOpacity) {
5714
5848
  const LUT_SIZE = 1024;
5715
- const lut = new Uint8ClampedArray(LUT_SIZE * 4);
5716
5849
  const rangeSpan = (rangeMax - rangeMin) || 1;
5717
- for (let i = 0; i < LUT_SIZE; i++) {
5718
- const domainVal = rangeMin + (i / (LUT_SIZE - 1)) * rangeSpan;
5719
- if ((options.clipLow != null && domainVal <= options.clipLow) ||
5720
- (options.clipHigh != null && domainVal >= options.clipHigh)) {
5721
- lut.set(options.clippedColor, i * 4);
5722
- }
5723
- else {
5724
- const rgb = colorScale(domainVal).rgb();
5725
- lut[i * 4] = rgb[0];
5726
- lut[i * 4 + 1] = rgb[1];
5727
- lut[i * 4 + 2] = rgb[2];
5728
- lut[i * 4 + 3] = optAlpha;
5850
+ const cacheKey = !options.useAutoRange ? this.getLUTCacheKey(options, rangeMin, rangeMax, optAlpha) : null;
5851
+ let lut = cacheKey ? (this._floatLUTCache.get(cacheKey) ?? null) : null;
5852
+ if (!lut) {
5853
+ lut = new Uint8ClampedArray(LUT_SIZE * 4);
5854
+ for (let i = 0; i < LUT_SIZE; i++) {
5855
+ const domainVal = rangeMin + (i / (LUT_SIZE - 1)) * rangeSpan;
5856
+ if ((options.clipLow != null && domainVal <= options.clipLow) ||
5857
+ (options.clipHigh != null && domainVal >= options.clipHigh)) {
5858
+ lut.set(options.clippedColor, i * 4);
5859
+ }
5860
+ else {
5861
+ const rgb = colorScale(domainVal).rgb();
5862
+ lut[i * 4] = rgb[0];
5863
+ lut[i * 4 + 1] = rgb[1];
5864
+ lut[i * 4 + 2] = rgb[2];
5865
+ lut[i * 4 + 3] = optAlpha;
5866
+ }
5729
5867
  }
5868
+ if (cacheKey)
5869
+ this._floatLUTCache.set(cacheKey, lut);
5730
5870
  }
5731
5871
  for (let i = 0, sampleIndex = (options.useChannelIndex ?? 0); i < arrayLength; i += 4, sampleIndex += samplesPerPixel) {
5732
- const val = dataArray[sampleIndex];
5872
+ const val = primaryBuffer[sampleIndex];
5733
5873
  if (this.isInvalid(val, options)) {
5734
5874
  colorsArray.set(this.getInvalidColor(val, options), i);
5735
5875
  }
@@ -5744,10 +5884,10 @@ class BitmapGenerator {
5744
5884
  }
5745
5885
  return colorsArray;
5746
5886
  }
5747
- // 3. FALLBACK LOOP (Categorical Float, Opacity, or Single Color)
5887
+ // 4. FALLBACK LOOP (Categorical Float, Opacity, or Single Color)
5748
5888
  let sampleIndex = options.useChannelIndex ?? 0;
5749
5889
  for (let i = 0; i < arrayLength; i += 4) {
5750
- const val = dataArray[sampleIndex];
5890
+ const val = primaryBuffer[sampleIndex];
5751
5891
  let color;
5752
5892
  if ((options.clipLow != null && val <= options.clipLow) || (options.clipHigh != null && val >= options.clipHigh)) {
5753
5893
  color = options.clippedColor;
@@ -5763,6 +5903,50 @@ class BitmapGenerator {
5763
5903
  }
5764
5904
  return colorsArray;
5765
5905
  }
5906
+ /**
5907
+ * Generate relief glaze RGBA output.
5908
+ * Maps relief mask (0-255) to pure black/white glaze with variable alpha.
5909
+ * - reliefValue < 128: Pure black (0,0,0) darkens shadows
5910
+ * - reliefValue > 128: Pure white (255,255,255) brightens highlights
5911
+ * - reliefValue == 128: Transparent (no effect)
5912
+ *
5913
+ * High-performance implementation using pre-computed alpha LUT to avoid 65k Math.pow calls.
5914
+ *
5915
+ * @param rasters Array of [relief mask raster] (single raster expected)
5916
+ * @param options GeoImageOptions (alpha used for opacity scaling)
5917
+ * @param arrayLength Total RGBA array length
5918
+ * @returns Uint8ClampedArray of RGBA values
5919
+ */
5920
+ static getReliefGlazeRGBA(rasters, options, arrayLength) {
5921
+ const reliefMask = rasters[0];
5922
+ const opacityFactor = (options.maxGlazeAlpha ?? 128) / 255;
5923
+ // Pre-compute alpha lookup table (256 entries, one per relief value 0-255)
5924
+ const alphaLookup = new Uint8Array(256);
5925
+ for (let v = 0; v < 256; v++) {
5926
+ if (v === 0) {
5927
+ alphaLookup[v] = 0; // noData: fully transparent
5928
+ }
5929
+ else {
5930
+ const alphaDist = Math.abs(v - 128) / 128;
5931
+ const bias = v < 128 ? 0.6 : 0.8;
5932
+ alphaLookup[v] = Math.floor(Math.pow(alphaDist, bias) * 255 * opacityFactor);
5933
+ }
5934
+ }
5935
+ const glazeArray = new Uint8ClampedArray(arrayLength);
5936
+ let maskIndex = 0;
5937
+ for (let i = 0; i < arrayLength; i += 4) {
5938
+ const reliefValue = reliefMask[maskIndex];
5939
+ // Pure black for shadows, pure white for highlights (no muddy grays)
5940
+ const glaze = reliefValue < 128 ? 0 : 255;
5941
+ const alpha = alphaLookup[reliefValue];
5942
+ glazeArray[i] = glaze; // R
5943
+ glazeArray[i + 1] = glaze; // G
5944
+ glazeArray[i + 2] = glaze; // B
5945
+ glazeArray[i + 3] = alpha; // A
5946
+ maskIndex++;
5947
+ }
5948
+ return glazeArray;
5949
+ }
5766
5950
  static calculateSingleColor(val, colorScale, options, alpha) {
5767
5951
  if (this.isInvalid(val, options)) {
5768
5952
  return options.nullColor;
@@ -5843,6 +6027,21 @@ class BitmapGenerator {
5843
6027
  * Output: Float32Array of 256×256 computed values.
5844
6028
  */
5845
6029
  class KernelGenerator {
6030
+ /**
6031
+ * Compute terrain gradients (dzdx, dzdy) using Horn's method.
6032
+ * @param z1-z9 - 3×3 neighborhood elevation values (z5 is center)
6033
+ * @param cellSizeFactor - Pre-computed 1 / (8 * cellSize)
6034
+ * @param geographicConvention - If true, use north-minus-south for dzdy (hillshade). If false, use south-minus-north (slope).
6035
+ */
6036
+ static computeGradients(z1, z2, z3, z4, /* z5 not needed */ z6, z7, z8, z9, cellSizeFactor, geographicConvention = true) {
6037
+ const dzdx = ((z3 + 2 * z6 + z9) - (z1 + 2 * z4 + z7)) * cellSizeFactor;
6038
+ // Geographic convention (hillshade): north minus south (top rows minus bottom rows)
6039
+ // Slope convention: south minus north (reversed)
6040
+ const dzdy = geographicConvention
6041
+ ? ((z1 + 2 * z2 + z3) - (z7 + 2 * z8 + z9)) * cellSizeFactor
6042
+ : ((z7 + 2 * z8 + z9) - (z1 + 2 * z2 + z3)) * cellSizeFactor;
6043
+ return { dzdx, dzdy };
6044
+ }
5846
6045
  /**
5847
6046
  * Calculates slope (0–90 degrees) for each pixel using Horn's method.
5848
6047
  *
@@ -5855,12 +6054,18 @@ class KernelGenerator {
5855
6054
  const OUT = 256;
5856
6055
  const IN = 258;
5857
6056
  const out = new Float32Array(OUT * OUT);
6057
+ // Hoist division out of loop: multiplication is ~2-3x faster than division
6058
+ const cellSizeFactor = 1 / (8 * cellSize);
6059
+ // Cache constant for radians to degrees conversion
6060
+ const RAD_TO_DEG = 180 / Math.PI;
6061
+ const isNaNNoData = noDataValue !== undefined && Number.isNaN(noDataValue);
5858
6062
  for (let r = 0; r < OUT; r++) {
5859
6063
  for (let c = 0; c < OUT; c++) {
5860
6064
  // 3×3 neighborhood in the 258×258 input, centered at (r+1, c+1)
5861
6065
  const base = r * IN + c;
5862
6066
  const z5 = src[base + IN + 1]; // center pixel
5863
- if (noDataValue !== undefined && z5 === noDataValue) {
6067
+ const isNoData = noDataValue !== undefined && (isNaNNoData ? Number.isNaN(z5) : z5 === noDataValue);
6068
+ if (isNoData) {
5864
6069
  out[r * OUT + c] = NaN;
5865
6070
  continue;
5866
6071
  }
@@ -5872,10 +6077,9 @@ class KernelGenerator {
5872
6077
  const z7 = src[base + 2 * IN]; // sw
5873
6078
  const z8 = src[base + 2 * IN + 1]; // s
5874
6079
  const z9 = src[base + 2 * IN + 2]; // se
5875
- const dzdx = ((z3 + 2 * z6 + z9) - (z1 + 2 * z4 + z7)) / (8 * cellSize);
5876
- const dzdy = ((z7 + 2 * z8 + z9) - (z1 + 2 * z2 + z3)) / (8 * cellSize);
6080
+ const { dzdx, dzdy } = this.computeGradients(z1, z2, z3, z4, z6, z7, z8, z9, cellSizeFactor, false);
5877
6081
  const slopeRad = Math.atan(zFactor * Math.sqrt(dzdx * dzdx + dzdy * dzdy));
5878
- out[r * OUT + c] = slopeRad * (180 / Math.PI);
6082
+ out[r * OUT + c] = slopeRad * RAD_TO_DEG;
5879
6083
  }
5880
6084
  }
5881
6085
  return out;
@@ -5900,11 +6104,15 @@ class KernelGenerator {
5900
6104
  if (azimuthMath >= 360)
5901
6105
  azimuthMath -= 360;
5902
6106
  const azimuthRad = azimuthMath * (Math.PI / 180);
6107
+ // Hoist division out of loop: multiplication is ~2-3x faster than division
6108
+ const cellSizeFactor = 1 / (8 * cellSize);
6109
+ const isNaNNoData = noDataValue !== undefined && Number.isNaN(noDataValue);
5903
6110
  for (let r = 0; r < OUT; r++) {
5904
6111
  for (let c = 0; c < OUT; c++) {
5905
6112
  const base = r * IN + c;
5906
6113
  const z5 = src[base + IN + 1]; // center pixel
5907
- if (noDataValue !== undefined && z5 === noDataValue) {
6114
+ const isNoData = noDataValue !== undefined && (isNaNNoData ? Number.isNaN(z5) : z5 === noDataValue);
6115
+ if (isNoData) {
5908
6116
  out[r * OUT + c] = NaN;
5909
6117
  continue;
5910
6118
  }
@@ -5916,9 +6124,7 @@ class KernelGenerator {
5916
6124
  const z7 = src[base + 2 * IN]; // sw
5917
6125
  const z8 = src[base + 2 * IN + 1]; // s
5918
6126
  const z9 = src[base + 2 * IN + 2]; // se
5919
- const dzdx = ((z3 + 2 * z6 + z9) - (z1 + 2 * z4 + z7)) / (8 * cellSize);
5920
- // dzdy: north minus south (geographic convention — top rows minus bottom rows in raster)
5921
- const dzdy = ((z1 + 2 * z2 + z3) - (z7 + 2 * z8 + z9)) / (8 * cellSize);
6127
+ const { dzdx, dzdy } = this.computeGradients(z1, z2, z3, z4, z6, z7, z8, z9, cellSizeFactor, true);
5922
6128
  const slopeRad = Math.atan(zFactor * Math.sqrt(dzdx * dzdx + dzdy * dzdy));
5923
6129
  const aspectRad = Math.atan2(dzdy, -dzdx);
5924
6130
  const hillshade = 255 * (Math.cos(zenithRad) * Math.cos(slopeRad) +
@@ -5928,6 +6134,139 @@ class KernelGenerator {
5928
6134
  }
5929
6135
  return out;
5930
6136
  }
6137
+ /**
6138
+ * Calculates a weighted multi-directional hillshade (0–255).
6139
+ * Combines three light sources to reveal structure in shadows.
6140
+ */
6141
+ static calculateMultiHillshade(src, cellSize, zFactor = 1, noDataValue) {
6142
+ const OUT = 256;
6143
+ const IN = 258;
6144
+ const out = new Float32Array(OUT * OUT);
6145
+ // Hoist division out of loop: multiplication is ~2-3x faster than division
6146
+ const cellSizeFactor = 1 / (8 * cellSize);
6147
+ const isNaNNoData = noDataValue !== undefined && Number.isNaN(noDataValue);
6148
+ // Setup 3 light sources: NW (Main), W (Fill), N (Fill)
6149
+ const lights = [
6150
+ { az: 315, alt: 45, weight: 0.60 }, // Primary NW
6151
+ { az: 225, alt: 35, weight: 0.25 }, // Secondary West/SW
6152
+ { az: 0, alt: 35, weight: 0.15 } // Secondary North
6153
+ ].map(l => {
6154
+ const zenithRad = (90 - l.alt) * (Math.PI / 180);
6155
+ let azMath = 360 - l.az + 90;
6156
+ if (azMath >= 360)
6157
+ azMath -= 360;
6158
+ return {
6159
+ zCos: Math.cos(zenithRad),
6160
+ zSin: Math.sin(zenithRad),
6161
+ aRad: azMath * (Math.PI / 180),
6162
+ w: l.weight
6163
+ };
6164
+ });
6165
+ for (let r = 0; r < OUT; r++) {
6166
+ for (let c = 0; c < OUT; c++) {
6167
+ const base = r * IN + c;
6168
+ const z5 = src[base + IN + 1];
6169
+ const isNoData = noDataValue !== undefined && (isNaNNoData ? Number.isNaN(z5) : z5 === noDataValue);
6170
+ if (isNoData) {
6171
+ out[r * OUT + c] = NaN;
6172
+ continue;
6173
+ }
6174
+ // Neighbors
6175
+ const z1 = src[base], z2 = src[base + 1], z3 = src[base + 2];
6176
+ const z4 = src[base + IN], z6 = src[base + IN + 2];
6177
+ const z7 = src[base + 2 * IN], z8 = src[base + 2 * IN + 1], z9 = src[base + 2 * IN + 2];
6178
+ const { dzdx, dzdy } = this.computeGradients(z1, z2, z3, z4, z6, z7, z8, z9, cellSizeFactor, true);
6179
+ const slopeRad = Math.atan(zFactor * Math.sqrt(dzdx * dzdx + dzdy * dzdy));
6180
+ const aspectRad = Math.atan2(dzdy, -dzdx);
6181
+ const cosSlope = Math.cos(slopeRad);
6182
+ const sinSlope = Math.sin(slopeRad);
6183
+ // Accumulate light from all three directions
6184
+ let multiHillshade = 0;
6185
+ for (const L of lights) {
6186
+ const intensity = L.zCos * cosSlope + L.zSin * sinSlope * Math.cos(L.aRad - aspectRad);
6187
+ multiHillshade += Math.max(0, intensity) * L.w;
6188
+ }
6189
+ out[r * OUT + c] = Math.min(255, multiHillshade * 255);
6190
+ }
6191
+ }
6192
+ return out;
6193
+ }
6194
+ }
6195
+
6196
+ /**
6197
+ * Composes Swiss relief by combining slope and hillshade kernels via LUT.
6198
+ * Outputs a single 0-255 relief mask suitable for baking into hypsometry (terrain)
6199
+ * or creating transparent glaze overlays (bitmap).
6200
+ */
6201
+ class ReliefCompositor {
6202
+ /**
6203
+ * Precompute and cache a 256x256 LUT for Swiss relief compositing.
6204
+ * LUT[hillshade][slope] = (hillshade * (1.0 - (slope * weight)))
6205
+ * All values normalized to [0,1].
6206
+ * Only computed on first use of Swiss relief mode.
6207
+ */
6208
+ static _swissReliefLUT = null;
6209
+ static _lastWeight = null;
6210
+ static getSwissReliefLUT(weight = 0.5) {
6211
+ // Check if LUT exists AND if the weight matches the previous calculation
6212
+ if (this._swissReliefLUT && this._lastWeight === weight) {
6213
+ return this._swissReliefLUT;
6214
+ }
6215
+ const ambient = 0.010; // 1% minimum brightness to prevent pitch black northwest slopes
6216
+ const lut = new Float32Array(256 * 256); // 65536 values
6217
+ for (let h = 0; h < 256; h++) {
6218
+ const hillshade = h / 255;
6219
+ for (let s = 0; s < 256; s++) {
6220
+ const slope = s / 255;
6221
+ // 1. Calculate the 'Swiss Contrast'
6222
+ const contrast = 1.0 - (slope * weight);
6223
+ // Swiss Formula: (Hillshade) * (1.0 - (Slope * Weight))
6224
+ // This results in 0.0 to 1.0 multiplier
6225
+ lut[(h << 8) | s] = Math.max(ambient, hillshade * contrast);
6226
+ }
6227
+ }
6228
+ this._swissReliefLUT = lut;
6229
+ this._lastWeight = weight;
6230
+ return lut;
6231
+ }
6232
+ /**
6233
+ * Compute Swiss relief compositing: slope + hillshade → 0-255 relief mask.
6234
+ *
6235
+ * @param elevation - Padded elevation raster (258×258 for kernel input)
6236
+ * @param options - GeoImageOptions (must include zFactor, noDataValue, swissSlopeWeight)
6237
+ * @param cellSize - Grid cell size in meters
6238
+ * @param width - Output width (typically 256)
6239
+ * @param height - Output height (typically 256)
6240
+ * @returns Uint8ClampedArray of 0-255 relief values
6241
+ */
6242
+ static composeSwissRelief(elevation, options, cellSize, width, height) {
6243
+ const weight = options.swissSlopeWeight ?? 0.5;
6244
+ // 1. Compute slope and hillshade kernels
6245
+ const rawSlope = KernelGenerator.calculateSlope(elevation, cellSize, options.zFactor ?? 1, options.noDataValue);
6246
+ const rawHillshade = KernelGenerator.calculateMultiHillshade(elevation, cellSize, options.zFactor ?? 1, options.noDataValue);
6247
+ // 2. Fetch pre-computed LUT
6248
+ const lut = this.getSwissReliefLUT(weight);
6249
+ // 3. Compose relief mask: quantize slope/hillshade, apply LUT
6250
+ // reliefMask = 0 is reserved as noData sentinel → fully transparent in glaze output
6251
+ const reliefMask = new Uint8ClampedArray(width * height);
6252
+ // Hoist division out of loop: multiplication is faster than division
6253
+ const SLOPE_SCALE = 255 / 90; // ~2.833...
6254
+ for (let i = 0; i < width * height; i++) {
6255
+ // noData pixels: slope is NaN (set by KernelGenerator when z5 === noDataValue)
6256
+ if (isNaN(rawSlope[i])) {
6257
+ reliefMask[i] = 0; // sentinel: transparent in glaze
6258
+ continue;
6259
+ }
6260
+ // Quantize Slope: Normalize 0-90° to 0-255 integer (avoid division in loop)
6261
+ const sIdx = Math.max(0, Math.min(255, (rawSlope[i] * SLOPE_SCALE) | 0));
6262
+ // Quantize Hillshade: Ensure 0-255 integer
6263
+ const hIdx = Math.max(0, Math.min(255, rawHillshade[i])) | 0;
6264
+ // LUT Lookup: Result is 0.0 - 1.0 (float)
6265
+ // Clamp to 1 to ensure output stays in 1-255 (0 is reserved for noData)
6266
+ reliefMask[i] = Math.max(1, (lut[(hIdx << 8) | sIdx] * 255) | 0);
6267
+ }
6268
+ return reliefMask;
6269
+ }
5931
6270
  }
5932
6271
 
5933
6272
  class TerrainGenerator {
@@ -5991,7 +6330,20 @@ class TerrainGenerator {
5991
6330
  height: gridHeight,
5992
6331
  };
5993
6332
  // 3. Kernel path: compute slope or hillshade, store as rawDerived, generate texture
5994
- if (isKernel && (options.useSlope || options.useHillshade)) {
6333
+ if (isKernel && options.useSwissRelief) {
6334
+ const cellSize = input.cellSizeMeters ?? ((input.bounds[2] - input.bounds[0]) / 256);
6335
+ // Build a separate raster for kernel computation that preserves noData samples.
6336
+ const kernelTerrain = this.preserveNoDataForKernel(terrain, input.rasters[0], options.noDataValue);
6337
+ // Compose Swiss relief using ReliefCompositor
6338
+ const swissReliefResult = ReliefCompositor.composeSwissRelief(kernelTerrain, options, cellSize, 256, 256);
6339
+ tileResult.rawDerived = swissReliefResult;
6340
+ if (this.hasVisualizationOptions(options)) {
6341
+ const cropped = this.cropRaster(meshTerrain, gridWidth, gridHeight, 256, 256);
6342
+ const bitmapResult = await BitmapGenerator.generate({ width: 256, height: 256, rasters: [cropped, swissReliefResult] }, { ...options, type: 'image' });
6343
+ tileResult.texture = bitmapResult.map;
6344
+ }
6345
+ }
6346
+ else if (isKernel && (options.useSlope || options.useHillshade)) {
5995
6347
  // Use pre-computed geographic cellSize (meters/pixel) from tile indices.
5996
6348
  // Falls back to bounds-derived estimate if not provided.
5997
6349
  const cellSize = input.cellSizeMeters ?? ((input.bounds[2] - input.bounds[0]) / 256);
@@ -6001,23 +6353,7 @@ class TerrainGenerator {
6001
6353
  console.warn('[TerrainGenerator] useSlope and useHillshade are mutually exclusive; useSlope takes precedence.');
6002
6354
  }
6003
6355
  // Build a separate raster for kernel computation that preserves noData samples.
6004
- const kernelTerrain = new Float32Array(terrain.length);
6005
- const sourceRaster = input.rasters[0];
6006
- const noData = options.noDataValue;
6007
- if (noData !== undefined &&
6008
- noData !== null &&
6009
- sourceRaster &&
6010
- sourceRaster.length === terrain.length) {
6011
- for (let i = 0; i < terrain.length; i++) {
6012
- // If the source raster marks this sample as noData, keep it as noData for the kernel.
6013
- // Otherwise, use the processed terrain elevation value.
6014
- kernelTerrain[i] = sourceRaster[i] == noData ? noData : terrain[i];
6015
- }
6016
- }
6017
- else {
6018
- // Fallback: no usable noData metadata or mismatched lengths; mirror existing behavior.
6019
- kernelTerrain.set(terrain);
6020
- }
6356
+ const kernelTerrain = this.preserveNoDataForKernel(terrain, input.rasters[0], options.noDataValue);
6021
6357
  let kernelOutput;
6022
6358
  if (options.useSlope) {
6023
6359
  kernelOutput = KernelGenerator.calculateSlope(kernelTerrain, cellSize, zFactor, options.noDataValue);
@@ -6039,7 +6375,6 @@ class TerrainGenerator {
6039
6375
  }
6040
6376
  return tileResult;
6041
6377
  }
6042
- /** Extracts rows 1–257, cols 1–257 from a 258×258 terrain array → 257×257 for mesh generation. */
6043
6378
  static extractMeshRaster(terrain258) {
6044
6379
  const MESH = 257;
6045
6380
  const IN = 258;
@@ -6052,11 +6387,38 @@ class TerrainGenerator {
6052
6387
  return out;
6053
6388
  }
6054
6389
  static hasVisualizationOptions(options) {
6055
- return !!(options.useHeatMap ||
6056
- options.useSingleColor ||
6390
+ return !!(options.useSingleColor ||
6391
+ options.useHeatMap ||
6392
+ options.useSwissRelief ||
6057
6393
  options.useColorsBasedOnValues ||
6058
6394
  options.useColorClasses);
6059
6395
  }
6396
+ /**
6397
+ * Preserve noData values in a separate raster for kernel computation.
6398
+ * If the source raster marks a sample as noData, keep it as noData.
6399
+ * Otherwise, use the processed terrain elevation value.
6400
+ */
6401
+ static preserveNoDataForKernel(terrain, sourceRaster, noDataValue) {
6402
+ const kernelTerrain = new Float32Array(terrain.length);
6403
+ if (noDataValue !== undefined &&
6404
+ noDataValue !== null &&
6405
+ sourceRaster &&
6406
+ sourceRaster.length === terrain.length) {
6407
+ const preserveNaNNoData = Number.isNaN(noDataValue);
6408
+ for (let i = 0; i < terrain.length; i++) {
6409
+ const sourceValue = sourceRaster[i];
6410
+ const isNoData = preserveNaNNoData
6411
+ ? Number.isNaN(sourceValue)
6412
+ : sourceValue === noDataValue;
6413
+ kernelTerrain[i] = isNoData ? noDataValue : terrain[i];
6414
+ }
6415
+ }
6416
+ else {
6417
+ // Fallback: no usable noData metadata or mismatched lengths; mirror existing behavior.
6418
+ kernelTerrain.set(terrain);
6419
+ }
6420
+ return kernelTerrain;
6421
+ }
6060
6422
  static cropRaster(src, srcWidth, _srcHeight, dstWidth, dstHeight) {
6061
6423
  const out = new Float32Array(dstWidth * dstHeight);
6062
6424
  for (let y = 0; y < dstHeight; y++) {
@@ -6181,7 +6543,7 @@ class GeoImage {
6181
6543
  this.data = data;
6182
6544
  }
6183
6545
  async getMap(input, options, meshMaxError) {
6184
- const mergedOptions = { ...DefaultGeoImageOptions, ...options };
6546
+ const mergedOptions = GeoImage.resolveVisualizationMode({ ...DefaultGeoImageOptions, ...options }, options);
6185
6547
  switch (mergedOptions.type) {
6186
6548
  case 'image':
6187
6549
  return this.getBitmap(input, mergedOptions);
@@ -6191,6 +6553,56 @@ class GeoImage {
6191
6553
  return null;
6192
6554
  }
6193
6555
  }
6556
+ /**
6557
+ * Resolves the active visualization (coloring) mode after merging user options with defaults.
6558
+ *
6559
+ * Solves three key issues:
6560
+ *
6561
+ * 1. **Mutual exclusivity**: `DefaultGeoImageOptions` sets `useHeatMap: true`. If a user
6562
+ * explicitly enables a coloring mode without explicitly setting `useHeatMap: false`,
6563
+ * enforce that only the explicitly-enabled modes are active (all others forced to false).
6564
+ * This prevents the default `useHeatMap: true` from interfering with user-chosen modes.
6565
+ *
6566
+ * 2. **Bitmap default**: for `type === 'image'` with no user-specified coloring mode,
6567
+ * keep `useHeatMap: true` from defaults. This provides sensible data-driven visualization.
6568
+ *
6569
+ * 3. **Terrain default**: for `type === 'terrain'` with no user-specified coloring mode and
6570
+ * no kernel-texture mode (`useSwissRelief` / `useSlope` / `useHillshade`), enable
6571
+ * `useSingleColor` with `color = terrainColor`. This renders the mesh in the documented
6572
+ * default colour (grey) without a data-driven texture overlay. When a kernel mode IS
6573
+ * present but no coloring mode is specified, keep `useHeatMap: true` so the kernel output
6574
+ * is still colourised.
6575
+ */
6576
+ static resolveVisualizationMode(mergedOptions, userOptions) {
6577
+ const coloringModes = ['useSingleColor', 'useColorClasses', 'useColorsBasedOnValues', 'useHeatMap'];
6578
+ const userExplicitColoringModes = coloringModes.filter(m => userOptions[m] === true);
6579
+ const resolved = { ...mergedOptions };
6580
+ if (userExplicitColoringModes.length > 0) {
6581
+ // Enforce mutual exclusivity: disable all coloring modes, then enable only those
6582
+ // explicitly set by the user. This prevents the default useHeatMap from interfering.
6583
+ for (const mode of coloringModes) {
6584
+ resolved[mode] = false;
6585
+ }
6586
+ for (const mode of userExplicitColoringModes) {
6587
+ resolved[mode] = true;
6588
+ }
6589
+ }
6590
+ else if (mergedOptions.type === 'terrain') {
6591
+ // Terrain with no explicit coloring mode.
6592
+ const hasKernelMode = userOptions.useSwissRelief || userOptions.useSlope || userOptions.useHillshade;
6593
+ if (!hasKernelMode) {
6594
+ // No kernel mode: enable useSingleColor with terrainColor as the default.
6595
+ // This renders the mesh in the documented colour without a data-driven texture.
6596
+ resolved.useHeatMap = false;
6597
+ resolved.useSingleColor = true;
6598
+ resolved.color = mergedOptions.terrainColor;
6599
+ }
6600
+ // When a kernel mode is present without an explicit coloring mode, keep
6601
+ // useHeatMap: true from defaults so the kernel output is colourised.
6602
+ }
6603
+ // For 'image' with no explicit coloring mode: keep useHeatMap: true from DefaultGeoImageOptions.
6604
+ return resolved;
6605
+ }
6194
6606
  // GetHeightmap uses only "useChannel" and "multiplier" options
6195
6607
  async getHeightmap(input, options, meshMaxError) {
6196
6608
  let rasters = [];
@@ -6534,19 +6946,37 @@ class CogTiles {
6534
6946
  async getTile(x, y, z, bounds, meshMaxError) {
6535
6947
  let requiredSize = this.tileSize; // Default 256 for image/bitmap
6536
6948
  if (this.options.type === 'terrain') {
6537
- const isKernel = this.options.useSlope || this.options.useHillshade;
6949
+ const isKernel = this.options.useSlope || this.options.useHillshade || this.options.useSwissRelief;
6538
6950
  requiredSize = this.tileSize + (isKernel ? 2 : 1); // 258 for kernel (3×3 border), 257 for normal stitching
6539
6951
  }
6952
+ else if (this.options.type === 'image' && this.options.useReliefGlaze) {
6953
+ // Bitmap layer with relief glaze mode needs kernel padding for slope/hillshade computation
6954
+ requiredSize = this.tileSize + 2; // 258 for kernel
6955
+ }
6540
6956
  const tileData = await this.getTileFromImage(x, y, z, requiredSize);
6541
6957
  // Compute true ground cell size in meters from tile indices.
6542
6958
  // Tile y in slippy-map convention → center latitude → Web Mercator distortion correction.
6543
6959
  const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 0.5) / Math.pow(2, z))));
6544
6960
  const tileWidthMeters = (EARTH_CIRCUMFERENCE / Math.pow(2, z)) * Math.cos(latRad);
6545
6961
  const cellSizeMeters = tileWidthMeters / this.tileSize;
6962
+ let rasters = [tileData[0]];
6963
+ let tileWidth = requiredSize;
6964
+ let tileHeight = requiredSize;
6965
+ // Relief glaze computation for bitmap layers
6966
+ // Note: For multi-band support (band selection via useChannelIndex), see issue #98
6967
+ if (this.options.type === 'image' && this.options.useReliefGlaze) {
6968
+ const elevation = tileData[0];
6969
+ // Pass full 258×258 padded elevation directly — KernelGenerator expects IN=258 and outputs 256×256
6970
+ const reliefMask = ReliefCompositor.composeSwissRelief(elevation, this.options, cellSizeMeters, this.tileSize, this.tileSize);
6971
+ // For glaze-only mode, pass ONLY the 256×256 relief mask
6972
+ rasters = [reliefMask];
6973
+ tileWidth = this.tileSize;
6974
+ tileHeight = this.tileSize;
6975
+ }
6546
6976
  return this.geo.getMap({
6547
- rasters: [tileData[0]],
6548
- width: requiredSize,
6549
- height: requiredSize,
6977
+ rasters,
6978
+ width: tileWidth,
6979
+ height: tileHeight,
6550
6980
  bounds,
6551
6981
  cellSizeMeters,
6552
6982
  }, this.options, meshMaxError);
@@ -6759,13 +7189,20 @@ class CogBitmapLayer extends CompositeLayer {
6759
7189
  }
6760
7190
  }
6761
7191
  async getTiledBitmapData(tile) {
6762
- // TODO - pass signal to getTile
6763
- // abort request if signal is aborted
6764
- const tileData = await this.state.bitmapCogTiles.getTile(tile.index.x, tile.index.y, tile.index.z);
6765
- if (tileData && !this.props.pickable) {
6766
- tileData.raw = null;
7192
+ try {
7193
+ // TODO - pass signal to getTile
7194
+ // abort request if signal is aborted
7195
+ const tileData = await this.state.bitmapCogTiles.getTile(tile.index.x, tile.index.y, tile.index.z);
7196
+ if (tileData && !this.props.pickable) {
7197
+ tileData.raw = null;
7198
+ }
7199
+ return tileData;
7200
+ }
7201
+ catch (error) {
7202
+ // Log the error and rethrow so TileLayer can surface the failure via onTileError
7203
+ log.warn(`Failed to load bitmap tile at ${tile.index.z}/${tile.index.x}/${tile.index.y}:`, error)();
7204
+ throw error;
6767
7205
  }
6768
- return tileData;
6769
7206
  }
6770
7207
  renderSubLayers(props) {
6771
7208
  const SubLayerClass = this.getSubLayerClass('image', BitmapLayer);
@@ -7008,15 +7445,28 @@ class CogTerrainLayer extends CompositeLayer {
7008
7445
  }
7009
7446
  renderSubLayers(props) {
7010
7447
  const SubLayerClass = this.getSubLayerClass('mesh', SimpleMeshLayer);
7011
- const { color, wireframe, material } = this.props;
7448
+ const { color, wireframe, terrainOptions } = this.props;
7012
7449
  const { data } = props;
7013
7450
  if (!data) {
7014
7451
  return null;
7015
7452
  }
7016
- // const [mesh, texture] = data;
7017
7453
  const [meshResult] = data;
7018
7454
  const tileTexture = (!this.props.disableTexture && meshResult?.texture) ? meshResult.texture : null;
7455
+ const isSwiss = terrainOptions?.useSwissRelief;
7456
+ const disableLighting = terrainOptions?.disableLighting;
7457
+ const shouldDisableLighting = isSwiss || disableLighting;
7458
+ const lightingProps = shouldDisableLighting ? {
7459
+ material: {
7460
+ ambient: 1.0,
7461
+ diffuse: 0.0,
7462
+ shininess: 0.0,
7463
+ specularColor: [0, 0, 0]
7464
+ }
7465
+ } : {
7466
+ material: this.props.material
7467
+ };
7019
7468
  return new SubLayerClass({ ...props, tileSize: props.tileSize }, {
7469
+ ...lightingProps,
7020
7470
  data: DUMMY_DATA,
7021
7471
  mesh: meshResult?.map,
7022
7472
  texture: tileTexture,
@@ -7026,7 +7476,6 @@ class CogTerrainLayer extends CompositeLayer {
7026
7476
  // getPosition: (d) => [0, 0, 0],
7027
7477
  getColor: tileTexture ? [255, 255, 255] : color,
7028
7478
  wireframe,
7029
- material,
7030
7479
  });
7031
7480
  }
7032
7481
  // Update zRange of viewport
@@ -7073,13 +7522,14 @@ class CogTerrainLayer extends CompositeLayer {
7073
7522
  updateTriggers: {
7074
7523
  getTileData: {
7075
7524
  elevationData: urlTemplateToUpdateTrigger(elevationData),
7076
- // texture: urlTemplateToUpdateTrigger(texture),
7077
7525
  meshMaxError,
7078
7526
  elevationDecoder,
7079
- // When cogTiles instance is swapped (e.g. mode switch), refetch tiles.
7080
- // deck.gl keeps old tile content visible until new tiles are ready.
7081
7527
  terrainCogTiles: this.state.terrainCogTiles,
7082
7528
  },
7529
+ renderSubLayers: {
7530
+ disableTexture: this.props.disableTexture,
7531
+ terrainOptions: this.props.terrainOptions,
7532
+ },
7083
7533
  },
7084
7534
  onViewportLoad: this.onViewportLoad.bind(this),
7085
7535
  zRange: this.state.zRange || null,