@gisatcz/deckgl-geolib 2.5.0 → 2.5.1-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/esm/index.js CHANGED
@@ -6736,6 +6736,323 @@ const EARTH_CIRCUMFERENCE = 2 * Math.PI * 6378137;
6736
6736
  const EARTH_HALF_CIRCUMFERENCE = EARTH_CIRCUMFERENCE / 2;
6737
6737
  const webMercatorOrigin = [-20037508.342789244, 20037508.342789244];
6738
6738
  const webMercatorRes0 = 156543.03125;
6739
+ function getLatLon(input) {
6740
+ const x = input[0];
6741
+ const y = input[1];
6742
+ const lon = (x / EARTH_HALF_CIRCUMFERENCE) * 180;
6743
+ let lat = (y / EARTH_HALF_CIRCUMFERENCE) * 180;
6744
+ lat = (180 / Math.PI) * (2 * Math.atan(Math.exp((lat * Math.PI) / 180)) - Math.PI / 2);
6745
+ return [lon, lat];
6746
+ }
6747
+ function getZoomLevelFromResolution(tileSize, resolution) {
6748
+ return Math.round(Math.log2(EARTH_CIRCUMFERENCE / (resolution * tileSize)));
6749
+ }
6750
+ function calculateZoomRange(tileSize, resolution, imgCount) {
6751
+ const maxZoom = getZoomLevelFromResolution(tileSize, resolution);
6752
+ const minZoom = maxZoom - (imgCount - 1);
6753
+ return [minZoom, maxZoom];
6754
+ }
6755
+ function calculateBoundsAsLatLon(bbox) {
6756
+ const minX = Math.min(bbox[0], bbox[2]);
6757
+ const maxX = Math.max(bbox[0], bbox[2]);
6758
+ const minY = Math.min(bbox[1], bbox[3]);
6759
+ const maxY = Math.max(bbox[1], bbox[3]);
6760
+ const minXYDeg = getLatLon([minX, minY]);
6761
+ const maxXYDeg = getLatLon([maxX, maxY]);
6762
+ return [minXYDeg[0], minXYDeg[1], maxXYDeg[0], maxXYDeg[1]];
6763
+ }
6764
+
6765
+ /* eslint-disable no-console */
6766
+ /**
6767
+ * Reads TIFF tags to determine the numeric data type (e.g. "UInt8", "Int16", "Float32").
6768
+ * The implementation mirrors the logic previously present in CogTiles.
6769
+ */
6770
+ async function getDataTypeFromTags(fileDirectory) {
6771
+ const hasSampleFormat = fileDirectory.hasTag('SampleFormat');
6772
+ const hasBitsPerSample = fileDirectory.hasTag('BitsPerSample');
6773
+ if (!hasSampleFormat || !hasBitsPerSample) {
6774
+ console.warn("Missing SampleFormat or BitsPerSample tags, defaulting to UInt8");
6775
+ return 'UInt8';
6776
+ }
6777
+ // In GeoTIFF, BitsPerSample (tag 258) and SampleFormat (tag 339) provide the type info.
6778
+ // They can be either a single number or an array if there are multiple samples.
6779
+ const sampleFormat = fileDirectory.getValue('SampleFormat'); // Tag 339
6780
+ const bitsPerSample = fileDirectory.getValue('BitsPerSample'); // Tag 258
6781
+ // If multiple bands exist, assume all bands share the same type.
6782
+ const format = (sampleFormat && typeof sampleFormat.length === 'number' && sampleFormat.length > 0)
6783
+ ? sampleFormat[0]
6784
+ : sampleFormat;
6785
+ const bits = (bitsPerSample && typeof bitsPerSample.length === 'number' && bitsPerSample.length > 0)
6786
+ ? bitsPerSample[0]
6787
+ : bitsPerSample;
6788
+ let typePrefix;
6789
+ // 1 = Unsigned Integer, 2 = Signed Integer, 3 = Floating Point
6790
+ if (format === 1) {
6791
+ typePrefix = 'UInt';
6792
+ }
6793
+ else if (format === 2) {
6794
+ typePrefix = 'Int';
6795
+ }
6796
+ else if (format === 3) {
6797
+ typePrefix = 'Float';
6798
+ }
6799
+ else {
6800
+ typePrefix = 'Unknown';
6801
+ }
6802
+ return `${typePrefix}${bits}`;
6803
+ }
6804
+ /**
6805
+ * Extracts the noData value from a GeoTIFF.js image.
6806
+ * Returns the numeric value, NaN, or undefined if not present/parsable.
6807
+ */
6808
+ function getNoDataValue(image) {
6809
+ const noDataRaw = image.getGDALNoData();
6810
+ if (noDataRaw === undefined || noDataRaw === null) {
6811
+ console.warn('No noData value defined — raster might be rendered incorrectly.');
6812
+ return undefined;
6813
+ }
6814
+ const cleaned = String(noDataRaw).replace(/\0/g, '').trim();
6815
+ if (cleaned === '') {
6816
+ console.warn('noData value is an empty string after cleanup.');
6817
+ return undefined;
6818
+ }
6819
+ const parsed = Number(cleaned);
6820
+ // Allow NaN if explicitly declared
6821
+ if (cleaned.toLowerCase() === 'nan') {
6822
+ return NaN;
6823
+ }
6824
+ // If not declared as "nan" and still parsed to NaN, it's an error
6825
+ if (Number.isNaN(parsed)) {
6826
+ console.warn(`Failed to parse numeric noData value: '${cleaned}'`);
6827
+ return undefined;
6828
+ }
6829
+ return parsed;
6830
+ }
6831
+ /* eslint-enable no-console */
6832
+
6833
+ /**
6834
+ * LOD (level-of-detail) helpers extracted from CogTiles.
6835
+ */
6836
+ function getErrorMultiplierForZoom(z, minZ, maxZ) {
6837
+ if (z >= maxZ)
6838
+ return 0.5;
6839
+ if (z <= minZ)
6840
+ return 3.0;
6841
+ // Linear interpolation: 3.0 - ((z - minZ) / (maxZ - minZ)) * 2.5
6842
+ return 3.0 - ((z - minZ) / (maxZ - minZ)) * 2.5;
6843
+ }
6844
+ function calculateDynamicMeshMaxError(z, resolution, minZ, maxZ) {
6845
+ const multiplier = getErrorMultiplierForZoom(z, minZ, maxZ);
6846
+ const errorValue = resolution * multiplier;
6847
+ return Math.round(Math.max(0.5, Math.min(100, errorValue)));
6848
+ }
6849
+ async function buildCogZoomResolutionLookup(cog) {
6850
+ const imageCount = await cog.getImageCount();
6851
+ const baseImage = await cog.getImage(0);
6852
+ const baseResolution = baseImage.getResolution()[0];
6853
+ const baseWidth = baseImage.getWidth();
6854
+ const zoomLookup = [];
6855
+ const resolutionLookup = [];
6856
+ for (let idx = 0; idx < imageCount; idx++) {
6857
+ const image = await cog.getImage(idx);
6858
+ const width = image.getWidth();
6859
+ const scaleFactor = baseWidth / width;
6860
+ const estimatedResolution = baseResolution * scaleFactor;
6861
+ const zoomLevel = Math.round(Math.log2(webMercatorRes0 / estimatedResolution));
6862
+ zoomLookup[idx] = zoomLevel;
6863
+ resolutionLookup[idx] = estimatedResolution;
6864
+ }
6865
+ return [zoomLookup, resolutionLookup];
6866
+ }
6867
+ function getImageIndexForZoomLevel(zoom, cogZoomLookup) {
6868
+ const minZoom = cogZoomLookup[cogZoomLookup.length - 1];
6869
+ const maxZoom = cogZoomLookup[0];
6870
+ if (zoom > maxZoom)
6871
+ return 0;
6872
+ if (zoom < minZoom)
6873
+ return cogZoomLookup.length - 1;
6874
+ const exactMatchIndex = cogZoomLookup.indexOf(zoom);
6875
+ if (exactMatchIndex !== -1)
6876
+ return exactMatchIndex;
6877
+ let closestIndex = 0;
6878
+ let minDistance = Math.abs(cogZoomLookup[0] - zoom);
6879
+ for (let i = 1; i < cogZoomLookup.length; i += 1) {
6880
+ const distance = Math.abs(cogZoomLookup[i] - zoom);
6881
+ if (distance < minDistance) {
6882
+ minDistance = distance;
6883
+ closestIndex = i;
6884
+ }
6885
+ }
6886
+ return closestIndex;
6887
+ }
6888
+
6889
+ class TileCacheManager {
6890
+ tileResultCache = new Map();
6891
+ tileResultCacheMaxSize;
6892
+ rasterCache = new Map();
6893
+ rasterCacheMaxSize;
6894
+ reliefMaskCache = new Map();
6895
+ reliefMaskCacheMaxSize;
6896
+ imageCache = new Map();
6897
+ constructor(opts) {
6898
+ this.tileResultCacheMaxSize = opts?.tileResultCacheMaxSize ?? 32;
6899
+ this.rasterCacheMaxSize = opts?.rasterCacheMaxSize ?? 64;
6900
+ this.reliefMaskCacheMaxSize = opts?.reliefMaskCacheMaxSize ?? 64;
6901
+ }
6902
+ getTileResultCacheKey(x, y, z, meshMaxError, skipTexture) {
6903
+ return `${z}/${x}/${y}/${meshMaxError}/${skipTexture ? '1' : '0'}`;
6904
+ }
6905
+ getTileCacheKey(x, y, z) {
6906
+ return `${z}/${x}/${y}`;
6907
+ }
6908
+ // TileResult cache methods
6909
+ getTileResult(key) {
6910
+ const entry = this.tileResultCache.get(key);
6911
+ if (!entry)
6912
+ return undefined;
6913
+ // LRU touch
6914
+ this.tileResultCache.delete(key);
6915
+ this.tileResultCache.set(key, entry);
6916
+ return entry;
6917
+ }
6918
+ setTileResult(key, entry) {
6919
+ this.tileResultCache.set(key, entry);
6920
+ if (this.tileResultCache.size > this.tileResultCacheMaxSize) {
6921
+ const oldestKey = this.tileResultCache.keys().next().value;
6922
+ if (oldestKey) {
6923
+ const evicted = this.tileResultCache.get(oldestKey);
6924
+ if (evicted && !evicted.settled)
6925
+ evicted.controller.abort();
6926
+ this.tileResultCache.delete(oldestKey);
6927
+ }
6928
+ }
6929
+ }
6930
+ deleteTileResult(key) {
6931
+ return this.tileResultCache.delete(key);
6932
+ }
6933
+ clearTileResultCache() {
6934
+ for (const entry of this.tileResultCache.values()) {
6935
+ if (!entry.settled)
6936
+ entry.controller.abort();
6937
+ }
6938
+ this.tileResultCache.clear();
6939
+ }
6940
+ // Raster cache methods
6941
+ getRaster(key) {
6942
+ const p = this.rasterCache.get(key);
6943
+ if (!p)
6944
+ return undefined;
6945
+ // LRU touch
6946
+ this.rasterCache.delete(key);
6947
+ this.rasterCache.set(key, p);
6948
+ return p;
6949
+ }
6950
+ setRaster(key, p) {
6951
+ this.rasterCache.set(key, p);
6952
+ p.catch(() => this.rasterCache.delete(key));
6953
+ if (this.rasterCache.size > this.rasterCacheMaxSize) {
6954
+ const oldestKey = this.rasterCache.keys().next().value;
6955
+ if (oldestKey)
6956
+ this.rasterCache.delete(oldestKey);
6957
+ }
6958
+ }
6959
+ deleteRaster(key) {
6960
+ return this.rasterCache.delete(key);
6961
+ }
6962
+ clearRasterCache() {
6963
+ this.rasterCache.clear();
6964
+ }
6965
+ // Relief mask cache methods
6966
+ getReliefMask(key) {
6967
+ const p = this.reliefMaskCache.get(key);
6968
+ if (!p)
6969
+ return undefined;
6970
+ // LRU touch
6971
+ this.reliefMaskCache.delete(key);
6972
+ this.reliefMaskCache.set(key, p);
6973
+ return p;
6974
+ }
6975
+ setReliefMask(key, p) {
6976
+ this.reliefMaskCache.set(key, p);
6977
+ p.catch(() => this.reliefMaskCache.delete(key));
6978
+ if (this.reliefMaskCache.size > this.reliefMaskCacheMaxSize) {
6979
+ const oldestKey = this.reliefMaskCache.keys().next().value;
6980
+ if (oldestKey)
6981
+ this.reliefMaskCache.delete(oldestKey);
6982
+ }
6983
+ }
6984
+ deleteReliefMask(key) {
6985
+ return this.reliefMaskCache.delete(key);
6986
+ }
6987
+ clearReliefMaskCache() {
6988
+ this.reliefMaskCache.clear();
6989
+ }
6990
+ // Image cache methods
6991
+ getImage(index) {
6992
+ return this.imageCache.get(index);
6993
+ }
6994
+ setImage(index, p) {
6995
+ this.imageCache.set(index, p);
6996
+ }
6997
+ deleteImage(index) {
6998
+ return this.imageCache.delete(index);
6999
+ }
7000
+ clearImageCache() {
7001
+ this.imageCache.clear();
7002
+ }
7003
+ // Clear everything
7004
+ clearAll() {
7005
+ this.clearTileResultCache();
7006
+ this.clearRasterCache();
7007
+ this.clearReliefMaskCache();
7008
+ this.clearImageCache();
7009
+ }
7010
+ }
7011
+
7012
+ class TileReader {
7013
+ options;
7014
+ tileSize;
7015
+ constructor(params) {
7016
+ this.options = params.options;
7017
+ this.tileSize = params.tileSize;
7018
+ }
7019
+ createEmptyTile(size) {
7020
+ const s = size || this.tileSize;
7021
+ const channels = this.options.numOfChannels || 1;
7022
+ const totalSize = s * s * channels;
7023
+ const tileData = new Float32Array(totalSize);
7024
+ if (this.options.noDataValue !== undefined) {
7025
+ tileData.fill(this.options.noDataValue);
7026
+ }
7027
+ return tileData;
7028
+ }
7029
+ createTileBuffer(dataType, tileSize, multiplier = 1) {
7030
+ const length = tileSize * tileSize * multiplier;
7031
+ switch (dataType) {
7032
+ case 'UInt8':
7033
+ return new Uint8Array(length);
7034
+ case 'Int8':
7035
+ return new Int8Array(length);
7036
+ case 'UInt16':
7037
+ return new Uint16Array(length);
7038
+ case 'Int16':
7039
+ return new Int16Array(length);
7040
+ case 'UInt32':
7041
+ return new Uint32Array(length);
7042
+ case 'Int32':
7043
+ return new Int32Array(length);
7044
+ case 'Float32':
7045
+ return new Float32Array(length);
7046
+ case 'Float64':
7047
+ return new Float64Array(length);
7048
+ default:
7049
+ // eslint-disable-next-line no-console
7050
+ console.warn(`Unsupported data type: ${dataType}, defaulting to Float32`);
7051
+ return new Float32Array(length);
7052
+ }
7053
+ }
7054
+ }
7055
+
6739
7056
  const CogTilesGeoImageOptionsDefaults = {
6740
7057
  blurredTexture: true,
6741
7058
  };
