@walkthru-earth/objex 1.3.1 → 1.5.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 (199) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +28 -20
  3. package/dist/components/browser/FileTreeSidebar.svelte +32 -17
  4. package/dist/components/layout/AboutSheet.svelte +5 -2
  5. package/dist/components/layout/ConnectionDialog.svelte +7 -2
  6. package/dist/components/layout/SettingsSheet.svelte +238 -0
  7. package/dist/components/layout/SettingsSheet.svelte.d.ts +6 -0
  8. package/dist/components/layout/Sidebar.svelte +73 -6
  9. package/dist/components/layout/Sidebar.svelte.d.ts +4 -1
  10. package/dist/components/layout/StatusBar.svelte +17 -14
  11. package/dist/components/layout/TabBar.svelte +4 -4
  12. package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
  13. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
  14. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
  15. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  16. package/dist/components/ui/resizable/index.d.ts +1 -1
  17. package/dist/components/ui/resizable/index.js +2 -2
  18. package/dist/components/ui/slider/index.d.ts +3 -0
  19. package/dist/components/ui/slider/index.js +5 -0
  20. package/dist/components/ui/slider/range-slider.svelte +94 -0
  21. package/dist/components/ui/slider/range-slider.svelte.d.ts +21 -0
  22. package/dist/components/ui/slider/slider.svelte +83 -0
  23. package/dist/components/ui/slider/slider.svelte.d.ts +7 -0
  24. package/dist/components/viewers/ArchiveViewer.svelte +140 -113
  25. package/dist/components/viewers/CodeViewer.svelte +45 -48
  26. package/dist/components/viewers/CodeViewer.svelte.d.ts +1 -1
  27. package/dist/components/viewers/CogControls.svelte +338 -184
  28. package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
  29. package/dist/components/viewers/CogViewer.svelte +269 -116
  30. package/dist/components/viewers/CopcViewer.svelte +8 -15
  31. package/dist/components/viewers/DatabaseViewer.svelte +22 -21
  32. package/dist/components/viewers/FileInfo.svelte +16 -16
  33. package/dist/components/viewers/FlatGeobufViewer.svelte +16 -46
  34. package/dist/components/viewers/GeoParquetMapViewer.svelte +11 -9
  35. package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
  36. package/dist/components/viewers/ImageViewer.svelte +12 -14
  37. package/dist/components/viewers/LoadProgress.svelte +6 -6
  38. package/dist/components/viewers/MarkdownViewer.svelte +29 -30
  39. package/dist/components/viewers/MediaViewer.svelte +13 -14
  40. package/dist/components/viewers/ModelViewer.svelte +18 -21
  41. package/dist/components/viewers/MultiCogViewer.svelte +474 -106
  42. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
  43. package/dist/components/viewers/NotebookViewer.svelte +28 -29
  44. package/dist/components/viewers/PdfViewer.svelte +24 -33
  45. package/dist/components/viewers/PmtilesViewer.svelte +13 -15
  46. package/dist/components/viewers/QueryHistoryPanel.svelte +18 -18
  47. package/dist/components/viewers/RawViewer.svelte +27 -21
  48. package/dist/components/viewers/StacMapViewer.svelte +6 -13
  49. package/dist/components/viewers/StacMosaicViewer.svelte +1764 -410
  50. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
  51. package/dist/components/viewers/StacTabViewer.svelte +26 -15
  52. package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
  53. package/dist/components/viewers/TableGrid.svelte +38 -34
  54. package/dist/components/viewers/TableStatusBar.svelte +7 -7
  55. package/dist/components/viewers/TableToolbar.svelte +10 -9
  56. package/dist/components/viewers/TableViewer.svelte +47 -30
  57. package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
  58. package/dist/components/viewers/ViewerHeader.svelte +18 -0
  59. package/dist/components/viewers/ViewerHeader.svelte.d.ts +10 -0
  60. package/dist/components/viewers/ViewerRouter.svelte +16 -8
  61. package/dist/components/viewers/ViewerStatus.svelte +19 -0
  62. package/dist/components/viewers/ViewerStatus.svelte.d.ts +7 -0
  63. package/dist/components/viewers/ZarrMapViewer.svelte +24 -21
  64. package/dist/components/viewers/ZarrViewer.svelte +98 -65
  65. package/dist/components/viewers/cog/ChannelPicker.svelte +83 -0
  66. package/dist/components/viewers/cog/ChannelPicker.svelte.d.ts +13 -0
  67. package/dist/components/viewers/cog/PixelInspectorPanel.svelte +87 -0
  68. package/dist/components/viewers/cog/PixelInspectorPanel.svelte.d.ts +17 -0
  69. package/dist/components/viewers/cog/buildRgbLayer.d.ts +78 -0
  70. package/dist/components/viewers/cog/buildRgbLayer.js +176 -0
  71. package/dist/components/viewers/map/AttributeTable.svelte +7 -7
  72. package/dist/components/viewers/map/MapContainer.svelte +38 -12
  73. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +109 -83
  74. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +16 -16
  75. package/dist/components/viewers/stac/StacDatetimeBar.svelte +175 -0
  76. package/dist/components/viewers/stac/StacDatetimeBar.svelte.d.ts +10 -0
  77. package/dist/components/viewers/stac/StacFilterPanel.svelte +243 -0
  78. package/dist/components/viewers/stac/StacFilterPanel.svelte.d.ts +14 -0
  79. package/dist/components/viewers/stac/StacItemInspector.svelte +223 -0
  80. package/dist/components/viewers/stac/StacItemInspector.svelte.d.ts +10 -0
  81. package/dist/components/viewers/stac/StacItemStrip.svelte +228 -0
  82. package/dist/components/viewers/stac/StacItemStrip.svelte.d.ts +12 -0
  83. package/dist/constants.d.ts +6 -0
  84. package/dist/constants.js +8 -0
  85. package/dist/file-icons/index.d.ts +1 -1
  86. package/dist/file-icons/index.js +1 -1
  87. package/dist/i18n/ar.js +113 -2
  88. package/dist/i18n/en.js +113 -2
  89. package/dist/index.d.ts +2 -28
  90. package/dist/index.js +7 -23
  91. package/dist/query/engine.d.ts +10 -0
  92. package/dist/query/source.js +1 -1
  93. package/dist/query/stac-source-factory.d.ts +65 -0
  94. package/dist/query/stac-source-factory.js +77 -0
  95. package/dist/query/stac-source-parquet.d.ts +135 -0
  96. package/dist/query/stac-source-parquet.js +468 -0
  97. package/dist/query/wasm.d.ts +8 -0
  98. package/dist/query/wasm.js +310 -65
  99. package/dist/storage/presign.js +3 -2
  100. package/dist/storage/providers.js +7 -6
  101. package/dist/stores/config.svelte.d.ts +15 -0
  102. package/dist/stores/config.svelte.js +46 -0
  103. package/dist/stores/connections.svelte.d.ts +2 -2
  104. package/dist/stores/connections.svelte.js +1 -2
  105. package/dist/stores/files.svelte.d.ts +1 -1
  106. package/dist/stores/files.svelte.js +1 -1
  107. package/dist/stores/query-history.svelte.js +1 -1
  108. package/dist/stores/settings.svelte.d.ts +16 -1
  109. package/dist/stores/settings.svelte.js +104 -48
  110. package/dist/stores/tabs.svelte.d.ts +3 -0
  111. package/dist/stores/tabs.svelte.js +17 -0
  112. package/dist/utils/cog-histogram.d.ts +121 -0
  113. package/dist/utils/cog-histogram.js +424 -0
  114. package/dist/utils/cog.d.ts +177 -20
  115. package/dist/utils/cog.js +361 -76
  116. package/dist/utils/colormap-sprite.d.ts +0 -9
  117. package/dist/utils/colormap-sprite.js +0 -21
  118. package/dist/utils/deck.d.ts +18 -12
  119. package/dist/utils/deck.js +15 -7
  120. package/dist/utils/media-query.svelte.d.ts +14 -0
  121. package/dist/utils/media-query.svelte.js +29 -0
  122. package/dist/utils/pmtiles-tile.js +2 -2
  123. package/dist/utils/signed-url-effect.d.ts +7 -0
  124. package/dist/utils/signed-url-effect.js +19 -0
  125. package/dist/utils/{url.d.ts → signed-url.d.ts} +15 -1
  126. package/dist/utils/{url.js → signed-url.js} +32 -10
  127. package/dist/utils/url-state.d.ts +36 -0
  128. package/dist/utils/url-state.js +72 -2
  129. package/dist/utils/zarr-tab.d.ts +1 -2
  130. package/dist/utils/zarr-tab.js +1 -2
  131. package/dist/utils/zarr.d.ts +0 -17
  132. package/dist/utils/zarr.js +1 -45
  133. package/package.json +55 -84
  134. package/dist/components/browser/Breadcrumb.svelte +0 -50
  135. package/dist/components/browser/Breadcrumb.svelte.d.ts +0 -7
  136. package/dist/components/browser/CreateFolderDialog.svelte +0 -98
  137. package/dist/components/browser/CreateFolderDialog.svelte.d.ts +0 -6
  138. package/dist/components/browser/DeleteConfirmDialog.svelte +0 -90
  139. package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +0 -8
  140. package/dist/components/browser/DropZone.svelte +0 -83
  141. package/dist/components/browser/DropZone.svelte.d.ts +0 -7
  142. package/dist/components/browser/FileBrowser.svelte +0 -252
  143. package/dist/components/browser/FileBrowser.svelte.d.ts +0 -3
  144. package/dist/components/browser/FileRow.svelte +0 -117
  145. package/dist/components/browser/FileRow.svelte.d.ts +0 -9
  146. package/dist/components/browser/RenameDialog.svelte +0 -101
  147. package/dist/components/browser/RenameDialog.svelte.d.ts +0 -8
  148. package/dist/components/browser/SearchBar.svelte +0 -40
  149. package/dist/components/browser/SearchBar.svelte.d.ts +0 -6
  150. package/dist/components/browser/UploadButton.svelte +0 -65
  151. package/dist/components/browser/UploadButton.svelte.d.ts +0 -3
  152. package/dist/query/stac-geoparquet.d.ts +0 -31
  153. package/dist/query/stac-geoparquet.js +0 -136
  154. package/dist/utils/clipboard.d.ts +0 -13
  155. package/dist/utils/clipboard.js +0 -38
  156. package/dist/utils/cloud-url.d.ts +0 -27
  157. package/dist/utils/cloud-url.js +0 -61
  158. package/dist/utils/cog-pure.d.ts +0 -25
  159. package/dist/utils/cog-pure.js +0 -35
  160. package/dist/utils/column-types.d.ts +0 -5
  161. package/dist/utils/column-types.js +0 -137
  162. package/dist/utils/connection-identity.d.ts +0 -51
  163. package/dist/utils/connection-identity.js +0 -97
  164. package/dist/utils/error.d.ts +0 -8
  165. package/dist/utils/error.js +0 -12
  166. package/dist/utils/evidence-context.d.ts +0 -22
  167. package/dist/utils/evidence-context.js +0 -56
  168. package/dist/utils/export.d.ts +0 -22
  169. package/dist/utils/export.js +0 -76
  170. package/dist/utils/file-sort.d.ts +0 -20
  171. package/dist/utils/file-sort.js +0 -41
  172. package/dist/utils/format.d.ts +0 -24
  173. package/dist/utils/format.js +0 -78
  174. package/dist/utils/geoarrow.d.ts +0 -32
  175. package/dist/utils/geoarrow.js +0 -672
  176. package/dist/utils/geometry-type.d.ts +0 -52
  177. package/dist/utils/geometry-type.js +0 -76
  178. package/dist/utils/hex.d.ts +0 -10
  179. package/dist/utils/hex.js +0 -27
  180. package/dist/utils/host-detection.d.ts +0 -23
  181. package/dist/utils/host-detection.js +0 -95
  182. package/dist/utils/local-storage.d.ts +0 -16
  183. package/dist/utils/local-storage.js +0 -37
  184. package/dist/utils/markdown-sql.d.ts +0 -30
  185. package/dist/utils/markdown-sql.js +0 -72
  186. package/dist/utils/notebook.d.ts +0 -59
  187. package/dist/utils/notebook.js +0 -211
  188. package/dist/utils/parquet-metadata.d.ts +0 -64
  189. package/dist/utils/parquet-metadata.js +0 -262
  190. package/dist/utils/stac-geoparquet.d.ts +0 -90
  191. package/dist/utils/stac-geoparquet.js +0 -223
  192. package/dist/utils/stac-hydrate.d.ts +0 -38
  193. package/dist/utils/stac-hydrate.js +0 -243
  194. package/dist/utils/stac.d.ts +0 -136
  195. package/dist/utils/stac.js +0 -176
  196. package/dist/utils/storage-url.d.ts +0 -90
  197. package/dist/utils/storage-url.js +0 -568
  198. package/dist/utils/wkb.d.ts +0 -43
  199. package/dist/utils/wkb.js +0 -359
