@walkthru-earth/objex 1.2.1 → 1.3.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/README.md +6 -3
- package/dist/components/layout/ConnectionDialog.svelte +35 -3
- package/dist/components/layout/Sidebar.svelte +1 -2
- package/dist/components/viewers/CodeViewer.svelte +51 -14
- package/dist/components/viewers/CodeViewer.svelte.d.ts +11 -1
- package/dist/components/viewers/CogControls.svelte +151 -22
- package/dist/components/viewers/CogControls.svelte.d.ts +5 -1
- package/dist/components/viewers/CogViewer.svelte +75 -8
- package/dist/components/viewers/MultiCogViewer.svelte +416 -0
- package/dist/components/viewers/MultiCogViewer.svelte.d.ts +9 -0
- package/dist/components/viewers/StacMapViewer.svelte +19 -5
- package/dist/components/viewers/StacMapViewer.svelte.d.ts +1 -0
- package/dist/components/viewers/StacMosaicViewer.svelte +785 -0
- package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +9 -0
- package/dist/components/viewers/StacTabViewer.svelte +254 -0
- package/dist/components/viewers/StacTabViewer.svelte.d.ts +13 -0
- package/dist/components/viewers/ViewerRouter.svelte +155 -2
- package/dist/components/viewers/ViewerRouter.svelte.d.ts +1 -1
- package/dist/components/viewers/ZarrMapViewer.svelte +143 -4
- package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +8 -2
- package/dist/components/viewers/ZarrViewer.svelte +1 -0
- package/dist/i18n/ar.js +27 -0
- package/dist/i18n/en.js +27 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/query/stac-geoparquet.d.ts +31 -0
- package/dist/query/stac-geoparquet.js +136 -0
- package/dist/stores/connections.svelte.d.ts +38 -23
- package/dist/stores/connections.svelte.js +105 -114
- package/dist/utils/cog-pure.d.ts +25 -0
- package/dist/utils/cog-pure.js +35 -0
- package/dist/utils/cog.d.ts +88 -43
- package/dist/utils/cog.js +192 -152
- package/dist/utils/colormap-sprite.d.ts +39 -0
- package/dist/utils/colormap-sprite.js +77 -0
- package/dist/utils/connection-identity.d.ts +51 -0
- package/dist/utils/connection-identity.js +97 -0
- package/dist/utils/host-detection.js +48 -302
- package/dist/utils/parquet-metadata.d.ts +7 -1
- package/dist/utils/parquet-metadata.js +35 -1
- package/dist/utils/stac-geoparquet.d.ts +90 -0
- package/dist/utils/stac-geoparquet.js +223 -0
- package/dist/utils/stac-hydrate.d.ts +38 -0
- package/dist/utils/stac-hydrate.js +243 -0
- package/dist/utils/stac.d.ts +136 -0
- package/dist/utils/stac.js +176 -0
- package/dist/utils/storage-url.d.ts +26 -0
- package/dist/utils/storage-url.js +164 -28
- package/dist/utils/zarr.d.ts +34 -0
- package/dist/utils/zarr.js +94 -0
- package/package.json +14 -13
package/dist/utils/cog.js
CHANGED
|
@@ -1,103 +1,37 @@
|
|
|
1
1
|
import { inferRenderPipeline } from '@developmentseed/deck.gl-geotiff';
|
|
2
|
-
import { LinearRescale } from '@developmentseed/deck.gl-raster/gpu-modules';
|
|
2
|
+
import { Colormap, FilterNoDataVal, LinearRescale } from '@developmentseed/deck.gl-raster/gpu-modules';
|
|
3
3
|
import loadEpsg from '@developmentseed/epsg/all';
|
|
4
4
|
import epsgCsvUrl from '@developmentseed/epsg/all.csv.gz?url';
|
|
5
5
|
import { GeoTIFF } from '@developmentseed/geotiff';
|
|
6
6
|
import { parseWkt } from '@developmentseed/proj';
|
|
7
7
|
import proj4Lib from 'proj4';
|
|
8
|
+
import { buildDataTypeLabel, clampBounds, SF_LABELS, safeClamp } from './cog-pure.js';
|
|
9
|
+
import { COLORMAP_INDEX, getColormapTexture } from './colormap-sprite.js';
|
|
10
|
+
export { buildDataTypeLabel, clampBounds, SF_LABELS, safeClamp };
|
|
8
11
|
// ─── Constants ───────────────────────────────────────────────────
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
[200, 170, 60],
|
|
29
|
-
[180, 120, 50],
|
|
30
|
-
[140, 90, 40],
|
|
31
|
-
[200, 200, 200],
|
|
32
|
-
[255, 255, 255]
|
|
33
|
-
],
|
|
34
|
-
viridis: [
|
|
35
|
-
[68, 1, 84],
|
|
36
|
-
[72, 36, 117],
|
|
37
|
-
[64, 67, 135],
|
|
38
|
-
[52, 94, 141],
|
|
39
|
-
[33, 145, 140],
|
|
40
|
-
[43, 176, 127],
|
|
41
|
-
[95, 201, 97],
|
|
42
|
-
[186, 222, 39],
|
|
43
|
-
[253, 231, 37]
|
|
44
|
-
],
|
|
45
|
-
magma: [
|
|
46
|
-
[0, 0, 4],
|
|
47
|
-
[22, 11, 57],
|
|
48
|
-
[67, 15, 98],
|
|
49
|
-
[114, 24, 114],
|
|
50
|
-
[161, 48, 104],
|
|
51
|
-
[206, 82, 83],
|
|
52
|
-
[237, 132, 62],
|
|
53
|
-
[251, 192, 75],
|
|
54
|
-
[252, 253, 191]
|
|
55
|
-
],
|
|
56
|
-
turbo: [
|
|
57
|
-
[48, 18, 59],
|
|
58
|
-
[31, 82, 188],
|
|
59
|
-
[23, 158, 227],
|
|
60
|
-
[47, 212, 161],
|
|
61
|
-
[121, 238, 104],
|
|
62
|
-
[193, 241, 57],
|
|
63
|
-
[245, 206, 27],
|
|
64
|
-
[253, 141, 31],
|
|
65
|
-
[213, 47, 24]
|
|
66
|
-
],
|
|
67
|
-
spectral: [
|
|
68
|
-
[158, 1, 66],
|
|
69
|
-
[213, 62, 79],
|
|
70
|
-
[244, 109, 67],
|
|
71
|
-
[253, 174, 97],
|
|
72
|
-
[254, 224, 139],
|
|
73
|
-
[255, 255, 191],
|
|
74
|
-
[230, 245, 152],
|
|
75
|
-
[171, 221, 164],
|
|
76
|
-
[94, 79, 162]
|
|
77
|
-
]
|
|
12
|
+
/**
|
|
13
|
+
* Patches a GLSL ES 3.00 compile error in `@developmentseed/deck.gl-raster`
|
|
14
|
+
* v0.6.0-alpha.1. The `Colormap` shader module injects
|
|
15
|
+
* `uniform sampler2DArray colormapTexture;` without a precision qualifier,
|
|
16
|
+
* which the Apple-GPU path of luma.gl's WebGL2 backend rejects with
|
|
17
|
+
* `ERROR: 'sampler2DArray' : No precision specified`. In GLSL ES 3.00,
|
|
18
|
+
* every sampler type other than `sampler2D`/`samplerCube` needs explicit
|
|
19
|
+
* precision in fragment shaders.
|
|
20
|
+
*
|
|
21
|
+
* Chain this module immediately BEFORE `Colormap` in the renderPipeline so
|
|
22
|
+
* the combined `fs:#decl` inject emits the precision declaration first,
|
|
23
|
+
* then the sampler uniform. Remove once upstream ships the precision fix.
|
|
24
|
+
*/
|
|
25
|
+
const Sampler2DArrayPrecision = {
|
|
26
|
+
name: 'sampler2darray-precision',
|
|
27
|
+
fs: '',
|
|
28
|
+
inject: {
|
|
29
|
+
'fs:#decl': 'precision highp sampler2DArray;\n'
|
|
30
|
+
}
|
|
78
31
|
};
|
|
79
|
-
/** Interpolate a normalized value (0..1) into an RGB color from a ramp. */
|
|
80
|
-
export function interpolateRamp(stops, t) {
|
|
81
|
-
const n = stops.length - 1;
|
|
82
|
-
const idx = Math.max(0, Math.min(n, t * n));
|
|
83
|
-
const lo = Math.floor(idx);
|
|
84
|
-
const hi = Math.min(lo + 1, n);
|
|
85
|
-
const f = idx - lo;
|
|
86
|
-
return [
|
|
87
|
-
Math.round(stops[lo][0] + f * (stops[hi][0] - stops[lo][0])),
|
|
88
|
-
Math.round(stops[lo][1] + f * (stops[hi][1] - stops[lo][1])),
|
|
89
|
-
Math.round(stops[lo][2] + f * (stops[hi][2] - stops[lo][2]))
|
|
90
|
-
];
|
|
91
|
-
}
|
|
92
|
-
/** Generate a CSS linear-gradient string for a color ramp. */
|
|
93
|
-
export function rampToGradientCss(id) {
|
|
94
|
-
const stops = COLOR_RAMP_STOPS[id];
|
|
95
|
-
const colors = stops.map((s, i) => `rgb(${s[0]},${s[1]},${s[2]}) ${((i / (stops.length - 1)) * 100).toFixed(0)}%`);
|
|
96
|
-
return `linear-gradient(to right, ${colors.join(', ')})`;
|
|
97
|
-
}
|
|
98
32
|
/** Create a sensible default band config based on COG metadata. */
|
|
99
33
|
export function defaultBandConfig(bandCount, sampleFormat) {
|
|
100
|
-
if (bandCount >= 3) {
|
|
34
|
+
if (bandCount >= 3 && bandCount <= 4) {
|
|
101
35
|
return {
|
|
102
36
|
mode: 'rgb',
|
|
103
37
|
rBand: 0,
|
|
@@ -150,6 +84,12 @@ export function needsCustomPipelineForConfig(geotiff, config) {
|
|
|
150
84
|
const tags = geotiff.cachedTags;
|
|
151
85
|
const sf = tags.sampleFormat;
|
|
152
86
|
const isUint = sf !== null && sf[0] === 1;
|
|
87
|
+
// GPU textures accept 1-4 channels; COGs with more samples per pixel (embeddings,
|
|
88
|
+
// hyperspectral, multi-band features) must route through the CPU pipeline which
|
|
89
|
+
// reads selected band indices and bakes RGBA. Otherwise the library throws
|
|
90
|
+
// "Unsupported SamplesPerPixel N" in verifySamplesPerPixel.
|
|
91
|
+
if (geotiff.count > 4)
|
|
92
|
+
return true;
|
|
153
93
|
if (!isUint)
|
|
154
94
|
return true;
|
|
155
95
|
// Palette-indexed uint COGs with an embedded ColorMap tag are auto-rendered
|
|
@@ -165,6 +105,12 @@ export function needsCustomPipelineForConfig(geotiff, config) {
|
|
|
165
105
|
return true;
|
|
166
106
|
if (config.rBand !== 0 || config.gBand !== 1 || config.bBand !== 2)
|
|
167
107
|
return true;
|
|
108
|
+
// 4-band uint (e.g. NAIP RGB+NIR) must route through the CPU pipeline.
|
|
109
|
+
// The library default maps all 4 samples to RGBA, turning the extra band
|
|
110
|
+
// into a variable alpha channel even when it is not an alpha declaration.
|
|
111
|
+
// The custom pipeline explicitly picks 3 bands and bakes alpha=255.
|
|
112
|
+
if (geotiff.count === 4)
|
|
113
|
+
return true;
|
|
168
114
|
return false;
|
|
169
115
|
}
|
|
170
116
|
export const DEFAULT_RESCALE = { min: 0, max: 1 };
|
|
@@ -215,6 +161,27 @@ export function createRescaledPipeline(geotiff, rescale) {
|
|
|
215
161
|
}
|
|
216
162
|
};
|
|
217
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Build a `renderPipeline` array for `MultiCOGLayer` / raster mosaics.
|
|
166
|
+
* Combines optional `FilterNoDataVal` + `LinearRescale` stages in the order
|
|
167
|
+
* the GPU expects (no-data mask first, then rescale).
|
|
168
|
+
*/
|
|
169
|
+
export function buildBandRenderPipeline(opts = {}) {
|
|
170
|
+
const modules = [];
|
|
171
|
+
if (opts.noDataVal !== undefined && opts.noDataVal !== null) {
|
|
172
|
+
modules.push({
|
|
173
|
+
module: FilterNoDataVal,
|
|
174
|
+
props: { noDataVal: opts.noDataVal }
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (opts.rescale && isRescaleActive(opts.rescale)) {
|
|
178
|
+
modules.push({
|
|
179
|
+
module: LinearRescale,
|
|
180
|
+
props: { rescaleMin: opts.rescale.min, rescaleMax: opts.rescale.max }
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return modules;
|
|
184
|
+
}
|
|
218
185
|
// ─── GeoTIFF normalization for COGLayer ──────────────────────────
|
|
219
186
|
// Web Mercator's safe latitude limit. EPSG:4326 bboxes outside ±85.051129° hit
|
|
220
187
|
// out-of-domain proj4 NaN when the library generates its tile matrix set.
|
|
@@ -275,13 +242,17 @@ export function selectCogPipeline(geotiff, opts = {}) {
|
|
|
275
242
|
if (useCustom && bandConfig) {
|
|
276
243
|
return {
|
|
277
244
|
getTileData: createConfigurableGetTileData(geotiff, bandConfig),
|
|
278
|
-
renderTile:
|
|
245
|
+
renderTile: buildCustomRenderTile(bandConfig, rescale)
|
|
279
246
|
};
|
|
280
247
|
}
|
|
281
248
|
if (useCustom) {
|
|
249
|
+
// Synthesize a single-band config so the GPU Colormap path still
|
|
250
|
+
// applies when a non-uint COG renders without a user-chosen ramp.
|
|
251
|
+
const fallbackSf = geotiff.cachedTags.sampleFormat?.[0] ?? 1;
|
|
252
|
+
const fallbackConfig = defaultBandConfig(geotiff.count, fallbackSf);
|
|
282
253
|
return {
|
|
283
254
|
getTileData: createCustomGetTileData(geotiff),
|
|
284
|
-
renderTile:
|
|
255
|
+
renderTile: buildCustomRenderTile(fallbackConfig, rescale)
|
|
285
256
|
};
|
|
286
257
|
}
|
|
287
258
|
if (rescale && isRescaleActive(rescale)) {
|
|
@@ -295,27 +266,9 @@ export function selectCogPipeline(geotiff, opts = {}) {
|
|
|
295
266
|
}
|
|
296
267
|
const BITMAP_SOURCE = 'geotiff-bitmap-src';
|
|
297
268
|
const BITMAP_LAYER = 'geotiff-bitmap-layer';
|
|
298
|
-
// ───
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return Number.isFinite(v) ? Math.max(lo, Math.min(hi, v)) : fallback;
|
|
302
|
-
}
|
|
303
|
-
/** Clamp geographic bounds to valid MapLibre web-Mercator range. */
|
|
304
|
-
export function clampBounds(b) {
|
|
305
|
-
return {
|
|
306
|
-
west: safeClamp(b.west, -180, 180, -180),
|
|
307
|
-
south: safeClamp(b.south, -85.051129, 85.051129, -85.051129),
|
|
308
|
-
east: safeClamp(b.east, -180, 180, 180),
|
|
309
|
-
north: safeClamp(b.north, -85.051129, 85.051129, 85.051129)
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Build a data-type label from GeoTIFF sample format and bits per sample.
|
|
314
|
-
* e.g. "uint8", "float32", "int16"
|
|
315
|
-
*/
|
|
316
|
-
export function buildDataTypeLabel(sampleFormat, bitsPerSample) {
|
|
317
|
-
return `${SF_LABELS[sampleFormat] ?? `sf${sampleFormat}`}${bitsPerSample ?? ''}`;
|
|
318
|
-
}
|
|
269
|
+
// ─── Types & pure helpers ────────────────────────────────────────
|
|
270
|
+
// `GeoBounds`, `CogInfo`, `safeClamp`, `clampBounds`, `buildDataTypeLabel`
|
|
271
|
+
// live in `./cog-pure.ts` and are re-exported at the top of this file.
|
|
319
272
|
// ─── Map helpers (depend on maplibre-gl) ─────────────────────────
|
|
320
273
|
/**
|
|
321
274
|
* Query the GPU's MAX_TEXTURE_SIZE from MapLibre's WebGL context.
|
|
@@ -624,12 +577,17 @@ export function needsCustomPipeline(geotiff) {
|
|
|
624
577
|
// sampleFormat is null or not uint → needs custom
|
|
625
578
|
return sf === null || sf[0] !== 1;
|
|
626
579
|
}
|
|
580
|
+
/** Number of histogram buckets produced by the CPU bake. */
|
|
581
|
+
export const HISTOGRAM_BIN_COUNT = 64;
|
|
627
582
|
/**
|
|
628
583
|
* Create custom getTileData for non-uint COGs.
|
|
629
584
|
* Reads band 0, normalizes using GDAL statistics / per-tile adaptive stretch,
|
|
630
|
-
*
|
|
585
|
+
* bakes a grayscale `r`-channel image so the GPU `Colormap` shader module
|
|
586
|
+
* (wired downstream by `selectCogPipeline`) can apply the ramp by sampling
|
|
587
|
+
* `colormaps.png`. Reserves `r = 0` for nodata so `FilterNoDataVal` can
|
|
588
|
+
* discard those fragments before the ramp sample.
|
|
631
589
|
*/
|
|
632
|
-
export function createCustomGetTileData(geotiff) {
|
|
590
|
+
export function createCustomGetTileData(geotiff, _opts = {}) {
|
|
633
591
|
// Read Scale/Offset TIFF tags (GDAL convention for scaled datasets like DEMs)
|
|
634
592
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
635
593
|
const tags = geotiff.cachedTags;
|
|
@@ -656,26 +614,37 @@ export function createCustomGetTileData(geotiff) {
|
|
|
656
614
|
globalMax = globalMax * (gdalScale ?? 1) + (gdalOffset ?? 0);
|
|
657
615
|
}
|
|
658
616
|
const bandCount = geotiff.count;
|
|
659
|
-
const sf = tags.sampleFormat?.[0] ?? 1;
|
|
660
617
|
const isSingleBand = bandCount === 1;
|
|
661
618
|
// Shared range across all tiles — when no GDAL stats exist, the first
|
|
662
619
|
// tile's scan seeds the range and subsequent tiles widen it. This
|
|
663
620
|
// eliminates visible seams between tiles caused by per-tile normalization.
|
|
664
621
|
let sharedMin = globalMin;
|
|
665
622
|
let sharedMax = globalMax;
|
|
623
|
+
// Resolve the sprite texture from the first tile's device; reuse per-device.
|
|
624
|
+
let texturePromise = null;
|
|
666
625
|
return async (image, options) => {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
626
|
+
if (isSingleBand && !texturePromise) {
|
|
627
|
+
texturePromise = getColormapTexture(options.device);
|
|
628
|
+
}
|
|
629
|
+
const [tile, colormapTexture] = await Promise.all([
|
|
630
|
+
image.fetchTile(options.x, options.y, {
|
|
631
|
+
boundless: false,
|
|
632
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
633
|
+
pool: options.pool,
|
|
634
|
+
signal: options.signal
|
|
635
|
+
}),
|
|
636
|
+
texturePromise ?? Promise.resolve(undefined)
|
|
637
|
+
]);
|
|
673
638
|
const arr = tile.array;
|
|
674
639
|
const { width, height, nodata } = arr;
|
|
675
640
|
const bandData = arr.layout === 'band-separate' ? arr.bands[0] : arr.data;
|
|
676
641
|
const pixelCount = width * height;
|
|
677
642
|
const scale = gdalScale ?? 1;
|
|
678
643
|
const offset = gdalOffset ?? 0;
|
|
644
|
+
// Allocate per-tile histogram so deck.gl's tile cache retains it with
|
|
645
|
+
// the tile object. The viewer sums histograms of visible tiles from
|
|
646
|
+
// TileLayer's `onViewportLoad` hook, no shared accumulator needed.
|
|
647
|
+
const histogram = isSingleBand ? new Uint32Array(HISTOGRAM_BIN_COUNT) : null;
|
|
679
648
|
// When no global stats, scan this tile and widen the shared range
|
|
680
649
|
if (sharedMin === null || sharedMax === null) {
|
|
681
650
|
let tMin = Infinity;
|
|
@@ -704,7 +673,10 @@ export function createCustomGetTileData(geotiff) {
|
|
|
704
673
|
const rangeMin = sharedMin;
|
|
705
674
|
const rangeMax = sharedMax;
|
|
706
675
|
const range = rangeMax - rangeMin || 1;
|
|
707
|
-
// Render to RGBA
|
|
676
|
+
// Render to RGBA. For single-band we bake normalized value into the
|
|
677
|
+
// `r` channel and reserve `r = 0` for nodata (see CustomTileData
|
|
678
|
+
// docs). Multi-band non-uint keeps the plain grayscale + α=255
|
|
679
|
+
// output so the default library pipeline consumes it unchanged.
|
|
708
680
|
const rgba = new Uint8ClampedArray(pixelCount * 4);
|
|
709
681
|
for (let i = 0; i < pixelCount; i++) {
|
|
710
682
|
const raw = bandData[i];
|
|
@@ -719,15 +691,18 @@ export function createCustomGetTileData(geotiff) {
|
|
|
719
691
|
}
|
|
720
692
|
const v = hasScaleOffset ? raw * scale + offset : raw;
|
|
721
693
|
const t = Math.max(0, Math.min(1, (v - rangeMin) / range));
|
|
722
|
-
if (isSingleBand
|
|
723
|
-
//
|
|
724
|
-
const
|
|
725
|
-
rgba[idx] =
|
|
726
|
-
rgba[idx + 1] =
|
|
727
|
-
rgba[idx + 2] =
|
|
694
|
+
if (isSingleBand) {
|
|
695
|
+
// Reserve r=0 for nodata; valid data maps to [1, 255].
|
|
696
|
+
const gray = 1 + Math.round(t * 254);
|
|
697
|
+
rgba[idx] = gray;
|
|
698
|
+
rgba[idx + 1] = 0;
|
|
699
|
+
rgba[idx + 2] = 0;
|
|
700
|
+
if (histogram) {
|
|
701
|
+
const bin = Math.min(HISTOGRAM_BIN_COUNT - 1, Math.floor(t * HISTOGRAM_BIN_COUNT));
|
|
702
|
+
histogram[bin]++;
|
|
703
|
+
}
|
|
728
704
|
}
|
|
729
705
|
else {
|
|
730
|
-
// Grayscale for multi-band or other types
|
|
731
706
|
const gray = Math.round(t * 255);
|
|
732
707
|
rgba[idx] = gray;
|
|
733
708
|
rgba[idx + 1] = gray;
|
|
@@ -738,18 +713,57 @@ export function createCustomGetTileData(geotiff) {
|
|
|
738
713
|
return {
|
|
739
714
|
imageData: new ImageData(rgba, width, height),
|
|
740
715
|
width,
|
|
741
|
-
height
|
|
716
|
+
height,
|
|
717
|
+
colormapTexture: isSingleBand ? colormapTexture : undefined,
|
|
718
|
+
nodataSentinel: isSingleBand ? 0 : undefined,
|
|
719
|
+
histogram: histogram ?? undefined
|
|
742
720
|
};
|
|
743
721
|
};
|
|
744
722
|
}
|
|
745
723
|
/**
|
|
746
|
-
* Custom renderTile for
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
* the
|
|
724
|
+
* Custom renderTile for COGs that use the CPU pipeline. For RGB mode (and
|
|
725
|
+
* legacy multi-band non-uint), the `image` slot carries a fully-baked RGBA
|
|
726
|
+
* `ImageData` and there is nothing to append on the GPU. For single-band
|
|
727
|
+
* mode, the image carries a normalized `r`-channel and this function
|
|
728
|
+
* appends `FilterNoDataVal` (to discard r=0 nodata sentinels), optional
|
|
729
|
+
* `LinearRescale` (brightness/contrast slider), and the sprite-based
|
|
730
|
+
* `Colormap` module so switching ramps is a uniform update — no tile
|
|
731
|
+
* re-decode required. The `colormapTexture` is stashed on `data` by the
|
|
732
|
+
* corresponding `getTileData` factory; if the sprite failed to resolve we
|
|
733
|
+
* fall back to the plain grayscale image.
|
|
750
734
|
*/
|
|
751
|
-
export function
|
|
752
|
-
return
|
|
735
|
+
export function buildCustomRenderTile(config, rescale) {
|
|
736
|
+
return (data) => {
|
|
737
|
+
if (config.mode === 'rgb' || !data.colormapTexture) {
|
|
738
|
+
return { image: data.imageData };
|
|
739
|
+
}
|
|
740
|
+
const colormapIndex = COLORMAP_INDEX[config.colorRamp] ?? COLORMAP_INDEX.viridis;
|
|
741
|
+
const pipeline = [
|
|
742
|
+
{
|
|
743
|
+
module: FilterNoDataVal,
|
|
744
|
+
props: { value: (data.nodataSentinel ?? 0) / 255 }
|
|
745
|
+
}
|
|
746
|
+
];
|
|
747
|
+
if (rescale && isRescaleActive(rescale)) {
|
|
748
|
+
pipeline.push({
|
|
749
|
+
module: LinearRescale,
|
|
750
|
+
props: { rescaleMin: rescale.min, rescaleMax: rescale.max }
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
pipeline.push(
|
|
754
|
+
// Precision shim must come before Colormap, its `fs:#decl` inject
|
|
755
|
+
// declares `precision highp sampler2DArray;` so the subsequent
|
|
756
|
+
// sampler uniform compiles on WebGL2 / Apple GPU.
|
|
757
|
+
{ module: Sampler2DArrayPrecision, props: {} }, {
|
|
758
|
+
module: Colormap,
|
|
759
|
+
props: {
|
|
760
|
+
colormapTexture: data.colormapTexture,
|
|
761
|
+
colormapIndex,
|
|
762
|
+
reversed: false
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
return { image: data.imageData, renderPipeline: pipeline };
|
|
766
|
+
};
|
|
753
767
|
}
|
|
754
768
|
// ─── Configurable custom pipeline ────────────────────────────────
|
|
755
769
|
/**
|
|
@@ -806,25 +820,40 @@ function computeBandRanges(bands, bandIndices, pixelCount, nodata) {
|
|
|
806
820
|
}
|
|
807
821
|
/**
|
|
808
822
|
* Create a configurable getTileData that respects BandConfig.
|
|
809
|
-
* Supports
|
|
823
|
+
* Supports RGB mode (multi-band → R,G,B with alpha=255, fully baked) and
|
|
824
|
+
* single-band mode (band N normalized into the `r` channel; the ramp is
|
|
825
|
+
* applied downstream by the GPU `Colormap` module via `buildCustomRenderTile`).
|
|
810
826
|
*/
|
|
811
|
-
export function createConfigurableGetTileData(geotiff, config) {
|
|
827
|
+
export function createConfigurableGetTileData(geotiff, config, _opts = {}) {
|
|
812
828
|
const bandCount = geotiff.count;
|
|
813
829
|
// Shared per-band ranges across tiles (seeded on first tile, widened by subsequent)
|
|
814
830
|
const sharedMins = new Map();
|
|
815
831
|
const sharedMaxs = new Map();
|
|
832
|
+
// Resolve the sprite texture from the first tile's device; reuse per-device.
|
|
833
|
+
let texturePromise = null;
|
|
816
834
|
return async (image, options) => {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
835
|
+
if (config.mode === 'single' && !texturePromise) {
|
|
836
|
+
texturePromise = getColormapTexture(options.device);
|
|
837
|
+
}
|
|
838
|
+
const [tile, colormapTexture] = await Promise.all([
|
|
839
|
+
image.fetchTile(options.x, options.y, {
|
|
840
|
+
boundless: false,
|
|
841
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
842
|
+
pool: options.pool,
|
|
843
|
+
signal: options.signal
|
|
844
|
+
}),
|
|
845
|
+
texturePromise ?? Promise.resolve(undefined)
|
|
846
|
+
]);
|
|
823
847
|
const arr = tile.array;
|
|
824
848
|
const { width, height, nodata } = arr;
|
|
825
849
|
const pixelCount = width * height;
|
|
826
850
|
const bands = extractBands(arr, bandCount, pixelCount);
|
|
827
851
|
const rgba = new Uint8ClampedArray(pixelCount * 4);
|
|
852
|
+
// Per-tile histogram, cached by deck.gl's tile cache with the tile
|
|
853
|
+
// object. Cloud-native by construction: at each zoom level, COG only
|
|
854
|
+
// decodes the overview tiles that cover the viewport, so the summed
|
|
855
|
+
// histogram naturally reflects "what the user is looking at right now".
|
|
856
|
+
const histogram = config.mode === 'single' ? new Uint32Array(HISTOGRAM_BIN_COUNT) : null;
|
|
828
857
|
if (config.mode === 'rgb') {
|
|
829
858
|
// RGB mode: map 3 bands to R, G, B
|
|
830
859
|
const indices = [config.rBand, config.gBand, config.bBand];
|
|
@@ -870,7 +899,9 @@ export function createConfigurableGetTileData(geotiff, config) {
|
|
|
870
899
|
}
|
|
871
900
|
}
|
|
872
901
|
else {
|
|
873
|
-
// Single-band mode: normalize
|
|
902
|
+
// Single-band mode: normalize the selected band into the `r`
|
|
903
|
+
// channel and reserve `r = 0` as a nodata sentinel that
|
|
904
|
+
// `FilterNoDataVal` discards before the `Colormap` GPU lookup.
|
|
874
905
|
const bi = config.band;
|
|
875
906
|
const bandData = bands[bi];
|
|
876
907
|
if (!sharedMins.has(bi) && bandData) {
|
|
@@ -881,7 +912,6 @@ export function createConfigurableGetTileData(geotiff, config) {
|
|
|
881
912
|
const rangeMin = sharedMins.get(bi) ?? 0;
|
|
882
913
|
const rangeMax = sharedMaxs.get(bi) ?? 1;
|
|
883
914
|
const range = rangeMax - rangeMin || 1;
|
|
884
|
-
const rampStops = COLOR_RAMP_STOPS[config.colorRamp];
|
|
885
915
|
for (let i = 0; i < pixelCount; i++) {
|
|
886
916
|
const raw = bandData?.[i] ?? 0;
|
|
887
917
|
const isND = (nodata !== null && raw === nodata) || !Number.isFinite(raw);
|
|
@@ -894,15 +924,25 @@ export function createConfigurableGetTileData(geotiff, config) {
|
|
|
894
924
|
}
|
|
895
925
|
else {
|
|
896
926
|
const t = Math.max(0, Math.min(1, (raw - rangeMin) / range));
|
|
897
|
-
|
|
898
|
-
rgba[idx] =
|
|
899
|
-
rgba[idx +
|
|
900
|
-
rgba[idx + 2] = b;
|
|
927
|
+
rgba[idx] = 1 + Math.round(t * 254);
|
|
928
|
+
rgba[idx + 1] = 0;
|
|
929
|
+
rgba[idx + 2] = 0;
|
|
901
930
|
rgba[idx + 3] = 255;
|
|
931
|
+
if (histogram) {
|
|
932
|
+
const bin = Math.min(HISTOGRAM_BIN_COUNT - 1, Math.floor(t * HISTOGRAM_BIN_COUNT));
|
|
933
|
+
histogram[bin]++;
|
|
934
|
+
}
|
|
902
935
|
}
|
|
903
936
|
}
|
|
904
937
|
}
|
|
905
|
-
return {
|
|
938
|
+
return {
|
|
939
|
+
imageData: new ImageData(rgba, width, height),
|
|
940
|
+
width,
|
|
941
|
+
height,
|
|
942
|
+
colormapTexture: config.mode === 'single' ? colormapTexture : undefined,
|
|
943
|
+
nodataSentinel: config.mode === 'single' ? 0 : undefined,
|
|
944
|
+
histogram: histogram ?? undefined
|
|
945
|
+
};
|
|
906
946
|
};
|
|
907
947
|
}
|
|
908
948
|
// ─── EPSG resolution via bundled database ────────────────────────
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared loader for the `colormaps.png` sprite shipped by
|
|
3
|
+
* `@developmentseed/deck.gl-raster`. Decodes the sprite once per session and
|
|
4
|
+
* uploads it to the GPU once per `Device`, so every COG / mosaic / multicog
|
|
5
|
+
* viewer that renders a single-band colormap shares the same 2D-array
|
|
6
|
+
* texture.
|
|
7
|
+
*
|
|
8
|
+
* The sprite is ~16 KB. Decode is fire-and-forget on first touch and caches
|
|
9
|
+
* the resulting `ImageData` for the life of the tab; texture upload runs
|
|
10
|
+
* synchronously once the device is known. Switching color ramps becomes a
|
|
11
|
+
* uniform update (`colormapIndex`) rather than a texture rebind.
|
|
12
|
+
*/
|
|
13
|
+
import { type ColormapName } from '@developmentseed/deck.gl-raster/gpu-modules';
|
|
14
|
+
import type { Device, Texture } from '@luma.gl/core';
|
|
15
|
+
export { COLORMAP_INDEX, type ColormapName } from '@developmentseed/deck.gl-raster/gpu-modules';
|
|
16
|
+
/** URL of the shipped sprite, consumable as a CSS `background-image`. */
|
|
17
|
+
export declare const COLORMAP_SPRITE_URL: any;
|
|
18
|
+
/** Number of distinct ramps encoded as 1-pixel-tall rows in the sprite. */
|
|
19
|
+
export declare const COLORMAP_SPRITE_LAYERS: number;
|
|
20
|
+
/** Width of each ramp row in pixels (also the sampling resolution). */
|
|
21
|
+
export declare const COLORMAP_SPRITE_WIDTH = 256;
|
|
22
|
+
/** All ramp names, sorted alphabetically (matches `COLORMAP_INDEX` key order). */
|
|
23
|
+
export declare const COLORMAP_NAMES: ColormapName[];
|
|
24
|
+
/** Decode the shipped sprite once per session. Safe to call repeatedly. */
|
|
25
|
+
export declare function loadColormapSprite(): Promise<ImageData>;
|
|
26
|
+
/**
|
|
27
|
+
* Get (or lazily build) the `sampler2DArray` colormap texture for a given
|
|
28
|
+
* device. Returns the same `Texture` on subsequent calls for the same device
|
|
29
|
+
* so it can be passed directly as the `colormapTexture` prop of the
|
|
30
|
+
* `Colormap` shader module.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getColormapTexture(device: Device): Promise<Texture>;
|
|
33
|
+
/**
|
|
34
|
+
* CSS `background` properties that render a single colormap row from the
|
|
35
|
+
* shipped sprite. Vertically scales the sprite so each 1-pixel row fills
|
|
36
|
+
* the container's full height, then offsets to land on the requested layer.
|
|
37
|
+
* Returns `undefined` for unknown ramp names so the caller can fall back.
|
|
38
|
+
*/
|
|
39
|
+
export declare function spriteBackgroundStyle(name: ColormapName, heightPx: number): string | undefined;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared loader for the `colormaps.png` sprite shipped by
|
|
3
|
+
* `@developmentseed/deck.gl-raster`. Decodes the sprite once per session and
|
|
4
|
+
* uploads it to the GPU once per `Device`, so every COG / mosaic / multicog
|
|
5
|
+
* viewer that renders a single-band colormap shares the same 2D-array
|
|
6
|
+
* texture.
|
|
7
|
+
*
|
|
8
|
+
* The sprite is ~16 KB. Decode is fire-and-forget on first touch and caches
|
|
9
|
+
* the resulting `ImageData` for the life of the tab; texture upload runs
|
|
10
|
+
* synchronously once the device is known. Switching color ramps becomes a
|
|
11
|
+
* uniform update (`colormapIndex`) rather than a texture rebind.
|
|
12
|
+
*/
|
|
13
|
+
import { COLORMAP_INDEX, createColormapTexture, decodeColormapSprite } from '@developmentseed/deck.gl-raster/gpu-modules';
|
|
14
|
+
// The `?url` suffix makes Vite emit the PNG as a fingerprinted asset and
|
|
15
|
+
// hand us back a string URL. Bundlers like Rollup/webpack behave the same
|
|
16
|
+
// for this pattern; the build define in `vite.config.ts` already copies the
|
|
17
|
+
// PNG into the production bundle.
|
|
18
|
+
import colormapsPngUrl from '@developmentseed/deck.gl-raster/gpu-modules/colormaps.png?url';
|
|
19
|
+
export { COLORMAP_INDEX } from '@developmentseed/deck.gl-raster/gpu-modules';
|
|
20
|
+
/** URL of the shipped sprite, consumable as a CSS `background-image`. */
|
|
21
|
+
export const COLORMAP_SPRITE_URL = colormapsPngUrl;
|
|
22
|
+
/** Number of distinct ramps encoded as 1-pixel-tall rows in the sprite. */
|
|
23
|
+
export const COLORMAP_SPRITE_LAYERS = Object.keys(COLORMAP_INDEX).length;
|
|
24
|
+
/** Width of each ramp row in pixels (also the sampling resolution). */
|
|
25
|
+
export const COLORMAP_SPRITE_WIDTH = 256;
|
|
26
|
+
/** All ramp names, sorted alphabetically (matches `COLORMAP_INDEX` key order). */
|
|
27
|
+
export const COLORMAP_NAMES = Object.keys(COLORMAP_INDEX).sort();
|
|
28
|
+
let spritePromise = null;
|
|
29
|
+
const textureCache = new WeakMap();
|
|
30
|
+
/** Decode the shipped sprite once per session. Safe to call repeatedly. */
|
|
31
|
+
export function loadColormapSprite() {
|
|
32
|
+
if (!spritePromise) {
|
|
33
|
+
spritePromise = (async () => {
|
|
34
|
+
const res = await fetch(colormapsPngUrl);
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new Error(`Failed to fetch colormaps.png: ${res.status}`);
|
|
37
|
+
}
|
|
38
|
+
const bytes = await res.arrayBuffer();
|
|
39
|
+
return decodeColormapSprite(bytes);
|
|
40
|
+
})();
|
|
41
|
+
}
|
|
42
|
+
return spritePromise;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get (or lazily build) the `sampler2DArray` colormap texture for a given
|
|
46
|
+
* device. Returns the same `Texture` on subsequent calls for the same device
|
|
47
|
+
* so it can be passed directly as the `colormapTexture` prop of the
|
|
48
|
+
* `Colormap` shader module.
|
|
49
|
+
*/
|
|
50
|
+
export async function getColormapTexture(device) {
|
|
51
|
+
const cached = textureCache.get(device);
|
|
52
|
+
if (cached)
|
|
53
|
+
return cached;
|
|
54
|
+
const imageData = await loadColormapSprite();
|
|
55
|
+
const texture = createColormapTexture(device, imageData);
|
|
56
|
+
textureCache.set(device, texture);
|
|
57
|
+
return texture;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* CSS `background` properties that render a single colormap row from the
|
|
61
|
+
* shipped sprite. Vertically scales the sprite so each 1-pixel row fills
|
|
62
|
+
* the container's full height, then offsets to land on the requested layer.
|
|
63
|
+
* Returns `undefined` for unknown ramp names so the caller can fall back.
|
|
64
|
+
*/
|
|
65
|
+
export function spriteBackgroundStyle(name, heightPx) {
|
|
66
|
+
const index = COLORMAP_INDEX[name];
|
|
67
|
+
if (index === undefined)
|
|
68
|
+
return undefined;
|
|
69
|
+
const totalHeight = COLORMAP_SPRITE_LAYERS * heightPx;
|
|
70
|
+
const yOffset = index * heightPx;
|
|
71
|
+
return [
|
|
72
|
+
`background-image: url("${COLORMAP_SPRITE_URL}")`,
|
|
73
|
+
'background-repeat: no-repeat',
|
|
74
|
+
`background-size: 100% ${totalHeight}px`,
|
|
75
|
+
`background-position: 0 -${yOffset}px`
|
|
76
|
+
].join('; ');
|
|
77
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical connection identity.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for deciding when two connection configs point at
|
|
5
|
+
* the same bucket. Used by the connections store to deduplicate auto-detect,
|
|
6
|
+
* manual add, and edit flows, so one physical bucket never ends up with
|
|
7
|
+
* multiple competing local records.
|
|
8
|
+
*
|
|
9
|
+
* Identity rules, per provider:
|
|
10
|
+
*
|
|
11
|
+
* azure → (provider, endpoint, bucket) endpoint carries the account
|
|
12
|
+
* gcs → (provider, bucket) GCS bucket names are global
|
|
13
|
+
* s3 → (provider, bucket, region) AWS native: same bucket name
|
|
14
|
+
* can exist in exactly one region,
|
|
15
|
+
* but the region is load-bearing
|
|
16
|
+
* for signing, so a paste with a
|
|
17
|
+
* different region is a distinct
|
|
18
|
+
* connection until the user merges
|
|
19
|
+
* other → (provider, endpoint, bucket) r2, b2, minio, wasabi, storj,
|
|
20
|
+
* digitalocean, contabo, hetzner,
|
|
21
|
+
* linode, ovhcloud, custom
|
|
22
|
+
*
|
|
23
|
+
* Endpoint normalization is aggressive: scheme + host + non-default port +
|
|
24
|
+
* pathname, with trailing slashes and default ports stripped, host lowercased.
|
|
25
|
+
* That collapses the common trip hazards — http vs https, :443 vs empty,
|
|
26
|
+
* trailing slash drift, mixed case host.
|
|
27
|
+
*/
|
|
28
|
+
import type { ProviderId } from '../storage/providers.js';
|
|
29
|
+
export interface ConnectionIdentityInput {
|
|
30
|
+
provider: string;
|
|
31
|
+
endpoint: string;
|
|
32
|
+
bucket: string;
|
|
33
|
+
region: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Normalize an endpoint URL to a canonical form suitable for equality checks.
|
|
37
|
+
* Empty / whitespace-only input returns `''` (the "no endpoint" sentinel).
|
|
38
|
+
* Non-URL strings are lowercased and stripped of trailing slashes as a best
|
|
39
|
+
* effort so the comparison is still deterministic.
|
|
40
|
+
*/
|
|
41
|
+
export declare function normalizeEndpoint(raw: string | undefined | null): string;
|
|
42
|
+
/** Collapse unknown / empty providers to `'s3'`; otherwise lowercase. */
|
|
43
|
+
export declare function normalizeProvider(provider: string | undefined | null): ProviderId;
|
|
44
|
+
/**
|
|
45
|
+
* Produce a canonical key for a connection's identity. Two connection
|
|
46
|
+
* configs with the same identity key point at the same physical bucket.
|
|
47
|
+
* Returns `''` when the config is too incomplete to identify a bucket.
|
|
48
|
+
*/
|
|
49
|
+
export declare function connectionIdentityKey(input: ConnectionIdentityInput): string;
|
|
50
|
+
/** Convenience: true when both inputs share the same non-empty identity. */
|
|
51
|
+
export declare function isSameConnectionIdentity(a: ConnectionIdentityInput, b: ConnectionIdentityInput): boolean;
|