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