@gisatcz/deckgl-geolib 2.4.1-dev.1 → 2.5.0-dev.1
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/README.md +1 -0
- package/dist/cjs/index.js +421 -54
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/index.min.js +2 -2
- package/dist/cjs/index.min.js.map +1 -1
- package/dist/esm/index.js +421 -54
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.min.js +2 -2
- package/dist/esm/index.min.js.map +1 -1
- package/dist/esm/types/core/CogTiles.d.ts +2 -2
- package/dist/esm/types/core/GeoImage.d.ts +2 -0
- package/dist/esm/types/core/lib/BitmapGenerator.d.ts +21 -1
- package/dist/esm/types/core/lib/KernelGenerator.d.ts +12 -0
- package/dist/esm/types/core/lib/ReliefCompositor.d.ts +28 -0
- package/dist/esm/types/core/lib/TerrainGenerator.d.ts +6 -1
- package/dist/esm/types/core/types.d.ts +9 -0
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -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,11 @@ 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();
|
|
5596
5620
|
/**
|
|
5597
5621
|
* Main entry point: Generates an ImageBitmap from raw raster data.
|
|
5598
5622
|
*/
|
|
@@ -5619,7 +5643,31 @@ class BitmapGenerator {
|
|
|
5619
5643
|
// If planar support is added, this logic must be updated to handle both layouts correctly.
|
|
5620
5644
|
const numAvailableChannels = optionsLocal.numOfChannels ??
|
|
5621
5645
|
(rasters.length === 1 ? rasters[0].length / (width * height) : rasters.length);
|
|
5622
|
-
if (optionsLocal.
|
|
5646
|
+
if (optionsLocal.useReliefGlaze) {
|
|
5647
|
+
if (rasters.length >= 1) {
|
|
5648
|
+
// Relief glaze: pure black/white overlay with variable alpha
|
|
5649
|
+
imageData.data.set(this.getReliefGlazeRGBA(rasters, optionsLocal, size));
|
|
5650
|
+
}
|
|
5651
|
+
else {
|
|
5652
|
+
// Missing relief mask: fill with transparent
|
|
5653
|
+
const transparentData = new Uint8ClampedArray(size);
|
|
5654
|
+
transparentData.fill(0);
|
|
5655
|
+
imageData.data.set(transparentData);
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
else if (optionsLocal.useSwissRelief) {
|
|
5659
|
+
if (rasters.length === 2) {
|
|
5660
|
+
// Normal Swiss relief rendering: hypsometric color × relief mask
|
|
5661
|
+
imageData.data.set(this.getColorValue(rasters, optionsLocal, size));
|
|
5662
|
+
}
|
|
5663
|
+
else { // Missing mask: fill with null color (fully transparent or a fallback)
|
|
5664
|
+
const defaultColorData = this.getDefaultColor(size, optionsLocal.nullColor);
|
|
5665
|
+
defaultColorData.forEach((value, index) => {
|
|
5666
|
+
imageData.data[index] = value;
|
|
5667
|
+
});
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
else if (optionsLocal.useChannelIndex == null) {
|
|
5623
5671
|
if (isInterleaved) {
|
|
5624
5672
|
const ratio = rasters[0].length / (width * height);
|
|
5625
5673
|
if (ratio === 1) {
|
|
@@ -5687,9 +5735,68 @@ class BitmapGenerator {
|
|
|
5687
5735
|
const optAlpha = Math.floor((options.alpha ?? 100) * 2.55);
|
|
5688
5736
|
const rangeMin = options.colorScaleValueRange?.[0] ?? 0;
|
|
5689
5737
|
const rangeMax = options.colorScaleValueRange?.[1] ?? 255;
|
|
5690
|
-
const
|
|
5691
|
-
const
|
|
5692
|
-
|
|
5738
|
+
const isMultiRaster = Array.isArray(dataArray);
|
|
5739
|
+
const primaryBuffer = isMultiRaster ? dataArray[0] : dataArray;
|
|
5740
|
+
const isSwiss = options.useSwissRelief && isMultiRaster && dataArray.length >= 2;
|
|
5741
|
+
const is8Bit = primaryBuffer instanceof Uint8Array || primaryBuffer instanceof Uint8ClampedArray;
|
|
5742
|
+
const isFloatOrWide = !is8Bit && (primaryBuffer instanceof Float32Array || primaryBuffer instanceof Uint16Array || primaryBuffer instanceof Int16Array);
|
|
5743
|
+
// 1. SWISS MODE BRANCH
|
|
5744
|
+
if (isSwiss) {
|
|
5745
|
+
const reliefMask = dataArray[1];
|
|
5746
|
+
const rangeSpan = (rangeMax - rangeMin) || 1;
|
|
5747
|
+
// Only use LUT optimization for useHeatMap mode; other modes use calculateSingleColor per-pixel
|
|
5748
|
+
let lut = null;
|
|
5749
|
+
if (options.useHeatMap) {
|
|
5750
|
+
const LUT_SIZE = 1024;
|
|
5751
|
+
// Cache LUT: generate key from colorScale config + range + alpha
|
|
5752
|
+
const cacheKey = `${rangeMin}_${rangeMax}_${optAlpha}_${JSON.stringify(options.colorScale)}`;
|
|
5753
|
+
lut = this._swissColorLUTCache.get(cacheKey) || null;
|
|
5754
|
+
if (!lut) {
|
|
5755
|
+
// LUT not cached, generate it
|
|
5756
|
+
lut = new Uint8ClampedArray(LUT_SIZE * 4);
|
|
5757
|
+
for (let i = 0; i < LUT_SIZE; i++) {
|
|
5758
|
+
const domainVal = rangeMin + (i / (LUT_SIZE - 1)) * rangeSpan;
|
|
5759
|
+
const rgb = colorScale(domainVal).rgb();
|
|
5760
|
+
lut[i * 4] = rgb[0];
|
|
5761
|
+
lut[i * 4 + 1] = rgb[1];
|
|
5762
|
+
lut[i * 4 + 2] = rgb[2];
|
|
5763
|
+
lut[i * 4 + 3] = optAlpha;
|
|
5764
|
+
}
|
|
5765
|
+
this._swissColorLUTCache.set(cacheKey, lut);
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
for (let i = 0, sampleIndex = (options.useChannelIndex ?? 0); i < arrayLength; i += 4, sampleIndex += samplesPerPixel) {
|
|
5769
|
+
const elevationVal = primaryBuffer[sampleIndex];
|
|
5770
|
+
// NaN-aware noData check for Swiss relief
|
|
5771
|
+
const isNoData = options.noDataValue !== undefined && (Number.isNaN(options.noDataValue)
|
|
5772
|
+
? Number.isNaN(elevationVal)
|
|
5773
|
+
: elevationVal === options.noDataValue);
|
|
5774
|
+
if (Number.isNaN(elevationVal) || isNoData) {
|
|
5775
|
+
colorsArray.set(options.nullColor, i);
|
|
5776
|
+
continue;
|
|
5777
|
+
}
|
|
5778
|
+
let baseColor;
|
|
5779
|
+
if (lut) {
|
|
5780
|
+
// LUT-optimized path for useHeatMap
|
|
5781
|
+
const t = (elevationVal - rangeMin) / rangeSpan;
|
|
5782
|
+
const lutIdx = Math.min(1023, Math.max(0, Math.floor(t * 1023))) * 4;
|
|
5783
|
+
baseColor = [lut[lutIdx], lut[lutIdx + 1], lut[lutIdx + 2], lut[lutIdx + 3]];
|
|
5784
|
+
}
|
|
5785
|
+
else {
|
|
5786
|
+
// Per-pixel calculation for useSingleColor, useColorClasses, useColorsBasedOnValues
|
|
5787
|
+
baseColor = this.calculateSingleColor(elevationVal, colorScale, options, optAlpha);
|
|
5788
|
+
}
|
|
5789
|
+
// Apply relief mask as multiplier (Ambient Fill approach)
|
|
5790
|
+
const maskVal = reliefMask[sampleIndex];
|
|
5791
|
+
const multiplier = 0.4 + 0.6 * (maskVal / 255);
|
|
5792
|
+
colorsArray[i] = Math.floor(baseColor[0] * multiplier);
|
|
5793
|
+
colorsArray[i + 1] = Math.floor(baseColor[1] * multiplier);
|
|
5794
|
+
colorsArray[i + 2] = Math.floor(baseColor[2] * multiplier);
|
|
5795
|
+
colorsArray[i + 3] = baseColor[3];
|
|
5796
|
+
}
|
|
5797
|
+
return colorsArray;
|
|
5798
|
+
}
|
|
5799
|
+
// 2. 8-BIT COMPREHENSIVE LUT
|
|
5693
5800
|
// Single-band 8-bit (grayscale or indexed): use LUT for fast mapping
|
|
5694
5801
|
if (is8Bit && !options.useDataForOpacity) {
|
|
5695
5802
|
const lut = new Uint8ClampedArray(256 * 4);
|
|
@@ -5703,7 +5810,7 @@ class BitmapGenerator {
|
|
|
5703
5810
|
}
|
|
5704
5811
|
}
|
|
5705
5812
|
for (let i = 0, sampleIndex = (options.useChannelIndex ?? 0); i < arrayLength; i += 4, sampleIndex += samplesPerPixel) {
|
|
5706
|
-
const lutIdx =
|
|
5813
|
+
const lutIdx = primaryBuffer[sampleIndex] * 4;
|
|
5707
5814
|
colorsArray[i] = lut[lutIdx];
|
|
5708
5815
|
colorsArray[i + 1] = lut[lutIdx + 1];
|
|
5709
5816
|
colorsArray[i + 2] = lut[lutIdx + 2];
|
|
@@ -5711,7 +5818,7 @@ class BitmapGenerator {
|
|
|
5711
5818
|
}
|
|
5712
5819
|
return colorsArray;
|
|
5713
5820
|
}
|
|
5714
|
-
//
|
|
5821
|
+
// 3. FLOAT / 16-BIT LUT (HEATMAP ONLY)
|
|
5715
5822
|
if (isFloatOrWide && options.useHeatMap && !options.useDataForOpacity) {
|
|
5716
5823
|
const LUT_SIZE = 1024;
|
|
5717
5824
|
const lut = new Uint8ClampedArray(LUT_SIZE * 4);
|
|
@@ -5731,7 +5838,7 @@ class BitmapGenerator {
|
|
|
5731
5838
|
}
|
|
5732
5839
|
}
|
|
5733
5840
|
for (let i = 0, sampleIndex = (options.useChannelIndex ?? 0); i < arrayLength; i += 4, sampleIndex += samplesPerPixel) {
|
|
5734
|
-
const val =
|
|
5841
|
+
const val = primaryBuffer[sampleIndex];
|
|
5735
5842
|
if (this.isInvalid(val, options)) {
|
|
5736
5843
|
colorsArray.set(this.getInvalidColor(val, options), i);
|
|
5737
5844
|
}
|
|
@@ -5746,10 +5853,10 @@ class BitmapGenerator {
|
|
|
5746
5853
|
}
|
|
5747
5854
|
return colorsArray;
|
|
5748
5855
|
}
|
|
5749
|
-
//
|
|
5856
|
+
// 4. FALLBACK LOOP (Categorical Float, Opacity, or Single Color)
|
|
5750
5857
|
let sampleIndex = options.useChannelIndex ?? 0;
|
|
5751
5858
|
for (let i = 0; i < arrayLength; i += 4) {
|
|
5752
|
-
const val =
|
|
5859
|
+
const val = primaryBuffer[sampleIndex];
|
|
5753
5860
|
let color;
|
|
5754
5861
|
if ((options.clipLow != null && val <= options.clipLow) || (options.clipHigh != null && val >= options.clipHigh)) {
|
|
5755
5862
|
color = options.clippedColor;
|
|
@@ -5765,6 +5872,50 @@ class BitmapGenerator {
|
|
|
5765
5872
|
}
|
|
5766
5873
|
return colorsArray;
|
|
5767
5874
|
}
|
|
5875
|
+
/**
|
|
5876
|
+
* Generate relief glaze RGBA output.
|
|
5877
|
+
* Maps relief mask (0-255) to pure black/white glaze with variable alpha.
|
|
5878
|
+
* - reliefValue < 128: Pure black (0,0,0) darkens shadows
|
|
5879
|
+
* - reliefValue > 128: Pure white (255,255,255) brightens highlights
|
|
5880
|
+
* - reliefValue == 128: Transparent (no effect)
|
|
5881
|
+
*
|
|
5882
|
+
* High-performance implementation using pre-computed alpha LUT to avoid 65k Math.pow calls.
|
|
5883
|
+
*
|
|
5884
|
+
* @param rasters Array of [relief mask raster] (single raster expected)
|
|
5885
|
+
* @param options GeoImageOptions (alpha used for opacity scaling)
|
|
5886
|
+
* @param arrayLength Total RGBA array length
|
|
5887
|
+
* @returns Uint8ClampedArray of RGBA values
|
|
5888
|
+
*/
|
|
5889
|
+
static getReliefGlazeRGBA(rasters, options, arrayLength) {
|
|
5890
|
+
const reliefMask = rasters[0];
|
|
5891
|
+
const opacityFactor = (options.maxGlazeAlpha ?? 128) / 255;
|
|
5892
|
+
// Pre-compute alpha lookup table (256 entries, one per relief value 0-255)
|
|
5893
|
+
const alphaLookup = new Uint8Array(256);
|
|
5894
|
+
for (let v = 0; v < 256; v++) {
|
|
5895
|
+
if (v === 0) {
|
|
5896
|
+
alphaLookup[v] = 0; // noData: fully transparent
|
|
5897
|
+
}
|
|
5898
|
+
else {
|
|
5899
|
+
const alphaDist = Math.abs(v - 128) / 128;
|
|
5900
|
+
const bias = v < 128 ? 0.6 : 0.8;
|
|
5901
|
+
alphaLookup[v] = Math.floor(Math.pow(alphaDist, bias) * 255 * opacityFactor);
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
const glazeArray = new Uint8ClampedArray(arrayLength);
|
|
5905
|
+
let maskIndex = 0;
|
|
5906
|
+
for (let i = 0; i < arrayLength; i += 4) {
|
|
5907
|
+
const reliefValue = reliefMask[maskIndex];
|
|
5908
|
+
// Pure black for shadows, pure white for highlights (no muddy grays)
|
|
5909
|
+
const glaze = reliefValue < 128 ? 0 : 255;
|
|
5910
|
+
const alpha = alphaLookup[reliefValue];
|
|
5911
|
+
glazeArray[i] = glaze; // R
|
|
5912
|
+
glazeArray[i + 1] = glaze; // G
|
|
5913
|
+
glazeArray[i + 2] = glaze; // B
|
|
5914
|
+
glazeArray[i + 3] = alpha; // A
|
|
5915
|
+
maskIndex++;
|
|
5916
|
+
}
|
|
5917
|
+
return glazeArray;
|
|
5918
|
+
}
|
|
5768
5919
|
static calculateSingleColor(val, colorScale, options, alpha) {
|
|
5769
5920
|
if (this.isInvalid(val, options)) {
|
|
5770
5921
|
return options.nullColor;
|
|
@@ -5845,6 +5996,21 @@ class BitmapGenerator {
|
|
|
5845
5996
|
* Output: Float32Array of 256×256 computed values.
|
|
5846
5997
|
*/
|
|
5847
5998
|
class KernelGenerator {
|
|
5999
|
+
/**
|
|
6000
|
+
* Compute terrain gradients (dzdx, dzdy) using Horn's method.
|
|
6001
|
+
* @param z1-z9 - 3×3 neighborhood elevation values (z5 is center)
|
|
6002
|
+
* @param cellSizeFactor - Pre-computed 1 / (8 * cellSize)
|
|
6003
|
+
* @param geographicConvention - If true, use north-minus-south for dzdy (hillshade). If false, use south-minus-north (slope).
|
|
6004
|
+
*/
|
|
6005
|
+
static computeGradients(z1, z2, z3, z4, /* z5 not needed */ z6, z7, z8, z9, cellSizeFactor, geographicConvention = true) {
|
|
6006
|
+
const dzdx = ((z3 + 2 * z6 + z9) - (z1 + 2 * z4 + z7)) * cellSizeFactor;
|
|
6007
|
+
// Geographic convention (hillshade): north minus south (top rows minus bottom rows)
|
|
6008
|
+
// Slope convention: south minus north (reversed)
|
|
6009
|
+
const dzdy = geographicConvention
|
|
6010
|
+
? ((z1 + 2 * z2 + z3) - (z7 + 2 * z8 + z9)) * cellSizeFactor
|
|
6011
|
+
: ((z7 + 2 * z8 + z9) - (z1 + 2 * z2 + z3)) * cellSizeFactor;
|
|
6012
|
+
return { dzdx, dzdy };
|
|
6013
|
+
}
|
|
5848
6014
|
/**
|
|
5849
6015
|
* Calculates slope (0–90 degrees) for each pixel using Horn's method.
|
|
5850
6016
|
*
|
|
@@ -5857,12 +6023,18 @@ class KernelGenerator {
|
|
|
5857
6023
|
const OUT = 256;
|
|
5858
6024
|
const IN = 258;
|
|
5859
6025
|
const out = new Float32Array(OUT * OUT);
|
|
6026
|
+
// Hoist division out of loop: multiplication is ~2-3x faster than division
|
|
6027
|
+
const cellSizeFactor = 1 / (8 * cellSize);
|
|
6028
|
+
// Cache constant for radians to degrees conversion
|
|
6029
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
6030
|
+
const isNaNNoData = noDataValue !== undefined && Number.isNaN(noDataValue);
|
|
5860
6031
|
for (let r = 0; r < OUT; r++) {
|
|
5861
6032
|
for (let c = 0; c < OUT; c++) {
|
|
5862
6033
|
// 3×3 neighborhood in the 258×258 input, centered at (r+1, c+1)
|
|
5863
6034
|
const base = r * IN + c;
|
|
5864
6035
|
const z5 = src[base + IN + 1]; // center pixel
|
|
5865
|
-
|
|
6036
|
+
const isNoData = noDataValue !== undefined && (isNaNNoData ? Number.isNaN(z5) : z5 === noDataValue);
|
|
6037
|
+
if (isNoData) {
|
|
5866
6038
|
out[r * OUT + c] = NaN;
|
|
5867
6039
|
continue;
|
|
5868
6040
|
}
|
|
@@ -5874,10 +6046,9 @@ class KernelGenerator {
|
|
|
5874
6046
|
const z7 = src[base + 2 * IN]; // sw
|
|
5875
6047
|
const z8 = src[base + 2 * IN + 1]; // s
|
|
5876
6048
|
const z9 = src[base + 2 * IN + 2]; // se
|
|
5877
|
-
const dzdx
|
|
5878
|
-
const dzdy = ((z7 + 2 * z8 + z9) - (z1 + 2 * z2 + z3)) / (8 * cellSize);
|
|
6049
|
+
const { dzdx, dzdy } = this.computeGradients(z1, z2, z3, z4, z6, z7, z8, z9, cellSizeFactor, false);
|
|
5879
6050
|
const slopeRad = Math.atan(zFactor * Math.sqrt(dzdx * dzdx + dzdy * dzdy));
|
|
5880
|
-
out[r * OUT + c] = slopeRad *
|
|
6051
|
+
out[r * OUT + c] = slopeRad * RAD_TO_DEG;
|
|
5881
6052
|
}
|
|
5882
6053
|
}
|
|
5883
6054
|
return out;
|
|
@@ -5902,11 +6073,15 @@ class KernelGenerator {
|
|
|
5902
6073
|
if (azimuthMath >= 360)
|
|
5903
6074
|
azimuthMath -= 360;
|
|
5904
6075
|
const azimuthRad = azimuthMath * (Math.PI / 180);
|
|
6076
|
+
// Hoist division out of loop: multiplication is ~2-3x faster than division
|
|
6077
|
+
const cellSizeFactor = 1 / (8 * cellSize);
|
|
6078
|
+
const isNaNNoData = noDataValue !== undefined && Number.isNaN(noDataValue);
|
|
5905
6079
|
for (let r = 0; r < OUT; r++) {
|
|
5906
6080
|
for (let c = 0; c < OUT; c++) {
|
|
5907
6081
|
const base = r * IN + c;
|
|
5908
6082
|
const z5 = src[base + IN + 1]; // center pixel
|
|
5909
|
-
|
|
6083
|
+
const isNoData = noDataValue !== undefined && (isNaNNoData ? Number.isNaN(z5) : z5 === noDataValue);
|
|
6084
|
+
if (isNoData) {
|
|
5910
6085
|
out[r * OUT + c] = NaN;
|
|
5911
6086
|
continue;
|
|
5912
6087
|
}
|
|
@@ -5918,9 +6093,7 @@ class KernelGenerator {
|
|
|
5918
6093
|
const z7 = src[base + 2 * IN]; // sw
|
|
5919
6094
|
const z8 = src[base + 2 * IN + 1]; // s
|
|
5920
6095
|
const z9 = src[base + 2 * IN + 2]; // se
|
|
5921
|
-
const dzdx
|
|
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);
|
|
6096
|
+
const { dzdx, dzdy } = this.computeGradients(z1, z2, z3, z4, z6, z7, z8, z9, cellSizeFactor, true);
|
|
5924
6097
|
const slopeRad = Math.atan(zFactor * Math.sqrt(dzdx * dzdx + dzdy * dzdy));
|
|
5925
6098
|
const aspectRad = Math.atan2(dzdy, -dzdx);
|
|
5926
6099
|
const hillshade = 255 * (Math.cos(zenithRad) * Math.cos(slopeRad) +
|
|
@@ -5930,6 +6103,139 @@ class KernelGenerator {
|
|
|
5930
6103
|
}
|
|
5931
6104
|
return out;
|
|
5932
6105
|
}
|
|
6106
|
+
/**
|
|
6107
|
+
* Calculates a weighted multi-directional hillshade (0–255).
|
|
6108
|
+
* Combines three light sources to reveal structure in shadows.
|
|
6109
|
+
*/
|
|
6110
|
+
static calculateMultiHillshade(src, cellSize, zFactor = 1, noDataValue) {
|
|
6111
|
+
const OUT = 256;
|
|
6112
|
+
const IN = 258;
|
|
6113
|
+
const out = new Float32Array(OUT * OUT);
|
|
6114
|
+
// Hoist division out of loop: multiplication is ~2-3x faster than division
|
|
6115
|
+
const cellSizeFactor = 1 / (8 * cellSize);
|
|
6116
|
+
const isNaNNoData = noDataValue !== undefined && Number.isNaN(noDataValue);
|
|
6117
|
+
// Setup 3 light sources: NW (Main), W (Fill), N (Fill)
|
|
6118
|
+
const lights = [
|
|
6119
|
+
{ az: 315, alt: 45, weight: 0.60 }, // Primary NW
|
|
6120
|
+
{ az: 225, alt: 35, weight: 0.25 }, // Secondary West/SW
|
|
6121
|
+
{ az: 0, alt: 35, weight: 0.15 } // Secondary North
|
|
6122
|
+
].map(l => {
|
|
6123
|
+
const zenithRad = (90 - l.alt) * (Math.PI / 180);
|
|
6124
|
+
let azMath = 360 - l.az + 90;
|
|
6125
|
+
if (azMath >= 360)
|
|
6126
|
+
azMath -= 360;
|
|
6127
|
+
return {
|
|
6128
|
+
zCos: Math.cos(zenithRad),
|
|
6129
|
+
zSin: Math.sin(zenithRad),
|
|
6130
|
+
aRad: azMath * (Math.PI / 180),
|
|
6131
|
+
w: l.weight
|
|
6132
|
+
};
|
|
6133
|
+
});
|
|
6134
|
+
for (let r = 0; r < OUT; r++) {
|
|
6135
|
+
for (let c = 0; c < OUT; c++) {
|
|
6136
|
+
const base = r * IN + c;
|
|
6137
|
+
const z5 = src[base + IN + 1];
|
|
6138
|
+
const isNoData = noDataValue !== undefined && (isNaNNoData ? Number.isNaN(z5) : z5 === noDataValue);
|
|
6139
|
+
if (isNoData) {
|
|
6140
|
+
out[r * OUT + c] = NaN;
|
|
6141
|
+
continue;
|
|
6142
|
+
}
|
|
6143
|
+
// Neighbors
|
|
6144
|
+
const z1 = src[base], z2 = src[base + 1], z3 = src[base + 2];
|
|
6145
|
+
const z4 = src[base + IN], z6 = src[base + IN + 2];
|
|
6146
|
+
const z7 = src[base + 2 * IN], z8 = src[base + 2 * IN + 1], z9 = src[base + 2 * IN + 2];
|
|
6147
|
+
const { dzdx, dzdy } = this.computeGradients(z1, z2, z3, z4, z6, z7, z8, z9, cellSizeFactor, true);
|
|
6148
|
+
const slopeRad = Math.atan(zFactor * Math.sqrt(dzdx * dzdx + dzdy * dzdy));
|
|
6149
|
+
const aspectRad = Math.atan2(dzdy, -dzdx);
|
|
6150
|
+
const cosSlope = Math.cos(slopeRad);
|
|
6151
|
+
const sinSlope = Math.sin(slopeRad);
|
|
6152
|
+
// Accumulate light from all three directions
|
|
6153
|
+
let multiHillshade = 0;
|
|
6154
|
+
for (const L of lights) {
|
|
6155
|
+
const intensity = L.zCos * cosSlope + L.zSin * sinSlope * Math.cos(L.aRad - aspectRad);
|
|
6156
|
+
multiHillshade += Math.max(0, intensity) * L.w;
|
|
6157
|
+
}
|
|
6158
|
+
out[r * OUT + c] = Math.min(255, multiHillshade * 255);
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
return out;
|
|
6162
|
+
}
|
|
6163
|
+
}
|
|
6164
|
+
|
|
6165
|
+
/**
|
|
6166
|
+
* Composes Swiss relief by combining slope and hillshade kernels via LUT.
|
|
6167
|
+
* Outputs a single 0-255 relief mask suitable for baking into hypsometry (terrain)
|
|
6168
|
+
* or creating transparent glaze overlays (bitmap).
|
|
6169
|
+
*/
|
|
6170
|
+
class ReliefCompositor {
|
|
6171
|
+
/**
|
|
6172
|
+
* Precompute and cache a 256x256 LUT for Swiss relief compositing.
|
|
6173
|
+
* LUT[hillshade][slope] = (hillshade * (1.0 - (slope * weight)))
|
|
6174
|
+
* All values normalized to [0,1].
|
|
6175
|
+
* Only computed on first use of Swiss relief mode.
|
|
6176
|
+
*/
|
|
6177
|
+
static _swissReliefLUT = null;
|
|
6178
|
+
static _lastWeight = null;
|
|
6179
|
+
static getSwissReliefLUT(weight = 0.5) {
|
|
6180
|
+
// Check if LUT exists AND if the weight matches the previous calculation
|
|
6181
|
+
if (this._swissReliefLUT && this._lastWeight === weight) {
|
|
6182
|
+
return this._swissReliefLUT;
|
|
6183
|
+
}
|
|
6184
|
+
const ambient = 0.010; // 1% minimum brightness to prevent pitch black northwest slopes
|
|
6185
|
+
const lut = new Float32Array(256 * 256); // 65536 values
|
|
6186
|
+
for (let h = 0; h < 256; h++) {
|
|
6187
|
+
const hillshade = h / 255;
|
|
6188
|
+
for (let s = 0; s < 256; s++) {
|
|
6189
|
+
const slope = s / 255;
|
|
6190
|
+
// 1. Calculate the 'Swiss Contrast'
|
|
6191
|
+
const contrast = 1.0 - (slope * weight);
|
|
6192
|
+
// Swiss Formula: (Hillshade) * (1.0 - (Slope * Weight))
|
|
6193
|
+
// This results in 0.0 to 1.0 multiplier
|
|
6194
|
+
lut[(h << 8) | s] = Math.max(ambient, hillshade * contrast);
|
|
6195
|
+
}
|
|
6196
|
+
}
|
|
6197
|
+
this._swissReliefLUT = lut;
|
|
6198
|
+
this._lastWeight = weight;
|
|
6199
|
+
return lut;
|
|
6200
|
+
}
|
|
6201
|
+
/**
|
|
6202
|
+
* Compute Swiss relief compositing: slope + hillshade → 0-255 relief mask.
|
|
6203
|
+
*
|
|
6204
|
+
* @param elevation - Padded elevation raster (258×258 for kernel input)
|
|
6205
|
+
* @param options - GeoImageOptions (must include zFactor, noDataValue, swissSlopeWeight)
|
|
6206
|
+
* @param cellSize - Grid cell size in meters
|
|
6207
|
+
* @param width - Output width (typically 256)
|
|
6208
|
+
* @param height - Output height (typically 256)
|
|
6209
|
+
* @returns Uint8ClampedArray of 0-255 relief values
|
|
6210
|
+
*/
|
|
6211
|
+
static composeSwissRelief(elevation, options, cellSize, width, height) {
|
|
6212
|
+
const weight = options.swissSlopeWeight ?? 0.5;
|
|
6213
|
+
// 1. Compute slope and hillshade kernels
|
|
6214
|
+
const rawSlope = KernelGenerator.calculateSlope(elevation, cellSize, options.zFactor ?? 1, options.noDataValue);
|
|
6215
|
+
const rawHillshade = KernelGenerator.calculateMultiHillshade(elevation, cellSize, options.zFactor ?? 1, options.noDataValue);
|
|
6216
|
+
// 2. Fetch pre-computed LUT
|
|
6217
|
+
const lut = this.getSwissReliefLUT(weight);
|
|
6218
|
+
// 3. Compose relief mask: quantize slope/hillshade, apply LUT
|
|
6219
|
+
// reliefMask = 0 is reserved as noData sentinel → fully transparent in glaze output
|
|
6220
|
+
const reliefMask = new Uint8ClampedArray(width * height);
|
|
6221
|
+
// Hoist division out of loop: multiplication is faster than division
|
|
6222
|
+
const SLOPE_SCALE = 255 / 90; // ~2.833...
|
|
6223
|
+
for (let i = 0; i < width * height; i++) {
|
|
6224
|
+
// noData pixels: slope is NaN (set by KernelGenerator when z5 === noDataValue)
|
|
6225
|
+
if (isNaN(rawSlope[i])) {
|
|
6226
|
+
reliefMask[i] = 0; // sentinel: transparent in glaze
|
|
6227
|
+
continue;
|
|
6228
|
+
}
|
|
6229
|
+
// Quantize Slope: Normalize 0-90° to 0-255 integer (avoid division in loop)
|
|
6230
|
+
const sIdx = Math.max(0, Math.min(255, (rawSlope[i] * SLOPE_SCALE) | 0));
|
|
6231
|
+
// Quantize Hillshade: Ensure 0-255 integer
|
|
6232
|
+
const hIdx = Math.max(0, Math.min(255, rawHillshade[i])) | 0;
|
|
6233
|
+
// LUT Lookup: Result is 0.0 - 1.0 (float)
|
|
6234
|
+
// Clamp to 1 to ensure output stays in 1-255 (0 is reserved for noData)
|
|
6235
|
+
reliefMask[i] = Math.max(1, (lut[(hIdx << 8) | sIdx] * 255) | 0);
|
|
6236
|
+
}
|
|
6237
|
+
return reliefMask;
|
|
6238
|
+
}
|
|
5933
6239
|
}
|
|
5934
6240
|
|
|
5935
6241
|
class TerrainGenerator {
|
|
@@ -5993,7 +6299,20 @@ class TerrainGenerator {
|
|
|
5993
6299
|
height: gridHeight,
|
|
5994
6300
|
};
|
|
5995
6301
|
// 3. Kernel path: compute slope or hillshade, store as rawDerived, generate texture
|
|
5996
|
-
if (isKernel &&
|
|
6302
|
+
if (isKernel && options.useSwissRelief) {
|
|
6303
|
+
const cellSize = input.cellSizeMeters ?? ((input.bounds[2] - input.bounds[0]) / 256);
|
|
6304
|
+
// Build a separate raster for kernel computation that preserves noData samples.
|
|
6305
|
+
const kernelTerrain = this.preserveNoDataForKernel(terrain, input.rasters[0], options.noDataValue);
|
|
6306
|
+
// Compose Swiss relief using ReliefCompositor
|
|
6307
|
+
const swissReliefResult = ReliefCompositor.composeSwissRelief(kernelTerrain, options, cellSize, 256, 256);
|
|
6308
|
+
tileResult.rawDerived = swissReliefResult;
|
|
6309
|
+
if (this.hasVisualizationOptions(options)) {
|
|
6310
|
+
const cropped = this.cropRaster(meshTerrain, gridWidth, gridHeight, 256, 256);
|
|
6311
|
+
const bitmapResult = await BitmapGenerator.generate({ width: 256, height: 256, rasters: [cropped, swissReliefResult] }, { ...options, type: 'image' });
|
|
6312
|
+
tileResult.texture = bitmapResult.map;
|
|
6313
|
+
}
|
|
6314
|
+
}
|
|
6315
|
+
else if (isKernel && (options.useSlope || options.useHillshade)) {
|
|
5997
6316
|
// Use pre-computed geographic cellSize (meters/pixel) from tile indices.
|
|
5998
6317
|
// Falls back to bounds-derived estimate if not provided.
|
|
5999
6318
|
const cellSize = input.cellSizeMeters ?? ((input.bounds[2] - input.bounds[0]) / 256);
|
|
@@ -6003,23 +6322,7 @@ class TerrainGenerator {
|
|
|
6003
6322
|
console.warn('[TerrainGenerator] useSlope and useHillshade are mutually exclusive; useSlope takes precedence.');
|
|
6004
6323
|
}
|
|
6005
6324
|
// Build a separate raster for kernel computation that preserves noData samples.
|
|
6006
|
-
const kernelTerrain =
|
|
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
|
-
}
|
|
6325
|
+
const kernelTerrain = this.preserveNoDataForKernel(terrain, input.rasters[0], options.noDataValue);
|
|
6023
6326
|
let kernelOutput;
|
|
6024
6327
|
if (options.useSlope) {
|
|
6025
6328
|
kernelOutput = KernelGenerator.calculateSlope(kernelTerrain, cellSize, zFactor, options.noDataValue);
|
|
@@ -6041,7 +6344,6 @@ class TerrainGenerator {
|
|
|
6041
6344
|
}
|
|
6042
6345
|
return tileResult;
|
|
6043
6346
|
}
|
|
6044
|
-
/** Extracts rows 1–257, cols 1–257 from a 258×258 terrain array → 257×257 for mesh generation. */
|
|
6045
6347
|
static extractMeshRaster(terrain258) {
|
|
6046
6348
|
const MESH = 257;
|
|
6047
6349
|
const IN = 258;
|
|
@@ -6054,11 +6356,38 @@ class TerrainGenerator {
|
|
|
6054
6356
|
return out;
|
|
6055
6357
|
}
|
|
6056
6358
|
static hasVisualizationOptions(options) {
|
|
6057
|
-
return !!(options.
|
|
6058
|
-
options.
|
|
6359
|
+
return !!(options.useSingleColor ||
|
|
6360
|
+
options.useHeatMap ||
|
|
6361
|
+
options.useSwissRelief ||
|
|
6059
6362
|
options.useColorsBasedOnValues ||
|
|
6060
6363
|
options.useColorClasses);
|
|
6061
6364
|
}
|
|
6365
|
+
/**
|
|
6366
|
+
* Preserve noData values in a separate raster for kernel computation.
|
|
6367
|
+
* If the source raster marks a sample as noData, keep it as noData.
|
|
6368
|
+
* Otherwise, use the processed terrain elevation value.
|
|
6369
|
+
*/
|
|
6370
|
+
static preserveNoDataForKernel(terrain, sourceRaster, noDataValue) {
|
|
6371
|
+
const kernelTerrain = new Float32Array(terrain.length);
|
|
6372
|
+
if (noDataValue !== undefined &&
|
|
6373
|
+
noDataValue !== null &&
|
|
6374
|
+
sourceRaster &&
|
|
6375
|
+
sourceRaster.length === terrain.length) {
|
|
6376
|
+
const preserveNaNNoData = Number.isNaN(noDataValue);
|
|
6377
|
+
for (let i = 0; i < terrain.length; i++) {
|
|
6378
|
+
const sourceValue = sourceRaster[i];
|
|
6379
|
+
const isNoData = preserveNaNNoData
|
|
6380
|
+
? Number.isNaN(sourceValue)
|
|
6381
|
+
: sourceValue === noDataValue;
|
|
6382
|
+
kernelTerrain[i] = isNoData ? noDataValue : terrain[i];
|
|
6383
|
+
}
|
|
6384
|
+
}
|
|
6385
|
+
else {
|
|
6386
|
+
// Fallback: no usable noData metadata or mismatched lengths; mirror existing behavior.
|
|
6387
|
+
kernelTerrain.set(terrain);
|
|
6388
|
+
}
|
|
6389
|
+
return kernelTerrain;
|
|
6390
|
+
}
|
|
6062
6391
|
static cropRaster(src, srcWidth, _srcHeight, dstWidth, dstHeight) {
|
|
6063
6392
|
const out = new Float32Array(dstWidth * dstHeight);
|
|
6064
6393
|
for (let y = 0; y < dstHeight; y++) {
|
|
@@ -6536,19 +6865,37 @@ class CogTiles {
|
|
|
6536
6865
|
async getTile(x, y, z, bounds, meshMaxError) {
|
|
6537
6866
|
let requiredSize = this.tileSize; // Default 256 for image/bitmap
|
|
6538
6867
|
if (this.options.type === 'terrain') {
|
|
6539
|
-
const isKernel = this.options.useSlope || this.options.useHillshade;
|
|
6868
|
+
const isKernel = this.options.useSlope || this.options.useHillshade || this.options.useSwissRelief;
|
|
6540
6869
|
requiredSize = this.tileSize + (isKernel ? 2 : 1); // 258 for kernel (3×3 border), 257 for normal stitching
|
|
6541
6870
|
}
|
|
6871
|
+
else if (this.options.type === 'image' && this.options.useReliefGlaze) {
|
|
6872
|
+
// Bitmap layer with relief glaze mode needs kernel padding for slope/hillshade computation
|
|
6873
|
+
requiredSize = this.tileSize + 2; // 258 for kernel
|
|
6874
|
+
}
|
|
6542
6875
|
const tileData = await this.getTileFromImage(x, y, z, requiredSize);
|
|
6543
6876
|
// Compute true ground cell size in meters from tile indices.
|
|
6544
6877
|
// Tile y in slippy-map convention → center latitude → Web Mercator distortion correction.
|
|
6545
6878
|
const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 0.5) / Math.pow(2, z))));
|
|
6546
6879
|
const tileWidthMeters = (EARTH_CIRCUMFERENCE / Math.pow(2, z)) * Math.cos(latRad);
|
|
6547
6880
|
const cellSizeMeters = tileWidthMeters / this.tileSize;
|
|
6881
|
+
let rasters = [tileData[0]];
|
|
6882
|
+
let tileWidth = requiredSize;
|
|
6883
|
+
let tileHeight = requiredSize;
|
|
6884
|
+
// Relief glaze computation for bitmap layers
|
|
6885
|
+
// Note: For multi-band support (band selection via useChannelIndex), see issue #98
|
|
6886
|
+
if (this.options.type === 'image' && this.options.useReliefGlaze) {
|
|
6887
|
+
const elevation = tileData[0];
|
|
6888
|
+
// Pass full 258×258 padded elevation directly — KernelGenerator expects IN=258 and outputs 256×256
|
|
6889
|
+
const reliefMask = ReliefCompositor.composeSwissRelief(elevation, this.options, cellSizeMeters, this.tileSize, this.tileSize);
|
|
6890
|
+
// For glaze-only mode, pass ONLY the 256×256 relief mask
|
|
6891
|
+
rasters = [reliefMask];
|
|
6892
|
+
tileWidth = this.tileSize;
|
|
6893
|
+
tileHeight = this.tileSize;
|
|
6894
|
+
}
|
|
6548
6895
|
return this.geo.getMap({
|
|
6549
|
-
rasters
|
|
6550
|
-
width:
|
|
6551
|
-
height:
|
|
6896
|
+
rasters,
|
|
6897
|
+
width: tileWidth,
|
|
6898
|
+
height: tileHeight,
|
|
6552
6899
|
bounds,
|
|
6553
6900
|
cellSizeMeters,
|
|
6554
6901
|
}, this.options, meshMaxError);
|
|
@@ -6761,13 +7108,20 @@ class CogBitmapLayer extends core.CompositeLayer {
|
|
|
6761
7108
|
}
|
|
6762
7109
|
}
|
|
6763
7110
|
async getTiledBitmapData(tile) {
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
tileData.
|
|
7111
|
+
try {
|
|
7112
|
+
// TODO - pass signal to getTile
|
|
7113
|
+
// abort request if signal is aborted
|
|
7114
|
+
const tileData = await this.state.bitmapCogTiles.getTile(tile.index.x, tile.index.y, tile.index.z);
|
|
7115
|
+
if (tileData && !this.props.pickable) {
|
|
7116
|
+
tileData.raw = null;
|
|
7117
|
+
}
|
|
7118
|
+
return tileData;
|
|
7119
|
+
}
|
|
7120
|
+
catch (error) {
|
|
7121
|
+
// Log the error and rethrow so TileLayer can surface the failure via onTileError
|
|
7122
|
+
core.log.warn(`Failed to load bitmap tile at ${tile.index.z}/${tile.index.x}/${tile.index.y}:`, error)();
|
|
7123
|
+
throw error;
|
|
6769
7124
|
}
|
|
6770
|
-
return tileData;
|
|
6771
7125
|
}
|
|
6772
7126
|
renderSubLayers(props) {
|
|
6773
7127
|
const SubLayerClass = this.getSubLayerClass('image', layers.BitmapLayer);
|
|
@@ -7010,15 +7364,28 @@ class CogTerrainLayer extends core.CompositeLayer {
|
|
|
7010
7364
|
}
|
|
7011
7365
|
renderSubLayers(props) {
|
|
7012
7366
|
const SubLayerClass = this.getSubLayerClass('mesh', meshLayers.SimpleMeshLayer);
|
|
7013
|
-
const { color, wireframe,
|
|
7367
|
+
const { color, wireframe, terrainOptions } = this.props;
|
|
7014
7368
|
const { data } = props;
|
|
7015
7369
|
if (!data) {
|
|
7016
7370
|
return null;
|
|
7017
7371
|
}
|
|
7018
|
-
// const [mesh, texture] = data;
|
|
7019
7372
|
const [meshResult] = data;
|
|
7020
7373
|
const tileTexture = (!this.props.disableTexture && meshResult?.texture) ? meshResult.texture : null;
|
|
7374
|
+
const isSwiss = terrainOptions?.useSwissRelief;
|
|
7375
|
+
const disableLighting = terrainOptions?.disableLighting;
|
|
7376
|
+
const shouldDisableLighting = isSwiss || disableLighting;
|
|
7377
|
+
const lightingProps = shouldDisableLighting ? {
|
|
7378
|
+
material: {
|
|
7379
|
+
ambient: 1.0,
|
|
7380
|
+
diffuse: 0.0,
|
|
7381
|
+
shininess: 0.0,
|
|
7382
|
+
specularColor: [0, 0, 0]
|
|
7383
|
+
}
|
|
7384
|
+
} : {
|
|
7385
|
+
material: this.props.material
|
|
7386
|
+
};
|
|
7021
7387
|
return new SubLayerClass({ ...props, tileSize: props.tileSize }, {
|
|
7388
|
+
...lightingProps,
|
|
7022
7389
|
data: DUMMY_DATA,
|
|
7023
7390
|
mesh: meshResult?.map,
|
|
7024
7391
|
texture: tileTexture,
|
|
@@ -7028,7 +7395,6 @@ class CogTerrainLayer extends core.CompositeLayer {
|
|
|
7028
7395
|
// getPosition: (d) => [0, 0, 0],
|
|
7029
7396
|
getColor: tileTexture ? [255, 255, 255] : color,
|
|
7030
7397
|
wireframe,
|
|
7031
|
-
material,
|
|
7032
7398
|
});
|
|
7033
7399
|
}
|
|
7034
7400
|
// Update zRange of viewport
|
|
@@ -7075,13 +7441,14 @@ class CogTerrainLayer extends core.CompositeLayer {
|
|
|
7075
7441
|
updateTriggers: {
|
|
7076
7442
|
getTileData: {
|
|
7077
7443
|
elevationData: urlTemplateToUpdateTrigger(elevationData),
|
|
7078
|
-
// texture: urlTemplateToUpdateTrigger(texture),
|
|
7079
7444
|
meshMaxError,
|
|
7080
7445
|
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
7446
|
terrainCogTiles: this.state.terrainCogTiles,
|
|
7084
7447
|
},
|
|
7448
|
+
renderSubLayers: {
|
|
7449
|
+
disableTexture: this.props.disableTexture,
|
|
7450
|
+
terrainOptions: this.props.terrainOptions,
|
|
7451
|
+
},
|
|
7085
7452
|
},
|
|
7086
7453
|
onViewportLoad: this.onViewportLoad.bind(this),
|
|
7087
7454
|
zRange: this.state.zRange || null,
|