@gisatcz/deckgl-geolib 2.5.0-dev.5 → 2.5.0
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 +508 -108
- 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 +508 -108
- 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 +41 -7
- package/dist/esm/types/core/lib/KernelGenerator.d.ts +0 -8
- package/dist/esm/types/core/lib/numberUtils.d.ts +2 -0
- package/dist/esm/types/core/types.d.ts +13 -0
- package/dist/esm/types/layers/CogBitmapLayer.d.ts +6 -6
- package/dist/esm/types/layers/CogTerrainLayer.d.ts +9 -9
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -4928,6 +4928,9 @@ const DefaultGeoImageOptions = {
|
|
|
4928
4928
|
useDataForOpacity: false,
|
|
4929
4929
|
useSingleColor: false,
|
|
4930
4930
|
blurredTexture: true,
|
|
4931
|
+
skipTexture: false,
|
|
4932
|
+
/** Strategy for noData detection: 'full' | 'border+center' */
|
|
4933
|
+
noDataCheck: 'full',
|
|
4931
4934
|
clipLow: null,
|
|
4932
4935
|
clipHigh: null,
|
|
4933
4936
|
color: [255, 0, 255, 255],
|
|
@@ -5655,6 +5658,19 @@ function scale(num, inMin, inMax, outMin, outMax) {
|
|
|
5655
5658
|
return ((num - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
|
|
5656
5659
|
}
|
|
5657
5660
|
|
|
5661
|
+
function toF32(n) {
|
|
5662
|
+
return Math.fround(n);
|
|
5663
|
+
}
|
|
5664
|
+
function isF32NoData(val, noData) {
|
|
5665
|
+
if (noData === undefined || noData === null)
|
|
5666
|
+
return false;
|
|
5667
|
+
const a = toF32(val);
|
|
5668
|
+
const b = toF32(noData);
|
|
5669
|
+
if (Number.isNaN(b))
|
|
5670
|
+
return Number.isNaN(a);
|
|
5671
|
+
return a === b;
|
|
5672
|
+
}
|
|
5673
|
+
|
|
5658
5674
|
class BitmapGenerator {
|
|
5659
5675
|
/**
|
|
5660
5676
|
* Cache for Swiss relief color LUTs to avoid regenerating on every tile.
|
|
@@ -5828,9 +5844,7 @@ class BitmapGenerator {
|
|
|
5828
5844
|
for (let i = 0, sampleIndex = (options.useChannelIndex ?? 0); i < arrayLength; i += 4, sampleIndex += samplesPerPixel) {
|
|
5829
5845
|
const elevationVal = primaryBuffer[sampleIndex];
|
|
5830
5846
|
// NaN-aware noData check for Swiss relief
|
|
5831
|
-
const isNoData =
|
|
5832
|
-
? Number.isNaN(elevationVal)
|
|
5833
|
-
: elevationVal === options.noDataValue);
|
|
5847
|
+
const isNoData = isF32NoData(elevationVal, options.noDataValue);
|
|
5834
5848
|
if (Number.isNaN(elevationVal) || isNoData) {
|
|
5835
5849
|
colorsArray.set(options.nullColor, i);
|
|
5836
5850
|
continue;
|
|
@@ -6038,7 +6052,7 @@ class BitmapGenerator {
|
|
|
6038
6052
|
return colorsArray;
|
|
6039
6053
|
}
|
|
6040
6054
|
static isInvalid(val, options) {
|
|
6041
|
-
return Number.isNaN(val) || (
|
|
6055
|
+
return Number.isNaN(val) || isF32NoData(val, options.noDataValue);
|
|
6042
6056
|
}
|
|
6043
6057
|
static getInvalidColor(val, options) {
|
|
6044
6058
|
return options.nullColor;
|
|
@@ -6060,7 +6074,7 @@ class BitmapGenerator {
|
|
|
6060
6074
|
return (!Array.isArray(color) || color.length !== 4) ? [...chroma(color).rgb(), alpha] : color;
|
|
6061
6075
|
}
|
|
6062
6076
|
static hasPixelsNoData(pixels, noData) {
|
|
6063
|
-
return noData !== undefined && pixels.every(p => p
|
|
6077
|
+
return noData !== undefined && pixels.every(p => isF32NoData(p, noData));
|
|
6064
6078
|
}
|
|
6065
6079
|
}
|
|
6066
6080
|
|
|
@@ -6104,13 +6118,12 @@ class KernelGenerator {
|
|
|
6104
6118
|
const cellSizeFactor = 1 / (8 * cellSize);
|
|
6105
6119
|
// Cache constant for radians to degrees conversion
|
|
6106
6120
|
const RAD_TO_DEG = 180 / Math.PI;
|
|
6107
|
-
const isNaNNoData = noDataValue !== undefined && Number.isNaN(noDataValue);
|
|
6108
6121
|
for (let r = 0; r < OUT; r++) {
|
|
6109
6122
|
for (let c = 0; c < OUT; c++) {
|
|
6110
6123
|
// 3×3 neighborhood in the 258×258 input, centered at (r+1, c+1)
|
|
6111
6124
|
const base = r * IN + c;
|
|
6112
6125
|
const z5 = src[base + IN + 1]; // center pixel
|
|
6113
|
-
const isNoData =
|
|
6126
|
+
const isNoData = isF32NoData(z5, noDataValue);
|
|
6114
6127
|
if (isNoData) {
|
|
6115
6128
|
out[r * OUT + c] = NaN;
|
|
6116
6129
|
continue;
|
|
@@ -6152,12 +6165,11 @@ class KernelGenerator {
|
|
|
6152
6165
|
const azimuthRad = azimuthMath * (Math.PI / 180);
|
|
6153
6166
|
// Hoist division out of loop: multiplication is ~2-3x faster than division
|
|
6154
6167
|
const cellSizeFactor = 1 / (8 * cellSize);
|
|
6155
|
-
const isNaNNoData = noDataValue !== undefined && Number.isNaN(noDataValue);
|
|
6156
6168
|
for (let r = 0; r < OUT; r++) {
|
|
6157
6169
|
for (let c = 0; c < OUT; c++) {
|
|
6158
6170
|
const base = r * IN + c;
|
|
6159
6171
|
const z5 = src[base + IN + 1]; // center pixel
|
|
6160
|
-
const isNoData =
|
|
6172
|
+
const isNoData = isF32NoData(z5, noDataValue);
|
|
6161
6173
|
if (isNoData) {
|
|
6162
6174
|
out[r * OUT + c] = NaN;
|
|
6163
6175
|
continue;
|
|
@@ -6190,7 +6202,6 @@ class KernelGenerator {
|
|
|
6190
6202
|
const out = new Float32Array(OUT * OUT);
|
|
6191
6203
|
// Hoist division out of loop: multiplication is ~2-3x faster than division
|
|
6192
6204
|
const cellSizeFactor = 1 / (8 * cellSize);
|
|
6193
|
-
const isNaNNoData = noDataValue !== undefined && Number.isNaN(noDataValue);
|
|
6194
6205
|
// Setup 3 light sources: NW (Main), W (Fill), N (Fill)
|
|
6195
6206
|
const lights = [
|
|
6196
6207
|
{ az: 315, alt: 45, weight: 0.60 }, // Primary NW
|
|
@@ -6212,7 +6223,7 @@ class KernelGenerator {
|
|
|
6212
6223
|
for (let c = 0; c < OUT; c++) {
|
|
6213
6224
|
const base = r * IN + c;
|
|
6214
6225
|
const z5 = src[base + IN + 1];
|
|
6215
|
-
const isNoData =
|
|
6226
|
+
const isNoData = isF32NoData(z5, noDataValue);
|
|
6216
6227
|
if (isNoData) {
|
|
6217
6228
|
out[r * OUT + c] = NaN;
|
|
6218
6229
|
continue;
|
|
@@ -6380,6 +6391,7 @@ class TerrainGenerator {
|
|
|
6380
6391
|
height: gridHeight,
|
|
6381
6392
|
};
|
|
6382
6393
|
// 3. Kernel path: compute slope or hillshade, store as rawDerived, generate texture
|
|
6394
|
+
const shouldSkipTexture = !!options.skipTexture;
|
|
6383
6395
|
if (isKernel && options.useSwissRelief) {
|
|
6384
6396
|
const cellSize = input.cellSizeMeters ?? ((input.bounds[2] - input.bounds[0]) / 256);
|
|
6385
6397
|
// Build a separate raster for kernel computation that preserves noData samples.
|
|
@@ -6387,7 +6399,7 @@ class TerrainGenerator {
|
|
|
6387
6399
|
// Compose Swiss relief using ReliefCompositor
|
|
6388
6400
|
const swissReliefResult = ReliefCompositor.composeSwissRelief(kernelTerrain, options, cellSize, 256, 256);
|
|
6389
6401
|
tileResult.rawDerived = swissReliefResult;
|
|
6390
|
-
if (this.hasVisualizationOptions(options)) {
|
|
6402
|
+
if (!shouldSkipTexture && this.hasVisualizationOptions(options)) {
|
|
6391
6403
|
const cropped = this.cropRaster(meshTerrain, gridWidth, gridHeight, 256, 256);
|
|
6392
6404
|
const bitmapResult = await BitmapGenerator.generate({ width: 256, height: 256, rasters: [cropped, swissReliefResult] }, { ...options, type: 'image' });
|
|
6393
6405
|
tileResult.texture = bitmapResult.map;
|
|
@@ -6412,15 +6424,38 @@ class TerrainGenerator {
|
|
|
6412
6424
|
kernelOutput = KernelGenerator.calculateHillshade(kernelTerrain, cellSize, options.hillshadeAzimuth ?? 315, options.hillshadeAltitude ?? 45, zFactor, options.noDataValue);
|
|
6413
6425
|
}
|
|
6414
6426
|
tileResult.rawDerived = kernelOutput;
|
|
6415
|
-
if (this.hasVisualizationOptions(options)) {
|
|
6427
|
+
if (!shouldSkipTexture && this.hasVisualizationOptions(options)) {
|
|
6416
6428
|
const bitmapResult = await BitmapGenerator.generate({ width: 256, height: 256, rasters: [kernelOutput] }, { ...options, type: 'image' });
|
|
6417
6429
|
tileResult.texture = bitmapResult.map;
|
|
6418
6430
|
}
|
|
6419
6431
|
}
|
|
6420
|
-
else if (this.hasVisualizationOptions(options)) {
|
|
6421
|
-
// 4. Non-kernel path:
|
|
6422
|
-
|
|
6423
|
-
|
|
6432
|
+
else if (!shouldSkipTexture && this.hasVisualizationOptions(options)) {
|
|
6433
|
+
// 4. Non-kernel path: build texture raster from the ORIGINAL source data, preserving
|
|
6434
|
+
// noData sentinels so BitmapGenerator renders those pixels as transparent (via nullColor).
|
|
6435
|
+
// meshTerrain substitutes noData with terrainMinValue for mesh stability — we intentionally
|
|
6436
|
+
// avoid using it for texture generation to prevent fill values being coloured.
|
|
6437
|
+
const multiplier = options.multiplier ?? 1;
|
|
6438
|
+
const noDataValue = options.noDataValue;
|
|
6439
|
+
const srcRaster = input.rasters[0];
|
|
6440
|
+
const srcWidth = input.width;
|
|
6441
|
+
const srcHeight = input.height;
|
|
6442
|
+
// Determine samplesPerPixel for interleaved buffers (fallback to 1)
|
|
6443
|
+
const samplesPerPixel = Math.max(1, Math.round((srcRaster.length) / (srcWidth * srcHeight)));
|
|
6444
|
+
const channelIndex = options.useChannelIndex ?? (options.useChannel != null ? options.useChannel - 1 : 0);
|
|
6445
|
+
const textureRaster = new Float32Array(256 * 256);
|
|
6446
|
+
for (let ty = 0; ty < 256; ty++) {
|
|
6447
|
+
for (let tx = 0; tx < 256; tx++) {
|
|
6448
|
+
// Guard: if srcWidth < 256 (shouldn't happen), clamp indices
|
|
6449
|
+
const srcX = Math.min(tx, srcWidth - 1);
|
|
6450
|
+
const srcY = Math.min(ty, srcHeight - 1);
|
|
6451
|
+
const srcIdx = (srcY * srcWidth + srcX) * samplesPerPixel + channelIndex;
|
|
6452
|
+
const v = srcRaster[srcIdx];
|
|
6453
|
+
const isNoData = isF32NoData(v, noDataValue);
|
|
6454
|
+
textureRaster[ty * 256 + tx] = isNoData ? noDataValue * multiplier : v * multiplier;
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
const bitmapOptions = { ...options, type: 'image', useChannelIndex: 0, numOfChannels: 1, noDataValue: noDataValue !== undefined ? noDataValue * multiplier : undefined };
|
|
6458
|
+
const bitmapResult = await BitmapGenerator.generate({ width: 256, height: 256, rasters: [textureRaster] }, bitmapOptions);
|
|
6424
6459
|
tileResult.texture = bitmapResult.map;
|
|
6425
6460
|
}
|
|
6426
6461
|
return tileResult;
|
|
@@ -6454,12 +6489,9 @@ class TerrainGenerator {
|
|
|
6454
6489
|
noDataValue !== null &&
|
|
6455
6490
|
sourceRaster &&
|
|
6456
6491
|
sourceRaster.length === terrain.length) {
|
|
6457
|
-
const preserveNaNNoData = Number.isNaN(noDataValue);
|
|
6458
6492
|
for (let i = 0; i < terrain.length; i++) {
|
|
6459
6493
|
const sourceValue = sourceRaster[i];
|
|
6460
|
-
const isNoData =
|
|
6461
|
-
? Number.isNaN(sourceValue)
|
|
6462
|
-
: sourceValue === noDataValue;
|
|
6494
|
+
const isNoData = isF32NoData(sourceValue, noDataValue);
|
|
6463
6495
|
kernelTerrain[i] = isNoData ? noDataValue : terrain[i];
|
|
6464
6496
|
}
|
|
6465
6497
|
}
|
|
@@ -6505,9 +6537,7 @@ class TerrainGenerator {
|
|
|
6505
6537
|
for (let y = 0; y < height; y++) {
|
|
6506
6538
|
for (let x = 0; x < width; x++) {
|
|
6507
6539
|
const multiplier = options.multiplier ?? 1;
|
|
6508
|
-
let elevationValue = (options.noDataValue
|
|
6509
|
-
options.noDataValue !== null &&
|
|
6510
|
-
channel[pixel] === options.noDataValue)
|
|
6540
|
+
let elevationValue = isF32NoData(channel[pixel], options.noDataValue)
|
|
6511
6541
|
? fallbackValue
|
|
6512
6542
|
: channel[pixel] * multiplier;
|
|
6513
6543
|
// Validate that the elevation value is within the valid range for Float32.
|
|
@@ -6640,15 +6670,16 @@ class GeoImage {
|
|
|
6640
6670
|
else if (mergedOptions.type === 'terrain') {
|
|
6641
6671
|
// Terrain with no explicit coloring mode.
|
|
6642
6672
|
const hasKernelMode = userOptions.useSwissRelief || userOptions.useSlope || userOptions.useHillshade;
|
|
6643
|
-
|
|
6644
|
-
|
|
6673
|
+
// If skipTexture is requested, force single-color mesh rendering regardless of kernel modes.
|
|
6674
|
+
if (!hasKernelMode || mergedOptions.skipTexture) {
|
|
6675
|
+
// No kernel mode OR skipTexture: enable useSingleColor with terrainColor as the default.
|
|
6645
6676
|
// This renders the mesh in the documented colour without a data-driven texture.
|
|
6646
6677
|
resolved.useHeatMap = false;
|
|
6647
6678
|
resolved.useSingleColor = true;
|
|
6648
6679
|
resolved.color = mergedOptions.terrainColor;
|
|
6649
6680
|
}
|
|
6650
6681
|
// When a kernel mode is present without an explicit coloring mode, keep
|
|
6651
|
-
// useHeatMap: true from defaults so the kernel output is colourised.
|
|
6682
|
+
// useHeatMap: true from defaults so the kernel output is colourised (unless skipTexture).
|
|
6652
6683
|
}
|
|
6653
6684
|
// For 'image' with no explicit coloring mode: keep useHeatMap: true from DefaultGeoImageOptions.
|
|
6654
6685
|
return resolved;
|
|
@@ -6712,35 +6743,44 @@ class CogTiles {
|
|
|
6712
6743
|
cog;
|
|
6713
6744
|
cogZoomLookup = [];
|
|
6714
6745
|
cogResolutionLookup = [];
|
|
6746
|
+
cogMeshMaxErrorLookup = [];
|
|
6715
6747
|
cogOrigin = [0, 0];
|
|
6716
6748
|
zoomRange = [0, 0];
|
|
6717
6749
|
tileSize = 256;
|
|
6718
6750
|
bounds = [0, 0, 0, 0];
|
|
6719
6751
|
geo = new GeoImage();
|
|
6720
6752
|
options;
|
|
6721
|
-
//
|
|
6722
|
-
//
|
|
6753
|
+
// TileResult cache — keyed by z/x/y/meshMaxError.
|
|
6754
|
+
// Each entry owns an AbortController and a caller reference count.
|
|
6755
|
+
// The pipeline is aborted only when ALL callers have cancelled (ref-count → 0),
|
|
6756
|
+
// so concurrent deck.gl requests for the same tile share one in-flight fetch/tessellation
|
|
6757
|
+
// and individual tile cancellations (e.g. from panning) do not poison other callers.
|
|
6758
|
+
// Once the promise settles (resolved or rejected), controller/callerCount are irrelevant;
|
|
6759
|
+
// future cache hits just await the already-resolved promise directly.
|
|
6760
|
+
tileResultCache = new Map();
|
|
6761
|
+
tileResultCacheMaxSize = 32;
|
|
6762
|
+
getTileResultCacheKey(x, y, z, meshMaxError, skipTexture) {
|
|
6763
|
+
return `${z}/${x}/${y}/${meshMaxError}/${skipTexture ? '1' : '0'}`;
|
|
6764
|
+
}
|
|
6765
|
+
/** Clears the TileResult cache. Call when the COG URL or meshMaxError changes. */
|
|
6766
|
+
clearTileResultCache() {
|
|
6767
|
+
// Abort any in-flight pipelines so their network requests are cancelled
|
|
6768
|
+
for (const entry of this.tileResultCache.values()) {
|
|
6769
|
+
if (!entry.settled)
|
|
6770
|
+
entry.controller.abort();
|
|
6771
|
+
}
|
|
6772
|
+
this.tileResultCache.clear();
|
|
6773
|
+
}
|
|
6774
|
+
// Raw raster cache for ordinary bitmap layers — saves network fetch + decompression on revisit.
|
|
6775
|
+
// BitmapGenerator is cheap to re-run from cached raster; no need to hold ImageBitmaps in memory.
|
|
6723
6776
|
rasterCache = new Map();
|
|
6724
|
-
|
|
6725
|
-
//
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
this.rasterCache.set(key, value);
|
|
6732
|
-
}
|
|
6733
|
-
return value;
|
|
6734
|
-
}
|
|
6735
|
-
setCachedRaster(key, value) {
|
|
6736
|
-
this.rasterCache.set(key, value);
|
|
6737
|
-
if (this.rasterCache.size > this.maxCacheSize) {
|
|
6738
|
-
// Evict oldest
|
|
6739
|
-
const oldestKey = this.rasterCache.keys().next().value;
|
|
6740
|
-
if (typeof oldestKey === 'string') {
|
|
6741
|
-
this.rasterCache.delete(oldestKey);
|
|
6742
|
-
}
|
|
6743
|
-
}
|
|
6777
|
+
rasterCacheMaxSize = 64;
|
|
6778
|
+
// Relief mask cache for bitmap + glaze layers — saves network fetch + kernel convolution on revisit.
|
|
6779
|
+
// Stores the Float32Array output of composeSwissRelief; BitmapGenerator re-runs from it cheaply.
|
|
6780
|
+
reliefMaskCache = new Map();
|
|
6781
|
+
reliefMaskCacheMaxSize = 64;
|
|
6782
|
+
getTileCacheKey(x, y, z) {
|
|
6783
|
+
return `${z}/${x}/${y}`;
|
|
6744
6784
|
}
|
|
6745
6785
|
// Cache GeoTIFFImage Promises by index to prevent redundant HTTP requests from geotiff 3.0.4+ eager loading
|
|
6746
6786
|
// Stores Promises (not resolved values) so concurrent requests share the same getImage() call
|
|
@@ -6753,15 +6793,33 @@ class CogTiles {
|
|
|
6753
6793
|
this.options = { ...CogTilesGeoImageOptionsDefaults, ...options };
|
|
6754
6794
|
}
|
|
6755
6795
|
async initializeCog(url) {
|
|
6756
|
-
//
|
|
6757
|
-
|
|
6796
|
+
// Reuse existing initialization while it is in progress, or when the same URL
|
|
6797
|
+
// was already initialized on this instance.
|
|
6798
|
+
if (this.initializePromise && (!this.cog || this.lastInitializedUrl === url)) {
|
|
6758
6799
|
return this.initializePromise;
|
|
6759
|
-
|
|
6760
|
-
|
|
6800
|
+
}
|
|
6801
|
+
// Fully reset COG-derived state when the URL changes so the instance can be
|
|
6802
|
+
// safely reinitialized against a different source.
|
|
6803
|
+
if (this.lastInitializedUrl !== undefined && this.lastInitializedUrl !== url) {
|
|
6804
|
+
this.clearTileResultCache();
|
|
6761
6805
|
this.rasterCache.clear();
|
|
6806
|
+
this.reliefMaskCache.clear();
|
|
6807
|
+
this.imageCache.clear();
|
|
6808
|
+
this.cog = undefined;
|
|
6809
|
+
this.cogOrigin = [0, 0];
|
|
6810
|
+
this.cogZoomLookup = [];
|
|
6811
|
+
this.cogResolutionLookup = [];
|
|
6812
|
+
this.cogMeshMaxErrorLookup = [];
|
|
6813
|
+
this.tileSize = 256;
|
|
6814
|
+
this.zoomRange = [0, 0];
|
|
6815
|
+
this.bounds = [0, 0, 0, 0];
|
|
6816
|
+
this.initializePromise = undefined;
|
|
6817
|
+
this.lastInitializedUrl = undefined;
|
|
6818
|
+
}
|
|
6819
|
+
// If COG already loaded and URL matches, return the existing promise
|
|
6820
|
+
if (this.cog && this.lastInitializedUrl === url) {
|
|
6821
|
+
return this.initializePromise ?? Promise.resolve();
|
|
6762
6822
|
}
|
|
6763
|
-
if (this.cog)
|
|
6764
|
-
return;
|
|
6765
6823
|
this.initializePromise = (async () => {
|
|
6766
6824
|
try {
|
|
6767
6825
|
// fromUrl's type declaration only exposes RemoteSourceOptions, but the implementation
|
|
@@ -6781,6 +6839,10 @@ class CogTiles {
|
|
|
6781
6839
|
this.options.numOfChannels = fileDirectory.getValue('SamplesPerPixel');
|
|
6782
6840
|
this.options.planarConfig = fileDirectory.getValue('PlanarConfiguration');
|
|
6783
6841
|
[this.cogZoomLookup, this.cogResolutionLookup] = await this.buildCogZoomResolutionLookup(this.cog);
|
|
6842
|
+
// Only compute quantized meshMaxError lookup for terrain COGs
|
|
6843
|
+
if (this.options.type === 'terrain') {
|
|
6844
|
+
this.computeMeshMaxErrorLookup();
|
|
6845
|
+
}
|
|
6784
6846
|
this.tileSize = image.getTileWidth();
|
|
6785
6847
|
// 1. Validation: Ensure the image is tiled
|
|
6786
6848
|
if (!this.tileSize || !image.getTileHeight()) {
|
|
@@ -6833,8 +6895,39 @@ class CogTiles {
|
|
|
6833
6895
|
lat = (180 / Math.PI) * (2 * Math.atan(Math.exp((lat * Math.PI) / 180)) - Math.PI / 2);
|
|
6834
6896
|
return [lon, lat];
|
|
6835
6897
|
}
|
|
6836
|
-
|
|
6837
|
-
|
|
6898
|
+
/**
|
|
6899
|
+
* Calculates the error multiplier based on zoom level and COG zoom range.
|
|
6900
|
+
* - z >= maxZ: multiplier = 0.5 (fine meshes at high zoom, maximum precision)
|
|
6901
|
+
* - z <= minZ: multiplier = 3.0 (coarse meshes at low zoom, maximum performance)
|
|
6902
|
+
* - Otherwise: linear interpolation between 3.0 and 0.5
|
|
6903
|
+
*/
|
|
6904
|
+
getErrorMultiplierForZoom(z, minZ, maxZ) {
|
|
6905
|
+
if (z >= maxZ)
|
|
6906
|
+
return 0.5;
|
|
6907
|
+
if (z <= minZ)
|
|
6908
|
+
return 3.0;
|
|
6909
|
+
// Linear interpolation: 3.0 - ((z - minZ) / (maxZ - minZ)) * 2.5
|
|
6910
|
+
return 3.0 - ((z - minZ) / (maxZ - minZ)) * 2.5;
|
|
6911
|
+
}
|
|
6912
|
+
/**
|
|
6913
|
+
* Calculates dynamic meshMaxError for a given zoom level and resolution.
|
|
6914
|
+
* Formula: resolution * errorMultiplier, where multiplier scales from 3.0 (low zoom) to 0.5 (high zoom).
|
|
6915
|
+
* Results are clamped to 0.5–100 meters to prevent pathological tessellation:
|
|
6916
|
+
* - Min 0.5m ensures simplification always happens (no rounding to 0 with sub-meter pixels)
|
|
6917
|
+
* - Max 100m prevents excessive simplification at low zoom with low-resolution COGs
|
|
6918
|
+
*/
|
|
6919
|
+
calculateDynamicMeshMaxError(z, resolution, minZ, maxZ) {
|
|
6920
|
+
const multiplier = this.getErrorMultiplierForZoom(z, minZ, maxZ);
|
|
6921
|
+
const errorValue = resolution * multiplier;
|
|
6922
|
+
return Math.max(0.5, Math.min(100, errorValue));
|
|
6923
|
+
}
|
|
6924
|
+
/**
|
|
6925
|
+
* Gets the auto meshMaxError for a given overview index.
|
|
6926
|
+
* Returns undefined if auto lookup has not been computed.
|
|
6927
|
+
*/
|
|
6928
|
+
getMeshMaxErrorForImageIndex(imageIndex) {
|
|
6929
|
+
return this.cogMeshMaxErrorLookup[imageIndex];
|
|
6930
|
+
}
|
|
6838
6931
|
/**
|
|
6839
6932
|
* Builds lookup tables for zoom levels and estimated resolutions from a Cloud Optimized GeoTIFF (COG) object.
|
|
6840
6933
|
*
|
|
@@ -6875,6 +6968,20 @@ class CogTiles {
|
|
|
6875
6968
|
}
|
|
6876
6969
|
return [zoomLookup, resolutionLookup];
|
|
6877
6970
|
}
|
|
6971
|
+
/**
|
|
6972
|
+
* Computes dynamic meshMaxError values for each overview based on COG resolution and zoom level.
|
|
6973
|
+
* Called only for terrain COGs after buildCogZoomResolutionLookup() completes.
|
|
6974
|
+
* Each overview's meshMaxError is calculated as: resolution * zoom-based multiplier, rounded to nearest integer.
|
|
6975
|
+
* Multiplier ranges from 3.0 at minZ (coarse meshes) to 0.5 at maxZ (fine meshes).
|
|
6976
|
+
*/
|
|
6977
|
+
computeMeshMaxErrorLookup() {
|
|
6978
|
+
const minZ = this.cogZoomLookup[this.cogZoomLookup.length - 1];
|
|
6979
|
+
const maxZ = this.cogZoomLookup[0];
|
|
6980
|
+
this.cogMeshMaxErrorLookup = this.cogResolutionLookup.map((resolution, idx) => {
|
|
6981
|
+
const zoom = this.cogZoomLookup[idx];
|
|
6982
|
+
return this.calculateDynamicMeshMaxError(zoom, resolution, minZ, maxZ);
|
|
6983
|
+
});
|
|
6984
|
+
}
|
|
6878
6985
|
/**
|
|
6879
6986
|
* Determines the appropriate image index from the Cloud Optimized GeoTIFF (COG)
|
|
6880
6987
|
* that best matches a given zoom level.
|
|
@@ -6923,12 +7030,6 @@ class CogTiles {
|
|
|
6923
7030
|
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
6924
7031
|
}
|
|
6925
7032
|
const localSignal = controller.signal;
|
|
6926
|
-
// Check if raster is already cached
|
|
6927
|
-
const cacheKey = `${zoom}/${tileX}/${tileY}/${fetchSize ?? this.tileSize}`;
|
|
6928
|
-
const cachedRaster = this.getCachedRaster(cacheKey);
|
|
6929
|
-
if (cachedRaster) {
|
|
6930
|
-
return [cachedRaster];
|
|
6931
|
-
}
|
|
6932
7033
|
try {
|
|
6933
7034
|
const imageIndex = this.getImageIndexForZoomLevel(zoom);
|
|
6934
7035
|
// Cache Promises to share in-flight requests across concurrent tiles at the same overview
|
|
@@ -7027,28 +7128,42 @@ class CogTiles {
|
|
|
7027
7128
|
validImageData[i * numChannels + band] = tileBuffer[i];
|
|
7028
7129
|
}
|
|
7029
7130
|
}
|
|
7030
|
-
// Mark raster as cached after successful fetch
|
|
7031
|
-
this.setCachedRaster(cacheKey, validImageData); // for partial overlap
|
|
7032
7131
|
return [validImageData];
|
|
7033
7132
|
}
|
|
7034
7133
|
// Case B: Perfect Match (Optimization)
|
|
7035
7134
|
// If the read window is exactly 256x256 and aligned, we can read directly interleaved.
|
|
7036
7135
|
const tileData = await targetImage.readRasters({ window, interleave: true, signal: localSignal });
|
|
7037
|
-
// Mark raster as cached after successful fetch
|
|
7038
|
-
this.setCachedRaster(cacheKey, tileData); // for perfect match
|
|
7039
7136
|
return [tileData];
|
|
7040
7137
|
}
|
|
7041
7138
|
catch (error) {
|
|
7042
|
-
// If
|
|
7043
|
-
|
|
7044
|
-
|
|
7139
|
+
// If it's a single-error AggregateError, unwrap to the inner error for clearer diagnostics
|
|
7140
|
+
if (error instanceof AggregateError && error.errors.length === 1) {
|
|
7141
|
+
const innerError = error.errors[0];
|
|
7142
|
+
// Check if the unwrapped error is an abort — if so, throw it as AbortError
|
|
7143
|
+
if (innerError instanceof DOMException && innerError.name === 'AbortError') {
|
|
7144
|
+
throw innerError;
|
|
7145
|
+
}
|
|
7146
|
+
if (innerError instanceof Error && innerError.message === 'Request was aborted') {
|
|
7147
|
+
throw new DOMException('Tile request aborted', 'AbortError');
|
|
7148
|
+
}
|
|
7149
|
+
// Unwrap single error for better diagnostics (throw the real error, not the wrapper)
|
|
7150
|
+
throw innerError;
|
|
7151
|
+
}
|
|
7152
|
+
// Handle regular abort cases
|
|
7045
7153
|
const isAbortRelated = localSignal.aborted
|
|
7046
|
-
|| (error instanceof AggregateError && error.errors?.some((e) => e?.name === 'AbortError' || e?.message?.includes('aborted') || e?.message?.includes('abort')))
|
|
7047
7154
|
|| (error instanceof DOMException && error.name === 'AbortError')
|
|
7048
7155
|
|| (error instanceof Error && error.message === 'Request was aborted');
|
|
7049
7156
|
if (isAbortRelated) {
|
|
7050
7157
|
throw new DOMException('Tile request aborted', 'AbortError');
|
|
7051
7158
|
}
|
|
7159
|
+
// For multi-error AggregateError, check if ANY error is abort-related
|
|
7160
|
+
if (error instanceof AggregateError) {
|
|
7161
|
+
const hasAbort = error.errors.some((e) => (e instanceof DOMException && e.name === 'AbortError')
|
|
7162
|
+
|| (e instanceof Error && e.message === 'Request was aborted'));
|
|
7163
|
+
if (hasAbort) {
|
|
7164
|
+
throw new DOMException('Tile request aborted', 'AbortError');
|
|
7165
|
+
}
|
|
7166
|
+
}
|
|
7052
7167
|
throw error;
|
|
7053
7168
|
}
|
|
7054
7169
|
}
|
|
@@ -7066,44 +7181,285 @@ class CogTiles {
|
|
|
7066
7181
|
}
|
|
7067
7182
|
return tileData;
|
|
7068
7183
|
}
|
|
7069
|
-
async getTile(x, y, z, bounds, meshMaxError, signal) {
|
|
7070
|
-
|
|
7071
|
-
if (this.options.type === 'terrain') {
|
|
7072
|
-
const isKernel = this.options.useSlope || this.options.useHillshade || this.options.useSwissRelief;
|
|
7073
|
-
requiredSize = this.tileSize + (isKernel ? 2 : 1); // 258 for kernel (3×3 border), 257 for normal stitching
|
|
7074
|
-
}
|
|
7075
|
-
else if (this.options.type === 'image' && this.options.useReliefGlaze) {
|
|
7076
|
-
// Bitmap layer with relief glaze mode needs kernel padding for slope/hillshade computation
|
|
7077
|
-
requiredSize = this.tileSize + 2; // 258 for kernel
|
|
7078
|
-
}
|
|
7079
|
-
const tileData = await this.getTileFromImage(x, y, z, requiredSize, signal);
|
|
7080
|
-
// Compute true ground cell size in meters from tile indices.
|
|
7081
|
-
// Tile y in slippy-map convention → center latitude → Web Mercator distortion correction.
|
|
7184
|
+
async getTile(x, y, z, bounds, meshMaxError, signal, skipTexture) {
|
|
7185
|
+
// cellSizeMeters is derived purely from tile coordinates — compute once for all paths
|
|
7082
7186
|
const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 0.5) / Math.pow(2, z))));
|
|
7083
7187
|
const tileWidthMeters = (EARTH_CIRCUMFERENCE / Math.pow(2, z)) * Math.cos(latRad);
|
|
7084
7188
|
const cellSizeMeters = tileWidthMeters / this.tileSize;
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
const
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
//
|
|
7100
|
-
if (
|
|
7101
|
-
|
|
7189
|
+
const isTerrain = this.options.type === 'terrain';
|
|
7190
|
+
const isGlaze = this.options.type === 'image' && this.options.useReliefGlaze;
|
|
7191
|
+
// Resolve meshMaxError: if not provided or 0, use auto quantized value; otherwise use explicit value
|
|
7192
|
+
let resolvedMeshMaxError = meshMaxError;
|
|
7193
|
+
if (isTerrain && (!meshMaxError || meshMaxError === 0)) {
|
|
7194
|
+
const imageIndex = this.getImageIndexForZoomLevel(z);
|
|
7195
|
+
const autoMeshMaxError = this.getMeshMaxErrorForImageIndex(imageIndex);
|
|
7196
|
+
resolvedMeshMaxError = autoMeshMaxError ?? 4.0;
|
|
7197
|
+
}
|
|
7198
|
+
else {
|
|
7199
|
+
resolvedMeshMaxError = meshMaxError ?? 4.0;
|
|
7200
|
+
}
|
|
7201
|
+
// ── PATH A: Terrain ──────────────────────────────────────────────────────────
|
|
7202
|
+
// Full TileResult (mesh + raw + texture) cached with ref-counted abort so that
|
|
7203
|
+
// panning cancels in-flight fetches only when ALL callers have cancelled.
|
|
7204
|
+
if (isTerrain) {
|
|
7205
|
+
const skipTextureFlag = skipTexture ?? this.options.skipTexture ?? false;
|
|
7206
|
+
const cacheKey = this.getTileResultCacheKey(x, y, z, resolvedMeshMaxError, skipTextureFlag);
|
|
7207
|
+
const existing = this.tileResultCache.get(cacheKey);
|
|
7208
|
+
if (existing) {
|
|
7209
|
+
// LRU touch — move the key to the end of the Map to mark as recently used
|
|
7210
|
+
this.tileResultCache.delete(cacheKey);
|
|
7211
|
+
this.tileResultCache.set(cacheKey, existing);
|
|
7212
|
+
}
|
|
7213
|
+
if (existing) {
|
|
7214
|
+
if (existing.settled) {
|
|
7215
|
+
if (signal?.aborted)
|
|
7216
|
+
return null;
|
|
7217
|
+
return existing.promise;
|
|
7218
|
+
}
|
|
7219
|
+
existing.callerCount += 1;
|
|
7220
|
+
if (signal && !signal.aborted) {
|
|
7221
|
+
signal.addEventListener('abort', () => {
|
|
7222
|
+
existing.callerCount -= 1;
|
|
7223
|
+
if (existing.callerCount <= 0 && !existing.settled) {
|
|
7224
|
+
existing.controller.abort();
|
|
7225
|
+
}
|
|
7226
|
+
}, { once: true });
|
|
7227
|
+
}
|
|
7228
|
+
const result = await existing.promise;
|
|
7229
|
+
if (signal?.aborted)
|
|
7230
|
+
return null;
|
|
7231
|
+
return result;
|
|
7232
|
+
}
|
|
7233
|
+
const controller = new AbortController();
|
|
7234
|
+
const pipeline = (async () => {
|
|
7235
|
+
const isKernel = this.options.useSlope || this.options.useHillshade || this.options.useSwissRelief;
|
|
7236
|
+
const requiredSize = this.tileSize + (isKernel ? 2 : 1);
|
|
7237
|
+
const tileData = await this.getTileFromImage(x, y, z, requiredSize, controller.signal);
|
|
7238
|
+
// === Step F: detect all-noData tiles before tessellation ===
|
|
7239
|
+
const raster = tileData[0];
|
|
7240
|
+
const noData = this.options.noDataValue;
|
|
7241
|
+
if (noData !== undefined && raster) {
|
|
7242
|
+
const numChannels = this.options.numOfChannels || 1;
|
|
7243
|
+
let useChannelIndex = this.options.useChannelIndex ?? (this.options.useChannel ? (this.options.useChannel - 1) : 0);
|
|
7244
|
+
if (useChannelIndex == null)
|
|
7245
|
+
useChannelIndex = 0;
|
|
7246
|
+
const checkStrategy = this.options.noDataCheck ?? 'full';
|
|
7247
|
+
const width = requiredSize;
|
|
7248
|
+
const height = requiredSize;
|
|
7249
|
+
const isNoValue = (v) => (Number.isNaN(noData) ? Number.isNaN(v) : v === noData);
|
|
7250
|
+
let allNoData = true;
|
|
7251
|
+
if (checkStrategy === 'full') {
|
|
7252
|
+
// Full linear scan (safe)
|
|
7253
|
+
if (numChannels > 1) {
|
|
7254
|
+
for (let i = useChannelIndex; i < raster.length; i += numChannels) {
|
|
7255
|
+
const v = raster[i];
|
|
7256
|
+
if (!isNoValue(v)) {
|
|
7257
|
+
allNoData = false;
|
|
7258
|
+
break;
|
|
7259
|
+
}
|
|
7260
|
+
}
|
|
7261
|
+
}
|
|
7262
|
+
else {
|
|
7263
|
+
for (let i = 0; i < raster.length; i++) {
|
|
7264
|
+
const v = raster[i];
|
|
7265
|
+
if (!isNoValue(v)) {
|
|
7266
|
+
allNoData = false;
|
|
7267
|
+
break;
|
|
7268
|
+
}
|
|
7269
|
+
}
|
|
7270
|
+
}
|
|
7271
|
+
}
|
|
7272
|
+
else if (checkStrategy === 'border+center') {
|
|
7273
|
+
// Border scan: iterate over top/bottom rows and left/right cols
|
|
7274
|
+
const stepX = numChannels;
|
|
7275
|
+
// Top row
|
|
7276
|
+
for (let x = 0; x < width; x++) {
|
|
7277
|
+
const idx = x * stepX + useChannelIndex;
|
|
7278
|
+
const v = raster[idx];
|
|
7279
|
+
if (!isNoValue(v)) {
|
|
7280
|
+
allNoData = false;
|
|
7281
|
+
break;
|
|
7282
|
+
}
|
|
7283
|
+
}
|
|
7284
|
+
// Bottom row
|
|
7285
|
+
if (allNoData) {
|
|
7286
|
+
for (let x = 0; x < width; x++) {
|
|
7287
|
+
const idx = ((height - 1) * width + x) * stepX + useChannelIndex;
|
|
7288
|
+
const v = raster[idx];
|
|
7289
|
+
if (!isNoValue(v)) {
|
|
7290
|
+
allNoData = false;
|
|
7291
|
+
break;
|
|
7292
|
+
}
|
|
7293
|
+
}
|
|
7294
|
+
}
|
|
7295
|
+
// Left/Right cols
|
|
7296
|
+
if (allNoData) {
|
|
7297
|
+
for (let y = 1; y < height - 1; y++) {
|
|
7298
|
+
const leftIdx = (y * width) * stepX + useChannelIndex;
|
|
7299
|
+
const rightIdx = (y * width + (width - 1)) * stepX + useChannelIndex;
|
|
7300
|
+
const vl = raster[leftIdx];
|
|
7301
|
+
const vr = raster[rightIdx];
|
|
7302
|
+
if (!isNoValue(vl) || !isNoValue(vr)) {
|
|
7303
|
+
allNoData = false;
|
|
7304
|
+
break;
|
|
7305
|
+
}
|
|
7306
|
+
}
|
|
7307
|
+
}
|
|
7308
|
+
// Center probe + 4 quadrant probes
|
|
7309
|
+
if (allNoData) {
|
|
7310
|
+
const probes = [
|
|
7311
|
+
[Math.floor(width / 2), Math.floor(height / 2)],
|
|
7312
|
+
[Math.floor(width / 4), Math.floor(height / 4)],
|
|
7313
|
+
[Math.floor((3 * width) / 4), Math.floor(height / 4)],
|
|
7314
|
+
[Math.floor(width / 4), Math.floor((3 * height) / 4)],
|
|
7315
|
+
[Math.floor((3 * width) / 4), Math.floor((3 * height) / 4)],
|
|
7316
|
+
];
|
|
7317
|
+
for (const [px, py] of probes) {
|
|
7318
|
+
const idx = (py * width + px) * stepX + useChannelIndex;
|
|
7319
|
+
const v = raster[idx];
|
|
7320
|
+
if (!isNoValue(v)) {
|
|
7321
|
+
allNoData = false;
|
|
7322
|
+
break;
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
else {
|
|
7328
|
+
// Unknown strategy — fallback to full
|
|
7329
|
+
for (let i = 0; i < raster.length; i++) {
|
|
7330
|
+
const v = raster[i];
|
|
7331
|
+
if (!isNoValue(v)) {
|
|
7332
|
+
allNoData = false;
|
|
7333
|
+
break;
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
if (allNoData) {
|
|
7338
|
+
// Do not cache all-noData result; remove cache entry so future requests re-evaluate if COG/metadata changes.
|
|
7339
|
+
this.tileResultCache.delete(cacheKey);
|
|
7340
|
+
return null;
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
// Create generator options with skipTextureFlag applied (don't mutate shared this.options)
|
|
7344
|
+
const generatorOptions = {
|
|
7345
|
+
...this.options,
|
|
7346
|
+
skipTexture: skipTextureFlag,
|
|
7347
|
+
};
|
|
7348
|
+
return this.geo.getMap({
|
|
7349
|
+
rasters: [tileData[0]],
|
|
7350
|
+
width: requiredSize,
|
|
7351
|
+
height: requiredSize,
|
|
7352
|
+
bounds: bounds ?? [0, 0, 0, 0],
|
|
7353
|
+
cellSizeMeters,
|
|
7354
|
+
}, generatorOptions, resolvedMeshMaxError);
|
|
7355
|
+
})();
|
|
7356
|
+
const entry = {
|
|
7357
|
+
promise: pipeline,
|
|
7358
|
+
controller,
|
|
7359
|
+
callerCount: 1,
|
|
7360
|
+
settled: false,
|
|
7361
|
+
};
|
|
7362
|
+
if (signal && !signal.aborted) {
|
|
7363
|
+
signal.addEventListener('abort', () => {
|
|
7364
|
+
entry.callerCount -= 1;
|
|
7365
|
+
if (entry.callerCount <= 0 && !entry.settled) {
|
|
7366
|
+
entry.controller.abort();
|
|
7367
|
+
}
|
|
7368
|
+
}, { once: true });
|
|
7369
|
+
}
|
|
7370
|
+
entry.promise = pipeline;
|
|
7371
|
+
this.tileResultCache.set(cacheKey, entry);
|
|
7372
|
+
if (this.tileResultCache.size > this.tileResultCacheMaxSize) {
|
|
7373
|
+
const oldestKey = this.tileResultCache.keys().next().value;
|
|
7374
|
+
if (typeof oldestKey === 'string') {
|
|
7375
|
+
const evicted = this.tileResultCache.get(oldestKey);
|
|
7376
|
+
if (evicted && !evicted.settled)
|
|
7377
|
+
evicted.controller.abort();
|
|
7378
|
+
this.tileResultCache.delete(oldestKey);
|
|
7379
|
+
}
|
|
7380
|
+
}
|
|
7381
|
+
try {
|
|
7382
|
+
const result = await pipeline;
|
|
7383
|
+
entry.settled = true;
|
|
7384
|
+
if (signal?.aborted)
|
|
7385
|
+
return null;
|
|
7386
|
+
return result;
|
|
7387
|
+
}
|
|
7388
|
+
catch (error) {
|
|
7389
|
+
entry.settled = true;
|
|
7390
|
+
this.tileResultCache.delete(cacheKey);
|
|
7391
|
+
throw error;
|
|
7392
|
+
}
|
|
7102
7393
|
}
|
|
7394
|
+
// ── PATH B: Bitmap + glaze ────────────────────────────────────────────────────
|
|
7395
|
+
// Relief mask (output of composeSwissRelief) cached — saves fetch + kernel on revisit.
|
|
7396
|
+
// BitmapGenerator re-runs cheaply from the cached Float32Array.
|
|
7397
|
+
// Signal is passed so cancelled tiles abort cleanly; cache entry is deleted on abort/error
|
|
7398
|
+
// so the next request retries fresh.
|
|
7399
|
+
if (isGlaze) {
|
|
7400
|
+
const maskKey = this.getTileCacheKey(x, y, z);
|
|
7401
|
+
let maskPromise = this.reliefMaskCache.get(maskKey);
|
|
7402
|
+
if (maskPromise) {
|
|
7403
|
+
// LRU touch — move the key to the end of the Map to mark as recently used
|
|
7404
|
+
this.reliefMaskCache.delete(maskKey);
|
|
7405
|
+
this.reliefMaskCache.set(maskKey, maskPromise);
|
|
7406
|
+
}
|
|
7407
|
+
if (!maskPromise) {
|
|
7408
|
+
maskPromise = (async () => {
|
|
7409
|
+
const tileData = await this.getTileFromImage(x, y, z, this.tileSize + 2, signal);
|
|
7410
|
+
return ReliefCompositor.composeSwissRelief(tileData[0], this.options, cellSizeMeters, this.tileSize, this.tileSize);
|
|
7411
|
+
})();
|
|
7412
|
+
this.reliefMaskCache.set(maskKey, maskPromise);
|
|
7413
|
+
maskPromise.catch(() => this.reliefMaskCache.delete(maskKey));
|
|
7414
|
+
if (this.reliefMaskCache.size > this.reliefMaskCacheMaxSize) {
|
|
7415
|
+
const oldestKey = this.reliefMaskCache.keys().next().value;
|
|
7416
|
+
if (typeof oldestKey === 'string')
|
|
7417
|
+
this.reliefMaskCache.delete(oldestKey);
|
|
7418
|
+
}
|
|
7419
|
+
}
|
|
7420
|
+
if (signal?.aborted)
|
|
7421
|
+
return null;
|
|
7422
|
+
const reliefMask = await maskPromise;
|
|
7423
|
+
if (signal?.aborted)
|
|
7424
|
+
return null;
|
|
7425
|
+
return this.geo.getMap({
|
|
7426
|
+
rasters: [reliefMask],
|
|
7427
|
+
width: this.tileSize,
|
|
7428
|
+
height: this.tileSize,
|
|
7429
|
+
bounds: bounds ?? [0, 0, 0, 0],
|
|
7430
|
+
cellSizeMeters,
|
|
7431
|
+
}, this.options, meshMaxError ?? 4.0);
|
|
7432
|
+
}
|
|
7433
|
+
// ── PATH C: Ordinary bitmap ───────────────────────────────────────────────────
|
|
7434
|
+
// Raw raster cached — saves fetch + decompression on revisit.
|
|
7435
|
+
// BitmapGenerator re-runs cheaply from the cached TypedArray.
|
|
7436
|
+
// Signal is passed so cancelled tiles abort cleanly; cache entry deleted on abort/error.
|
|
7437
|
+
const rasterKey = this.getTileCacheKey(x, y, z);
|
|
7438
|
+
let rasterPromise = this.rasterCache.get(rasterKey);
|
|
7439
|
+
if (rasterPromise) {
|
|
7440
|
+
// LRU touch — move the key to the end of the Map to mark as recently used
|
|
7441
|
+
this.rasterCache.delete(rasterKey);
|
|
7442
|
+
this.rasterCache.set(rasterKey, rasterPromise);
|
|
7443
|
+
}
|
|
7444
|
+
if (!rasterPromise) {
|
|
7445
|
+
rasterPromise = this.getTileFromImage(x, y, z, this.tileSize, signal);
|
|
7446
|
+
this.rasterCache.set(rasterKey, rasterPromise);
|
|
7447
|
+
rasterPromise.catch(() => this.rasterCache.delete(rasterKey));
|
|
7448
|
+
if (this.rasterCache.size > this.rasterCacheMaxSize) {
|
|
7449
|
+
const oldestKey = this.rasterCache.keys().next().value;
|
|
7450
|
+
if (typeof oldestKey === 'string')
|
|
7451
|
+
this.rasterCache.delete(oldestKey);
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
if (signal?.aborted)
|
|
7455
|
+
return null;
|
|
7456
|
+
const tileData = await rasterPromise;
|
|
7457
|
+
if (signal?.aborted)
|
|
7458
|
+
return null;
|
|
7103
7459
|
return this.geo.getMap({
|
|
7104
|
-
rasters,
|
|
7105
|
-
width:
|
|
7106
|
-
height:
|
|
7460
|
+
rasters: [tileData[0]],
|
|
7461
|
+
width: this.tileSize,
|
|
7462
|
+
height: this.tileSize,
|
|
7107
7463
|
bounds: bounds ?? [0, 0, 0, 0],
|
|
7108
7464
|
cellSizeMeters,
|
|
7109
7465
|
}, this.options, meshMaxError ?? 4.0);
|
|
@@ -7316,7 +7672,16 @@ class CogBitmapLayer extends CompositeLayer {
|
|
|
7316
7672
|
}
|
|
7317
7673
|
}
|
|
7318
7674
|
async getTiledBitmapData(tile) {
|
|
7319
|
-
|
|
7675
|
+
let resolvedTileData;
|
|
7676
|
+
try {
|
|
7677
|
+
resolvedTileData = await this.state.bitmapCogTiles.getTile(tile.index.x, tile.index.y, tile.index.z, undefined, undefined, tile.signal);
|
|
7678
|
+
}
|
|
7679
|
+
catch (error) {
|
|
7680
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
7681
|
+
return null;
|
|
7682
|
+
}
|
|
7683
|
+
throw error;
|
|
7684
|
+
}
|
|
7320
7685
|
if (resolvedTileData && !this.props.pickable) {
|
|
7321
7686
|
resolvedTileData.raw = null;
|
|
7322
7687
|
}
|
|
@@ -7413,6 +7778,12 @@ const urlType = {
|
|
|
7413
7778
|
return true;
|
|
7414
7779
|
},
|
|
7415
7780
|
};
|
|
7781
|
+
const meshMaxErrorValidation = {
|
|
7782
|
+
type: 'object',
|
|
7783
|
+
value: 'auto',
|
|
7784
|
+
validate: (value) => typeof value === 'number' || value === 'auto',
|
|
7785
|
+
equal: (v1, v2) => v1 === v2,
|
|
7786
|
+
};
|
|
7416
7787
|
const DUMMY_DATA = [1];
|
|
7417
7788
|
const defaultProps = {
|
|
7418
7789
|
...TileLayer.defaultProps,
|
|
@@ -7421,7 +7792,9 @@ const defaultProps = {
|
|
|
7421
7792
|
// Image url to use as texture
|
|
7422
7793
|
texture: { ...urlType, optional: true },
|
|
7423
7794
|
// Martini error tolerance in meters, smaller number -> more detailed mesh
|
|
7424
|
-
|
|
7795
|
+
// Set to a number for fixed tessellation across all zooms, or 'auto' (default)
|
|
7796
|
+
// for zoom-adaptive meshMaxError based on COG resolution
|
|
7797
|
+
meshMaxError: meshMaxErrorValidation,
|
|
7425
7798
|
// Bounding box of the terrain image, [minX, minY, maxX, maxY] in world coordinates
|
|
7426
7799
|
bounds: {
|
|
7427
7800
|
type: 'array', value: null, optional: true, compare: true,
|
|
@@ -7499,6 +7872,11 @@ class CogTerrainLayer extends CompositeLayer {
|
|
|
7499
7872
|
|| props.meshMaxError !== oldProps.meshMaxError
|
|
7500
7873
|
|| props.elevationDecoder !== oldProps.elevationDecoder
|
|
7501
7874
|
|| props.bounds !== oldProps.bounds;
|
|
7875
|
+
// When meshMaxError changes, cached meshes are stale — clear so new tiles are tessellated
|
|
7876
|
+
// at the correct error tolerance
|
|
7877
|
+
if (props.meshMaxError !== oldProps.meshMaxError && this.state.terrainCogTiles) {
|
|
7878
|
+
this.state.terrainCogTiles.clearTileResultCache();
|
|
7879
|
+
}
|
|
7502
7880
|
if (!this.state.isTiled && shouldReload) ;
|
|
7503
7881
|
// Update the useChannel option for terrainCogTiles when terrainOptions.useChannel changes.
|
|
7504
7882
|
if (props?.terrainOptions?.useChannel !== oldProps.terrainOptions?.useChannel) {
|
|
@@ -7506,6 +7884,13 @@ class CogTerrainLayer extends CompositeLayer {
|
|
|
7506
7884
|
// Trigger a refresh of the tiles
|
|
7507
7885
|
this.state.terrainCogTiles.options.useChannelIndex = null; // Clear cached index
|
|
7508
7886
|
}
|
|
7887
|
+
// Update skipTexture when wireframe/operation/disableTexture changes so cache keys are correct
|
|
7888
|
+
const newSkipTexture = !!(props?.wireframe || props?.operation === 'terrain' || props?.disableTexture);
|
|
7889
|
+
const oldSkipTexture = !!(oldProps?.wireframe || oldProps?.operation === 'terrain' || oldProps?.disableTexture);
|
|
7890
|
+
if (newSkipTexture !== oldSkipTexture && this.state.terrainCogTiles) {
|
|
7891
|
+
this.state.terrainCogTiles.options.skipTexture = newSkipTexture;
|
|
7892
|
+
this.state.terrainCogTiles.clearTileResultCache();
|
|
7893
|
+
}
|
|
7509
7894
|
// When the external cogTiles instance is swapped (e.g. mode switch), update state so
|
|
7510
7895
|
// renderLayers picks up the new reference and the TileLayer updateTrigger fires a refetch
|
|
7511
7896
|
// while keeping old tile content visible until new tiles are ready.
|
|
@@ -7552,11 +7937,25 @@ class CogTerrainLayer extends CompositeLayer {
|
|
|
7552
7937
|
topRight = [bbox.right, bbox.top];
|
|
7553
7938
|
}
|
|
7554
7939
|
const bounds = [bottomLeft[0], bottomLeft[1], topRight[0], topRight[1]];
|
|
7555
|
-
|
|
7940
|
+
let resolvedTerrain = null;
|
|
7941
|
+
try {
|
|
7942
|
+
const skipTexture = !!(this.props.wireframe || this.props.operation === 'terrain' || this.props.disableTexture);
|
|
7943
|
+
// Convert 'auto' to undefined so CogTiles.getTile uses the quantized meshMaxError for the zoom level
|
|
7944
|
+
const meshMaxErrorValue = this.props.meshMaxError === 'auto' ? undefined : this.props.meshMaxError;
|
|
7945
|
+
resolvedTerrain = await this.state.terrainCogTiles.getTile(tile.index.x, tile.index.y, tile.index.z, bounds, meshMaxErrorValue, tile.signal, skipTexture);
|
|
7946
|
+
}
|
|
7947
|
+
catch (error) {
|
|
7948
|
+
// Tile was cancelled (AbortError) — return null so deck.gl discards it cleanly
|
|
7949
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
7950
|
+
return null;
|
|
7951
|
+
}
|
|
7952
|
+
throw error;
|
|
7953
|
+
}
|
|
7556
7954
|
if (resolvedTerrain && !this.props.pickable) {
|
|
7557
7955
|
resolvedTerrain.raw = null;
|
|
7558
7956
|
}
|
|
7559
|
-
|
|
7957
|
+
// Return a tuple [TileResult|null, Texture|null] when data is available, otherwise null
|
|
7958
|
+
return resolvedTerrain ? [resolvedTerrain, null] : null;
|
|
7560
7959
|
}
|
|
7561
7960
|
renderSubLayers(props) {
|
|
7562
7961
|
const SubLayerClass = this.getSubLayerClass('mesh', SimpleMeshLayer);
|
|
@@ -7640,6 +8039,7 @@ class CogTerrainLayer extends CompositeLayer {
|
|
|
7640
8039
|
meshMaxError,
|
|
7641
8040
|
elevationDecoder,
|
|
7642
8041
|
terrainCogTiles: this.state.terrainCogTiles,
|
|
8042
|
+
skipTexture: !!(this.props.wireframe || this.props.operation === 'terrain' || this.props.disableTexture),
|
|
7643
8043
|
},
|
|
7644
8044
|
renderSubLayers: {
|
|
7645
8045
|
disableTexture: this.props.disableTexture,
|