@gisatcz/deckgl-geolib 2.5.1-dev.1 → 2.6.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/dist/cjs/index.js +217 -2
- 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 +217 -2
- 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 +14 -2
- package/dist/esm/types/core/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -4969,6 +4969,8 @@ const DefaultGeoImageOptions = {
|
|
|
4969
4969
|
useReliefGlaze: false,
|
|
4970
4970
|
// --- Lighting control ---
|
|
4971
4971
|
disableLighting: false,
|
|
4972
|
+
// --- Animation / Caching ---
|
|
4973
|
+
cacheAllBands: false,
|
|
4972
4974
|
};
|
|
4973
4975
|
|
|
4974
4976
|
class Martini {
|
|
@@ -7055,8 +7057,16 @@ class TileReader {
|
|
|
7055
7057
|
}
|
|
7056
7058
|
}
|
|
7057
7059
|
|
|
7060
|
+
// ── Global Multi-Band Cache ──
|
|
7061
|
+
// Survives across CogTiles instance recreations (e.g., during React re-renders).
|
|
7062
|
+
// Keyed by ${url}_${z}_${x}_${y}_${meshMaxError}_${skipTextureFlag}_band_${channel}
|
|
7063
|
+
// to account for dataset changes, tessellation options, and rendering modes.
|
|
7064
|
+
// Maps to pre-computed terrain meshes (same structure as single-band cache).
|
|
7065
|
+
const GLOBAL_MULTI_BAND_CACHE = new Map();
|
|
7058
7066
|
const CogTilesGeoImageOptionsDefaults = {
|
|
7059
7067
|
blurredTexture: true,
|
|
7068
|
+
// When true, log per-tile per-band min/max values for debugging (disabled by default)
|
|
7069
|
+
debugTileStats: false,
|
|
7060
7070
|
};
|
|
7061
7071
|
class CogTiles {
|
|
7062
7072
|
cog;
|
|
@@ -7067,6 +7077,7 @@ class CogTiles {
|
|
|
7067
7077
|
zoomRange = [0, 0];
|
|
7068
7078
|
tileSize = 256;
|
|
7069
7079
|
bounds = [0, 0, 0, 0];
|
|
7080
|
+
bandDescriptions = [];
|
|
7070
7081
|
geo = new GeoImage();
|
|
7071
7082
|
options;
|
|
7072
7083
|
// TileResult cache — keyed by z/x/y/meshMaxError.
|
|
@@ -7095,6 +7106,14 @@ class CogTiles {
|
|
|
7095
7106
|
// safely reinitialized against a different source.
|
|
7096
7107
|
if (this.lastInitializedUrl !== undefined && this.lastInitializedUrl !== url) {
|
|
7097
7108
|
this.cache.clearAll();
|
|
7109
|
+
// Clear multi-band cache entries for the old URL to prevent stale data
|
|
7110
|
+
const keysToDelete = [];
|
|
7111
|
+
GLOBAL_MULTI_BAND_CACHE.forEach((_, key) => {
|
|
7112
|
+
if (key.startsWith(this.lastInitializedUrl)) {
|
|
7113
|
+
keysToDelete.push(key);
|
|
7114
|
+
}
|
|
7115
|
+
});
|
|
7116
|
+
keysToDelete.forEach(key => GLOBAL_MULTI_BAND_CACHE.delete(key));
|
|
7098
7117
|
this.cog = undefined;
|
|
7099
7118
|
this.cogOrigin = [0, 0];
|
|
7100
7119
|
this.cogZoomLookup = [];
|
|
@@ -7103,6 +7122,7 @@ class CogTiles {
|
|
|
7103
7122
|
this.tileSize = 256;
|
|
7104
7123
|
this.zoomRange = [0, 0];
|
|
7105
7124
|
this.bounds = [0, 0, 0, 0];
|
|
7125
|
+
this.bandDescriptions = [];
|
|
7106
7126
|
this.initializePromise = undefined;
|
|
7107
7127
|
this.lastInitializedUrl = undefined;
|
|
7108
7128
|
}
|
|
@@ -7128,6 +7148,31 @@ class CogTiles {
|
|
|
7128
7148
|
this.options.format ??= await getDataTypeFromTags(fileDirectory);
|
|
7129
7149
|
this.options.numOfChannels = fileDirectory.getValue('SamplesPerPixel');
|
|
7130
7150
|
this.options.planarConfig = fileDirectory.getValue('PlanarConfiguration');
|
|
7151
|
+
// Load per-band descriptions from GDAL_METADATA tag
|
|
7152
|
+
const numBands = this.options.numOfChannels ?? 1;
|
|
7153
|
+
const descriptions = Array(numBands).fill('');
|
|
7154
|
+
if (image.fileDirectory.hasTag('GDAL_METADATA')) {
|
|
7155
|
+
const gdalMetadataStr = await image.fileDirectory.loadValue('GDAL_METADATA');
|
|
7156
|
+
// Parse XML GDAL metadata to extract per-band descriptions.
|
|
7157
|
+
// Regex is flexible with attribute order; GDAL generates items with name="DESCRIPTION"
|
|
7158
|
+
// before sample="N", but [^>]* allows other attributes to appear in any order.
|
|
7159
|
+
// Format: <Item name="DESCRIPTION" sample="0" role="description">20170101</Item>
|
|
7160
|
+
const bandDescRegex = /<Item[^>]*name="DESCRIPTION"[^>]*sample="(\d+)"[^>]*>([^<]*)<\/Item>/g;
|
|
7161
|
+
let match;
|
|
7162
|
+
while ((match = bandDescRegex.exec(gdalMetadataStr)) !== null) {
|
|
7163
|
+
const bandIdx = parseInt(match[1], 10);
|
|
7164
|
+
const desc = match[2];
|
|
7165
|
+
if (bandIdx < numBands) {
|
|
7166
|
+
descriptions[bandIdx] = desc;
|
|
7167
|
+
}
|
|
7168
|
+
}
|
|
7169
|
+
// Debug: Log if no descriptions were found in metadata
|
|
7170
|
+
if (descriptions.every(d => d === '')) {
|
|
7171
|
+
// eslint-disable-next-line no-console
|
|
7172
|
+
console.debug('[CogTiles] GDAL_METADATA present but no DESCRIPTION items found');
|
|
7173
|
+
}
|
|
7174
|
+
}
|
|
7175
|
+
this.bandDescriptions = descriptions;
|
|
7131
7176
|
[this.cogZoomLookup, this.cogResolutionLookup] = await buildCogZoomResolutionLookup(this.cog);
|
|
7132
7177
|
// Only compute quantized meshMaxError lookup for terrain COGs
|
|
7133
7178
|
if (this.options.type === 'terrain') {
|
|
@@ -7166,9 +7211,23 @@ class CogTiles {
|
|
|
7166
7211
|
return this.bounds;
|
|
7167
7212
|
}
|
|
7168
7213
|
/**
|
|
7169
|
-
* Gets the
|
|
7170
|
-
* Returns
|
|
7214
|
+
* Gets the number of channels/bands in the COG.
|
|
7215
|
+
* Returns the value from COG metadata, or 1 if not initialized.
|
|
7171
7216
|
*/
|
|
7217
|
+
getNumChannels() {
|
|
7218
|
+
return this.options.numOfChannels || 1;
|
|
7219
|
+
}
|
|
7220
|
+
/**
|
|
7221
|
+
* Returns per-band descriptions loaded from GDAL_METADATA during initialization.
|
|
7222
|
+
* Index is 0-based. Returns an empty string for bands without a description.
|
|
7223
|
+
*/
|
|
7224
|
+
getBandDescriptions() {
|
|
7225
|
+
return this.bandDescriptions;
|
|
7226
|
+
}
|
|
7227
|
+
/**
|
|
7228
|
+
* Gets the auto meshMaxError for a given overview index.
|
|
7229
|
+
* Returns undefined if auto lookup has not been computed.
|
|
7230
|
+
*/
|
|
7172
7231
|
getMeshMaxErrorForImageIndex(imageIndex) {
|
|
7173
7232
|
return this.cogMeshMaxErrorLookup[imageIndex];
|
|
7174
7233
|
}
|
|
@@ -7376,6 +7435,54 @@ class CogTiles {
|
|
|
7376
7435
|
}
|
|
7377
7436
|
async getTerrainTile(x, y, z, bounds, resolvedMeshMaxError, cellSizeMeters, signal, skipTexture) {
|
|
7378
7437
|
const skipTextureFlag = skipTexture ?? this.options.skipTexture ?? false;
|
|
7438
|
+
// ── Multi-Band Cache Check ──
|
|
7439
|
+
// If cacheAllBands is enabled, check if this tile+band combination is in the cache.
|
|
7440
|
+
// If so, return it instantly. If not, fetch all bands and populate the cache.
|
|
7441
|
+
if (this.options.cacheAllBands) {
|
|
7442
|
+
if (signal?.aborted) {
|
|
7443
|
+
return null; // Early exit if already aborted
|
|
7444
|
+
}
|
|
7445
|
+
const currentChannel = this.options.useChannel || 1; // 1-based
|
|
7446
|
+
// Cache key includes URL, meshMaxError, and skipTexture to account for dataset/option changes
|
|
7447
|
+
const multiBandCacheKey = `${this.lastInitializedUrl}_${z}_${x}_${y}_${resolvedMeshMaxError}_${skipTextureFlag}_band_${currentChannel}`;
|
|
7448
|
+
// Check if this specific band is already cached
|
|
7449
|
+
if (GLOBAL_MULTI_BAND_CACHE.has(multiBandCacheKey)) {
|
|
7450
|
+
const cached = GLOBAL_MULTI_BAND_CACHE.get(multiBandCacheKey);
|
|
7451
|
+
if (cached) {
|
|
7452
|
+
return cached;
|
|
7453
|
+
}
|
|
7454
|
+
}
|
|
7455
|
+
// Not in cache — fetch all bands for this tile and populate the cache
|
|
7456
|
+
try {
|
|
7457
|
+
// Pass NO signal to getTileAllBands to avoid early abortion
|
|
7458
|
+
// The tile lifecycle is managed by TileLayer, we want this fetch to complete
|
|
7459
|
+
const allBands = await this.getTileAllBands(x, y, z, resolvedMeshMaxError, undefined, bounds);
|
|
7460
|
+
// Only cache and return if we got valid data
|
|
7461
|
+
if (allBands && allBands.length > 0) {
|
|
7462
|
+
if (signal?.aborted) {
|
|
7463
|
+
return null; // Check abort after fetch completes
|
|
7464
|
+
}
|
|
7465
|
+
// Cache all bands (indexed by 1-based channel: band 1, band 2, ..., band N)
|
|
7466
|
+
allBands.forEach((bandResult, idx) => {
|
|
7467
|
+
const bandChannel = idx + 1; // Convert 0-based index to 1-based channel
|
|
7468
|
+
const cacheKey = `${this.lastInitializedUrl}_${z}_${x}_${y}_${resolvedMeshMaxError}_${skipTextureFlag}_band_${bandChannel}`;
|
|
7469
|
+
GLOBAL_MULTI_BAND_CACHE.set(cacheKey, bandResult);
|
|
7470
|
+
});
|
|
7471
|
+
// Return the requested band
|
|
7472
|
+
const requestedBandIndex = (currentChannel || 1) - 1; // Convert 1-based to 0-based
|
|
7473
|
+
if (requestedBandIndex < 0 || requestedBandIndex >= allBands.length) {
|
|
7474
|
+
console.error(`[CogTiles] Requested band index ${requestedBandIndex} out of range (0-${allBands.length - 1})`);
|
|
7475
|
+
return null;
|
|
7476
|
+
}
|
|
7477
|
+
return allBands[requestedBandIndex];
|
|
7478
|
+
}
|
|
7479
|
+
// If getTileAllBands returned empty, fall through to normal fetch
|
|
7480
|
+
}
|
|
7481
|
+
catch (error) {
|
|
7482
|
+
// If multi-band fetch fails, fall through to normal tile fetch
|
|
7483
|
+
console.warn('[CogTiles] Multi-band fetch failed, falling back to single-band:', error);
|
|
7484
|
+
}
|
|
7485
|
+
}
|
|
7379
7486
|
const cacheKey = this.cache.getTileResultCacheKey(x, y, z, resolvedMeshMaxError, skipTextureFlag);
|
|
7380
7487
|
const existing = this.cache.getTileResult(cacheKey);
|
|
7381
7488
|
if (existing) {
|
|
@@ -7597,6 +7704,114 @@ class CogTiles {
|
|
|
7597
7704
|
cellSizeMeters,
|
|
7598
7705
|
}, this.options, meshMaxError ?? 4.0);
|
|
7599
7706
|
}
|
|
7707
|
+
async getTileAllBands(x, y, z, meshMaxError, signal, bounds) {
|
|
7708
|
+
if (!this.cog) {
|
|
7709
|
+
return [];
|
|
7710
|
+
}
|
|
7711
|
+
// Compute cell size for this tile
|
|
7712
|
+
const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 0.5) / Math.pow(2, z))));
|
|
7713
|
+
const tileWidthMeters = (EARTH_CIRCUMFERENCE / Math.pow(2, z)) * Math.cos(latRad);
|
|
7714
|
+
const cellSizeMeters = tileWidthMeters / this.tileSize;
|
|
7715
|
+
// Resolve meshMaxError
|
|
7716
|
+
let resolvedMeshMaxError;
|
|
7717
|
+
if (!meshMaxError || meshMaxError === 0) {
|
|
7718
|
+
const imageIndex = this.getImageIndexForZoomLevel(z);
|
|
7719
|
+
const autoMeshMaxError = this.getMeshMaxErrorForImageIndex(imageIndex);
|
|
7720
|
+
resolvedMeshMaxError = autoMeshMaxError ?? 4.0;
|
|
7721
|
+
}
|
|
7722
|
+
else {
|
|
7723
|
+
resolvedMeshMaxError = meshMaxError;
|
|
7724
|
+
}
|
|
7725
|
+
// Create a fresh local AbortController for this fetch
|
|
7726
|
+
const controller = new AbortController();
|
|
7727
|
+
if (signal && !signal.aborted) {
|
|
7728
|
+
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
7729
|
+
}
|
|
7730
|
+
const localSignal = controller.signal;
|
|
7731
|
+
try {
|
|
7732
|
+
const imageIndex = this.getImageIndexForZoomLevel(z);
|
|
7733
|
+
let imagePromise = this.cache.getImage(imageIndex);
|
|
7734
|
+
if (!imagePromise) {
|
|
7735
|
+
imagePromise = this.cog.getImage(imageIndex);
|
|
7736
|
+
this.cache.setImage(imageIndex, imagePromise);
|
|
7737
|
+
}
|
|
7738
|
+
const targetImage = await imagePromise;
|
|
7739
|
+
const imageResolution = this.cogResolutionLookup[imageIndex];
|
|
7740
|
+
const imageHeight = targetImage.getHeight();
|
|
7741
|
+
const imageWidth = targetImage.getWidth();
|
|
7742
|
+
const [imgOriginX, imgOriginY] = this.cogOrigin;
|
|
7743
|
+
const TILE_SIZE = this.tileSize;
|
|
7744
|
+
const ORIGIN_X = webMercatorOrigin[0];
|
|
7745
|
+
const ORIGIN_Y = webMercatorOrigin[1];
|
|
7746
|
+
// ── Martini Grid Size Fix ──
|
|
7747
|
+
// Martini requires 2^n + 1 grid (e.g., 257x257), but we fetch at TILE_SIZE (256).
|
|
7748
|
+
// Add padding for relief kernels (2) or just 1 for basic terrain.
|
|
7749
|
+
const isKernel = this.options.useSlope || this.options.useHillshade || this.options.useSwissRelief;
|
|
7750
|
+
const FETCH_SIZE = this.tileSize + (isKernel ? 2 : 1);
|
|
7751
|
+
const tileGridResolution = (EARTH_CIRCUMFERENCE / TILE_SIZE) / (2 ** z);
|
|
7752
|
+
const tileMinXMeters = ORIGIN_X + (x * TILE_SIZE * tileGridResolution);
|
|
7753
|
+
const tileMaxYMeters = ORIGIN_Y - (y * TILE_SIZE * tileGridResolution);
|
|
7754
|
+
const windowMinX = (tileMinXMeters - imgOriginX) / imageResolution;
|
|
7755
|
+
const windowMinY = (imgOriginY - tileMaxYMeters) / imageResolution;
|
|
7756
|
+
const startX = Math.round(windowMinX);
|
|
7757
|
+
const startY = Math.round(windowMinY);
|
|
7758
|
+
const endX = startX + FETCH_SIZE;
|
|
7759
|
+
const endY = startY + FETCH_SIZE;
|
|
7760
|
+
const validReadX = Math.max(0, startX);
|
|
7761
|
+
const validReadY = Math.max(0, startY);
|
|
7762
|
+
const validReadMaxX = Math.min(imageWidth, endX);
|
|
7763
|
+
const validReadMaxY = Math.min(imageHeight, endY);
|
|
7764
|
+
const readWidth = validReadMaxX - validReadX;
|
|
7765
|
+
const readHeight = validReadMaxY - validReadY;
|
|
7766
|
+
// If no overlap, return empty array
|
|
7767
|
+
if (readWidth <= 0 || readHeight <= 0) {
|
|
7768
|
+
return [];
|
|
7769
|
+
}
|
|
7770
|
+
const missingLeft = validReadX - startX;
|
|
7771
|
+
const missingTop = validReadY - startY;
|
|
7772
|
+
const window = [validReadX, validReadY, validReadMaxX, validReadMaxY];
|
|
7773
|
+
const validRasterData = await targetImage.readRasters({ window, signal: localSignal });
|
|
7774
|
+
if (signal?.aborted)
|
|
7775
|
+
return [];
|
|
7776
|
+
const results = [];
|
|
7777
|
+
const numBands = validRasterData.length;
|
|
7778
|
+
for (let bandIndex = 0; bandIndex < numBands; bandIndex += 1) {
|
|
7779
|
+
const sourceBandArray = validRasterData[bandIndex];
|
|
7780
|
+
const processedBandRaster = this.tileReader.createTileBuffer(this.options.format || 'Float32', FETCH_SIZE);
|
|
7781
|
+
if (this.options.noDataValue !== undefined) {
|
|
7782
|
+
processedBandRaster.fill(this.options.noDataValue);
|
|
7783
|
+
}
|
|
7784
|
+
for (let row = 0; row < readHeight; row += 1) {
|
|
7785
|
+
const destRow = missingTop + row;
|
|
7786
|
+
const destRowOffset = destRow * FETCH_SIZE;
|
|
7787
|
+
const srcRowOffset = row * readWidth;
|
|
7788
|
+
for (let col = 0; col < readWidth; col += 1) {
|
|
7789
|
+
const destCol = missingLeft + col;
|
|
7790
|
+
if (destRow < FETCH_SIZE && destCol < FETCH_SIZE) {
|
|
7791
|
+
processedBandRaster[destRowOffset + destCol] = sourceBandArray[srcRowOffset + col];
|
|
7792
|
+
}
|
|
7793
|
+
}
|
|
7794
|
+
}
|
|
7795
|
+
const generatorOptions = { ...this.options, useChannel: 1, useChannelIndex: 0, numOfChannels: 1 };
|
|
7796
|
+
const tileResult = await this.geo.getMap({
|
|
7797
|
+
rasters: [processedBandRaster],
|
|
7798
|
+
width: FETCH_SIZE,
|
|
7799
|
+
height: FETCH_SIZE,
|
|
7800
|
+
bounds: bounds ?? [0, 0, 0, 0],
|
|
7801
|
+
cellSizeMeters,
|
|
7802
|
+
}, generatorOptions, resolvedMeshMaxError);
|
|
7803
|
+
if (tileResult)
|
|
7804
|
+
results.push(tileResult);
|
|
7805
|
+
}
|
|
7806
|
+
return results;
|
|
7807
|
+
}
|
|
7808
|
+
catch (error) {
|
|
7809
|
+
if (signal?.aborted)
|
|
7810
|
+
return [];
|
|
7811
|
+
console.error('[CogTiles.getTileAllBands] Error fetching all bands:', error);
|
|
7812
|
+
return [];
|
|
7813
|
+
}
|
|
7814
|
+
}
|
|
7600
7815
|
// Expose legacy API for clearing tile result cache (used by external layers)
|
|
7601
7816
|
clearTileResultCache() {
|
|
7602
7817
|
this.cache.clearTileResultCache();
|