package/dist/utils/cog.js CHANGED
@@ -1,34 +1,65 @@
1
+ import { SourceCache, SourceChunk } from '@chunkd/middleware';
2
+ import { SourceView } from '@chunkd/source';
3
+ import { SourceHttp } from '@chunkd/source-http';
1
4
  import { inferRenderPipeline } from '@developmentseed/deck.gl-geotiff';
2
5
  import { Colormap, FilterNoDataVal, LinearRescale } from '@developmentseed/deck.gl-raster/gpu-modules';
3
6
  import loadEpsg from '@developmentseed/epsg/all';
4
7
  import epsgCsvUrl from '@developmentseed/epsg/all.csv.gz?url';
5
8
  import { GeoTIFF } from '@developmentseed/geotiff';
6
9
  import { parseWkt } from '@developmentseed/proj';
10
+ import { buildDataTypeLabel, clampBounds, SF_LABELS, safeClamp } from '@walkthru-earth/objex-utils';
7
11
  import proj4Lib from 'proj4';
8
- import { buildDataTypeLabel, clampBounds, SF_LABELS, safeClamp } from './cog-pure.js';
12
+ import { HISTOGRAM_BINS, readGdalStats, streamHistogram } from './cog-histogram.js';
9
13
  import { COLORMAP_INDEX, getColormapTexture } from './colormap-sprite.js';
