@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.
- package/LICENSE +5 -0
- package/README.md +20 -12
- package/dist/components/browser/FileTreeSidebar.svelte +32 -17
- package/dist/components/layout/AboutSheet.svelte +5 -2
- package/dist/components/layout/ConnectionDialog.svelte +1 -1
- package/dist/components/layout/SettingsSheet.svelte +237 -0
- package/dist/components/layout/SettingsSheet.svelte.d.ts +6 -0
- package/dist/components/layout/Sidebar.svelte +73 -6
- package/dist/components/layout/Sidebar.svelte.d.ts +4 -1
- package/dist/components/layout/StatusBar.svelte +1 -1
- package/dist/components/layout/TabBar.svelte +2 -2
- package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/input/input.svelte.d.ts +1 -1
- package/dist/components/ui/resizable/index.d.ts +1 -1
- package/dist/components/ui/resizable/index.js +2 -2
- package/dist/components/ui/slider/index.d.ts +3 -0
- package/dist/components/ui/slider/index.js +5 -0
- package/dist/components/ui/slider/range-slider.svelte +94 -0
- package/dist/components/ui/slider/range-slider.svelte.d.ts +21 -0
- package/dist/components/ui/slider/slider.svelte +83 -0
- package/dist/components/ui/slider/slider.svelte.d.ts +7 -0
- package/dist/components/viewers/ArchiveViewer.svelte +2 -2
- package/dist/components/viewers/CodeViewer.svelte +31 -22
- package/dist/components/viewers/CogControls.svelte +338 -184
- package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
- package/dist/components/viewers/CogViewer.svelte +320 -119
- package/dist/components/viewers/CopcViewer.svelte +1 -1
- package/dist/components/viewers/FlatGeobufViewer.svelte +1 -1
- package/dist/components/viewers/GeoParquetMapViewer.svelte +6 -6
- package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/ImageViewer.svelte +2 -2
- package/dist/components/viewers/MarkdownViewer.svelte +12 -9
- package/dist/components/viewers/MediaViewer.svelte +2 -2
- package/dist/components/viewers/ModelViewer.svelte +1 -1
- package/dist/components/viewers/MultiCogViewer.svelte +467 -102
- package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/NotebookViewer.svelte +6 -3
- package/dist/components/viewers/PdfViewer.svelte +2 -2
- package/dist/components/viewers/PmtilesViewer.svelte +3 -6
- package/dist/components/viewers/RawViewer.svelte +6 -3
- package/dist/components/viewers/StacMapViewer.svelte +10 -2
- package/dist/components/viewers/StacMosaicViewer.svelte +1800 -362
- package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/StacTabViewer.svelte +24 -13
- package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/TableGrid.svelte +4 -4
- package/dist/components/viewers/TableStatusBar.svelte +1 -1
- package/dist/components/viewers/TableToolbar.svelte +1 -1
- package/dist/components/viewers/TableViewer.svelte +25 -17
- package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
- package/dist/components/viewers/ViewerRouter.svelte +16 -8
- package/dist/components/viewers/ZarrMapViewer.svelte +11 -9
- package/dist/components/viewers/ZarrViewer.svelte +4 -4
- package/dist/components/viewers/cog/ChannelPicker.svelte +83 -0
- package/dist/components/viewers/cog/ChannelPicker.svelte.d.ts +13 -0
- package/dist/components/viewers/cog/PixelInspectorPanel.svelte +87 -0
- package/dist/components/viewers/cog/PixelInspectorPanel.svelte.d.ts +17 -0
- package/dist/components/viewers/cog/buildRgbLayer.d.ts +78 -0
- package/dist/components/viewers/cog/buildRgbLayer.js +176 -0
- package/dist/components/viewers/map/AttributeTable.svelte +1 -1
- package/dist/components/viewers/map/MapContainer.svelte +37 -11
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +1 -1
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +1 -1
- package/dist/components/viewers/stac/StacDatetimeBar.svelte +175 -0
- package/dist/components/viewers/stac/StacDatetimeBar.svelte.d.ts +10 -0
- package/dist/components/viewers/stac/StacFilterPanel.svelte +243 -0
- package/dist/components/viewers/stac/StacFilterPanel.svelte.d.ts +14 -0
- package/dist/components/viewers/stac/StacItemInspector.svelte +223 -0
- package/dist/components/viewers/stac/StacItemInspector.svelte.d.ts +10 -0
- package/dist/components/viewers/stac/StacItemStrip.svelte +228 -0
- package/dist/components/viewers/stac/StacItemStrip.svelte.d.ts +12 -0
- package/dist/file-icons/index.d.ts +1 -1
- package/dist/file-icons/index.js +1 -1
- package/dist/i18n/ar.js +110 -2
- package/dist/i18n/en.js +110 -2
- package/dist/index.d.ts +2 -28
- package/dist/index.js +7 -23
- package/dist/query/engine.d.ts +10 -0
- package/dist/query/source.js +1 -1
- package/dist/query/stac-source-factory.d.ts +65 -0
- package/dist/query/stac-source-factory.js +77 -0
- package/dist/query/stac-source-parquet.d.ts +135 -0
- package/dist/query/stac-source-parquet.js +465 -0
- package/dist/query/wasm.d.ts +8 -0
- package/dist/query/wasm.js +304 -2
- package/dist/storage/presign.js +1 -1
- package/dist/storage/providers.js +5 -5
- package/dist/stores/config.svelte.d.ts +15 -0
- package/dist/stores/config.svelte.js +46 -0
- package/dist/stores/connections.svelte.d.ts +2 -2
- package/dist/stores/connections.svelte.js +1 -2
- package/dist/stores/files.svelte.d.ts +1 -1
- package/dist/stores/files.svelte.js +1 -1
- package/dist/stores/query-history.svelte.js +1 -1
- package/dist/stores/settings.svelte.d.ts +16 -1
- package/dist/stores/settings.svelte.js +104 -48
- package/dist/stores/tabs.svelte.d.ts +3 -0
- package/dist/stores/tabs.svelte.js +17 -0
- package/dist/utils/cog-histogram.d.ts +121 -0
- package/dist/utils/cog-histogram.js +424 -0
- package/dist/utils/cog.d.ts +200 -60
- package/dist/utils/cog.js +377 -114
- package/dist/utils/colormap-sprite.d.ts +0 -9
- package/dist/utils/colormap-sprite.js +0 -21
- package/dist/utils/deck.d.ts +16 -12
- package/dist/utils/deck.js +10 -4
- package/dist/utils/pmtiles-tile.js +2 -2
- package/dist/utils/{url.d.ts → signed-url.d.ts} +15 -1
- package/dist/utils/{url.js → signed-url.js} +32 -10
- package/dist/utils/url-state.d.ts +36 -0
- package/dist/utils/url-state.js +72 -2
- package/dist/utils/zarr-tab.d.ts +1 -2
- package/dist/utils/zarr-tab.js +1 -2
- package/dist/utils/zarr.d.ts +0 -17
- package/dist/utils/zarr.js +1 -45
- package/package.json +55 -84
- package/dist/components/browser/Breadcrumb.svelte +0 -50
- package/dist/components/browser/Breadcrumb.svelte.d.ts +0 -7
- package/dist/components/browser/CreateFolderDialog.svelte +0 -98
- package/dist/components/browser/CreateFolderDialog.svelte.d.ts +0 -6
- package/dist/components/browser/DeleteConfirmDialog.svelte +0 -90
- package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +0 -8
- package/dist/components/browser/DropZone.svelte +0 -83
- package/dist/components/browser/DropZone.svelte.d.ts +0 -7
- package/dist/components/browser/FileBrowser.svelte +0 -252
- package/dist/components/browser/FileBrowser.svelte.d.ts +0 -3
- package/dist/components/browser/FileRow.svelte +0 -117
- package/dist/components/browser/FileRow.svelte.d.ts +0 -9
- package/dist/components/browser/RenameDialog.svelte +0 -101
- package/dist/components/browser/RenameDialog.svelte.d.ts +0 -8
- package/dist/components/browser/SearchBar.svelte +0 -40
- package/dist/components/browser/SearchBar.svelte.d.ts +0 -6
- package/dist/components/browser/UploadButton.svelte +0 -65
- package/dist/components/browser/UploadButton.svelte.d.ts +0 -3
- package/dist/query/stac-geoparquet.d.ts +0 -31
- package/dist/query/stac-geoparquet.js +0 -136
- package/dist/utils/clipboard.d.ts +0 -13
- package/dist/utils/clipboard.js +0 -38
- package/dist/utils/cloud-url.d.ts +0 -27
- package/dist/utils/cloud-url.js +0 -61
- package/dist/utils/column-types.d.ts +0 -5
- package/dist/utils/column-types.js +0 -137
- package/dist/utils/connection-identity.d.ts +0 -51
- package/dist/utils/connection-identity.js +0 -97
- package/dist/utils/error.d.ts +0 -8
- package/dist/utils/error.js +0 -12
- package/dist/utils/evidence-context.d.ts +0 -22
- package/dist/utils/evidence-context.js +0 -56
- package/dist/utils/export.d.ts +0 -22
- package/dist/utils/export.js +0 -76
- package/dist/utils/file-sort.d.ts +0 -20
- package/dist/utils/file-sort.js +0 -41
- package/dist/utils/format.d.ts +0 -24
- package/dist/utils/format.js +0 -78
- package/dist/utils/geoarrow.d.ts +0 -32
- package/dist/utils/geoarrow.js +0 -672
- package/dist/utils/geometry-type.d.ts +0 -52
- package/dist/utils/geometry-type.js +0 -76
- package/dist/utils/hex.d.ts +0 -10
- package/dist/utils/hex.js +0 -27
- package/dist/utils/host-detection.d.ts +0 -23
- package/dist/utils/host-detection.js +0 -95
- package/dist/utils/local-storage.d.ts +0 -16
- package/dist/utils/local-storage.js +0 -37
- package/dist/utils/markdown-sql.d.ts +0 -30
- package/dist/utils/markdown-sql.js +0 -72
- package/dist/utils/notebook.d.ts +0 -59
- package/dist/utils/notebook.js +0 -211
- package/dist/utils/parquet-metadata.d.ts +0 -64
- package/dist/utils/parquet-metadata.js +0 -262
- package/dist/utils/stac-geoparquet.d.ts +0 -90
- package/dist/utils/stac-geoparquet.js +0 -223
- package/dist/utils/stac-hydrate.d.ts +0 -38
- package/dist/utils/stac-hydrate.js +0 -243
- package/dist/utils/stac.d.ts +0 -136
- package/dist/utils/stac.js +0 -176
- package/dist/utils/storage-url.d.ts +0 -90
- package/dist/utils/storage-url.js +0 -568
- package/dist/utils/wkb.d.ts +0 -43
- package/dist/utils/wkb.js +0 -359
package/dist/utils/cog.js
CHANGED
|
@@ -1,41 +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';
|
|
12
|
+
import { HISTOGRAM_BINS, readGdalStats, streamHistogram } from './cog-histogram.js';
|
|
8
13
|
import { COLORMAP_INDEX, getColormapTexture } from './colormap-sprite.js';
|
|
9
|
-
// ─── Constants ───────────────────────────────────────────────────
|
|
10
14
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
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).
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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.
|
|
22
37
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'
|
|
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');
|
|
28
43
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
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 };
|
|
39
63
|
/** Create a sensible default band config based on COG metadata. */
|
|
40
64
|
export function defaultBandConfig(bandCount, sampleFormat) {
|
|
41
65
|
if (bandCount >= 3 && bandCount <= 4) {
|
|
@@ -120,11 +144,183 @@ export function needsCustomPipelineForConfig(geotiff, config) {
|
|
|
120
144
|
return true;
|
|
121
145
|
return false;
|
|
122
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
|
+
}
|
|
123
212
|
export const DEFAULT_RESCALE = { min: 0, max: 1 };
|
|
124
213
|
/** True when the rescale values would produce a visible change on the GPU. */
|
|
125
214
|
export function isRescaleActive(cfg) {
|
|
126
215
|
return cfg.min !== DEFAULT_RESCALE.min || cfg.max !== DEFAULT_RESCALE.max;
|
|
127
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
|
+
}
|
|
128
324
|
/**
|
|
129
325
|
* Build a `getTileData` + `renderTile` pair that reuses the library-default
|
|
130
326
|
* uint pipeline (via `inferRenderPipeline`) and appends `LinearRescale` to the
|
|
@@ -144,7 +340,7 @@ export function createRescaledPipeline(geotiff, rescale) {
|
|
|
144
340
|
if (builtFor === device && defaultGetTileData && defaultRenderTile)
|
|
145
341
|
return;
|
|
146
342
|
const inferred = inferRenderPipeline(geotiff, device);
|
|
147
|
-
// `inferRenderPipeline` returns generic callbacks. `
|
|
343
|
+
// `inferRenderPipeline` returns generic callbacks. `MinimalTileData` is the
|
|
148
344
|
// contractual superset used by COGLayer — safe upcast.
|
|
149
345
|
defaultGetTileData = inferred.getTileData;
|
|
150
346
|
defaultRenderTile = inferred.renderTile;
|
|
@@ -170,17 +366,18 @@ export function createRescaledPipeline(geotiff, rescale) {
|
|
|
170
366
|
}
|
|
171
367
|
/**
|
|
172
368
|
* Build a `renderPipeline` array for `MultiCOGLayer` / raster mosaics.
|
|
173
|
-
* Combines optional
|
|
174
|
-
* 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`.
|
|
175
373
|
*/
|
|
176
374
|
export function buildBandRenderPipeline(opts = {}) {
|
|
177
375
|
const modules = [];
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
376
|
+
const nodataMod = opts.noDataVal !== undefined
|
|
377
|
+
? nodataModule(opts.noDataVal ?? null, opts.noDataSampleScale ?? 1)
|
|
378
|
+
: null;
|
|
379
|
+
if (nodataMod)
|
|
380
|
+
modules.push(nodataMod);
|
|
184
381
|
if (opts.rescale && isRescaleActive(opts.rescale)) {
|
|
185
382
|
modules.push({
|
|
186
383
|
module: LinearRescale,
|
|
@@ -247,10 +444,11 @@ export function selectCogPipeline(geotiff, opts = {}) {
|
|
|
247
444
|
? needsCustomPipelineForConfig(geotiff, bandConfig)
|
|
248
445
|
: needsCustomPipeline(geotiff);
|
|
249
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.
|
|
250
450
|
return {
|
|
251
|
-
getTileData: createConfigurableGetTileData(geotiff, bandConfig,
|
|
252
|
-
onHistogram: opts.onHistogram
|
|
253
|
-
}),
|
|
451
|
+
getTileData: createConfigurableGetTileData(geotiff, bandConfig).getTileData,
|
|
254
452
|
renderTile: buildCustomRenderTile(bandConfig, rescale)
|
|
255
453
|
};
|
|
256
454
|
}
|
|
@@ -260,7 +458,7 @@ export function selectCogPipeline(geotiff, opts = {}) {
|
|
|
260
458
|
const fallbackSf = geotiff.cachedTags.sampleFormat?.[0] ?? 1;
|
|
261
459
|
const fallbackConfig = defaultBandConfig(geotiff.count, fallbackSf);
|
|
262
460
|
return {
|
|
263
|
-
getTileData: createCustomGetTileData(geotiff
|
|
461
|
+
getTileData: createCustomGetTileData(geotiff),
|
|
264
462
|
renderTile: buildCustomRenderTile(fallbackConfig, rescale)
|
|
265
463
|
};
|
|
266
464
|
}
|
|
@@ -275,27 +473,9 @@ export function selectCogPipeline(geotiff, opts = {}) {
|
|
|
275
473
|
}
|
|
276
474
|
const BITMAP_SOURCE = 'geotiff-bitmap-src';
|
|
277
475
|
const BITMAP_LAYER = 'geotiff-bitmap-layer';
|
|
278
|
-
// ───
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return Number.isFinite(v) ? Math.max(lo, Math.min(hi, v)) : fallback;
|
|
282
|
-
}
|
|
283
|
-
/** Clamp geographic bounds to valid MapLibre web-Mercator range. */
|
|
284
|
-
export function clampBounds(b) {
|
|
285
|
-
return {
|
|
286
|
-
west: safeClamp(b.west, -180, 180, -180),
|
|
287
|
-
south: safeClamp(b.south, -85.051129, 85.051129, -85.051129),
|
|
288
|
-
east: safeClamp(b.east, -180, 180, 180),
|
|
289
|
-
north: safeClamp(b.north, -85.051129, 85.051129, 85.051129)
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Build a data-type label from GeoTIFF sample format and bits per sample.
|
|
294
|
-
* e.g. "uint8", "float32", "int16"
|
|
295
|
-
*/
|
|
296
|
-
export function buildDataTypeLabel(sampleFormat, bitsPerSample) {
|
|
297
|
-
return `${SF_LABELS[sampleFormat] ?? `sf${sampleFormat}`}${bitsPerSample ?? ''}`;
|
|
298
|
-
}
|
|
476
|
+
// ─── Types & pure helpers ────────────────────────────────────────
|
|
477
|
+
// `GeoBounds`, `CogInfo`, `safeClamp`, `clampBounds`, `buildDataTypeLabel`
|
|
478
|
+
// live in `cog-info.ts` in @walkthru-earth/objex-utils and are re-exported at the top of this file.
|
|
299
479
|
// ─── Map helpers (depend on maplibre-gl) ─────────────────────────
|
|
300
480
|
/**
|
|
301
481
|
* Query the GPU's MAX_TEXTURE_SIZE from MapLibre's WebGL context.
|
|
@@ -458,7 +638,7 @@ const MAX_NONTILED_PIXELS = 100_000_000;
|
|
|
458
638
|
export async function renderNonTiledBitmap(options) {
|
|
459
639
|
const { url, map, signal } = options;
|
|
460
640
|
// Open GeoTIFF (reuse if already opened for pre-flight)
|
|
461
|
-
const geotiff = options.geotiff ?? (await
|
|
641
|
+
const geotiff = options.geotiff ?? (await loadGeoTIFF(url));
|
|
462
642
|
if (signal.aborted)
|
|
463
643
|
throw new DOMException('Aborted', 'AbortError');
|
|
464
644
|
const imgW = geotiff.width;
|
|
@@ -604,8 +784,15 @@ export function needsCustomPipeline(geotiff) {
|
|
|
604
784
|
// sampleFormat is null or not uint → needs custom
|
|
605
785
|
return sf === null || sf[0] !== 1;
|
|
606
786
|
}
|
|
607
|
-
/**
|
|
608
|
-
|
|
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;
|
|
609
796
|
/**
|
|
610
797
|
* Create custom getTileData for non-uint COGs.
|
|
611
798
|
* Reads band 0, normalizes using GDAL statistics / per-tile adaptive stretch,
|
|
@@ -614,7 +801,7 @@ export const HISTOGRAM_BIN_COUNT = 64;
|
|
|
614
801
|
* `colormaps.png`. Reserves `r = 0` for nodata so `FilterNoDataVal` can
|
|
615
802
|
* discard those fragments before the ramp sample.
|
|
616
803
|
*/
|
|
617
|
-
export function createCustomGetTileData(geotiff,
|
|
804
|
+
export function createCustomGetTileData(geotiff, _opts = {}) {
|
|
618
805
|
// Read Scale/Offset TIFF tags (GDAL convention for scaled datasets like DEMs)
|
|
619
806
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
620
807
|
const tags = geotiff.cachedTags;
|
|
@@ -649,7 +836,6 @@ export function createCustomGetTileData(geotiff, opts = {}) {
|
|
|
649
836
|
let sharedMax = globalMax;
|
|
650
837
|
// Resolve the sprite texture from the first tile's device; reuse per-device.
|
|
651
838
|
let texturePromise = null;
|
|
652
|
-
const histogram = isSingleBand ? new Uint32Array(HISTOGRAM_BIN_COUNT) : null;
|
|
653
839
|
return async (image, options) => {
|
|
654
840
|
if (isSingleBand && !texturePromise) {
|
|
655
841
|
texturePromise = getColormapTexture(options.device);
|
|
@@ -669,6 +855,10 @@ export function createCustomGetTileData(geotiff, opts = {}) {
|
|
|
669
855
|
const pixelCount = width * height;
|
|
670
856
|
const scale = gdalScale ?? 1;
|
|
671
857
|
const offset = gdalOffset ?? 0;
|
|
858
|
+
// Allocate per-tile histogram so deck.gl's tile cache retains it with
|
|
859
|
+
// the tile object. The viewer sums histograms of visible tiles from
|
|
860
|
+
// TileLayer's `onViewportLoad` hook, no shared accumulator needed.
|
|
861
|
+
const histogram = isSingleBand ? new Uint32Array(HISTOGRAM_BIN_COUNT) : null;
|
|
672
862
|
// When no global stats, scan this tile and widen the shared range
|
|
673
863
|
if (sharedMin === null || sharedMax === null) {
|
|
674
864
|
let tMin = Infinity;
|
|
@@ -734,14 +924,13 @@ export function createCustomGetTileData(geotiff, opts = {}) {
|
|
|
734
924
|
}
|
|
735
925
|
rgba[idx + 3] = 255;
|
|
736
926
|
}
|
|
737
|
-
if (histogram && opts.onHistogram)
|
|
738
|
-
opts.onHistogram(histogram);
|
|
739
927
|
return {
|
|
740
928
|
imageData: new ImageData(rgba, width, height),
|
|
741
929
|
width,
|
|
742
930
|
height,
|
|
743
931
|
colormapTexture: isSingleBand ? colormapTexture : undefined,
|
|
744
|
-
nodataSentinel: isSingleBand ? 0 : undefined
|
|
932
|
+
nodataSentinel: isSingleBand ? 0 : undefined,
|
|
933
|
+
histogram: histogram ?? undefined
|
|
745
934
|
};
|
|
746
935
|
};
|
|
747
936
|
}
|
|
@@ -775,11 +964,7 @@ export function buildCustomRenderTile(config, rescale) {
|
|
|
775
964
|
props: { rescaleMin: rescale.min, rescaleMax: rescale.max }
|
|
776
965
|
});
|
|
777
966
|
}
|
|
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: {} }, {
|
|
967
|
+
pipeline.push({
|
|
783
968
|
module: Colormap,
|
|
784
969
|
props: {
|
|
785
970
|
colormapTexture: data.colormapTexture,
|
|
@@ -848,18 +1033,27 @@ function computeBandRanges(bands, bandIndices, pixelCount, nodata) {
|
|
|
848
1033
|
* Supports RGB mode (multi-band → R,G,B with alpha=255, fully baked) and
|
|
849
1034
|
* single-band mode (band N normalized into the `r` channel; the ramp is
|
|
850
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.
|
|
851
1040
|
*/
|
|
852
|
-
export function createConfigurableGetTileData(geotiff, config,
|
|
1041
|
+
export function createConfigurableGetTileData(geotiff, config, _opts = {}) {
|
|
853
1042
|
const bandCount = geotiff.count;
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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);
|
|
863
1057
|
}
|
|
864
1058
|
const [tile, colormapTexture] = await Promise.all([
|
|
865
1059
|
image.fetchTile(options.x, options.y, {
|
|
@@ -868,33 +1062,38 @@ export function createConfigurableGetTileData(geotiff, config, opts = {}) {
|
|
|
868
1062
|
pool: options.pool,
|
|
869
1063
|
signal: options.signal
|
|
870
1064
|
}),
|
|
871
|
-
texturePromise ?? Promise.resolve(undefined)
|
|
1065
|
+
refs.texturePromise ?? Promise.resolve(undefined)
|
|
872
1066
|
]);
|
|
873
1067
|
const arr = tile.array;
|
|
874
1068
|
const { width, height, nodata } = arr;
|
|
875
1069
|
const pixelCount = width * height;
|
|
876
1070
|
const bands = extractBands(arr, bandCount, pixelCount);
|
|
877
1071
|
const rgba = new Uint8ClampedArray(pixelCount * 4);
|
|
878
|
-
|
|
1072
|
+
// Per-tile histogram, cached by deck.gl's tile cache with the tile
|
|
1073
|
+
// object. Cloud-native by construction: at each zoom level, COG only
|
|
1074
|
+
// decodes the overview tiles that cover the viewport, so the summed
|
|
1075
|
+
// histogram naturally reflects "what the user is looking at right now".
|
|
1076
|
+
const histogram = currentConfig.mode === 'single' ? new Uint32Array(HISTOGRAM_BIN_COUNT) : null;
|
|
1077
|
+
if (currentConfig.mode === 'rgb') {
|
|
879
1078
|
// RGB mode: map 3 bands to R, G, B
|
|
880
|
-
const indices = [
|
|
1079
|
+
const indices = [currentConfig.rBand, currentConfig.gBand, currentConfig.bBand];
|
|
881
1080
|
// Compute ranges for the 3 selected bands
|
|
882
1081
|
for (const bi of indices) {
|
|
883
|
-
if (!sharedMins.has(bi)) {
|
|
1082
|
+
if (!refs.sharedMins.has(bi)) {
|
|
884
1083
|
const { mins, maxs } = computeBandRanges(bands, [bi], pixelCount, nodata);
|
|
885
|
-
sharedMins.set(bi, mins[0]);
|
|
886
|
-
sharedMaxs.set(bi, maxs[0]);
|
|
1084
|
+
refs.sharedMins.set(bi, mins[0]);
|
|
1085
|
+
refs.sharedMaxs.set(bi, maxs[0]);
|
|
887
1086
|
}
|
|
888
1087
|
}
|
|
889
|
-
const rBand = bands[
|
|
890
|
-
const gBand = bands[
|
|
891
|
-
const bBand = bands[
|
|
892
|
-
const rMin = sharedMins.get(
|
|
893
|
-
const rMax = sharedMaxs.get(
|
|
894
|
-
const gMin = sharedMins.get(
|
|
895
|
-
const gMax = sharedMaxs.get(
|
|
896
|
-
const bMin = sharedMins.get(
|
|
897
|
-
const bMax = sharedMaxs.get(
|
|
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);
|
|
898
1097
|
const rRange = rMax - rMin || 1;
|
|
899
1098
|
const gRange = gMax - gMin || 1;
|
|
900
1099
|
const bRange = bMax - bMin || 1;
|
|
@@ -923,15 +1122,15 @@ export function createConfigurableGetTileData(geotiff, config, opts = {}) {
|
|
|
923
1122
|
// Single-band mode: normalize the selected band into the `r`
|
|
924
1123
|
// channel and reserve `r = 0` as a nodata sentinel that
|
|
925
1124
|
// `FilterNoDataVal` discards before the `Colormap` GPU lookup.
|
|
926
|
-
const bi =
|
|
1125
|
+
const bi = currentConfig.band;
|
|
927
1126
|
const bandData = bands[bi];
|
|
928
|
-
if (!sharedMins.has(bi) && bandData) {
|
|
1127
|
+
if (!refs.sharedMins.has(bi) && bandData) {
|
|
929
1128
|
const { mins, maxs } = computeBandRanges(bands, [bi], pixelCount, nodata);
|
|
930
|
-
sharedMins.set(bi, mins[0]);
|
|
931
|
-
sharedMaxs.set(bi, maxs[0]);
|
|
1129
|
+
refs.sharedMins.set(bi, mins[0]);
|
|
1130
|
+
refs.sharedMaxs.set(bi, maxs[0]);
|
|
932
1131
|
}
|
|
933
|
-
const rangeMin = sharedMins.get(bi) ?? 0;
|
|
934
|
-
const rangeMax = sharedMaxs.get(bi) ?? 1;
|
|
1132
|
+
const rangeMin = refs.sharedMins.get(bi) ?? 0;
|
|
1133
|
+
const rangeMax = refs.sharedMaxs.get(bi) ?? 1;
|
|
935
1134
|
const range = rangeMax - rangeMin || 1;
|
|
936
1135
|
for (let i = 0; i < pixelCount; i++) {
|
|
937
1136
|
const raw = bandData?.[i] ?? 0;
|
|
@@ -955,17 +1154,34 @@ export function createConfigurableGetTileData(geotiff, config, opts = {}) {
|
|
|
955
1154
|
}
|
|
956
1155
|
}
|
|
957
1156
|
}
|
|
958
|
-
if (histogram && opts.onHistogram)
|
|
959
|
-
opts.onHistogram(histogram);
|
|
960
1157
|
}
|
|
961
1158
|
return {
|
|
962
1159
|
imageData: new ImageData(rgba, width, height),
|
|
963
1160
|
width,
|
|
964
1161
|
height,
|
|
965
|
-
colormapTexture:
|
|
966
|
-
nodataSentinel:
|
|
1162
|
+
colormapTexture: currentConfig.mode === 'single' ? colormapTexture : undefined,
|
|
1163
|
+
nodataSentinel: currentConfig.mode === 'single' ? 0 : undefined,
|
|
1164
|
+
histogram: histogram ?? undefined
|
|
967
1165
|
};
|
|
968
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
|
+
};
|
|
969
1185
|
}
|
|
970
1186
|
// ─── EPSG resolution via bundled database ────────────────────────
|
|
971
1187
|
/**
|
|
@@ -1052,13 +1268,58 @@ export async function resolveProj4Def(crs, _signal) {
|
|
|
1052
1268
|
// ProjJSON — stringify for proj4
|
|
1053
1269
|
return JSON.stringify(crs);
|
|
1054
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
|
+
}
|
|
1055
1311
|
/**
|
|
1056
1312
|
* Read pixel values at a given lng/lat from a GeoTIFF.
|
|
1057
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.
|
|
1058
1319
|
*/
|
|
1059
1320
|
export async function readPixelAtLngLat(geotiff, lng, lat, proj4Def,
|
|
1060
1321
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1061
|
-
pool, signal) {
|
|
1322
|
+
pool, signal, options) {
|
|
1062
1323
|
// Convert WGS84 to source CRS
|
|
1063
1324
|
let srcX = lng;
|
|
1064
1325
|
let srcY = lat;
|
|
@@ -1072,15 +1333,17 @@ pool, signal) {
|
|
|
1072
1333
|
return null;
|
|
1073
1334
|
}
|
|
1074
1335
|
}
|
|
1336
|
+
// Read against the selected overview if one was supplied, else full res.
|
|
1337
|
+
const source = options?.overview ?? geotiff;
|
|
1075
1338
|
// Get pixel indices (row, col)
|
|
1076
|
-
const [row, col] =
|
|
1077
|
-
if (row < 0 || row >=
|
|
1339
|
+
const [row, col] = source.index(srcX, srcY);
|
|
1340
|
+
if (row < 0 || row >= source.height || col < 0 || col >= source.width)
|
|
1078
1341
|
return null;
|
|
1079
1342
|
// Compute tile indices
|
|
1080
|
-
const tileX = Math.floor(col /
|
|
1081
|
-
const tileY = Math.floor(row /
|
|
1343
|
+
const tileX = Math.floor(col / source.tileWidth);
|
|
1344
|
+
const tileY = Math.floor(row / source.tileHeight);
|
|
1082
1345
|
// Fetch tile
|
|
1083
|
-
const tile = await
|
|
1346
|
+
const tile = await source.fetchTile(tileX, tileY, { pool, signal });
|
|
1084
1347
|
const arr = tile.array;
|
|
1085
1348
|
// Read all band values at this pixel
|
|
1086
1349
|
const localCol = col - tileX * arr.width;
|