@walkthru-earth/objex 1.2.1 → 1.3.0
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 +24 -7
- package/dist/components/viewers/MultiCogViewer.svelte +416 -0
- package/dist/components/viewers/MultiCogViewer.svelte.d.ts +9 -0
- package/dist/components/viewers/StacMapViewer.svelte +11 -5
- package/dist/components/viewers/StacMapViewer.svelte.d.ts +1 -0
- package/dist/components/viewers/StacMosaicViewer.svelte +699 -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.d.ts +80 -18
- package/dist/utils/cog.js +187 -125
- 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,11 +1,32 @@
|
|
|
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 { COLORMAP_INDEX, getColormapTexture } from './colormap-sprite.js';
|
|
8
9
|
// ─── Constants ───────────────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* Patches a GLSL ES 3.00 compile error in `@developmentseed/deck.gl-raster`
|
|
12
|
+
* v0.6.0-alpha.1. The `Colormap` shader module injects
|
|
13
|
+
* `uniform sampler2DArray colormapTexture;` without a precision qualifier,
|
|
14
|
+
* which the Apple-GPU path of luma.gl's WebGL2 backend rejects with
|
|
15
|
+
* `ERROR: 'sampler2DArray' : No precision specified`. In GLSL ES 3.00,
|
|
16
|
+
* every sampler type other than `sampler2D`/`samplerCube` needs explicit
|
|
17
|
+
* precision in fragment shaders.
|
|
18
|
+
*
|
|
19
|
+
* Chain this module immediately BEFORE `Colormap` in the renderPipeline so
|
|
20
|
+
* the combined `fs:#decl` inject emits the precision declaration first,
|
|
21
|
+
* then the sampler uniform. Remove once upstream ships the precision fix.
|
|
22
|
+
*/
|
|
23
|
+
const Sampler2DArrayPrecision = {
|
|
24
|
+
name: 'sampler2darray-precision',
|
|
25
|
+
fs: '',
|
|
26
|
+
inject: {
|
|
27
|
+
'fs:#decl': 'precision highp sampler2DArray;\n'
|
|
28
|
+
}
|
|
29
|
+
};
|
|
9
30
|
/** SampleFormat tag value → human label. */
|
|
10
31
|
export const SF_LABELS = {
|
|
11
32
|
1: 'uint',
|
|
@@ -15,89 +36,9 @@ export const SF_LABELS = {
|
|
|
15
36
|
5: 'complex int',
|
|
16
37
|
6: 'complex float'
|
|
17
38
|
};
|
|
18
|
-
export const COLOR_RAMP_STOPS = {
|
|
19
|
-
grayscale: [
|
|
20
|
-
[0, 0, 0],
|
|
21
|
-
[255, 255, 255]
|
|
22
|
-
],
|
|
23
|
-
terrain: [
|
|
24
|
-
[0, 0, 128],
|
|
25
|
-
[0, 100, 200],
|
|
26
|
-
[0, 154, 80],
|
|
27
|
-
[120, 180, 50],
|
|
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
|
-
]
|
|
78
|
-
};
|
|
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
39
|
/** Create a sensible default band config based on COG metadata. */
|
|
99
40
|
export function defaultBandConfig(bandCount, sampleFormat) {
|
|
100
|
-
if (bandCount >= 3) {
|
|
41
|
+
if (bandCount >= 3 && bandCount <= 4) {
|
|
101
42
|
return {
|
|
102
43
|
mode: 'rgb',
|
|
103
44
|
rBand: 0,
|
|
@@ -150,6 +91,12 @@ export function needsCustomPipelineForConfig(geotiff, config) {
|
|
|
150
91
|
const tags = geotiff.cachedTags;
|
|
151
92
|
const sf = tags.sampleFormat;
|
|
152
93
|
const isUint = sf !== null && sf[0] === 1;
|
|
94
|
+
// GPU textures accept 1-4 channels; COGs with more samples per pixel (embeddings,
|
|
95
|
+
// hyperspectral, multi-band features) must route through the CPU pipeline which
|
|
96
|
+
// reads selected band indices and bakes RGBA. Otherwise the library throws
|
|
97
|
+
// "Unsupported SamplesPerPixel N" in verifySamplesPerPixel.
|
|
98
|
+
if (geotiff.count > 4)
|
|
99
|
+
return true;
|
|
153
100
|
if (!isUint)
|
|
154
101
|
return true;
|
|
155
102
|
// Palette-indexed uint COGs with an embedded ColorMap tag are auto-rendered
|
|
@@ -165,6 +112,12 @@ export function needsCustomPipelineForConfig(geotiff, config) {
|
|
|
165
112
|
return true;
|
|
166
113
|
if (config.rBand !== 0 || config.gBand !== 1 || config.bBand !== 2)
|
|
167
114
|
return true;
|
|
115
|
+
// 4-band uint (e.g. NAIP RGB+NIR) must route through the CPU pipeline.
|
|
116
|
+
// The library default maps all 4 samples to RGBA, turning the extra band
|
|
117
|
+
// into a variable alpha channel even when it is not an alpha declaration.
|
|
118
|
+
// The custom pipeline explicitly picks 3 bands and bakes alpha=255.
|
|
119
|
+
if (geotiff.count === 4)
|
|
120
|
+
return true;
|
|
168
121
|
return false;
|
|
169
122
|
}
|
|
170
123
|
export const DEFAULT_RESCALE = { min: 0, max: 1 };
|
|
@@ -215,6 +168,27 @@ export function createRescaledPipeline(geotiff, rescale) {
|
|
|
215
168
|
}
|
|
216
169
|
};
|
|
217
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Build a `renderPipeline` array for `MultiCOGLayer` / raster mosaics.
|
|
173
|
+
* Combines optional `FilterNoDataVal` + `LinearRescale` stages in the order
|
|
174
|
+
* the GPU expects (no-data mask first, then rescale).
|
|
175
|
+
*/
|
|
176
|
+
export function buildBandRenderPipeline(opts = {}) {
|
|
177
|
+
const modules = [];
|
|
178
|
+
if (opts.noDataVal !== undefined && opts.noDataVal !== null) {
|
|
179
|
+
modules.push({
|
|
180
|
+
module: FilterNoDataVal,
|
|
181
|
+
props: { noDataVal: opts.noDataVal }
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (opts.rescale && isRescaleActive(opts.rescale)) {
|
|
185
|
+
modules.push({
|
|
186
|
+
module: LinearRescale,
|
|
187
|
+
props: { rescaleMin: opts.rescale.min, rescaleMax: opts.rescale.max }
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return modules;
|
|
191
|
+
}
|
|
218
192
|
// ─── GeoTIFF normalization for COGLayer ──────────────────────────
|
|
219
193
|
// Web Mercator's safe latitude limit. EPSG:4326 bboxes outside ±85.051129° hit
|
|
220
194
|
// out-of-domain proj4 NaN when the library generates its tile matrix set.
|
|
@@ -274,14 +248,20 @@ export function selectCogPipeline(geotiff, opts = {}) {
|
|
|
274
248
|
: needsCustomPipeline(geotiff);
|
|
275
249
|
if (useCustom && bandConfig) {
|
|
276
250
|
return {
|
|
277
|
-
getTileData: createConfigurableGetTileData(geotiff, bandConfig
|
|
278
|
-
|
|
251
|
+
getTileData: createConfigurableGetTileData(geotiff, bandConfig, {
|
|
252
|
+
onHistogram: opts.onHistogram
|
|
253
|
+
}),
|
|
254
|
+
renderTile: buildCustomRenderTile(bandConfig, rescale)
|
|
279
255
|
};
|
|
280
256
|
}
|
|
281
257
|
if (useCustom) {
|
|
258
|
+
// Synthesize a single-band config so the GPU Colormap path still
|
|
259
|
+
// applies when a non-uint COG renders without a user-chosen ramp.
|
|
260
|
+
const fallbackSf = geotiff.cachedTags.sampleFormat?.[0] ?? 1;
|
|
261
|
+
const fallbackConfig = defaultBandConfig(geotiff.count, fallbackSf);
|
|
282
262
|
return {
|
|
283
|
-
getTileData: createCustomGetTileData(geotiff),
|
|
284
|
-
renderTile:
|
|
263
|
+
getTileData: createCustomGetTileData(geotiff, { onHistogram: opts.onHistogram }),
|
|
264
|
+
renderTile: buildCustomRenderTile(fallbackConfig, rescale)
|
|
285
265
|
};
|
|
286
266
|
}
|
|
287
267
|
if (rescale && isRescaleActive(rescale)) {
|
|
@@ -624,12 +604,17 @@ export function needsCustomPipeline(geotiff) {
|
|
|
624
604
|
// sampleFormat is null or not uint → needs custom
|
|
625
605
|
return sf === null || sf[0] !== 1;
|
|
626
606
|
}
|
|
607
|
+
/** Number of histogram buckets produced by the CPU bake. */
|
|
608
|
+
export const HISTOGRAM_BIN_COUNT = 64;
|
|
627
609
|
/**
|
|
628
610
|
* Create custom getTileData for non-uint COGs.
|
|
629
611
|
* Reads band 0, normalizes using GDAL statistics / per-tile adaptive stretch,
|
|
630
|
-
*
|
|
612
|
+
* bakes a grayscale `r`-channel image so the GPU `Colormap` shader module
|
|
613
|
+
* (wired downstream by `selectCogPipeline`) can apply the ramp by sampling
|
|
614
|
+
* `colormaps.png`. Reserves `r = 0` for nodata so `FilterNoDataVal` can
|
|
615
|
+
* discard those fragments before the ramp sample.
|
|
631
616
|
*/
|
|
632
|
-
export function createCustomGetTileData(geotiff) {
|
|
617
|
+
export function createCustomGetTileData(geotiff, opts = {}) {
|
|
633
618
|
// Read Scale/Offset TIFF tags (GDAL convention for scaled datasets like DEMs)
|
|
634
619
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
635
620
|
const tags = geotiff.cachedTags;
|
|
@@ -656,20 +641,28 @@ export function createCustomGetTileData(geotiff) {
|
|
|
656
641
|
globalMax = globalMax * (gdalScale ?? 1) + (gdalOffset ?? 0);
|
|
657
642
|
}
|
|
658
643
|
const bandCount = geotiff.count;
|
|
659
|
-
const sf = tags.sampleFormat?.[0] ?? 1;
|
|
660
644
|
const isSingleBand = bandCount === 1;
|
|
661
645
|
// Shared range across all tiles — when no GDAL stats exist, the first
|
|
662
646
|
// tile's scan seeds the range and subsequent tiles widen it. This
|
|
663
647
|
// eliminates visible seams between tiles caused by per-tile normalization.
|
|
664
648
|
let sharedMin = globalMin;
|
|
665
649
|
let sharedMax = globalMax;
|
|
650
|
+
// Resolve the sprite texture from the first tile's device; reuse per-device.
|
|
651
|
+
let texturePromise = null;
|
|
652
|
+
const histogram = isSingleBand ? new Uint32Array(HISTOGRAM_BIN_COUNT) : null;
|
|
666
653
|
return async (image, options) => {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
654
|
+
if (isSingleBand && !texturePromise) {
|
|
655
|
+
texturePromise = getColormapTexture(options.device);
|
|
656
|
+
}
|
|
657
|
+
const [tile, colormapTexture] = await Promise.all([
|
|
658
|
+
image.fetchTile(options.x, options.y, {
|
|
659
|
+
boundless: false,
|
|
660
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
661
|
+
pool: options.pool,
|
|
662
|
+
signal: options.signal
|
|
663
|
+
}),
|
|
664
|
+
texturePromise ?? Promise.resolve(undefined)
|
|
665
|
+
]);
|
|
673
666
|
const arr = tile.array;
|
|
674
667
|
const { width, height, nodata } = arr;
|
|
675
668
|
const bandData = arr.layout === 'band-separate' ? arr.bands[0] : arr.data;
|
|
@@ -704,7 +697,10 @@ export function createCustomGetTileData(geotiff) {
|
|
|
704
697
|
const rangeMin = sharedMin;
|
|
705
698
|
const rangeMax = sharedMax;
|
|
706
699
|
const range = rangeMax - rangeMin || 1;
|
|
707
|
-
// Render to RGBA
|
|
700
|
+
// Render to RGBA. For single-band we bake normalized value into the
|
|
701
|
+
// `r` channel and reserve `r = 0` for nodata (see CustomTileData
|
|
702
|
+
// docs). Multi-band non-uint keeps the plain grayscale + α=255
|
|
703
|
+
// output so the default library pipeline consumes it unchanged.
|
|
708
704
|
const rgba = new Uint8ClampedArray(pixelCount * 4);
|
|
709
705
|
for (let i = 0; i < pixelCount; i++) {
|
|
710
706
|
const raw = bandData[i];
|
|
@@ -719,15 +715,18 @@ export function createCustomGetTileData(geotiff) {
|
|
|
719
715
|
}
|
|
720
716
|
const v = hasScaleOffset ? raw * scale + offset : raw;
|
|
721
717
|
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] =
|
|
718
|
+
if (isSingleBand) {
|
|
719
|
+
// Reserve r=0 for nodata; valid data maps to [1, 255].
|
|
720
|
+
const gray = 1 + Math.round(t * 254);
|
|
721
|
+
rgba[idx] = gray;
|
|
722
|
+
rgba[idx + 1] = 0;
|
|
723
|
+
rgba[idx + 2] = 0;
|
|
724
|
+
if (histogram) {
|
|
725
|
+
const bin = Math.min(HISTOGRAM_BIN_COUNT - 1, Math.floor(t * HISTOGRAM_BIN_COUNT));
|
|
726
|
+
histogram[bin]++;
|
|
727
|
+
}
|
|
728
728
|
}
|
|
729
729
|
else {
|
|
730
|
-
// Grayscale for multi-band or other types
|
|
731
730
|
const gray = Math.round(t * 255);
|
|
732
731
|
rgba[idx] = gray;
|
|
733
732
|
rgba[idx + 1] = gray;
|
|
@@ -735,21 +734,61 @@ export function createCustomGetTileData(geotiff) {
|
|
|
735
734
|
}
|
|
736
735
|
rgba[idx + 3] = 255;
|
|
737
736
|
}
|
|
737
|
+
if (histogram && opts.onHistogram)
|
|
738
|
+
opts.onHistogram(histogram);
|
|
738
739
|
return {
|
|
739
740
|
imageData: new ImageData(rgba, width, height),
|
|
740
741
|
width,
|
|
741
|
-
height
|
|
742
|
+
height,
|
|
743
|
+
colormapTexture: isSingleBand ? colormapTexture : undefined,
|
|
744
|
+
nodataSentinel: isSingleBand ? 0 : undefined
|
|
742
745
|
};
|
|
743
746
|
};
|
|
744
747
|
}
|
|
745
748
|
/**
|
|
746
|
-
* Custom renderTile for
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
* the
|
|
749
|
+
* Custom renderTile for COGs that use the CPU pipeline. For RGB mode (and
|
|
750
|
+
* legacy multi-band non-uint), the `image` slot carries a fully-baked RGBA
|
|
751
|
+
* `ImageData` and there is nothing to append on the GPU. For single-band
|
|
752
|
+
* mode, the image carries a normalized `r`-channel and this function
|
|
753
|
+
* appends `FilterNoDataVal` (to discard r=0 nodata sentinels), optional
|
|
754
|
+
* `LinearRescale` (brightness/contrast slider), and the sprite-based
|
|
755
|
+
* `Colormap` module so switching ramps is a uniform update — no tile
|
|
756
|
+
* re-decode required. The `colormapTexture` is stashed on `data` by the
|
|
757
|
+
* corresponding `getTileData` factory; if the sprite failed to resolve we
|
|
758
|
+
* fall back to the plain grayscale image.
|
|
750
759
|
*/
|
|
751
|
-
export function
|
|
752
|
-
return
|
|
760
|
+
export function buildCustomRenderTile(config, rescale) {
|
|
761
|
+
return (data) => {
|
|
762
|
+
if (config.mode === 'rgb' || !data.colormapTexture) {
|
|
763
|
+
return { image: data.imageData };
|
|
764
|
+
}
|
|
765
|
+
const colormapIndex = COLORMAP_INDEX[config.colorRamp] ?? COLORMAP_INDEX.viridis;
|
|
766
|
+
const pipeline = [
|
|
767
|
+
{
|
|
768
|
+
module: FilterNoDataVal,
|
|
769
|
+
props: { value: (data.nodataSentinel ?? 0) / 255 }
|
|
770
|
+
}
|
|
771
|
+
];
|
|
772
|
+
if (rescale && isRescaleActive(rescale)) {
|
|
773
|
+
pipeline.push({
|
|
774
|
+
module: LinearRescale,
|
|
775
|
+
props: { rescaleMin: rescale.min, rescaleMax: rescale.max }
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
pipeline.push(
|
|
779
|
+
// Precision shim must come before Colormap, its `fs:#decl` inject
|
|
780
|
+
// declares `precision highp sampler2DArray;` so the subsequent
|
|
781
|
+
// sampler uniform compiles on WebGL2 / Apple GPU.
|
|
782
|
+
{ module: Sampler2DArrayPrecision, props: {} }, {
|
|
783
|
+
module: Colormap,
|
|
784
|
+
props: {
|
|
785
|
+
colormapTexture: data.colormapTexture,
|
|
786
|
+
colormapIndex,
|
|
787
|
+
reversed: false
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
return { image: data.imageData, renderPipeline: pipeline };
|
|
791
|
+
};
|
|
753
792
|
}
|
|
754
793
|
// ─── Configurable custom pipeline ────────────────────────────────
|
|
755
794
|
/**
|
|
@@ -806,20 +845,31 @@ function computeBandRanges(bands, bandIndices, pixelCount, nodata) {
|
|
|
806
845
|
}
|
|
807
846
|
/**
|
|
808
847
|
* Create a configurable getTileData that respects BandConfig.
|
|
809
|
-
* Supports
|
|
848
|
+
* Supports RGB mode (multi-band → R,G,B with alpha=255, fully baked) and
|
|
849
|
+
* single-band mode (band N normalized into the `r` channel; the ramp is
|
|
850
|
+
* applied downstream by the GPU `Colormap` module via `buildCustomRenderTile`).
|
|
810
851
|
*/
|
|
811
|
-
export function createConfigurableGetTileData(geotiff, config) {
|
|
852
|
+
export function createConfigurableGetTileData(geotiff, config, opts = {}) {
|
|
812
853
|
const bandCount = geotiff.count;
|
|
813
854
|
// Shared per-band ranges across tiles (seeded on first tile, widened by subsequent)
|
|
814
855
|
const sharedMins = new Map();
|
|
815
856
|
const sharedMaxs = new Map();
|
|
857
|
+
// Resolve the sprite texture from the first tile's device; reuse per-device.
|
|
858
|
+
let texturePromise = null;
|
|
859
|
+
const histogram = config.mode === 'single' ? new Uint32Array(HISTOGRAM_BIN_COUNT) : null;
|
|
816
860
|
return async (image, options) => {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
861
|
+
if (config.mode === 'single' && !texturePromise) {
|
|
862
|
+
texturePromise = getColormapTexture(options.device);
|
|
863
|
+
}
|
|
864
|
+
const [tile, colormapTexture] = await Promise.all([
|
|
865
|
+
image.fetchTile(options.x, options.y, {
|
|
866
|
+
boundless: false,
|
|
867
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
868
|
+
pool: options.pool,
|
|
869
|
+
signal: options.signal
|
|
870
|
+
}),
|
|
871
|
+
texturePromise ?? Promise.resolve(undefined)
|
|
872
|
+
]);
|
|
823
873
|
const arr = tile.array;
|
|
824
874
|
const { width, height, nodata } = arr;
|
|
825
875
|
const pixelCount = width * height;
|
|
@@ -870,7 +920,9 @@ export function createConfigurableGetTileData(geotiff, config) {
|
|
|
870
920
|
}
|
|
871
921
|
}
|
|
872
922
|
else {
|
|
873
|
-
// Single-band mode: normalize
|
|
923
|
+
// Single-band mode: normalize the selected band into the `r`
|
|
924
|
+
// channel and reserve `r = 0` as a nodata sentinel that
|
|
925
|
+
// `FilterNoDataVal` discards before the `Colormap` GPU lookup.
|
|
874
926
|
const bi = config.band;
|
|
875
927
|
const bandData = bands[bi];
|
|
876
928
|
if (!sharedMins.has(bi) && bandData) {
|
|
@@ -881,7 +933,6 @@ export function createConfigurableGetTileData(geotiff, config) {
|
|
|
881
933
|
const rangeMin = sharedMins.get(bi) ?? 0;
|
|
882
934
|
const rangeMax = sharedMaxs.get(bi) ?? 1;
|
|
883
935
|
const range = rangeMax - rangeMin || 1;
|
|
884
|
-
const rampStops = COLOR_RAMP_STOPS[config.colorRamp];
|
|
885
936
|
for (let i = 0; i < pixelCount; i++) {
|
|
886
937
|
const raw = bandData?.[i] ?? 0;
|
|
887
938
|
const isND = (nodata !== null && raw === nodata) || !Number.isFinite(raw);
|
|
@@ -894,15 +945,26 @@ export function createConfigurableGetTileData(geotiff, config) {
|
|
|
894
945
|
}
|
|
895
946
|
else {
|
|
896
947
|
const t = Math.max(0, Math.min(1, (raw - rangeMin) / range));
|
|
897
|
-
|
|
898
|
-
rgba[idx] =
|
|
899
|
-
rgba[idx +
|
|
900
|
-
rgba[idx + 2] = b;
|
|
948
|
+
rgba[idx] = 1 + Math.round(t * 254);
|
|
949
|
+
rgba[idx + 1] = 0;
|
|
950
|
+
rgba[idx + 2] = 0;
|
|
901
951
|
rgba[idx + 3] = 255;
|
|
952
|
+
if (histogram) {
|
|
953
|
+
const bin = Math.min(HISTOGRAM_BIN_COUNT - 1, Math.floor(t * HISTOGRAM_BIN_COUNT));
|
|
954
|
+
histogram[bin]++;
|
|
955
|
+
}
|
|
902
956
|
}
|
|
903
957
|
}
|
|
958
|
+
if (histogram && opts.onHistogram)
|
|
959
|
+
opts.onHistogram(histogram);
|
|
904
960
|
}
|
|
905
|
-
return {
|
|
961
|
+
return {
|
|
962
|
+
imageData: new ImageData(rgba, width, height),
|
|
963
|
+
width,
|
|
964
|
+
height,
|
|
965
|
+
colormapTexture: config.mode === 'single' ? colormapTexture : undefined,
|
|
966
|
+
nodataSentinel: config.mode === 'single' ? 0 : undefined
|
|
967
|
+
};
|
|
906
968
|
};
|
|
907
969
|
}
|
|
908
970
|
// ─── 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;
|