@walkthru-earth/objex 1.2.0 → 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.
Files changed (76) hide show
  1. package/README.md +6 -3
  2. package/dist/components/browser/FileTreeSidebar.svelte +1 -1
  3. package/dist/components/layout/ConnectionDialog.svelte +35 -3
  4. package/dist/components/layout/Sidebar.svelte +28 -2
  5. package/dist/components/viewers/ArchiveViewer.svelte +4 -4
  6. package/dist/components/viewers/CodeViewer.svelte +72 -19
  7. package/dist/components/viewers/CodeViewer.svelte.d.ts +11 -1
  8. package/dist/components/viewers/CogControls.svelte +151 -22
  9. package/dist/components/viewers/CogControls.svelte.d.ts +5 -1
  10. package/dist/components/viewers/CogViewer.svelte +45 -10
  11. package/dist/components/viewers/CopcViewer.svelte +20 -2
  12. package/dist/components/viewers/FlatGeobufViewer.svelte +15 -9
  13. package/dist/components/viewers/MultiCogViewer.svelte +416 -0
  14. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +9 -0
  15. package/dist/components/viewers/PmtilesViewer.svelte +2 -2
  16. package/dist/components/viewers/StacMapViewer.svelte +34 -12
  17. package/dist/components/viewers/StacMapViewer.svelte.d.ts +1 -0
  18. package/dist/components/viewers/StacMosaicViewer.svelte +699 -0
  19. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +9 -0
  20. package/dist/components/viewers/StacTabViewer.svelte +254 -0
  21. package/dist/components/viewers/StacTabViewer.svelte.d.ts +13 -0
  22. package/dist/components/viewers/TableViewer.svelte +50 -21
  23. package/dist/components/viewers/ViewerRouter.svelte +155 -2
  24. package/dist/components/viewers/ViewerRouter.svelte.d.ts +1 -1
  25. package/dist/components/viewers/ZarrMapViewer.svelte +147 -8
  26. package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +8 -2
  27. package/dist/components/viewers/ZarrViewer.svelte +3 -2
  28. package/dist/components/viewers/pmtiles/PmtilesMapView.svelte +0 -1
  29. package/dist/i18n/ar.js +28 -0
  30. package/dist/i18n/en.js +28 -0
  31. package/dist/index.d.ts +4 -0
  32. package/dist/index.js +2 -0
  33. package/dist/query/index.d.ts +1 -1
  34. package/dist/query/index.js +1 -1
  35. package/dist/query/source.d.ts +12 -0
  36. package/dist/query/source.js +25 -8
  37. package/dist/query/stac-geoparquet.d.ts +31 -0
  38. package/dist/query/stac-geoparquet.js +136 -0
  39. package/dist/query/wasm.js +130 -23
  40. package/dist/storage/adapter.d.ts +9 -0
  41. package/dist/storage/adapter.js +13 -1
  42. package/dist/storage/browser-azure.d.ts +1 -1
  43. package/dist/storage/browser-azure.js +4 -0
  44. package/dist/storage/browser-cloud.d.ts +1 -1
  45. package/dist/storage/browser-cloud.js +7 -0
  46. package/dist/storage/presign.d.ts +13 -0
  47. package/dist/storage/presign.js +55 -0
  48. package/dist/storage/providers.d.ts +6 -0
  49. package/dist/storage/providers.js +13 -2
  50. package/dist/stores/browser.svelte.d.ts +2 -0
  51. package/dist/stores/browser.svelte.js +17 -1
  52. package/dist/stores/connections.svelte.d.ts +38 -23
  53. package/dist/stores/connections.svelte.js +105 -114
  54. package/dist/utils/cog.d.ts +80 -18
  55. package/dist/utils/cog.js +187 -125
  56. package/dist/utils/colormap-sprite.d.ts +39 -0
  57. package/dist/utils/colormap-sprite.js +77 -0
  58. package/dist/utils/connection-identity.d.ts +51 -0
  59. package/dist/utils/connection-identity.js +97 -0
  60. package/dist/utils/host-detection.js +48 -302
  61. package/dist/utils/parquet-metadata.d.ts +7 -1
  62. package/dist/utils/parquet-metadata.js +35 -1
  63. package/dist/utils/stac-geoparquet.d.ts +90 -0
  64. package/dist/utils/stac-geoparquet.js +223 -0
  65. package/dist/utils/stac-hydrate.d.ts +38 -0
  66. package/dist/utils/stac-hydrate.js +243 -0
  67. package/dist/utils/stac.d.ts +136 -0
  68. package/dist/utils/stac.js +176 -0
  69. package/dist/utils/storage-url.d.ts +26 -0
  70. package/dist/utils/storage-url.js +164 -28
  71. package/dist/utils/url.d.ts +13 -0
  72. package/dist/utils/url.js +36 -0
  73. package/dist/utils/wkb.js +22 -8
  74. package/dist/utils/zarr.d.ts +34 -0
  75. package/dist/utils/zarr.js +94 -0
  76. 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
- renderTile: customRenderTile
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: customRenderTile
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
- * applies terrain color ramp for single-band data.
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
- const tile = await image.fetchTile(options.x, options.y, {
668
- boundless: false,
669
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
670
- pool: options.pool,
671
- signal: options.signal
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 && (sf === 2 || sf === 3)) {
723
- // Terrain color ramp for single-band int/float (likely elevation/DEM)
724
- const [r, g, b] = terrainColor(t);
725
- rgba[idx] = r;
726
- rgba[idx + 1] = g;
727
- rgba[idx + 2] = b;
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 non-uint COGs.
747
- * v0.5 RasterLayer requires a RenderTileResult with `image` or `renderPipeline`.
748
- * We produce an ImageData and pass it through the `image` slot. deck.gl manages
749
- * the texture lifecycle and prepends a CreateTexture module automatically.
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 customRenderTile(data) {
752
- return { image: data.imageData };
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 both RGB mode (multi-band → R,G,B) and single-band mode (color ramp).
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
- const tile = await image.fetchTile(options.x, options.y, {
818
- boundless: false,
819
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
820
- pool: options.pool,
821
- signal: options.signal
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 + color ramp
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
- const [r, g, b] = interpolateRamp(rampStops, t);
898
- rgba[idx] = r;
899
- rgba[idx + 1] = g;
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 { imageData: new ImageData(rgba, width, height), width, height };
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;