10
- export { buildDataTypeLabel, clampBounds, SF_LABELS, safeClamp };
11
- // ─── Constants ───────────────────────────────────────────────────
12
14
  /**
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.
15
+ * Open a `GeoTIFF` from a URL, priming `SourceHttp.metadata.size` before
16
+ * chunked reads start. Replaces `GeoTIFF.fromUrl` which skips the head and
17
+ * can leave size unset (chunkd#1666, stac-map#459).
20
18
  *
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.
19
+ * Primer strategy depends on whether the URL is SigV4-query-string signed.
20
+ * `presign.ts` signs URLs with `aws4fetch.signQuery({method: 'GET'})`, which
21
+ * binds the HTTP method into the signature, so a bare `HEAD` against a
22
+ * `GET`-signed URL 403s with `SignatureDoesNotMatch`. For signed URLs we
23
+ * use a 1-byte `Range: bytes=0-0` GET that returns `Content-Range:
24
+ * bytes 0-0/<TOTAL>`. For unsigned public URLs we use `source.head()`,
25
+ * which returns `Content-Length: <TOTAL>` directly.
26
+ *
27
+ * Why the split: the Range primer's total-size depends on the
28
+ * `Content-Range` response header being CORS-exposed via
29
+ * `Access-Control-Expose-Headers`. GCS public buckets only expose
30
+ * `Content-Length` and a handful of `X-Goog-*` headers (NOT
31
+ * `Content-Range`), so `response.headers.get('content-range')` returns
32
+ * null in the browser, `SourceHttp.metadata.size` falls back to
33
+ * `Content-Length` = 1, and `Tiff.readHeader` throws "offset is outside
34
+ * the bounds of the DataView" on the first IFD read past byte 0. HEAD
35
+ * avoids this entirely because `Content-Length` is a CORS-safelisted
36
+ * response header.
24
37
  */
