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