@@ -6757,34 +7074,8 @@ class CogTiles {
6757
7074
  // and individual tile cancellations (e.g. from panning) do not poison other callers.
6758
7075
  // Once the promise settles (resolved or rejected), controller/callerCount are irrelevant;
6759
7076
  // 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.
6776
- rasterCache = new Map();
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}`;
6784
- }
6785
- // Cache GeoTIFFImage Promises by index to prevent redundant HTTP requests from geotiff 3.0.4+ eager loading
6786
- // Stores Promises (not resolved values) so concurrent requests share the same getImage() call
6787
- imageCache = new Map();
7077
+ cache = new TileCacheManager();
7078
+ tileReader;
6788
7079
  // Store initialization promise to prevent concurrent duplicate initializations
6789
7080
  initializePromise;
6790
7081
  // Track the last successfully initialized URL to detect URL changes
@@ -6801,10 +7092,7 @@ class CogTiles {
6801
7092
  // Fully reset COG-derived state when the URL changes so the instance can be
6802
7093
  // safely reinitialized against a different source.
6803
7094
  if (this.lastInitializedUrl !== undefined && this.lastInitializedUrl !== url) {
6804
- this.clearTileResultCache();
6805
- this.rasterCache.clear();
6806
- this.reliefMaskCache.clear();
6807
- this.imageCache.clear();
7095
+ this.cache.clearAll();
6808
7096
  this.cog = undefined;
6809
7097
  this.cogOrigin = [0, 0];
6810
7098
  this.cogZoomLookup = [];
@@ -6830,15 +7118,15 @@ class CogTiles {
6830
7118
  const blockSize = this.options.blockSize ?? 65536;
6831
7119
  this.cog = await fromUrl(url, { blockSize });
6832
7120
  const imagePromise = this.cog.getImage();
6833
- this.imageCache.set(0, imagePromise); // Cache base image (index 0) to avoid re-fetching during getTileFromImage
7121
+ this.cache.setImage(0, imagePromise); // Cache base image (index 0) to avoid re-fetching during getTileFromImage
6834
7122
  const image = await imagePromise;
6835
7123
  const fileDirectory = image.fileDirectory;
6836
7124
  this.cogOrigin = image.getOrigin();
6837
- this.options.noDataValue ??= await this.getNoDataValue(image);
6838
- this.options.format ??= await this.getDataTypeFromTags(fileDirectory);
7125
+ this.options.noDataValue ??= getNoDataValue(image);
7126
+ this.options.format ??= await getDataTypeFromTags(fileDirectory);
6839
7127
  this.options.numOfChannels = fileDirectory.getValue('SamplesPerPixel');
6840
7128
  this.options.planarConfig = fileDirectory.getValue('PlanarConfiguration');
6841
- [this.cogZoomLookup, this.cogResolutionLookup] = await this.buildCogZoomResolutionLookup(this.cog);
7129
+ [this.cogZoomLookup, this.cogResolutionLookup] = await buildCogZoomResolutionLookup(this.cog);
6842
7130
  // Only compute quantized meshMaxError lookup for terrain COGs
6843
7131
  if (this.options.type === 'terrain') {
6844
7132
  this.computeMeshMaxErrorLookup();
@@ -6849,8 +7137,13 @@ class CogTiles {
6849
7137
  throw new Error('GeoTIFF Error: The provided image is not tiled. '
6850
7138
  + 'Please use "rio cogeo create --web-optimized" to fix this.');
6851
7139
  }
6852
- this.zoomRange = this.calculateZoomRange(this.tileSize, image.getResolution()[0], await this.cog.getImageCount());
6853
- this.bounds = this.calculateBoundsAsLatLon(image.getBoundingBox());
7140
+ this.zoomRange = calculateZoomRange(this.tileSize, image.getResolution()[0], await this.cog.getImageCount());
7141
+ this.bounds = calculateBoundsAsLatLon(image.getBoundingBox());
7142
+ // Initialize TileReader with buffer utilities
7143
+ this.tileReader = new TileReader({
7144
+ options: this.options,
7145
+ tileSize: this.tileSize,
7146
+ });
6854
7147
  // Mark initialization complete for this URL (used to detect URL changes)
6855
7148
  this.lastInitializedUrl = url;
6856
7149
  }
@@ -6867,60 +7160,9 @@ class CogTiles {
6867
7160
  getZoomRange() {
6868
7161
  return this.zoomRange;
6869
7162
  }
6870
- calculateZoomRange(tileSize, resolution, imgCount) {
6871
- const maxZoom = this.getZoomLevelFromResolution(tileSize, resolution);
6872
- const minZoom = maxZoom - (imgCount - 1);
6873
- return [minZoom, maxZoom];
6874
- }
6875
- calculateBoundsAsLatLon(bbox) {
6876
- const minX = Math.min(bbox[0], bbox[2]);
6877
- const maxX = Math.max(bbox[0], bbox[2]);
6878
- const minY = Math.min(bbox[1], bbox[3]);
6879
- const maxY = Math.max(bbox[1], bbox[3]);
6880
- const minXYDeg = this.getLatLon([minX, minY]);
6881
- const maxXYDeg = this.getLatLon([maxX, maxY]);
6882
- return [minXYDeg[0], minXYDeg[1], maxXYDeg[0], maxXYDeg[1]];
6883
- }
6884
- getZoomLevelFromResolution(tileSize, resolution) {
6885
- return Math.round(Math.log2(EARTH_CIRCUMFERENCE / (resolution * tileSize)));
6886
- }
6887
7163
  getBoundsAsLatLon() {
6888
7164
  return this.bounds;
6889
7165
  }
6890
- getLatLon(input) {
6891
- const x = input[0];
6892
- const y = input[1];
6893
- const lon = (x / EARTH_HALF_CIRCUMFERENCE) * 180;
6894
- let lat = (y / EARTH_HALF_CIRCUMFERENCE) * 180;
6895
- lat = (180 / Math.PI) * (2 * Math.atan(Math.exp((lat * Math.PI) / 180)) - Math.PI / 2);
6896
- return [lon, lat];
6897
- }
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
7166
  /**
6925
7167
  * Gets the auto meshMaxError for a given overview index.
6926
7168
  * Returns undefined if auto lookup has not been computed.
@@ -6928,46 +7170,6 @@ class CogTiles {
6928
7170
  getMeshMaxErrorForImageIndex(imageIndex) {
6929
7171
  return this.cogMeshMaxErrorLookup[imageIndex];
6930
7172
  }
6931
- /**
6932
- * Builds lookup tables for zoom levels and estimated resolutions from a Cloud Optimized GeoTIFF (COG) object.
6933
- *
6934
- * It is assumed that inn web mapping, COG data is visualized in the Web Mercator coordinate system.
6935
- * At zoom level 0, the Web Mercator resolution is defined by the constant `webMercatorRes0`
6936
- * (e.g., 156543.03125 m/pixel). At each subsequent zoom level, this resolution is halved.
6937
- *
6938
- * This function calculates, for each image (overview) in the COG, its estimated resolution and
6939
- * corresponding zoom level based on the base image's resolution and width.
6940
- *
6941
- * @param {object} cog - A Cloud Optimized GeoTIFF object loaded via geotiff.js.
6942
- * @returns {Promise<[number[], number[]]>} A promise resolving to a tuple of two arrays:
6943
- * - The first array (`zoomLookup`) maps each image index to its computed zoom level.
6944
- * - The second array (`resolutionLookup`) maps each image index to its estimated resolution (m/pixel).
6945
- */
6946
- async buildCogZoomResolutionLookup(cog) {
6947
- // Retrieve the total number of images (overviews) in the COG.
6948
- const imageCount = await cog.getImageCount();
6949
- // Use the first image as the base reference.
6950
- const baseImage = await cog.getImage(0);
6951
- const baseResolution = baseImage.getResolution()[0]; // Resolution (m/pixel) of the base image.
6952
- const baseWidth = baseImage.getWidth();
6953
- // Initialize arrays to store the zoom level and resolution for each image.
6954
- const zoomLookup = [];
6955
- const resolutionLookup = [];
6956
- // Iterate over each image (overview) in the COG.
6957
- for (let idx = 0; idx < imageCount; idx++) {
6958
- const image = await cog.getImage(idx);
6959
- const width = image.getWidth();
6960
- // Calculate the scale factor relative to the base image.
6961
- const scaleFactor = baseWidth / width;
6962
- const estimatedResolution = baseResolution * scaleFactor;
6963
- // Calculate the zoom level using the Web Mercator resolution standard:
6964
- // webMercatorRes0 is the resolution at zoom level 0; each zoom level halves the resolution.
6965
- const zoomLevel = Math.round(Math.log2(webMercatorRes0 / estimatedResolution));
6966
- zoomLookup[idx] = zoomLevel;
6967
- resolutionLookup[idx] = estimatedResolution;
6968
- }
6969
- return [zoomLookup, resolutionLookup];
6970
- }
6971
7173
  /**
6972
7174
  * Computes dynamic meshMaxError values for each overview based on COG resolution and zoom level.
6973
7175
  * Called only for terrain COGs after buildCogZoomResolutionLookup() completes.
@@ -6979,7 +7181,7 @@ class CogTiles {
6979
7181
  const maxZ = this.cogZoomLookup[0];
6980
7182
  this.cogMeshMaxErrorLookup = this.cogResolutionLookup.map((resolution, idx) => {
6981
7183
  const zoom = this.cogZoomLookup[idx];
6982
- return this.calculateDynamicMeshMaxError(zoom, resolution, minZ, maxZ);
7184
+ return calculateDynamicMeshMaxError(zoom, resolution, minZ, maxZ);
6983
7185
  });
6984
7186
  }
6985
7187
  /**
@@ -6994,29 +7196,7 @@ class CogTiles {
6994
7196
  * @returns {number} The index of the image in the COG that best matches the specified zoom level.
6995
7197
  */
6996
7198
  getImageIndexForZoomLevel(zoom) {
6997
- // Retrieve the minimum and maximum zoom levels from the lookup table.
6998
- const minZoom = this.cogZoomLookup[this.cogZoomLookup.length - 1];
6999
- const maxZoom = this.cogZoomLookup[0];
7000
- if (zoom > maxZoom)
7001
- return 0;
7002
- if (zoom < minZoom)
7003
- return this.cogZoomLookup.length - 1;
7004
- // For zoom levels within the available range, find the exact or closest matching index.
7005
- const exactMatchIndex = this.cogZoomLookup.indexOf(zoom);
7006
- if (exactMatchIndex !== -1) {
7007
- return exactMatchIndex;
7008
- }
7009
- // No exact match: find the closest zoom level
7010
- let closestIndex = 0;
7011
- let minDistance = Math.abs(this.cogZoomLookup[0] - zoom);
7012
- for (let i = 1; i < this.cogZoomLookup.length; i += 1) {
7013
- const distance = Math.abs(this.cogZoomLookup[i] - zoom);
7014
- if (distance < minDistance) {
7015
- minDistance = distance;
7016
- closestIndex = i;
7017
- }
7018
- }
7019
- return closestIndex;
7199
+ return getImageIndexForZoomLevel(zoom, this.cogZoomLookup);
7020
7200
  }
7021
7201
  async getTileFromImage(tileX, tileY, zoom, fetchSize, signal) {
7022
7202
  // Create a fresh local AbortController for this specific fetch.
@@ -7033,10 +7213,10 @@ class CogTiles {
7033
7213
  try {
7034
7214
  const imageIndex = this.getImageIndexForZoomLevel(zoom);
7035
7215
  // Cache Promises to share in-flight requests across concurrent tiles at the same overview
7036
- let imagePromise = this.imageCache.get(imageIndex);
7216
+ let imagePromise = this.cache.getImage(imageIndex);
7037
7217
  if (!imagePromise) {
7038
7218
  imagePromise = this.cog.getImage(imageIndex);
7039
- this.imageCache.set(imageIndex, imagePromise);
7219
+ this.cache.setImage(imageIndex, imagePromise);
7040
7220
  }
7041
7221
  const targetImage = await imagePromise;
7042
7222
  // --- STEP 1: CALCULATE BOUNDS IN METERS ---
@@ -7079,7 +7259,7 @@ class CogTiles {
7079
7259
  const readHeight = validReadMaxY - validReadY;
7080
7260
  // CHECK: If no overlap, return empty
7081
7261
  if (readWidth <= 0 || readHeight <= 0) {
7082
- return [this.createEmptyTile(FETCH_SIZE)];
7262
+ return [this.tileReader.createEmptyTile(FETCH_SIZE)];
7083
7263
  }
7084
7264
  // 8. Calculate Offsets (Padding)
7085
7265
  // "missingLeft" is how many blank pixels we need to insert before the image data starts.
@@ -7095,7 +7275,7 @@ class CogTiles {
7095
7275
  if (missingLeft > 0 || missingTop > 0 || readWidth < FETCH_SIZE || readHeight < FETCH_SIZE) {
7096
7276
  const numChannels = this.options.numOfChannels || 1;
7097
7277
  // Initialize with a TypedArray of the full target size and correct data type
7098
- const validImageData = this.createTileBuffer(this.options.format || 'Float32', FETCH_SIZE, numChannels);
7278
+ const validImageData = this.tileReader.createTileBuffer(this.options.format || 'Float32', FETCH_SIZE, numChannels);
7099
7279
  if (this.options.noDataValue !== undefined) {
7100
7280
  validImageData.fill(this.options.noDataValue);
7101
7281
  }
@@ -7104,7 +7284,7 @@ class CogTiles {
7104
7284
  // Place the valid pixel data into the tile buffer.
7105
7285
  for (let band = 0; band < validRasterData.length; band += 1) {
7106
7286
  // We must reset the buffer for each band, otherwise data from previous band persists in padding areas
7107
- const tileBuffer = this.createTileBuffer(this.options.format || 'Float32', FETCH_SIZE);
7287
+ const tileBuffer = this.tileReader.createTileBuffer(this.options.format || 'Float32', FETCH_SIZE);
7108
7288
  if (this.options.noDataValue !== undefined) {
7109
7289
  tileBuffer.fill(this.options.noDataValue);
7110
7290
  }
@@ -7167,20 +7347,6 @@ class CogTiles {
7167
7347
  throw error;
7168
7348
  }
7169
7349
  }
7170
- /**
7171
- * Creates a blank tile buffer filled with the "No Data" value.
7172
- * @param size The width/height of the square tile (e.g., 256 or 257)
7173
- */
7174
- createEmptyTile(size) {
7175
- const s = size || this.tileSize; // Defaults to 256
7176
- const channels = this.options.numOfChannels || 1;
7177
- const totalSize = s * s * channels;
7178
- const tileData = new Float32Array(totalSize);
7179
- if (this.options.noDataValue !== undefined) {
7180
- tileData.fill(this.options.noDataValue);
7181
- }
7182
- return tileData;
7183
- }
7184
7350
  async getTile(x, y, z, bounds, meshMaxError, signal, skipTexture) {
7185
7351
  // cellSizeMeters is derived purely from tile coordinates — compute once for all paths
7186
7352
  const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 0.5) / Math.pow(2, z))));
@@ -7198,258 +7364,223 @@ class CogTiles {
7198
7364
  else {
7199
7365
  resolvedMeshMaxError = meshMaxError ?? 4.0;
7200
7366
  }
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
7367
  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;
7368
+ return this.getTerrainTile(x, y, z, bounds, resolvedMeshMaxError, cellSizeMeters, signal, skipTexture);
7369
+ }
7370
+ if (isGlaze) {
7371
+ return this.getGlazeTile(x, y, z, bounds, cellSizeMeters, meshMaxError, signal);
7372
+ }
7373
+ return this.getBitmapTile(x, y, z, bounds, cellSizeMeters, meshMaxError, signal);
7374
+ }
7375
+ async getTerrainTile(x, y, z, bounds, resolvedMeshMaxError, cellSizeMeters, signal, skipTexture) {
7376
+ const skipTextureFlag = skipTexture ?? this.options.skipTexture ?? false;
7377
+ const cacheKey = this.cache.getTileResultCacheKey(x, y, z, resolvedMeshMaxError, skipTextureFlag);
7378
+ const existing = this.cache.getTileResult(cacheKey);
7379
+ if (existing) {
7380
+ if (existing.settled) {
7229
7381
  if (signal?.aborted)
7230
7382
  return null;
7231
- return result;
7383
+ return existing.promise;
7232
7384
  }
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
- }
7385
+ existing.callerCount += 1;
7386
+ if (signal && !signal.aborted) {
7387
+ signal.addEventListener('abort', () => {
7388
+ existing.callerCount -= 1;
7389
+ if (existing.callerCount <= 0 && !existing.settled) {
7390
+ existing.controller.abort();
7391
+ }
7392
+ }, { once: true });
7393
+ }
7394
+ const result = await existing.promise;
7395
+ if (signal?.aborted)
7396
+ return null;
7397
+ return result;
7398
+ }
7399
+ const controller = new AbortController();
7400
+ const pipeline = (async () => {
7401
+ const isKernel = this.options.useSlope || this.options.useHillshade || this.options.useSwissRelief;
7402
+ const requiredSize = this.tileSize + (isKernel ? 2 : 1);
7403
+ const tileData = await this.getTileFromImage(x, y, z, requiredSize, controller.signal);
7404
+ // === Step F: detect all-noData tiles before tessellation ===
7405
+ const raster = tileData[0];
7406
+ const noData = this.options.noDataValue;
7407
+ if (noData !== undefined && raster) {
7408
+ const numChannels = this.options.numOfChannels || 1;
7409
+ let useChannelIndex = this.options.useChannelIndex ?? (this.options.useChannel ? (this.options.useChannel - 1) : 0);
7410
+ if (useChannelIndex == null)
7411
+ useChannelIndex = 0;
7412
+ const checkStrategy = this.options.noDataCheck ?? 'full';
7413
+ const width = requiredSize;
7414
+ const height = requiredSize;
7415
+ const isNoValue = (v) => isF32NoData(v, noData);
7416
+ let allNoData = true;
7417
+ if (checkStrategy === 'full') {
7418
+ // Full linear scan (safe)
7419
+ if (numChannels > 1) {
7420
+ for (let i = useChannelIndex; i < raster.length; i += numChannels) {
7421
+ const v = raster[i];
7422
+ if (!isNoValue(v)) {
7423
+ allNoData = false;
7424
+ break;
7260
7425
  }
7261
7426
  }
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
- }
7427
+ }
7428
+ else {
7429
+ for (let i = 0; i < raster.length; i++) {
7430
+ const v = raster[i];
7431
+ if (!isNoValue(v)) {
7432
+ allNoData = false;
7433
+ break;
7269
7434
  }
7270
7435
  }
7271
7436
  }
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
7437
+ }
7438
+ else if (checkStrategy === 'border+center') {
7439
+ // Border scan: iterate over top/bottom rows and left/right cols
7440
+ const stepX = numChannels;
7441
+ // Top row
7442
+ for (let x = 0; x < width; x++) {
7443
+ const idx = x * stepX + useChannelIndex;
7444
+ const v = raster[idx];
7445
+ if (!isNoValue(v)) {
7446
+ allNoData = false;
7447
+ break;
7448
+ }
7449
+ }
7450
+ // Bottom row
7451
+ if (allNoData) {
7276
7452
  for (let x = 0; x < width; x++) {
7277
- const idx = x * stepX + useChannelIndex;
7453
+ const idx = ((height - 1) * width + x) * stepX + useChannelIndex;
7278
7454
  const v = raster[idx];
7279
7455
  if (!isNoValue(v)) {
7280
7456
  allNoData = false;
7281
7457
  break;
7282
7458
  }
7283
7459
  }
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
- }
7460
+ }
7461
+ // Left/Right cols
7462
+ if (allNoData) {
7463
+ for (let y = 1; y < height - 1; y++) {
7464
+ const leftIdx = (y * width) * stepX + useChannelIndex;
7465
+ const rightIdx = (y * width + (width - 1)) * stepX + useChannelIndex;
7466
+ const vl = raster[leftIdx];
7467
+ const vr = raster[rightIdx];
7468
+ if (!isNoValue(vl) || !isNoValue(vr)) {
7469
+ allNoData = false;
7470
+ break;
7324
7471
  }
7325
7472
  }
7326
7473
  }
7327
- else {
7328
- // Unknown strategy — fallback to full
7329
- for (let i = 0; i < raster.length; i++) {
7330
- const v = raster[i];
7474
+ // Center probe + 4 quadrant probes
7475
+ if (allNoData) {
7476
+ const probes = [
7477
+ [Math.floor(width / 2), Math.floor(height / 2)],
7478
+ [Math.floor(width / 4), Math.floor(height / 4)],
7479
+ [Math.floor((3 * width) / 4), Math.floor(height / 4)],
7480
+ [Math.floor(width / 4), Math.floor((3 * height) / 4)],
7481
+ [Math.floor((3 * width) / 4), Math.floor((3 * height) / 4)],
7482
+ ];
7483
+ for (const [px, py] of probes) {
7484
+ const idx = (py * width + px) * stepX + useChannelIndex;
7485
+ const v = raster[idx];
7331
7486
  if (!isNoValue(v)) {
7332
7487
  allNoData = false;
7333
7488
  break;
7334
7489
  }
7335
7490
  }
7336
7491
  }
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
7492
  }
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();
7493
+ else {
7494
+ // Unknown strategy — fallback to full
7495
+ for (let i = 0; i < raster.length; i++) {
7496
+ const v = raster[i];
7497
+ if (!isNoValue(v)) {
7498
+ allNoData = false;
7499
+ break;
7500
+ }
7367
7501
  }
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
7502
  }
7380
- }
7381
- try {
7382
- const result = await pipeline;
7383
- entry.settled = true;
7384
- if (signal?.aborted)
7503
+ if (allNoData) {
7504
+ // Do not cache all-noData result; remove cache entry so future requests re-evaluate if COG/metadata changes.
7505
+ this.cache.deleteTileResult(cacheKey);
7385
7506
  return null;
7386
- return result;
7387
- }
7388
- catch (error) {
7389
- entry.settled = true;
7390
- this.tileResultCache.delete(cacheKey);
7391
- throw error;
7392
- }
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
7507
  }
7419
7508
  }
7420
- if (signal?.aborted)
7421
- return null;
7422
- const reliefMask = await maskPromise;
7423
- if (signal?.aborted)
7424
- return null;
7509
+ // Create generator options with skipTextureFlag applied (don't mutate shared this.options)
7510
+ const generatorOptions = {
7511
+ ...this.options,
7512
+ skipTexture: skipTextureFlag,
7513
+ };
7425
7514
  return this.geo.getMap({
7426
- rasters: [reliefMask],
7427
- width: this.tileSize,
7428
- height: this.tileSize,
7515
+ rasters: [tileData[0]],
7516
+ width: requiredSize,
7517
+ height: requiredSize,
7429
7518
  bounds: bounds ?? [0, 0, 0, 0],
7430
7519
  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);
7520
+ }, generatorOptions, resolvedMeshMaxError);
7521
+ })();
7522
+ const entry = {
7523
+ promise: pipeline,
7524
+ controller,
7525
+ callerCount: 1,
7526
+ settled: false,
7527
+ };
7528
+ if (signal && !signal.aborted) {
7529
+ signal.addEventListener('abort', () => {
7530
+ entry.callerCount -= 1;
7531
+ if (entry.callerCount <= 0 && !entry.settled) {
7532
+ entry.controller.abort();
7533
+ }
7534
+ }, { once: true });
7535
+ }
7536
+ entry.promise = pipeline;
7537
+ this.cache.setTileResult(cacheKey, entry);
7538
+ try {
7539
+ const result = await pipeline;
7540
+ entry.settled = true;
7541
+ if (signal?.aborted)
7542
+ return null;
7543
+ return result;
7443
7544
  }
7545
+ catch (error) {
7546
+ entry.settled = true;
7547
+ this.cache.deleteTileResult(cacheKey);
7548
+ throw error;
7549
+ }
7550
+ }
7551
+ async getGlazeTile(x, y, z, bounds, cellSizeMeters, meshMaxError, signal) {
7552
+ const maskKey = this.cache.getTileCacheKey(x, y, z);
7553
+ let maskPromise = this.cache.getReliefMask(maskKey);
7554
+ if (!maskPromise) {
7555
+ const controller = new AbortController();
7556
+ maskPromise = (async () => {
7557
+ const tileData = await this.getTileFromImage(x, y, z, this.tileSize + 2, controller.signal);
7558
+ return ReliefCompositor.composeSwissRelief(tileData[0], this.options, cellSizeMeters, this.tileSize, this.tileSize);
7559
+ })();
7560
+ this.cache.setReliefMask(maskKey, maskPromise);
7561
+ maskPromise.catch(() => this.cache.deleteReliefMask(maskKey));
7562
+ }
7563
+ if (signal?.aborted)
7564
+ return null;
7565
+ const reliefMask = await maskPromise;
7566
+ if (signal?.aborted)
7567
+ return null;
7568
+ return this.geo.getMap({
7569
+ rasters: [reliefMask],
7570
+ width: this.tileSize,
7571
+ height: this.tileSize,
7572
+ bounds: bounds ?? [0, 0, 0, 0],
7573
+ cellSizeMeters,
7574
+ }, this.options, meshMaxError ?? 4.0);
7575
+ }
7576
+ async getBitmapTile(x, y, z, bounds, cellSizeMeters, meshMaxError, signal) {
7577
+ const rasterKey = this.cache.getTileCacheKey(x, y, z);
7578
+ let rasterPromise = this.cache.getRaster(rasterKey);
7444
7579
  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
- }
7580
+ const controller = new AbortController();
7581
+ rasterPromise = this.getTileFromImage(x, y, z, this.tileSize, controller.signal);
7582
+ this.cache.setRaster(rasterKey, rasterPromise);
7583
+ rasterPromise.catch(() => this.cache.deleteRaster(rasterKey));
7453
7584
  }
7454
7585
  if (signal?.aborted)
7455
7586
  return null;
@@ -7464,111 +7595,9 @@ class CogTiles {
7464
7595
  cellSizeMeters,
7465
7596
  }, this.options, meshMaxError ?? 4.0);
7466
7597
  }
7467
- /**
7468
- * Determines the data type (e.g., "Int32", "Float64") of a GeoTIFF image
7469
- * by reading its TIFF tags.
7470
- *
7471
- * @param {GeoTIFFImage} image - A GeoTIFF.js image.
7472
- * @returns {Promise<string>} - A string representing the data type.
7473
- */
7474
- async getDataTypeFromTags(fileDirectory) {
7475
- const hasSampleFormat = fileDirectory.hasTag('SampleFormat');
7476
- const hasBitsPerSample = fileDirectory.hasTag('BitsPerSample');
7477
- if (!hasSampleFormat || !hasBitsPerSample) {
7478
- console.warn("Missing SampleFormat or BitsPerSample tags, defaulting to UInt8");
7479
- return 'UInt8';
7480
- }
7481
- // In GeoTIFF, BitsPerSample (tag 258) and SampleFormat (tag 339) provide the type info.
7482
- // They can be either a single number or an array if there are multiple samples.
7483
- const sampleFormat = fileDirectory.getValue('SampleFormat'); // Tag 339
7484
- const bitsPerSample = fileDirectory.getValue('BitsPerSample'); // Tag 258
7485
- // If multiple bands exist, we assume all bands share the same type.
7486
- const format = (sampleFormat && typeof sampleFormat.length === 'number' && sampleFormat.length > 0)
7487
- ? sampleFormat[0]
7488
- : sampleFormat;
7489
- const bits = (bitsPerSample && typeof bitsPerSample.length === 'number' && bitsPerSample.length > 0)
7490
- ? bitsPerSample[0]
7491
- : bitsPerSample;
7492
- let typePrefix;
7493
- // 1 = Unsigned Integer, 2 = Signed Integer, 3 = Floating Point
7494
- if (format === 1) {
7495
- typePrefix = 'UInt';
7496
- }
7497
- else if (format === 2) {
7498
- typePrefix = 'Int';
7499
- }
7500
- else if (format === 3) {
7501
- typePrefix = 'Float';
7502
- }
7503
- else {
7504
- typePrefix = 'Unknown';
7505
- }
7506
- return `${typePrefix}${bits}`;
7507
- }
7508
- /**
7509
- * Extracts the noData value from a GeoTIFF.js image.
7510
- * Returns the noData value as a number (including NaN) if available, otherwise undefined.
7511
- *
7512
- * @param {GeoTIFFImage} image - The GeoTIFF.js image.
7513
- * @returns {number|undefined} The noData value, possibly NaN, or undefined if not set or invalid.
7514
- */
7515
- getNoDataValue(image) {
7516
- const noDataRaw = image.getGDALNoData();
7517
- if (noDataRaw === undefined || noDataRaw === null) {
7518
- /* eslint-disable no-console */
7519
- console.warn('No noData value defined — raster might be rendered incorrectly.');
7520
- return undefined;
7521
- }
7522
- const cleaned = String(noDataRaw).replace(/\0/g, '').trim();
7523
- if (cleaned === '') {
7524
- /* eslint-disable no-console */
7525
- console.warn('noData value is an empty string after cleanup.');
7526
- return undefined;
7527
- }
7528
- const parsed = Number(cleaned);
7529
- // Allow NaN if explicitly declared
7530
- if (cleaned.toLowerCase() === 'nan') {
7531
- return NaN;
7532
- }
7533
- // If not declared as "nan" and still parsed to NaN, it's an error
7534
- if (Number.isNaN(parsed)) {
7535
- /* eslint-disable no-console */
7536
- console.warn(`Failed to parse numeric noData value: '${cleaned}'`);
7537
- return undefined;
7538
- }
7539
- return parsed;
7540
- }
7541
- /**
7542
- * Creates a tile buffer of the specified size using a typed array corresponding to the provided data type.
7543
- *
7544
- * @param {string} dataType - A string specifying the data type (e.g., "Int32", "Float64", "UInt16", etc.).
7545
- * @param {number} tileSize - The width/height of the square tile.
7546
- * @param {number} multiplier - Optional multiplier for interleaved buffers (e.g., numChannels).
7547
- * @returns {TypedArray} A typed array buffer of length (tileSize * tileSize * multiplier).
7548
- */
7549
- createTileBuffer(dataType, tileSize, multiplier = 1) {
7550
- const length = tileSize * tileSize * multiplier;
7551
- switch (dataType) {
7552
- case 'UInt8':
7553
- return new Uint8Array(length);
7554
- case 'Int8':
7555
- return new Int8Array(length);
7556
- case 'UInt16':
7557
- return new Uint16Array(length);
7558
- case 'Int16':
7559
- return new Int16Array(length);
7560
- case 'UInt32':
7561
- return new Uint32Array(length);
7562
- case 'Int32':
7563
- return new Int32Array(length);
7564
- case 'Float32':
7565
- return new Float32Array(length);
7566
- case 'Float64':
7567
- return new Float64Array(length);
7568
- default:
7569
- console.warn(`Unsupported data type: ${dataType}, defaulting to Float32`);
7570
- return new Float32Array(length);
7571
- }
7598
+ // Expose legacy API for clearing tile result cache (used by external layers)
7599
+ clearTileResultCache() {
7600
+ this.cache.clearTileResultCache();
7572
7601
  }
7573
7602
  }
7574
7603
 
@@ -7879,10 +7908,10 @@ class CogTerrainLayer extends CompositeLayer {
7879
7908
  }
7880
7909
  if (!this.state.isTiled && shouldReload) ;
7881
7910
  // Update the useChannel option for terrainCogTiles when terrainOptions.useChannel changes.
7882
- if (props?.terrainOptions?.useChannel !== oldProps.terrainOptions?.useChannel) {
7911
+ if (props?.terrainOptions?.useChannel !== oldProps.terrainOptions?.useChannel && this.state.terrainCogTiles) {
7883
7912
  this.state.terrainCogTiles.options.useChannel = props.terrainOptions.useChannel;
7884
- // Trigger a refresh of the tiles
7885
- this.state.terrainCogTiles.options.useChannelIndex = null; // Clear cached index
7913
+ this.state.terrainCogTiles.options.useChannelIndex = null; // Clear derived channel index
7914
+ this.state.terrainCogTiles.clearTileResultCache(); // Invalidate cached tiles from previous channel
7886
7915
  }
7887
7916
  // Update skipTexture when wireframe/operation/disableTexture changes so cache keys are correct
7888
7917
  const newSkipTexture = !!(props?.wireframe || props?.operation === 'terrain' || props?.disableTexture);
@@ -8040,6 +8069,7 @@ class CogTerrainLayer extends CompositeLayer {
8040
8069
  elevationDecoder,
8041
8070
  terrainCogTiles: this.state.terrainCogTiles,
8042
8071
  skipTexture: !!(this.props.wireframe || this.props.operation === 'terrain' || this.props.disableTexture),
8072
+ useChannel: this.props.terrainOptions?.useChannel,
8043
8073
  },
8044
8074
  renderSubLayers: {
8045
8075
  disableTexture: this.props.disableTexture,