@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 +526 -496
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/index.min.js +2 -2
- package/dist/cjs/index.min.js.map +1 -1
- package/dist/esm/index.js +526 -496
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.min.js +2 -2
- package/dist/esm/index.min.js.map +1 -1
- package/dist/esm/types/core/CogTiles.d.ts +9 -79
- package/dist/esm/types/core/lib/TileCacheManager.d.ts +41 -0
- package/dist/esm/types/core/lib/TileReader.d.ts +11 -0
- package/dist/esm/types/utils/geo.d.ts +8 -0
- package/dist/esm/types/utils/lod.d.ts +8 -0
- package/dist/esm/types/utils/tiffUtils.d.ts +11 -0
- package/package.json +1 -1
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
|
-
|
|
6761
|
-
|
|
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.
|
|
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.
|
|
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 ??=
|
|
6838
|
-
this.options.format ??= await
|
|
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
|
|
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 =
|
|
6853
|
-
this.bounds =
|
|
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
|
|
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
|
-
|
|
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.
|
|
7216
|
+
let imagePromise = this.cache.getImage(imageIndex);
|
|
7037
7217
|
if (!imagePromise) {
|
|
7038
7218
|
imagePromise = this.cog.getImage(imageIndex);
|
|
7039
|
-
this.
|
|
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
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
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
|
|
7383
|
+
return existing.promise;
|
|
7232
7384
|
}
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
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
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
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
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
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
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
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
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
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
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
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
|
-
|
|
7382
|
-
|
|
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
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
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: [
|
|
7427
|
-
width:
|
|
7428
|
-
height:
|
|
7515
|
+
rasters: [tileData[0]],
|
|
7516
|
+
width: requiredSize,
|
|
7517
|
+
height: requiredSize,
|
|
7429
7518
|
bounds: bounds ?? [0, 0, 0, 0],
|
|
7430
7519
|
cellSizeMeters,
|
|
7431
|
-
},
|
|
7432
|
-
}
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
if (
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
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
|
-
|
|
7446
|
-
this.
|
|
7447
|
-
|
|
7448
|
-
|
|
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
|
-
|
|
7469
|
-
|
|
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
|
-
|
|
7885
|
-
this.state.terrainCogTiles.
|
|
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,
|