@walkthru-earth/objex 1.3.0 → 1.4.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 (182) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +20 -12
  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 +1 -1
  6. package/dist/components/layout/SettingsSheet.svelte +237 -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 +1 -1
  11. package/dist/components/layout/TabBar.svelte +2 -2
  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 +2 -2
  25. package/dist/components/viewers/CodeViewer.svelte +31 -22
  26. package/dist/components/viewers/CogControls.svelte +338 -184
  27. package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
  28. package/dist/components/viewers/CogViewer.svelte +320 -119
  29. package/dist/components/viewers/CopcViewer.svelte +1 -1
  30. package/dist/components/viewers/FlatGeobufViewer.svelte +1 -1
  31. package/dist/components/viewers/GeoParquetMapViewer.svelte +6 -6
  32. package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
  33. package/dist/components/viewers/ImageViewer.svelte +2 -2
  34. package/dist/components/viewers/MarkdownViewer.svelte +12 -9
  35. package/dist/components/viewers/MediaViewer.svelte +2 -2
  36. package/dist/components/viewers/ModelViewer.svelte +1 -1
  37. package/dist/components/viewers/MultiCogViewer.svelte +467 -102
  38. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
  39. package/dist/components/viewers/NotebookViewer.svelte +6 -3
  40. package/dist/components/viewers/PdfViewer.svelte +2 -2
  41. package/dist/components/viewers/PmtilesViewer.svelte +3 -6
  42. package/dist/components/viewers/RawViewer.svelte +6 -3
  43. package/dist/components/viewers/StacMapViewer.svelte +10 -2
  44. package/dist/components/viewers/StacMosaicViewer.svelte +1800 -362
  45. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
  46. package/dist/components/viewers/StacTabViewer.svelte +24 -13
  47. package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
  48. package/dist/components/viewers/TableGrid.svelte +4 -4
  49. package/dist/components/viewers/TableStatusBar.svelte +1 -1
  50. package/dist/components/viewers/TableToolbar.svelte +1 -1
  51. package/dist/components/viewers/TableViewer.svelte +25 -17
  52. package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
  53. package/dist/components/viewers/ViewerRouter.svelte +16 -8
  54. package/dist/components/viewers/ZarrMapViewer.svelte +11 -9
  55. package/dist/components/viewers/ZarrViewer.svelte +4 -4
  56. package/dist/components/viewers/cog/ChannelPicker.svelte +83 -0
  57. package/dist/components/viewers/cog/ChannelPicker.svelte.d.ts +13 -0
  58. package/dist/components/viewers/cog/PixelInspectorPanel.svelte +87 -0
  59. package/dist/components/viewers/cog/PixelInspectorPanel.svelte.d.ts +17 -0
  60. package/dist/components/viewers/cog/buildRgbLayer.d.ts +78 -0
  61. package/dist/components/viewers/cog/buildRgbLayer.js +176 -0
  62. package/dist/components/viewers/map/AttributeTable.svelte +1 -1
  63. package/dist/components/viewers/map/MapContainer.svelte +37 -11
  64. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +1 -1
  65. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +1 -1
  66. package/dist/components/viewers/stac/StacDatetimeBar.svelte +175 -0
  67. package/dist/components/viewers/stac/StacDatetimeBar.svelte.d.ts +10 -0
  68. package/dist/components/viewers/stac/StacFilterPanel.svelte +243 -0
  69. package/dist/components/viewers/stac/StacFilterPanel.svelte.d.ts +14 -0
  70. package/dist/components/viewers/stac/StacItemInspector.svelte +223 -0
  71. package/dist/components/viewers/stac/StacItemInspector.svelte.d.ts +10 -0
  72. package/dist/components/viewers/stac/StacItemStrip.svelte +228 -0
  73. package/dist/components/viewers/stac/StacItemStrip.svelte.d.ts +12 -0
  74. package/dist/file-icons/index.d.ts +1 -1
  75. package/dist/file-icons/index.js +1 -1
  76. package/dist/i18n/ar.js +110 -2
  77. package/dist/i18n/en.js +110 -2
  78. package/dist/index.d.ts +2 -28
  79. package/dist/index.js +7 -23
  80. package/dist/query/engine.d.ts +10 -0
  81. package/dist/query/source.js +1 -1
  82. package/dist/query/stac-source-factory.d.ts +65 -0
  83. package/dist/query/stac-source-factory.js +77 -0
  84. package/dist/query/stac-source-parquet.d.ts +135 -0
  85. package/dist/query/stac-source-parquet.js +465 -0
  86. package/dist/query/wasm.d.ts +8 -0
  87. package/dist/query/wasm.js +304 -2
  88. package/dist/storage/presign.js +1 -1
  89. package/dist/storage/providers.js +5 -5
  90. package/dist/stores/config.svelte.d.ts +15 -0
  91. package/dist/stores/config.svelte.js +46 -0
  92. package/dist/stores/connections.svelte.d.ts +2 -2
  93. package/dist/stores/connections.svelte.js +1 -2
  94. package/dist/stores/files.svelte.d.ts +1 -1
  95. package/dist/stores/files.svelte.js +1 -1
  96. package/dist/stores/query-history.svelte.js +1 -1
  97. package/dist/stores/settings.svelte.d.ts +16 -1
  98. package/dist/stores/settings.svelte.js +104 -48
  99. package/dist/stores/tabs.svelte.d.ts +3 -0
  100. package/dist/stores/tabs.svelte.js +17 -0
  101. package/dist/utils/cog-histogram.d.ts +121 -0
  102. package/dist/utils/cog-histogram.js +424 -0
  103. package/dist/utils/cog.d.ts +200 -60
  104. package/dist/utils/cog.js +377 -114
  105. package/dist/utils/colormap-sprite.d.ts +0 -9
  106. package/dist/utils/colormap-sprite.js +0 -21
  107. package/dist/utils/deck.d.ts +16 -12
  108. package/dist/utils/deck.js +10 -4
  109. package/dist/utils/pmtiles-tile.js +2 -2
  110. package/dist/utils/{url.d.ts → signed-url.d.ts} +15 -1
  111. package/dist/utils/{url.js → signed-url.js} +32 -10
  112. package/dist/utils/url-state.d.ts +36 -0
  113. package/dist/utils/url-state.js +72 -2
  114. package/dist/utils/zarr-tab.d.ts +1 -2
  115. package/dist/utils/zarr-tab.js +1 -2
  116. package/dist/utils/zarr.d.ts +0 -17
  117. package/dist/utils/zarr.js +1 -45
  118. package/package.json +55 -84
  119. package/dist/components/browser/Breadcrumb.svelte +0 -50
  120. package/dist/components/browser/Breadcrumb.svelte.d.ts +0 -7
  121. package/dist/components/browser/CreateFolderDialog.svelte +0 -98
  122. package/dist/components/browser/CreateFolderDialog.svelte.d.ts +0 -6
  123. package/dist/components/browser/DeleteConfirmDialog.svelte +0 -90
  124. package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +0 -8
  125. package/dist/components/browser/DropZone.svelte +0 -83
  126. package/dist/components/browser/DropZone.svelte.d.ts +0 -7
  127. package/dist/components/browser/FileBrowser.svelte +0 -252
  128. package/dist/components/browser/FileBrowser.svelte.d.ts +0 -3
  129. package/dist/components/browser/FileRow.svelte +0 -117
  130. package/dist/components/browser/FileRow.svelte.d.ts +0 -9
  131. package/dist/components/browser/RenameDialog.svelte +0 -101
  132. package/dist/components/browser/RenameDialog.svelte.d.ts +0 -8
  133. package/dist/components/browser/SearchBar.svelte +0 -40
  134. package/dist/components/browser/SearchBar.svelte.d.ts +0 -6
  135. package/dist/components/browser/UploadButton.svelte +0 -65
  136. package/dist/components/browser/UploadButton.svelte.d.ts +0 -3
  137. package/dist/query/stac-geoparquet.d.ts +0 -31
  138. package/dist/query/stac-geoparquet.js +0 -136
  139. package/dist/utils/clipboard.d.ts +0 -13
  140. package/dist/utils/clipboard.js +0 -38
  141. package/dist/utils/cloud-url.d.ts +0 -27
  142. package/dist/utils/cloud-url.js +0 -61
  143. package/dist/utils/column-types.d.ts +0 -5
  144. package/dist/utils/column-types.js +0 -137
  145. package/dist/utils/connection-identity.d.ts +0 -51
  146. package/dist/utils/connection-identity.js +0 -97
  147. package/dist/utils/error.d.ts +0 -8
  148. package/dist/utils/error.js +0 -12
  149. package/dist/utils/evidence-context.d.ts +0 -22
  150. package/dist/utils/evidence-context.js +0 -56
  151. package/dist/utils/export.d.ts +0 -22
  152. package/dist/utils/export.js +0 -76
  153. package/dist/utils/file-sort.d.ts +0 -20
  154. package/dist/utils/file-sort.js +0 -41
  155. package/dist/utils/format.d.ts +0 -24
  156. package/dist/utils/format.js +0 -78
  157. package/dist/utils/geoarrow.d.ts +0 -32
  158. package/dist/utils/geoarrow.js +0 -672
  159. package/dist/utils/geometry-type.d.ts +0 -52
  160. package/dist/utils/geometry-type.js +0 -76
  161. package/dist/utils/hex.d.ts +0 -10
  162. package/dist/utils/hex.js +0 -27
  163. package/dist/utils/host-detection.d.ts +0 -23
  164. package/dist/utils/host-detection.js +0 -95
  165. package/dist/utils/local-storage.d.ts +0 -16
  166. package/dist/utils/local-storage.js +0 -37
  167. package/dist/utils/markdown-sql.d.ts +0 -30
  168. package/dist/utils/markdown-sql.js +0 -72
  169. package/dist/utils/notebook.d.ts +0 -59
  170. package/dist/utils/notebook.js +0 -211
  171. package/dist/utils/parquet-metadata.d.ts +0 -64
  172. package/dist/utils/parquet-metadata.js +0 -262
  173. package/dist/utils/stac-geoparquet.d.ts +0 -90
  174. package/dist/utils/stac-geoparquet.js +0 -223
  175. package/dist/utils/stac-hydrate.d.ts +0 -38
  176. package/dist/utils/stac-hydrate.js +0 -243
  177. package/dist/utils/stac.d.ts +0 -136
  178. package/dist/utils/stac.js +0 -176
  179. package/dist/utils/storage-url.d.ts +0 -90
  180. package/dist/utils/storage-url.js +0 -568
  181. package/dist/utils/wkb.d.ts +0 -43
  182. package/dist/utils/wkb.js +0 -359
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Streaming histogram + GDAL stats reader for COGs.
3
+ *
4
+ * This module owns the canonical `HISTOGRAM_BINS` constant (128) and a
5
+ * tile-by-tile histogram builder. The streamer fetches tiles from the
6
+ * coarsest overview, bins pixel values into a `Uint32Array` of length
7
+ * `HISTOGRAM_BINS`, and emits a snapshot via `onProgress` after each
8
+ * tile so the UI can fill in the histogram chart incrementally.
9
+ *
10
+ * When GDAL_METADATA `STATISTICS_MINIMUM` / `STATISTICS_MAXIMUM` are
11
+ * present we use them as fixed bin edges (single-pass streaming). When
12
+ * they are absent we fall back to a two-pass scan (min/max first, then
13
+ * bin), in which case progress fires only at the end, bin edges are
14
+ * not stable until the first pass finishes.
15
+ *
16
+ * Runs in workers and on the main thread. No DOM APIs (window,
17
+ * document, DOMParser), no Svelte runes. XML parsing is a tolerant
18
+ * regex over the GDAL_METADATA tag string, not a DOMParser pass.
19
+ */
20
+ import { buildHistogramFromGeotiff, defaultRescaleForGeotiff, percentileFromHistogram } from './cog.js';
21
+ /** Number of histogram buckets. PR #3 bumps from 64 to 128. */
22
+ export const HISTOGRAM_BINS = 128;
23
+ const DEFAULT_MAX_TILES = 32;
24
+ /**
25
+ * Parse per-band stats out of the GDAL_METADATA tag. Tolerant of
26
+ * malformed XML, uses a regex pass over `<Item ...>...</Item>` rather
27
+ * than DOMParser so this can run inside a Web Worker.
28
+ *
29
+ * Prefers the library's pre-parsed `geotiff.storedStats` when present,
30
+ * falls back to scanning the raw `cachedTags.gdalMetadata` string when
31
+ * the library could not parse it but the tag is still present (defensive
32
+ * path, rarely fires with current @developmentseed/geotiff but cheap).
33
+ *
34
+ * Returns an empty Map when no usable per-band ranges are found.
35
+ */
36
+ export function readGdalStats(geotiff) {
37
+ const out = new Map();
38
+ const stored = geotiff.storedStats;
39
+ if (stored) {
40
+ for (const [band, stats] of stored) {
41
+ if (stats.min === null || stats.max === null)
42
+ continue;
43
+ if (!(stats.min < stats.max))
44
+ continue;
45
+ const entry = { min: stats.min, max: stats.max };
46
+ if (stats.mean !== null && Number.isFinite(stats.mean))
47
+ entry.mean = stats.mean;
48
+ if (stats.std !== null && Number.isFinite(stats.std))
49
+ entry.stddev = stats.std;
50
+ out.set(band, entry);
51
+ }
52
+ if (out.size > 0)
53
+ return out;
54
+ }
55
+ // Defensive fallback, parse the raw XML if the library handed us a
56
+ // string but no parsed stats. Most builds will never hit this.
57
+ const rawTags = geotiff.cachedTags;
58
+ const xml = rawTags?.gdalMetadata ?? rawTags?.GdalMetadata ?? rawTags?.GDAL_METADATA;
59
+ if (typeof xml !== 'string' || xml.length === 0)
60
+ return out;
61
+ parseGdalMetadataXml(xml, out);
62
+ return out;
63
+ }
64
+ /**
65
+ * Append per-band stats parsed from a GDAL_METADATA XML string. Tolerant
66
+ * regex parser. Handles entries like:
67
+ * `<Item name="STATISTICS_MINIMUM" sample="0">123.4</Item>`
68
+ * Mean and stddev are picked up from STATISTICS_MEAN / STATISTICS_STDDEV.
69
+ */
70
+ function parseGdalMetadataXml(xml, out) {
71
+ // `sample` is 0-based in GDAL_METADATA, we expose 1-based to mirror the
72
+ // rest of the codebase and @developmentseed/geotiff's `storedStats`.
73
+ const itemRe = /<Item\b([^>]*)>([\s\S]*?)<\/Item>/g;
74
+ const attrRe = /(\w+)\s*=\s*"([^"]*)"/g;
75
+ const partials = new Map();
76
+ let m;
77
+ // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex iteration
78
+ while ((m = itemRe.exec(xml)) !== null) {
79
+ const attrs = {};
80
+ let a;
81
+ attrRe.lastIndex = 0;
82
+ // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex iteration
83
+ while ((a = attrRe.exec(m[1] ?? '')) !== null)
84
+ attrs[a[1]] = a[2];
85
+ const sampleStr = attrs.sample;
86
+ const nameAttr = attrs.name;
87
+ if (sampleStr === undefined || nameAttr === undefined)
88
+ continue;
89
+ const sample = Number.parseInt(sampleStr, 10);
90
+ if (!Number.isFinite(sample) || sample < 0)
91
+ continue;
92
+ const bandIdx = sample + 1;
93
+ const valueStr = (m[2] ?? '').trim();
94
+ const value = Number.parseFloat(valueStr);
95
+ if (!Number.isFinite(value))
96
+ continue;
97
+ let p = partials.get(bandIdx);
98
+ if (!p) {
99
+ p = {};
100
+ partials.set(bandIdx, p);
101
+ }
102
+ switch (nameAttr) {
103
+ case 'STATISTICS_MINIMUM':
104
+ p.min = value;
105
+ break;
106
+ case 'STATISTICS_MAXIMUM':
107
+ p.max = value;
108
+ break;
109
+ case 'STATISTICS_MEAN':
110
+ p.mean = value;
111
+ break;
112
+ case 'STATISTICS_STDDEV':
113
+ p.stddev = value;
114
+ break;
115
+ default:
116
+ break;
117
+ }
118
+ }
119
+ for (const [band, p] of partials) {
120
+ if (p.min === undefined || p.max === undefined)
121
+ continue;
122
+ if (!(p.min < p.max))
123
+ continue;
124
+ const entry = { min: p.min, max: p.max };
125
+ if (p.mean !== undefined)
126
+ entry.mean = p.mean;
127
+ if (p.stddev !== undefined)
128
+ entry.stddev = p.stddev;
129
+ out.set(band, entry);
130
+ }
131
+ }
132
+ /**
133
+ * Iterate band `b` (0-based) of a `RasterArray`, layout-agnostic. Yields
134
+ * raw sample values in row-major order without copying.
135
+ */
136
+ function* iterBand(arr, b) {
137
+ if (arr.layout === 'band-separate') {
138
+ const data = arr.bands?.[b];
139
+ if (!data)
140
+ return;
141
+ const len = data.length;
142
+ for (let i = 0; i < len; i++)
143
+ yield data[i];
144
+ }
145
+ else {
146
+ const data = arr.data;
147
+ if (!data)
148
+ return;
149
+ const stride = arr.count ?? 1;
150
+ const len = data.length;
151
+ for (let i = b; i < len; i += stride)
152
+ yield data[i];
153
+ }
154
+ }
155
+ /**
156
+ * Pick which tiles of the chosen overview to read. Below the cap we read
157
+ * every tile (exact min/max plus every-pixel histogram), above the cap we
158
+ * fall back to a 3x3 spatial sample (corners + edge midpoints + center,
159
+ * deduplicated) so a COG without a deep overview pyramid does not trigger
160
+ * a multi-GB scan.
161
+ */
162
+ function pickSampleCoords(tileCount, cap) {
163
+ if (tileCount.x <= 0 || tileCount.y <= 0)
164
+ return [];
165
+ if (tileCount.x * tileCount.y <= cap) {
166
+ const out = [];
167
+ for (let y = 0; y < tileCount.y; y++) {
168
+ for (let x = 0; x < tileCount.x; x++)
169
+ out.push([x, y]);
170
+ }
171
+ return out;
172
+ }
173
+ const xs = Array.from(new Set([0, Math.floor(tileCount.x / 2), tileCount.x - 1])).filter((n) => n >= 0 && n < tileCount.x);
174
+ const ys = Array.from(new Set([0, Math.floor(tileCount.y / 2), tileCount.y - 1])).filter((n) => n >= 0 && n < tileCount.y);
175
+ const out = [];
176
+ for (const y of ys) {
177
+ for (const x of xs)
178
+ out.push([x, y]);
179
+ }
180
+ return out;
181
+ }
182
+ /** Resolve the source IFD for histogram sampling. */
183
+ function resolveSource(geotiff, overviewIndex) {
184
+ const ovs = geotiff.overviews ?? [];
185
+ if (typeof overviewIndex === 'number') {
186
+ if (overviewIndex >= 0 && overviewIndex < ovs.length)
187
+ return ovs[overviewIndex];
188
+ }
189
+ // Default, lowest-resolution overview (smallest source).
190
+ if (ovs.length > 0)
191
+ return ovs[ovs.length - 1];
192
+ return geotiff;
193
+ }
194
+ /**
195
+ * Stream a 128-bin histogram for a single band. Fires `onProgress` after
196
+ * each tile so the UI can fill in the chart incrementally, the snapshots
197
+ * share no buffer references between calls (each `bins` is a fresh
198
+ * `Uint32Array`).
199
+ *
200
+ * Algorithm:
201
+ * - With GDAL priors, one pass, known bin edges, progressive snapshots.
202
+ * - Without priors, two passes, pass 1 finds min/max across all
203
+ * selected tiles, pass 2 bins into 128 buckets. Snapshots fire
204
+ * per-cached-tile in pass 2 so the UI still sees the histogram fill
205
+ * in on the no-priors path, bin edges aren't stable during pass 1.
206
+ *
207
+ * Honors `signal.aborted` between every tile fetch and after each bin
208
+ * pass. Throws no error on abort, returns silently with whatever
209
+ * partial snapshot the caller already received.
210
+ */
211
+ export async function streamHistogram(opts) {
212
+ const { geotiff, bandIndex, signal, onProgress } = opts;
213
+ const maxTiles = opts.maxTiles ?? DEFAULT_MAX_TILES;
214
+ if (bandIndex < 1)
215
+ return;
216
+ const source = resolveSource(geotiff, opts.overviewIndex);
217
+ const tileCount = source.tileCount;
218
+ const coords = pickSampleCoords(tileCount, maxTiles);
219
+ if (coords.length === 0)
220
+ return;
221
+ const nodata = geotiff.nodata;
222
+ const priors = readGdalStats(geotiff);
223
+ const prior = priors.get(bandIndex);
224
+ if (prior) {
225
+ await streamWithPriors({
226
+ source,
227
+ coords,
228
+ bandIdx0: bandIndex - 1,
229
+ min: prior.min,
230
+ max: prior.max,
231
+ nodata,
232
+ signal,
233
+ onProgress
234
+ });
235
+ return;
236
+ }
237
+ await streamTwoPass({
238
+ source,
239
+ coords,
240
+ bandIdx0: bandIndex - 1,
241
+ nodata,
242
+ signal,
243
+ onProgress
244
+ });
245
+ }
246
+ async function streamWithPriors(ctx) {
247
+ const { source, coords, bandIdx0, min, max, nodata, signal, onProgress } = ctx;
248
+ const range = max - min;
249
+ if (!(range > 0))
250
+ return;
251
+ const scale = HISTOGRAM_BINS / range;
252
+ const bins = new Uint32Array(HISTOGRAM_BINS);
253
+ const total = coords.length;
254
+ for (let i = 0; i < total; i++) {
255
+ if (signal.aborted)
256
+ return;
257
+ const [x, y] = coords[i];
258
+ let tile;
259
+ try {
260
+ tile = await source.fetchTile(x, y, { signal });
261
+ }
262
+ catch {
263
+ // Honor abort, but also tolerate edge-tile fetch failures.
264
+ if (signal.aborted)
265
+ return;
266
+ continue;
267
+ }
268
+ if (signal.aborted)
269
+ return;
270
+ const arr = tile.array;
271
+ if (bandIdx0 < 0 || bandIdx0 >= arr.count)
272
+ return;
273
+ for (const v of iterBand(arr, bandIdx0)) {
274
+ if (nodata !== null && v === nodata)
275
+ continue;
276
+ if (!Number.isFinite(v))
277
+ continue;
278
+ let idx = Math.floor((v - min) * scale);
279
+ if (idx < 0)
280
+ idx = 0;
281
+ else if (idx >= HISTOGRAM_BINS)
282
+ idx = HISTOGRAM_BINS - 1;
283
+ bins[idx]++;
284
+ }
285
+ // Fresh snapshot each emit so subscribers can hold refs without
286
+ // risk of mutation.
287
+ onProgress({
288
+ bins: new Uint32Array(bins),
289
+ min,
290
+ max,
291
+ tilesProcessed: i + 1,
292
+ tilesTotal: total
293
+ });
294
+ }
295
+ }
296
+ async function streamTwoPass(ctx) {
297
+ const { source, coords, bandIdx0, nodata, signal, onProgress } = ctx;
298
+ // Pass 1, min/max. Cache the decoded tiles so pass 2 doesn't refetch.
299
+ const decoded = [];
300
+ let min = Number.POSITIVE_INFINITY;
301
+ let max = Number.NEGATIVE_INFINITY;
302
+ let any = false;
303
+ for (let i = 0; i < coords.length; i++) {
304
+ if (signal.aborted)
305
+ return;
306
+ const [x, y] = coords[i];
307
+ try {
308
+ const tile = await source.fetchTile(x, y, { signal });
309
+ const arr = tile.array;
310
+ if (bandIdx0 < 0 || bandIdx0 >= arr.count)
311
+ return;
312
+ decoded.push(arr);
313
+ for (const v of iterBand(arr, bandIdx0)) {
314
+ if (nodata !== null && v === nodata)
315
+ continue;
316
+ if (!Number.isFinite(v))
317
+ continue;
318
+ if (v < min)
319
+ min = v;
320
+ if (v > max)
321
+ max = v;
322
+ any = true;
323
+ }
324
+ }
325
+ catch {
326
+ if (signal.aborted)
327
+ return;
328
+ }
329
+ }
330
+ if (signal.aborted)
331
+ return;
332
+ if (!any || !(min < max))
333
+ return;
334
+ // Pass 2, bin. Emits an incremental snapshot per cached tile so the
335
+ // UI sees the histogram fill in even on the no-priors path.
336
+ const range = max - min;
337
+ const scale = HISTOGRAM_BINS / range;
338
+ const bins = new Uint32Array(HISTOGRAM_BINS);
339
+ const total = decoded.length;
340
+ for (let i = 0; i < total; i++) {
341
+ if (signal.aborted)
342
+ return;
343
+ const arr = decoded[i];
344
+ for (const v of iterBand(arr, bandIdx0)) {
345
+ if (nodata !== null && v === nodata)
346
+ continue;
347
+ if (!Number.isFinite(v))
348
+ continue;
349
+ let idx = Math.floor((v - min) * scale);
350
+ if (idx < 0)
351
+ idx = 0;
352
+ else if (idx >= HISTOGRAM_BINS)
353
+ idx = HISTOGRAM_BINS - 1;
354
+ bins[idx]++;
355
+ }
356
+ onProgress({
357
+ bins: new Uint32Array(bins),
358
+ min,
359
+ max,
360
+ tilesProcessed: i + 1,
361
+ tilesTotal: total
362
+ });
363
+ }
364
+ }
365
+ /**
366
+ * Seed a {@link RescaleConfig} for a freshly opened COG in the same
367
+ * normalized shader-space [0, 1] coordinate system the rescale slider
368
+ * operates on. The GPU's hardware normalization (`r8unorm` / `r16unorm`)
369
+ * and the `Colormap` CPU baker both divide raw integer samples by the
370
+ * format's max (255 for uint8, 65535 for uint16) before sampling, and
371
+ * `CogControls`'s `setRescaleMin/Max` clamps the slider via `clamp01`.
372
+ * Storing raw GeoTIFF stats (e.g. `{min: 0, max: 10000}` for uint16
373
+ * reflectance) here would snap back to `{0, 1}` on the user's first
374
+ * slider touch.
375
+ *
376
+ * Fallback chain:
377
+ * 1. GDAL `STATISTICS_MINIMUM` / `STATISTICS_MAXIMUM` for the band,
378
+ * normalized to shader space by dividing by the sample-format
379
+ * factor (255 for uint8, 65535 for uint ≥ 8 bps). Float bands are
380
+ * passed through unchanged because they already live near [0, 1].
381
+ * 2. `buildHistogramFromGeotiff` + p2 / p98 percentile lookup so a
382
+ * uint16 reflectance band with no STATISTICS tag still gets a
383
+ * contrasted preview from the first tile of the smallest overview.
384
+ * 3. {@link defaultRescaleForGeotiff} bit-depth-aware constants.
385
+ */
386
+ export async function seedRescaleFromGeotiff(geotiff, options) {
387
+ const signal = options?.signal;
388
+ const bandIndex = options?.bandIndex ?? 1;
389
+ // 1) GDAL_METADATA STATISTICS_MIN/MAX, normalize raw values into shader [0, 1].
390
+ const stats = readGdalStats(geotiff).get(bandIndex);
391
+ if (stats && Number.isFinite(stats.min) && Number.isFinite(stats.max) && stats.min < stats.max) {
392
+ const tags = geotiff.cachedTags;
393
+ const sampleFormat = tags.sampleFormat?.[0] ?? 1;
394
+ const bps = tags.bitsPerSample?.[0] ?? 8;
395
+ // Float bands already live in [0, 1]-ish space; integer bands need to
396
+ // be divided by their format max so the slider operates on shader-space.
397
+ const norm = sampleFormat === 1 ? (bps <= 8 ? 255 : 65535) : 1;
398
+ const min = stats.min / norm;
399
+ const max = stats.max / norm;
400
+ if (Number.isFinite(min) && Number.isFinite(max) && min < max) {
401
+ return { min, max };
402
+ }
403
+ }
404
+ if (signal?.aborted)
405
+ return defaultRescaleForGeotiff(geotiff);
406
+ // 2) p2 / p98 from a single-tile shader-space histogram.
407
+ try {
408
+ const bins = await buildHistogramFromGeotiff(geotiff, signal);
409
+ if (signal?.aborted)
410
+ return defaultRescaleForGeotiff(geotiff);
411
+ if (bins) {
412
+ const p2 = percentileFromHistogram(bins, 0.02);
413
+ const p98 = percentileFromHistogram(bins, 0.98);
414
+ if (p2 !== null && p98 !== null && p2 < p98) {
415
+ return { min: p2, max: p98 };
416
+ }
417
+ }
418
+ }
419
+ catch {
420
+ // Fall through to defaults.
421
+ }
422
+ // 3) Bit-depth-aware defaults.
423
+ return defaultRescaleForGeotiff(geotiff);
424
+ }