@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/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
|
-
|
|
6763
|
-
|
|
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.
|
|
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.
|
|
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 ??=
|
|
6840
|
-
this.options.format ??= await
|
|
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
|
|
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 =
|
|
6855
|
-
this.bounds =
|
|
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
|
|
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
|
-
|
|
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.
|
|
7218
|
+
let imagePromise = this.cache.getImage(imageIndex);
|
|
7039
7219
|
if (!imagePromise) {
|
|
7040
7220
|
imagePromise = this.cog.getImage(imageIndex);
|
|
7041
|
-
this.
|
|
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
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
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
|
|
7385
|
+
return existing.promise;
|
|
7234
7386
|
}
|
|
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
|
-
|
|
7260
|
-
|
|
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
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
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
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
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
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
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
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
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
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
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
|
-
|
|
7384
|
-
|
|
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
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
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: [
|
|
7429
|
-
width:
|
|
7430
|
-
height:
|
|
7517
|
+
rasters: [tileData[0]],
|
|
7518
|
+
width: requiredSize,
|
|
7519
|
+
height: requiredSize,
|
|
7431
7520
|
bounds: bounds ?? [0, 0, 0, 0],
|
|
7432
7521
|
cellSizeMeters,
|
|
7433
|
-
},
|
|
7434
|
-
}
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
if (
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
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
|
-
|
|
7448
|
-
this.
|
|
7449
|
-
|
|
7450
|
-
|
|
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
|
-
|
|
7471
|
-
|
|
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
|
-
|
|
7887
|
-
this.state.terrainCogTiles.
|
|
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,
|