25
- const Sampler2DArrayPrecision = {
26
- name: 'sampler2darray-precision',
27
- fs: '',
28
- inject: {
29
- 'fs:#decl': 'precision highp sampler2DArray;\n'
38
+ function isSignedQueryUrl(href) {
39
+ try {
40
+ const url = new URL(href);
41
+ const params = url.searchParams;
42
+ return params.has('X-Amz-Signature') || params.has('X-Goog-Signature') || params.has('sig');
30
43
  }
31
- };
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ export async function loadGeoTIFF(href, options = {}) {
49
+ const { chunkSize = 32 * 1024, cacheSize = 1024 * 1024 } = options;
50
+ const source = new SourceHttp(href, {});
51
+ if (isSignedQueryUrl(href)) {
52
+ await source.fetch(0, 1);
53
+ }
54
+ else {
55
+ await source.head();
56
+ }
57
+ const chunk = new SourceChunk({ size: chunkSize });
58
+ const cache = new SourceCache({ size: cacheSize });
59
+ const view = new SourceView(source, [chunk, cache]);
60
+ return await GeoTIFF.open({ dataSource: source, headerSource: view });
61
+ }
62
+ export { buildDataTypeLabel, clampBounds, HISTOGRAM_BINS, readGdalStats, SF_LABELS, safeClamp, streamHistogram };
32
63
  /** Create a sensible default band config based on COG metadata. */
33
64
  export function defaultBandConfig(bandCount, sampleFormat) {
34
65
  if (bandCount >= 3 && bandCount <= 4) {
@@ -113,11 +144,183 @@ export function needsCustomPipelineForConfig(geotiff, config) {
113
144
  return true;
114
145
  return false;
115
146
  }
147
+ export const DEFAULT_NODATA_CONFIG = { mode: 'auto' };
148
+ /**
149
+ * Read the GDAL_NODATA tag from a GeoTIFF.
150
+ *
151
+ * GDAL convention: the tag is an ASCII string serialized via `Number()`; the
152
+ * literal `"nan"` (case-insensitive) round-trips to JS `NaN`. The library's
153
+ * `geotiff.nodata` getter already performs that `Number(rawString)` conversion,
154
+ * so this helper just exposes the resulting `number | null` (preserving `NaN`)
155
+ * behind a stable name for callers that want to drive `NodataConfig`.
156
+ */
157
+ export function readGdalNodata(geotiff) {
158
+ return geotiff.nodata;
159
+ }
160
+ /**
161
+ * Custom shader module that discards pixels whose `color.r` is NaN. Float COGs
162
+ * commonly encode nodata as NaN, but the shipped `FilterNoDataVal` does a
163
+ * `color.r == nodata.value` comparison which IEEE 754 always returns false for
164
+ * NaN. Mirrors the reference implementation in `src/render/shader-modules.ts`.
165
+ *
166
+ * Slots into a `RasterModule[]` render pipeline the same way as the upstream
167
+ * modules (e.g. `LinearRescale`).
168
+ */
169
+ // Typed as RasterModule['module'] (a luma.gl ShaderModule) so it composes
170
+ // with the upstream-shipped modules. No uniforms / no getUniforms needed.
171
+ export const FilterNaN = {
172
+ name: 'filterNaN',
173
+ inject: {
174
+ 'fs:DECKGL_FILTER_COLOR': /* glsl */ `
175
+ if (isnan(color.r)) {
176
+ discard;
177
+ }
178
+ `
179
+ }
180
+ };
181
+ /**
182
+ * Pick the correct nodata-discard module for a resolved nodata value.
183
+ *
184
+ * - `NaN` → `FilterNaN` (IEEE 754: `x == NaN` is always false).
185
+ * - finite number → `FilterNoDataVal { value: nodata / sampleScale }`.
186
+ * - `null` / non-finite (e.g. ±Infinity) → no module (nodata off).
187
+ *
188
+ * `sampleScale` divides the raw nodata value to put it in the same coordinate
189
+ * space the shader sees after hardware normalization (e.g. 255 for r8unorm,
190
+ * 65535 for r16unorm). Pass `1` when the shader receives raw float values.
191
+ */
192
+ export function nodataModule(nodata, sampleScale = 1) {
193
+ if (nodata === null)
194
+ return null;
195
+ if (Number.isNaN(nodata))
196
+ return { module: FilterNaN };
197
+ if (!Number.isFinite(nodata))
198
+ return null;
199
+ return {
200
+ module: FilterNoDataVal,
201
+ props: { value: nodata / sampleScale }
202
+ };
203
+ }
204
+ /** Resolve a tri-state `NodataConfig` against the auto-detected GDAL_NODATA value. */
205
+ export function resolveNodata(cfg, autoNodata) {
206
+ if (cfg.mode === 'off')
207
+ return null;
208
+ if (cfg.mode === 'value')
209
+ return cfg.value ?? null;
210
+ return autoNodata;
211
+ }
116
212
  export const DEFAULT_RESCALE = { min: 0, max: 1 };
117
213
  /** True when the rescale values would produce a visible change on the GPU. */
118
214
  export function isRescaleActive(cfg) {
119
215
  return cfg.min !== DEFAULT_RESCALE.min || cfg.max !== DEFAULT_RESCALE.max;
120
216
  }
217
+ /**
218
+ * Pick a sensible default `RescaleConfig` for a freshly opened COG. The slider
219
+ * operates in normalized shader space [0, 1], but the GPU's hardware
220
+ * normalization (`r8unorm` / `r16unorm` in `MultiCOGLayer`, or the library
221
+ * default uint pipeline elsewhere) collapses raw integer values onto that
222
+ * range by dividing by the format's max (255 for uint8, 65535 for uint16).
223
+ *
224
+ * For uint8 visual COGs (Sentinel-2 `visual` TCI, NAIP `image`) the natural
225
+ * land brightness sits around raw 50-100, so `max: 0.3` (≈ raw 76) gives a
226
+ * nicely contrasted preview. For uint16 reflectance bands (Sentinel-2 raw
227
+ * `nir` / `swir16` / `red`, Landsat C2 L2 `*_B*`) typical land surfaces sit at
228
+ * raw 800-3000 (reflectance × 10000), which is `0.012-0.046` after r16unorm.
229
+ * `max: 0.3` would render those near-black; `max: 0.05` (≈ raw 3277) keeps
230
+ * vegetation, soil, and water in the visible range while leaving headroom for
231
+ * brighter targets.
232
+ *
233
+ * Float / int sample formats fall back to the conservative `{0, 1}` no-op so
234
+ * the user can dial in their own range via the slider.
235
+ */
236
+ export function defaultRescaleForGeotiff(geotiff) {
237
+ const tags = geotiff.cachedTags;
238
+ const sampleFormat = tags.sampleFormat?.[0] ?? 1;
239
+ if (sampleFormat !== 1)
240
+ return { ...DEFAULT_RESCALE };
241
+ const bps = tags.bitsPerSample?.[0] ?? 8;
242
+ if (bps <= 8)
243
+ return { min: 0, max: 0.3 };
244
+ return { min: 0, max: 0.05 };
245
+ }
246
+ /**
247
+ * Build a 64-bin histogram of band 0 from a GeoTIFF's smallest overview, in
248
+ * the same shader-space [0, 1] coordinate system the rescale slider operates
249
+ * on (raw / 65535 for uint16, raw / 255 for uint8, raw clamped to [0, 1] for
250
+ * float). Used by viewers on the multi-asset MultiCOGLayer path to give the
251
+ * rescale slider a histogram backdrop without hooking per-tile sampling into
252
+ * the layer.
253
+ *
254
+ * Returns null if the smallest overview cannot be fetched. Skips the GeoTIFF's
255
+ * declared nodata value and non-finite values.
256
+ */
257
+ /**
258
+ * Walk a cumulative histogram (`HISTOGRAM_BIN_COUNT` bins covering [0, 1])
259
+ * and return the shader-space value at percentile `p` (0..1). Returns null
260
+ * when the histogram is empty. Linearly interpolates within the matching bin
261
+ * so the result is monotonic across calls with adjacent percentiles, instead
262
+ * of jumping in `1/HISTOGRAM_BIN_COUNT` increments.
263
+ */
264
+ export function percentileFromHistogram(histogram, p) {
265
+ if (!histogram || histogram.length !== HISTOGRAM_BIN_COUNT)
266
+ return null;
267
+ let total = 0;
268
+ for (let i = 0; i < HISTOGRAM_BIN_COUNT; i++)
269
+ total += histogram[i];
270
+ if (total === 0)
271
+ return null;
272
+ const target = total * Math.max(0, Math.min(1, p));
273
+ let acc = 0;
274
+ for (let i = 0; i < HISTOGRAM_BIN_COUNT; i++) {
275
+ const next = acc + histogram[i];
276
+ if (next >= target) {
277
+ const frac = histogram[i] === 0 ? 0 : (target - acc) / histogram[i];
278
+ return (i + frac) / HISTOGRAM_BIN_COUNT;
279
+ }
280
+ acc = next;
281
+ }
282
+ return 1;
283
+ }
284
+ export async function buildHistogramFromGeotiff(geotiff, signal) {
285
+ const tags = geotiff.cachedTags;
286
+ const sampleFormat = tags.sampleFormat?.[0] ?? 1;
287
+ const bps = tags.bitsPerSample?.[0] ?? 8;
288
+ const norm = sampleFormat === 1 ? (bps <= 8 ? 255 : 65535) : 1;
289
+ const nodata = geotiff.nodata;
290
+ const overviews = geotiff.overviews ?? [];
291
+ const sourceImage = overviews.length ? overviews[overviews.length - 1] : geotiff;
292
+ try {
293
+ const tile = await sourceImage.fetchTile(0, 0, { signal });
294
+ if (signal?.aborted)
295
+ return null;
296
+ const arr = tile.array;
297
+ const data = arr.layout === 'band-separate' ? arr.bands[0] : arr.data;
298
+ const stride = arr.layout === 'band-separate' ? 1 : (arr.count ?? 1);
299
+ const histogram = new Uint32Array(HISTOGRAM_BIN_COUNT);
300
+ let counted = 0;
301
+ const len = data.length;
302
+ for (let i = 0; i < len; i += stride) {
303
+ const raw = data[i];
304
+ if (!Number.isFinite(raw))
305
+ continue;
306
+ if (nodata !== null && raw === nodata)
307
+ continue;
308
+ const t = sampleFormat === 1 ? raw / norm : Math.max(0, Math.min(1, raw));
309
+ if (t < 0 || t > 1)
310
+ continue;
311
+ const bin = Math.min(HISTOGRAM_BIN_COUNT - 1, Math.floor(t * HISTOGRAM_BIN_COUNT));
312
+ histogram[bin]++;
313
+ counted++;
314
+ }
315
+ if (counted === 0)
316
+ return null;
317
+ return histogram;
318
+ }
319
+ catch {
320
+ /* swallow */
321
+ return null;
322
+ }
323
+ }
121
324
  /**
122
325
  * Build a `getTileData` + `renderTile` pair that reuses the library-default
123
326
  * uint pipeline (via `inferRenderPipeline`) and appends `LinearRescale` to the
@@ -137,7 +340,7 @@ export function createRescaledPipeline(geotiff, rescale) {
137
340
  if (builtFor === device && defaultGetTileData && defaultRenderTile)
138
341
  return;
139
342
  const inferred = inferRenderPipeline(geotiff, device);
140
- // `inferRenderPipeline` returns generic callbacks. `MinimalDataT` is the
343
+ // `inferRenderPipeline` returns generic callbacks. `MinimalTileData` is the
141
344
  // contractual superset used by COGLayer — safe upcast.
142
345
  defaultGetTileData = inferred.getTileData;
143
346
  defaultRenderTile = inferred.renderTile;
@@ -163,17 +366,18 @@ export function createRescaledPipeline(geotiff, rescale) {
163
366
  }
164
367
  /**
165
368
  * 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).
369
+ * Combines an optional nodata-discard module + `LinearRescale` in the order
370
+ * the GPU expects (no-data mask first, then rescale). The nodata module is
371
+ * selected by `nodataModule()` so NaN sentinels route through `FilterNaN`
372
+ * instead of the always-false `==` comparison in `FilterNoDataVal`.
168
373
  */
169
374
  export function buildBandRenderPipeline(opts = {}) {
170
375
  const modules = [];
171
- if (opts.noDataVal !== undefined && opts.noDataVal !== null) {
172
- modules.push({
173
- module: FilterNoDataVal,
174
- props: { noDataVal: opts.noDataVal }
175
- });
176
- }
376
+ const nodataMod = opts.noDataVal !== undefined
377
+ ? nodataModule(opts.noDataVal ?? null, opts.noDataSampleScale ?? 1)
378
+ : null;
379
+ if (nodataMod)
380
+ modules.push(nodataMod);
177
381
  if (opts.rescale && isRescaleActive(opts.rescale)) {
178
382
  modules.push({
179
383
  module: LinearRescale,
@@ -240,8 +444,11 @@ export function selectCogPipeline(geotiff, opts = {}) {
240
444
  ? needsCustomPipelineForConfig(geotiff, bandConfig)
241
445
  : needsCustomPipeline(geotiff);
242
446
  if (useCustom && bandConfig) {
447
+ // Note: callers that want identity-stable getTileData across config
448
+ // swaps should call `createConfigurableGetTileData` directly and reuse
449
+ // the loader, rather than re-running selectCogPipeline.
243
450
  return {
244
- getTileData: createConfigurableGetTileData(geotiff, bandConfig),
451
+ getTileData: createConfigurableGetTileData(geotiff, bandConfig).getTileData,
245
452
  renderTile: buildCustomRenderTile(bandConfig, rescale)
246
453
  };
247
454
  }
@@ -268,7 +475,7 @@ const BITMAP_SOURCE = 'geotiff-bitmap-src';
268
475
  const BITMAP_LAYER = 'geotiff-bitmap-layer';
269
476
  // ─── Types & pure helpers ────────────────────────────────────────
270
477
  // `GeoBounds`, `CogInfo`, `safeClamp`, `clampBounds`, `buildDataTypeLabel`
271
- // live in `./cog-pure.ts` and are re-exported at the top of this file.
478
+ // live in `cog-info.ts` in @walkthru-earth/objex-utils and are re-exported at the top of this file.
272
479
  // ─── Map helpers (depend on maplibre-gl) ─────────────────────────
273
480
  /**
274
481
  * Query the GPU's MAX_TEXTURE_SIZE from MapLibre's WebGL context.
@@ -431,7 +638,7 @@ const MAX_NONTILED_PIXELS = 100_000_000;
431
638
  export async function renderNonTiledBitmap(options) {
432
639
  const { url, map, signal } = options;
433
640
  // Open GeoTIFF (reuse if already opened for pre-flight)
434
- const geotiff = options.geotiff ?? (await GeoTIFF.fromUrl(url));
641
+ const geotiff = options.geotiff ?? (await loadGeoTIFF(url));
435
642
  if (signal.aborted)
436
643
  throw new DOMException('Aborted', 'AbortError');
437
644
  const imgW = geotiff.width;
@@ -577,8 +784,15 @@ export function needsCustomPipeline(geotiff) {
577
784
  // sampleFormat is null or not uint → needs custom
578
785
  return sf === null || sf[0] !== 1;
579
786
  }
580
- /** Number of histogram buckets produced by the CPU bake. */
581
- export const HISTOGRAM_BIN_COUNT = 64;
787
+ /**
788
+ * Number of histogram buckets produced by the CPU bake.
789
+ *
790
+ * Canonical value lives in `./cog-histogram.ts` as `HISTOGRAM_BINS`; this
791
+ * is kept as an alias so existing CogViewer / StacMosaicViewer imports
792
+ * continue to compile. Both names resolve to the same number — see
793
+ * `./cog-histogram.ts` for the streaming histogram + GDAL stats reader.
794
+ */
795
+ export const HISTOGRAM_BIN_COUNT = HISTOGRAM_BINS;
582
796
  /**
583
797
  * Create custom getTileData for non-uint COGs.
584
798
  * Reads band 0, normalizes using GDAL statistics / per-tile adaptive stretch,
@@ -750,11 +964,7 @@ export function buildCustomRenderTile(config, rescale) {
750
964
  props: { rescaleMin: rescale.min, rescaleMax: rescale.max }
751
965
  });
752
966
  }
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: {} }, {
967
+ pipeline.push({
758
968
  module: Colormap,
759
969
  props: {
760
970
  colormapTexture: data.colormapTexture,
@@ -823,17 +1033,27 @@ function computeBandRanges(bands, bandIndices, pixelCount, nodata) {
823
1033
  * Supports RGB mode (multi-band → R,G,B with alpha=255, fully baked) and
824
1034
  * single-band mode (band N normalized into the `r` channel; the ramp is
825
1035
  * applied downstream by the GPU `Colormap` module via `buildCustomRenderTile`).
1036
+ *
1037
+ * The returned `getTileData` reference is stable across `updateConfig` calls.
1038
+ * deck.gl's TileLayer treats a changed `getTileData` identity as a cache
1039
+ * invalidation, so reusing this loader across band/ramp swaps preserves tiles.
826
1040
  */
827
1041
  export function createConfigurableGetTileData(geotiff, config, _opts = {}) {
828
1042
  const bandCount = geotiff.count;
829
- // Shared per-band ranges across tiles (seeded on first tile, widened by subsequent)
830
- const sharedMins = new Map();
831
- const sharedMaxs = new Map();
832
- // Resolve the sprite texture from the first tile's device; reuse per-device.
833
- let texturePromise = null;
834
- return async (image, options) => {
835
- if (config.mode === 'single' && !texturePromise) {
836
- texturePromise = getColormapTexture(options.device);
1043
+ // Mutable refs read on every tile bake. Mutating in place keeps the closure
1044
+ // and therefore the function reference — stable across config swaps.
1045
+ const refs = {
1046
+ config,
1047
+ // Shared per-band ranges across tiles (seeded on first tile, widened by subsequent)
1048
+ sharedMins: new Map(),
1049
+ sharedMaxs: new Map(),
1050
+ // Resolve the sprite texture from the first tile's device; reuse per-device.
1051
+ texturePromise: null
1052
+ };
1053
+ const getTileData = async (image, options) => {
1054
+ const currentConfig = refs.config;
1055
+ if (currentConfig.mode === 'single' && !refs.texturePromise) {
1056
+ refs.texturePromise = getColormapTexture(options.device);
837
1057
  }
838
1058
  const [tile, colormapTexture] = await Promise.all([
839
1059
  image.fetchTile(options.x, options.y, {
@@ -842,7 +1062,7 @@ export function createConfigurableGetTileData(geotiff, config, _opts = {}) {
842
1062
  pool: options.pool,
843
1063
  signal: options.signal
844
1064
  }),
845
- texturePromise ?? Promise.resolve(undefined)
1065
+ refs.texturePromise ?? Promise.resolve(undefined)
846
1066
  ]);
847
1067
  const arr = tile.array;
848
1068
  const { width, height, nodata } = arr;
@@ -853,27 +1073,27 @@ export function createConfigurableGetTileData(geotiff, config, _opts = {}) {
853
1073
  // object. Cloud-native by construction: at each zoom level, COG only
854
1074
  // decodes the overview tiles that cover the viewport, so the summed
855
1075
  // histogram naturally reflects "what the user is looking at right now".
856
- const histogram = config.mode === 'single' ? new Uint32Array(HISTOGRAM_BIN_COUNT) : null;
857
- if (config.mode === 'rgb') {
1076
+ const histogram = currentConfig.mode === 'single' ? new Uint32Array(HISTOGRAM_BIN_COUNT) : null;
1077
+ if (currentConfig.mode === 'rgb') {
858
1078
  // RGB mode: map 3 bands to R, G, B
859
- const indices = [config.rBand, config.gBand, config.bBand];
1079
+ const indices = [currentConfig.rBand, currentConfig.gBand, currentConfig.bBand];
860
1080
  // Compute ranges for the 3 selected bands
861
1081
  for (const bi of indices) {
862
- if (!sharedMins.has(bi)) {
1082
+ if (!refs.sharedMins.has(bi)) {
863
1083
  const { mins, maxs } = computeBandRanges(bands, [bi], pixelCount, nodata);
864
- sharedMins.set(bi, mins[0]);
865
- sharedMaxs.set(bi, maxs[0]);
1084
+ refs.sharedMins.set(bi, mins[0]);
1085
+ refs.sharedMaxs.set(bi, maxs[0]);
866
1086
  }
867
1087
  }
868
- const rBand = bands[config.rBand];
869
- const gBand = bands[config.gBand];
870
- const bBand = bands[config.bBand];
871
- const rMin = sharedMins.get(config.rBand);
872
- const rMax = sharedMaxs.get(config.rBand);
873
- const gMin = sharedMins.get(config.gBand);
874
- const gMax = sharedMaxs.get(config.gBand);
875
- const bMin = sharedMins.get(config.bBand);
876
- const bMax = sharedMaxs.get(config.bBand);
1088
+ const rBand = bands[currentConfig.rBand];
1089
+ const gBand = bands[currentConfig.gBand];
1090
+ const bBand = bands[currentConfig.bBand];
1091
+ const rMin = refs.sharedMins.get(currentConfig.rBand);
1092
+ const rMax = refs.sharedMaxs.get(currentConfig.rBand);
1093
+ const gMin = refs.sharedMins.get(currentConfig.gBand);
1094
+ const gMax = refs.sharedMaxs.get(currentConfig.gBand);
1095
+ const bMin = refs.sharedMins.get(currentConfig.bBand);
1096
+ const bMax = refs.sharedMaxs.get(currentConfig.bBand);
877
1097
  const rRange = rMax - rMin || 1;
878
1098
  const gRange = gMax - gMin || 1;
879
1099
  const bRange = bMax - bMin || 1;
@@ -902,15 +1122,15 @@ export function createConfigurableGetTileData(geotiff, config, _opts = {}) {
902
1122
  // Single-band mode: normalize the selected band into the `r`
903
1123
  // channel and reserve `r = 0` as a nodata sentinel that
904
1124
  // `FilterNoDataVal` discards before the `Colormap` GPU lookup.
905
- const bi = config.band;
1125
+ const bi = currentConfig.band;
906
1126
  const bandData = bands[bi];
907
- if (!sharedMins.has(bi) && bandData) {
1127
+ if (!refs.sharedMins.has(bi) && bandData) {
908
1128
  const { mins, maxs } = computeBandRanges(bands, [bi], pixelCount, nodata);
909
- sharedMins.set(bi, mins[0]);
910
- sharedMaxs.set(bi, maxs[0]);
1129
+ refs.sharedMins.set(bi, mins[0]);
1130
+ refs.sharedMaxs.set(bi, maxs[0]);
911
1131
  }
912
- const rangeMin = sharedMins.get(bi) ?? 0;
913
- const rangeMax = sharedMaxs.get(bi) ?? 1;
1132
+ const rangeMin = refs.sharedMins.get(bi) ?? 0;
1133
+ const rangeMax = refs.sharedMaxs.get(bi) ?? 1;
914
1134
  const range = rangeMax - rangeMin || 1;
915
1135
  for (let i = 0; i < pixelCount; i++) {
916
1136
  const raw = bandData?.[i] ?? 0;
@@ -939,11 +1159,29 @@ export function createConfigurableGetTileData(geotiff, config, _opts = {}) {
939
1159
  imageData: new ImageData(rgba, width, height),
940
1160
  width,
941
1161
  height,
942
- colormapTexture: config.mode === 'single' ? colormapTexture : undefined,
943
- nodataSentinel: config.mode === 'single' ? 0 : undefined,
1162
+ colormapTexture: currentConfig.mode === 'single' ? colormapTexture : undefined,
1163
+ nodataSentinel: currentConfig.mode === 'single' ? 0 : undefined,
944
1164
  histogram: histogram ?? undefined
945
1165
  };
946
1166
  };
1167
+ return {
1168
+ getTileData,
1169
+ updateConfig(next) {
1170
+ // Mode/band swap implies different per-band ranges; clear so the next
1171
+ // tile reseeds them. The function identity itself is preserved.
1172
+ const prev = refs.config;
1173
+ refs.config = next;
1174
+ const bandsChanged = prev.mode !== next.mode ||
1175
+ (prev.mode === 'rgb' &&
1176
+ next.mode === 'rgb' &&
1177
+ (prev.rBand !== next.rBand || prev.gBand !== next.gBand || prev.bBand !== next.bBand)) ||
1178
+ (prev.mode === 'single' && next.mode === 'single' && prev.band !== next.band);
1179
+ if (bandsChanged) {
1180
+ refs.sharedMins.clear();
1181
+ refs.sharedMaxs.clear();
1182
+ }
1183
+ }
1184
+ };
947
1185
  }
948
1186
  // ─── EPSG resolution via bundled database ────────────────────────
949
1187
  /**
@@ -1030,13 +1268,58 @@ export async function resolveProj4Def(crs, _signal) {
1030
1268
  // ProjJSON — stringify for proj4
1031
1269
  return JSON.stringify(crs);
1032
1270
  }
1271
+ /**
1272
+ * Standard Web Mercator ground resolution (meters per screen pixel) at a given
1273
+ * zoom and latitude. Equivalent to MapLibre's `metersPerPixel`. Use this to
1274
+ * pick a COG overview level that matches what's painted on screen.
1275
+ */
1276
+ export function mapResolutionMetersPerPixel(zoom, latitude) {
1277
+ return (156543.03392 * Math.cos((latitude * Math.PI) / 180)) / 2 ** zoom;
1278
+ }
1279
+ /**
1280
+ * Pick the coarsest overview whose pixel size is ≤ `targetMetersPerPixel`,
1281
+ * matching lazycogs' `_select_overview`. Walks `geotiff.overviews` in
1282
+ * finest → coarsest order (the documented sort) and returns the last entry
1283
+ * that still satisfies the constraint. Returns `null` to mean "use full
1284
+ * resolution" (either the COG has no overviews, the target is already finer
1285
+ * than native, or every overview is coarser than the target — never
1286
+ * upsample).
1287
+ *
1288
+ * The COG's affine `a` component is the X-pixel-size in the COG's native CRS
1289
+ * units. For Web Mercator and other meter-based CRSes this is meters/pixel
1290
+ * and lines up directly with `mapResolutionMetersPerPixel`. For degree-based
1291
+ * CRSes (EPSG:4326) the comparison is against degrees/pixel, so callers
1292
+ * should derive the target in the same units.
1293
+ */
1294
+ export function selectOverviewForResolution(geotiff, targetMetersPerPixel) {
1295
+ if (!geotiff.overviews.length)
1296
+ return null;
1297
+ const nativeRes = Math.abs(geotiff.transform[0]);
1298
+ if (targetMetersPerPixel <= nativeRes)
1299
+ return null;
1300
+ let selected = null;
1301
+ for (const overview of geotiff.overviews) {
1302
+ if (Math.abs(overview.transform[0]) <= targetMetersPerPixel) {
1303
+ selected = overview;
1304
+ }
1305
+ else {
1306
+ break;
1307
+ }
1308
+ }
1309
+ return selected;
1310
+ }
1033
1311
  /**
1034
1312
  * Read pixel values at a given lng/lat from a GeoTIFF.
1035
1313
  * Converts WGS84 → source CRS → pixel coords, fetches the tile, reads all bands.
1314
+ *
1315
+ * If `options.overview` is supplied, the read happens against that overview
1316
+ * level instead of the full-resolution image (so the inspected value matches
1317
+ * the overview deck.gl is currently painting on screen). Pass `null` /
1318
+ * `undefined` to keep the legacy full-resolution behaviour.
1036
1319
  */
1037
1320
  export async function readPixelAtLngLat(geotiff, lng, lat, proj4Def,
1038
1321
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1039
- pool, signal) {
1322
+ pool, signal, options) {
1040
1323
  // Convert WGS84 to source CRS
1041
1324
  let srcX = lng;
1042
1325
  let srcY = lat;
@@ -1050,15 +1333,17 @@ pool, signal) {
1050
1333
  return null;
1051
1334
  }
1052
1335
  }
1336
+ // Read against the selected overview if one was supplied, else full res.
1337
+ const source = options?.overview ?? geotiff;
1053
1338
  // Get pixel indices (row, col)
1054
- const [row, col] = geotiff.index(srcX, srcY);
1055
- if (row < 0 || row >= geotiff.height || col < 0 || col >= geotiff.width)
1339
+ const [row, col] = source.index(srcX, srcY);
1340
+ if (row < 0 || row >= source.height || col < 0 || col >= source.width)
1056
1341
  return null;
1057
1342
  // Compute tile indices
1058
- const tileX = Math.floor(col / geotiff.tileWidth);
1059
- const tileY = Math.floor(row / geotiff.tileHeight);
1343
+ const tileX = Math.floor(col / source.tileWidth);
1344
+ const tileY = Math.floor(row / source.tileHeight);
1060
1345
  // Fetch tile
1061
- const tile = await geotiff.fetchTile(tileX, tileY, { pool, signal });
1346
+ const tile = await source.fetchTile(tileX, tileY, { pool, signal });
1062
1347
  const arr = tile.array;
1063
1348
  // Read all band values at this pixel
1064
1349
  const localCol = col - tileX * arr.width;
@@ -17,8 +17,6 @@ export { COLORMAP_INDEX, type ColormapName } from '@developmentseed/deck.gl-rast
17
17
  export declare const COLORMAP_SPRITE_URL: any;
18
18
  /** Number of distinct ramps encoded as 1-pixel-tall rows in the sprite. */
19
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
20
  /** All ramp names, sorted alphabetically (matches `COLORMAP_INDEX` key order). */
23
21
  export declare const COLORMAP_NAMES: ColormapName[];
24
22
  /** Decode the shipped sprite once per session. Safe to call repeatedly. */
@@ -30,10 +28,3 @@ export declare function loadColormapSprite(): Promise<ImageData>;
30
28
  * `Colormap` shader module.
31
29
  */
32
30
  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;
@@ -21,8 +21,6 @@ export { COLORMAP_INDEX } from '@developmentseed/deck.gl-raster/gpu-modules';
21
21
  export const COLORMAP_SPRITE_URL = colormapsPngUrl;
22
22
  /** Number of distinct ramps encoded as 1-pixel-tall rows in the sprite. */
23
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
24
  /** All ramp names, sorted alphabetically (matches `COLORMAP_INDEX` key order). */
27
25
  export const COLORMAP_NAMES = Object.keys(COLORMAP_INDEX).sort();
28
26
  let spritePromise = null;
@@ -56,22 +54,3 @@ export async function getColormapTexture(device) {
56
54
  textureCache.set(device, texture);
57
55
  return texture;
58
56
  }
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
- }