@walkthru-earth/objex 1.3.1 → 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 +263 -112
- 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 +1 -1
- package/dist/components/viewers/StacMosaicViewer.svelte +1760 -408
- 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 +177 -20
- package/dist/utils/cog.js +361 -76
- 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/cog-pure.d.ts +0 -25
- package/dist/utils/cog-pure.js +0 -35
- 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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer-construction dispatch for the unified RGB picker.
|
|
3
|
+
*
|
|
4
|
+
* Decision rule:
|
|
5
|
+
* - All three RGB channels point to the SAME asset → COGLayer. When a
|
|
6
|
+
* `preflightGeotiff` is supplied, the per-channel `bandIndex` values are
|
|
7
|
+
* translated into a `BandConfig` and run through `selectCogPipeline`,
|
|
8
|
+
* which returns a custom `getTileData` / `renderTile` pair that swaps
|
|
9
|
+
* bands as requested (the library's COGLayer does not accept a
|
|
10
|
+
* `bandConfig` prop, only the resolved pipeline). Without a preflight
|
|
11
|
+
* GeoTIFF the layer falls back to the library default pipeline, which
|
|
12
|
+
* reads bands 0/1/2 in that order, correct for single-band per-asset
|
|
13
|
+
* COGs and for the default natural-color order on pre-baked multi-band
|
|
14
|
+
* visuals.
|
|
15
|
+
* - Channels point to DIFFERENT assets → MultiCOGLayer with the legacy
|
|
16
|
+
* `composite: { r, g, b }` keyed on asset keys. MultiCOGLayer reads band 0
|
|
17
|
+
* of each source, per-channel band index is silently ignored on this path
|
|
18
|
+
* (library limitation, see spec Known Limitations).
|
|
19
|
+
*
|
|
20
|
+
* `buildRgbLayer` ONLY constructs the layer. It does not add overlays,
|
|
21
|
+
* register cleanup, or touch deck.gl state. Caller owns lifecycle.
|
|
22
|
+
*/
|
|
23
|
+
import { COGLayer, MultiCOGLayer } from '@developmentseed/deck.gl-geotiff';
|
|
24
|
+
import { allChannelsBand0, isSingleAssetComposite } from '@walkthru-earth/objex-utils';
|
|
25
|
+
import { buildBandRenderPipeline, selectCogPipeline } from '../../../utils/cog.js';
|
|
26
|
+
/**
|
|
27
|
+
* Build the appropriate deck.gl layer for an RGB composite.
|
|
28
|
+
*
|
|
29
|
+
* For single-asset composites the band indices flow through `selectCogPipeline`
|
|
30
|
+
* (when `preflightGeotiff` is provided) into a custom `getTileData` /
|
|
31
|
+
* `renderTile` pair that honors the requested R/G/B band order. Without a
|
|
32
|
+
* preflight GeoTIFF the layer uses the library's default pipeline (bands 0/1/2).
|
|
33
|
+
* For multi-asset composites a warning is logged (once per call) when any
|
|
34
|
+
* non-band-0 index is requested, since MultiCOGLayer cannot honor it today.
|
|
35
|
+
*/
|
|
36
|
+
export async function buildRgbLayer(opts) {
|
|
37
|
+
const assetByKey = new Map(opts.assets.map((a) => [a.key, a]));
|
|
38
|
+
const c = opts.composite;
|
|
39
|
+
console.debug('[buildRgbLayer]', {
|
|
40
|
+
id: opts.id,
|
|
41
|
+
composite: c,
|
|
42
|
+
single: isSingleAssetComposite(c),
|
|
43
|
+
assetKeys: opts.assets.map((a) => a.key),
|
|
44
|
+
hasPreflightGeotiff: !!opts.preflightGeotiff
|
|
45
|
+
});
|
|
46
|
+
if (isSingleAssetComposite(c)) {
|
|
47
|
+
const asset = assetByKey.get(c.r.assetKey);
|
|
48
|
+
if (!asset)
|
|
49
|
+
throw new Error(`unknown asset key: ${c.r.assetKey}`);
|
|
50
|
+
const url = await opts.resolveHref(asset.href);
|
|
51
|
+
if (opts.signal.aborted)
|
|
52
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
53
|
+
const onGeoTIFFLoad = (_g, info) => {
|
|
54
|
+
opts.onLoad?.({
|
|
55
|
+
kind: 'cog',
|
|
56
|
+
bounds: info.geographicBounds
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
// Branch on whether we have a pre-opened GeoTIFF.
|
|
60
|
+
// - Present: build a per-channel BandConfig from the composite, hand
|
|
61
|
+
// it to selectCogPipeline (which inspects sampleFormat / bandCount)
|
|
62
|
+
// and spread the resolved {getTileData?, renderTile?} into COGLayer.
|
|
63
|
+
// This is the only path that honors a non-default per-channel
|
|
64
|
+
// bandIndex on a single-asset multi-band COG (e.g. NAIP NIR-R-G).
|
|
65
|
+
// - Absent: fall back to the library's default render pipeline. Bands
|
|
66
|
+
// 0/1/2 are read in that order, which is correct for single-band
|
|
67
|
+
// per-asset COGs (every bandIndex is 0 anyway) and for the default
|
|
68
|
+
// natural-color order on pre-baked multi-band visuals.
|
|
69
|
+
if (opts.preflightGeotiff) {
|
|
70
|
+
const bandConfig = {
|
|
71
|
+
mode: 'rgb',
|
|
72
|
+
rBand: c.r.bandIndex,
|
|
73
|
+
gBand: c.g.bandIndex,
|
|
74
|
+
bBand: c.b.bandIndex,
|
|
75
|
+
band: 0,
|
|
76
|
+
colorRamp: 'viridis'
|
|
77
|
+
};
|
|
78
|
+
console.debug('[buildRgbLayer] cog single-asset with preflight', {
|
|
79
|
+
id: opts.id,
|
|
80
|
+
bandConfig,
|
|
81
|
+
url
|
|
82
|
+
});
|
|
83
|
+
const pipeline = selectCogPipeline(opts.preflightGeotiff, {
|
|
84
|
+
bandConfig,
|
|
85
|
+
rescale: opts.rescale
|
|
86
|
+
});
|
|
87
|
+
const layer = new COGLayer({
|
|
88
|
+
id: opts.id,
|
|
89
|
+
geotiff: url,
|
|
90
|
+
...pipeline,
|
|
91
|
+
pool: opts.pool ?? undefined,
|
|
92
|
+
epsgResolver: opts.epsgResolver,
|
|
93
|
+
signal: opts.signal,
|
|
94
|
+
onGeoTIFFLoad
|
|
95
|
+
});
|
|
96
|
+
return { kind: 'cog', layer };
|
|
97
|
+
}
|
|
98
|
+
console.debug('[buildRgbLayer] cog single-asset (library default pipeline)', {
|
|
99
|
+
id: opts.id,
|
|
100
|
+
url
|
|
101
|
+
});
|
|
102
|
+
// Fallback: no preflight GeoTIFF supplied. COGLayer's typed prop surface
|
|
103
|
+
// does not include `renderPipeline` (only `getTileData` + `renderTile`),
|
|
104
|
+
// so we cannot apply the band render pipeline statically here. Without
|
|
105
|
+
// a preflight to feed `selectCogPipeline`, we have no way to inspect
|
|
106
|
+
// sample format / band count up front, so we let the library infer its
|
|
107
|
+
// own pipeline from the GeoTIFF metadata at load time. Bands 0/1/2 are
|
|
108
|
+
// read in that order, which is correct for single-band per-asset COGs
|
|
109
|
+
// (every bandIndex is 0 anyway) and for the default natural-color order
|
|
110
|
+
// on pre-baked multi-band visuals (NAIP `image`, S2 `visual`).
|
|
111
|
+
const layer = new COGLayer({
|
|
112
|
+
id: opts.id,
|
|
113
|
+
geotiff: url,
|
|
114
|
+
pool: opts.pool ?? undefined,
|
|
115
|
+
epsgResolver: opts.epsgResolver,
|
|
116
|
+
signal: opts.signal,
|
|
117
|
+
onGeoTIFFLoad
|
|
118
|
+
});
|
|
119
|
+
return { kind: 'cog', layer };
|
|
120
|
+
}
|
|
121
|
+
if (!allChannelsBand0(c)) {
|
|
122
|
+
// Library limitation: MultiCOGLayer always reads band 0. Surface a
|
|
123
|
+
// console warning once per call so the consumer sees that the user's
|
|
124
|
+
// per-channel band index was dropped.
|
|
125
|
+
console.warn('[buildRgbLayer] multi-asset composite with non-band-0 indices, band index ignored on multi-asset path');
|
|
126
|
+
}
|
|
127
|
+
const sources = {};
|
|
128
|
+
for (const ref of [c.r, c.g, c.b, c.a].filter((x) => Boolean(x))) {
|
|
129
|
+
if (sources[ref.assetKey])
|
|
130
|
+
continue;
|
|
131
|
+
const asset = assetByKey.get(ref.assetKey);
|
|
132
|
+
if (!asset) {
|
|
133
|
+
console.warn('[buildRgbLayer] missing asset for ref', ref);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const url = await opts.resolveHref(asset.href);
|
|
137
|
+
if (opts.signal.aborted)
|
|
138
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
139
|
+
sources[ref.assetKey] = { url };
|
|
140
|
+
}
|
|
141
|
+
const compositeSpec = {
|
|
142
|
+
r: c.r.assetKey,
|
|
143
|
+
g: c.g.assetKey,
|
|
144
|
+
b: c.b.assetKey
|
|
145
|
+
};
|
|
146
|
+
if (c.a && sources[c.a.assetKey])
|
|
147
|
+
compositeSpec.a = c.a.assetKey;
|
|
148
|
+
console.debug('[buildRgbLayer] multicog sources resolved', {
|
|
149
|
+
sourceKeys: Object.keys(sources),
|
|
150
|
+
composite: compositeSpec,
|
|
151
|
+
urls: Object.fromEntries(Object.entries(sources).map(([k, v]) => [k, v.url]))
|
|
152
|
+
});
|
|
153
|
+
const layer = new MultiCOGLayer({
|
|
154
|
+
id: opts.id,
|
|
155
|
+
sources,
|
|
156
|
+
composite: compositeSpec,
|
|
157
|
+
renderPipeline: buildBandRenderPipeline({
|
|
158
|
+
noDataVal: opts.noDataVal ?? null,
|
|
159
|
+
rescale: { ...opts.rescale }
|
|
160
|
+
}),
|
|
161
|
+
pool: opts.pool ?? undefined,
|
|
162
|
+
epsgResolver: opts.epsgResolver,
|
|
163
|
+
signal: opts.signal,
|
|
164
|
+
onGeoTIFFLoad: (_tiffs, info) => {
|
|
165
|
+
console.debug('[buildRgbLayer] MultiCOG onGeoTIFFLoad', {
|
|
166
|
+
id: opts.id,
|
|
167
|
+
bounds: info.geographicBounds
|
|
168
|
+
});
|
|
169
|
+
opts.onLoad?.({
|
|
170
|
+
kind: 'multicog',
|
|
171
|
+
bounds: info.geographicBounds
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return { kind: 'multicog', layer };
|
|
176
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import maplibregl from 'maplibre-gl';
|
|
3
3
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
4
|
+
import { resolveBasemap } from '@walkthru-earth/objex-utils';
|
|
4
5
|
import { onDestroy } from 'svelte';
|
|
5
6
|
import { t } from '../../../i18n/index.svelte.js';
|
|
7
|
+
import { appConfig } from '../../../stores/config.svelte.js';
|
|
6
8
|
import { settings } from '../../../stores/settings.svelte.js';
|
|
7
9
|
|
|
8
10
|
const MAP_STYLES = {
|
|
@@ -37,11 +39,34 @@ let {
|
|
|
37
39
|
bounds?: [number, number, number, number];
|
|
38
40
|
} = $props();
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
function toMapStyle(variant: 'light' | 'dark'): string | maplibregl.StyleSpecification {
|
|
43
|
+
const bm = resolveBasemap(appConfig.value, variant, settings.basemapId);
|
|
44
|
+
if (!bm) return MAP_STYLES[variant];
|
|
45
|
+
if (bm.type === 'raster') {
|
|
46
|
+
return {
|
|
47
|
+
version: 8,
|
|
48
|
+
sources: {
|
|
49
|
+
'objex-basemap': { type: 'raster', tiles: [bm.url], tileSize: 256 }
|
|
50
|
+
},
|
|
51
|
+
layers: [{ id: 'objex-basemap', type: 'raster', source: 'objex-basemap' }]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return bm.url;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const resolvedBasemap = $derived(
|
|
58
|
+
style ? undefined : resolveBasemap(appConfig.value, settings.resolved, settings.basemapId)
|
|
59
|
+
);
|
|
60
|
+
const resolvedStyle = $derived(style ?? toMapStyle(settings.resolved));
|
|
61
|
+
// Stable identity for style-swap comparison: a raster StyleSpecification is a
|
|
62
|
+
// fresh object on every derive, so compare by basemap id + variant instead.
|
|
63
|
+
const styleKey = $derived(
|
|
64
|
+
style ? 'custom' : `${resolvedBasemap?.id ?? 'fallback'}:${settings.resolved}`
|
|
65
|
+
);
|
|
41
66
|
|
|
42
67
|
let containerEl: HTMLDivElement | undefined = $state();
|
|
43
68
|
let map: maplibregl.Map | null = null;
|
|
44
|
-
let
|
|
69
|
+
let currentStyleKey: string | null = null;
|
|
45
70
|
let currentZoom = $state(2);
|
|
46
71
|
let webglLost = $state(false);
|
|
47
72
|
|
|
@@ -56,7 +81,7 @@ function initMap() {
|
|
|
56
81
|
zoom
|
|
57
82
|
});
|
|
58
83
|
|
|
59
|
-
|
|
84
|
+
currentStyleKey = styleKey;
|
|
60
85
|
|
|
61
86
|
map.addControl(
|
|
62
87
|
new maplibregl.NavigationControl({ showCompass: true, visualizePitch: true }),
|
|
@@ -117,12 +142,13 @@ $effect(() => {
|
|
|
117
142
|
}
|
|
118
143
|
});
|
|
119
144
|
|
|
120
|
-
// React to theme changes — swap basemap style
|
|
145
|
+
// React to theme / basemap changes — swap basemap style
|
|
121
146
|
$effect(() => {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
147
|
+
const nextKey = styleKey;
|
|
148
|
+
const nextStyle = resolvedStyle;
|
|
149
|
+
if (map && currentStyleKey !== nextKey && !style) {
|
|
150
|
+
currentStyleKey = nextKey;
|
|
151
|
+
map.setStyle(nextStyle);
|
|
126
152
|
}
|
|
127
153
|
});
|
|
128
154
|
|
|
@@ -132,11 +158,11 @@ onDestroy(() => {
|
|
|
132
158
|
});
|
|
133
159
|
</script>
|
|
134
160
|
|
|
135
|
-
<div class="relative h-full w-full">
|
|
136
|
-
<div bind:this={containerEl} class="h-full w-full"></div>
|
|
161
|
+
<div class="relative h-full w-full" style="touch-action: pan-x pan-y;">
|
|
162
|
+
<div bind:this={containerEl} class="h-full w-full" style="touch-action: none;"></div>
|
|
137
163
|
<!-- Zoom level indicator — positioned above nav controls -->
|
|
138
164
|
<div
|
|
139
|
-
class="pointer-events-none absolute bottom-[
|
|
165
|
+
class="pointer-events-none absolute bottom-[7rem] right-[10px] z-10 flex size-[29px] items-center justify-center rounded-full border border-zinc-300 bg-white shadow-sm dark:border-zinc-600 dark:bg-zinc-800 sm:bottom-[10rem]"
|
|
140
166
|
>
|
|
141
167
|
<span class="text-[10px] font-semibold tabular-nums text-zinc-600 dark:text-zinc-300">
|
|
142
168
|
{currentZoom.toFixed(1)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
|
3
|
+
import { formatFileSize } from '@walkthru-earth/objex-utils';
|
|
3
4
|
import type { PMTiles } from 'pmtiles';
|
|
4
5
|
import { tileIdToZxy } from 'pmtiles';
|
|
5
6
|
import {
|
|
@@ -8,7 +9,6 @@ import {
|
|
|
8
9
|
ResizablePaneGroup
|
|
9
10
|
} from '../../ui/resizable/index.js';
|
|
10
11
|
import { t } from '../../../i18n/index.svelte.js';
|
|
11
|
-
import { formatFileSize } from '../../../utils/format.js';
|
|
12
12
|
import type { PmtilesMetadata } from '../../../utils/pmtiles';
|
|
13
13
|
import { highlightCode } from '../../../utils/shiki';
|
|
14
14
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
|
3
3
|
import XIcon from '@lucide/svelte/icons/x';
|
|
4
|
+
import { formatFileSize } from '@walkthru-earth/objex-utils';
|
|
4
5
|
import type { PMTiles } from 'pmtiles';
|
|
5
6
|
import { onDestroy } from 'svelte';
|
|
6
7
|
import { t } from '../../../i18n/index.svelte.js';
|
|
7
|
-
import { formatFileSize } from '../../../utils/format.js';
|
|
8
8
|
import type { PmtilesMetadata } from '../../../utils/pmtiles';
|
|
9
9
|
import {
|
|
10
10
|
type DecodedTile,
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { DatetimeFacet, FacetState } from '@walkthru-earth/objex-utils';
|
|
3
|
+
import { formatDate } from '@walkthru-earth/objex-utils';
|
|
4
|
+
import { t } from '../../../i18n/index.svelte.js';
|
|
5
|
+
import { RangeSlider } from '../../ui/slider/index.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Datetime range picker that sits above the item strip. Replaces the older
|
|
9
|
+
* preset dropdown ("last 7 days / 30 days / ...") with a continuous slider
|
|
10
|
+
* over the loaded items' min/max datetime, plus two `<input type="date">`
|
|
11
|
+
* fields for exact start / end picking. The histogram of loaded items is
|
|
12
|
+
* drawn behind the slider so the user can see where data is dense.
|
|
13
|
+
*
|
|
14
|
+
* State flows one-way: this component reads `facet` (built from the loaded
|
|
15
|
+
* items) and `state.datetime`, and emits `onChange(next)` with the merged
|
|
16
|
+
* `FacetState`. The parent decides how to apply it (push-down vs client-side).
|
|
17
|
+
*
|
|
18
|
+
* **Bbox scoping**: The parent (`StacMosaicViewer`) builds `facet` from
|
|
19
|
+
* `committedViews`, which is bbox-scoped in `api` and `parquet` modes (those
|
|
20
|
+
* sources push the viewport bbox to the server / SQL). So in viewport modes
|
|
21
|
+
* the histogram always reflects "what's available in the current bbox" and
|
|
22
|
+
* a pan triggers a fresh build via `reloadViewport()`. In `static` mode the
|
|
23
|
+
* histogram is global to the catalog by design (see the parent's `facets`
|
|
24
|
+
* derivation comment for why we do not client-side clip there).
|
|
25
|
+
*/
|
|
26
|
+
let {
|
|
27
|
+
facet,
|
|
28
|
+
state,
|
|
29
|
+
onChange
|
|
30
|
+
}: {
|
|
31
|
+
/** DatetimeFacet built from the loaded items, or null when no datetime variance. */
|
|
32
|
+
facet: DatetimeFacet | null;
|
|
33
|
+
state: FacetState;
|
|
34
|
+
onChange: (next: FacetState) => void;
|
|
35
|
+
} = $props();
|
|
36
|
+
|
|
37
|
+
const bounds = $derived(
|
|
38
|
+
facet ? ([Date.parse(facet.min), Date.parse(facet.max)] as [number, number]) : null
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const sliderValue = $derived.by((): [number, number] | null => {
|
|
42
|
+
if (!bounds) return null;
|
|
43
|
+
const [lo, hi] = bounds;
|
|
44
|
+
const stateLo = state.datetime?.min ? Date.parse(state.datetime.min) : lo;
|
|
45
|
+
const stateHi = state.datetime?.max ? Date.parse(state.datetime.max) : hi;
|
|
46
|
+
return [Number.isFinite(stateLo) ? stateLo : lo, Number.isFinite(stateHi) ? stateHi : hi];
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function emit(min: string | undefined, max: string | undefined): void {
|
|
50
|
+
onChange({
|
|
51
|
+
...state,
|
|
52
|
+
datetime: min || max ? { min, max } : undefined
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setSlider(next: [number, number]): void {
|
|
57
|
+
if (!bounds) return;
|
|
58
|
+
const lo = next[0] <= bounds[0] ? undefined : new Date(next[0]).toISOString();
|
|
59
|
+
const hi = next[1] >= bounds[1] ? undefined : new Date(next[1]).toISOString();
|
|
60
|
+
emit(lo, hi);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** ISO 8601 → `YYYY-MM-DD` for `<input type="date">` value. */
|
|
64
|
+
function isoToDateInput(iso: string | undefined): string {
|
|
65
|
+
if (!iso) return '';
|
|
66
|
+
const t = Date.parse(iso);
|
|
67
|
+
if (!Number.isFinite(t)) return '';
|
|
68
|
+
return new Date(t).toISOString().slice(0, 10);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `<input type="date">` value → ISO 8601 (start of UTC day for min, end for max). */
|
|
72
|
+
function dateInputToIso(value: string, kind: 'min' | 'max'): string | undefined {
|
|
73
|
+
if (!value) return undefined;
|
|
74
|
+
const stamp = kind === 'min' ? `${value}T00:00:00.000Z` : `${value}T23:59:59.999Z`;
|
|
75
|
+
const t = Date.parse(stamp);
|
|
76
|
+
return Number.isFinite(t) ? new Date(t).toISOString() : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onMinInput(e: Event): void {
|
|
80
|
+
const v = (e.target as HTMLInputElement).value;
|
|
81
|
+
emit(dateInputToIso(v, 'min'), state.datetime?.max);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function onMaxInput(e: Event): void {
|
|
85
|
+
const v = (e.target as HTMLInputElement).value;
|
|
86
|
+
emit(state.datetime?.min, dateInputToIso(v, 'max'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clearRange(): void {
|
|
90
|
+
emit(undefined, undefined);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `YYYY-MM-DD` for today (UTC) — used as the max-input default. */
|
|
94
|
+
function todayDateInput(): string {
|
|
95
|
+
return new Date().toISOString().slice(0, 10);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Display defaults when the user has not set a filter yet:
|
|
99
|
+
// - min input falls back to the earliest datetime in the loaded data
|
|
100
|
+
// (`facet.min`), so the input hints at the available range instead of
|
|
101
|
+
// showing `mm / dd / yyyy`.
|
|
102
|
+
// - max input falls back to "today" so the visible window always extends
|
|
103
|
+
// to "now" regardless of whether items in the current viewport are
|
|
104
|
+
// stale. Both fallbacks are display-only — the actual `state.datetime`
|
|
105
|
+
// stays undefined until the user picks a value, so an empty `state`
|
|
106
|
+
// means "no filter" not "filter by today".
|
|
107
|
+
const minInputValue = $derived(
|
|
108
|
+
isoToDateInput(state.datetime?.min) || (facet ? isoToDateInput(facet.min) : '')
|
|
109
|
+
);
|
|
110
|
+
const maxInputValue = $derived(isoToDateInput(state.datetime?.max) || todayDateInput());
|
|
111
|
+
const isActive = $derived(Boolean(state.datetime?.min || state.datetime?.max));
|
|
112
|
+
|
|
113
|
+
function fmtDate(ms: number): string {
|
|
114
|
+
if (!Number.isFinite(ms)) return '-';
|
|
115
|
+
return formatDate(ms);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const granularityLabel = $derived.by((): string | null => {
|
|
119
|
+
if (!facet) return null;
|
|
120
|
+
const word = t(`stac.granularity.${facet.granularity}`);
|
|
121
|
+
return t('stac.granularityLabel', { granularity: word });
|
|
122
|
+
});
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<div
|
|
126
|
+
class="pointer-events-auto flex flex-col gap-1.5 rounded-md border border-border bg-card/90 px-3 py-2 text-xs text-card-foreground shadow backdrop-blur-sm"
|
|
127
|
+
>
|
|
128
|
+
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
129
|
+
<span class="font-medium">{t('stac.filterDatetime')}</span>
|
|
130
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
131
|
+
<input
|
|
132
|
+
type="date"
|
|
133
|
+
value={minInputValue}
|
|
134
|
+
onchange={onMinInput}
|
|
135
|
+
class="min-h-8 rounded border border-input bg-background px-2 py-1 text-xs tabular-nums sm:min-h-0 sm:px-1.5 sm:py-0.5 sm:text-[11px]"
|
|
136
|
+
aria-label={t('stac.filterDatetime')}
|
|
137
|
+
/>
|
|
138
|
+
<span class="text-muted-foreground">→</span>
|
|
139
|
+
<input
|
|
140
|
+
type="date"
|
|
141
|
+
value={maxInputValue}
|
|
142
|
+
onchange={onMaxInput}
|
|
143
|
+
class="min-h-8 rounded border border-input bg-background px-2 py-1 text-xs tabular-nums sm:min-h-0 sm:px-1.5 sm:py-0.5 sm:text-[11px]"
|
|
144
|
+
aria-label={t('stac.filterDatetime')}
|
|
145
|
+
/>
|
|
146
|
+
{#if isActive}
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
class="inline-flex min-h-8 items-center rounded border border-input px-2 py-1 text-xs hover:bg-accent sm:min-h-0 sm:px-1.5 sm:py-0.5 sm:text-[10px]"
|
|
150
|
+
style="touch-action: manipulation;"
|
|
151
|
+
onclick={clearRange}
|
|
152
|
+
>
|
|
153
|
+
{t('stac.resetFilters')}
|
|
154
|
+
</button>
|
|
155
|
+
{/if}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{#if facet && bounds && sliderValue}
|
|
160
|
+
<RangeSlider
|
|
161
|
+
min={bounds[0]}
|
|
162
|
+
max={bounds[1]}
|
|
163
|
+
value={sliderValue}
|
|
164
|
+
step={86_400_000}
|
|
165
|
+
histogram={facet.bins}
|
|
166
|
+
formatLabel={fmtDate}
|
|
167
|
+
onValueCommit={setSlider}
|
|
168
|
+
/>
|
|
169
|
+
{#if granularityLabel}
|
|
170
|
+
<div class="text-[10px] text-muted-foreground">{granularityLabel}</div>
|
|
171
|
+
{/if}
|
|
172
|
+
{:else}
|
|
173
|
+
<div class="text-[10px] text-muted-foreground">{t('stac.facetNoneAvailable')}</div>
|
|
174
|
+
{/if}
|
|
175
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DatetimeFacet, FacetState } from '@walkthru-earth/objex-utils';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** DatetimeFacet built from the loaded items, or null when no datetime variance. */
|
|
4
|
+
facet: DatetimeFacet | null;
|
|
5
|
+
state: FacetState;
|
|
6
|
+
onChange: (next: FacetState) => void;
|
|
7
|
+
};
|
|
8
|
+
declare const StacDatetimeBar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type StacDatetimeBar = ReturnType<typeof StacDatetimeBar>;
|
|
10
|
+
export default StacDatetimeBar;
|