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