@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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
type FacetSet,
|
|
4
|
+
type FacetState,
|
|
5
|
+
formatDate,
|
|
6
|
+
hasActiveFilters
|
|
7
|
+
} from '@walkthru-earth/objex-utils';
|
|
8
|
+
import type { Snippet } from 'svelte';
|
|
9
|
+
import { t } from '../../../i18n/index.svelte.js';
|
|
10
|
+
import { RangeSlider } from '../../ui/slider/index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Auto-faceted filter panel. Reads a `FacetSet` derived from the loaded
|
|
14
|
+
* item views and renders only the controls that have variance for *this*
|
|
15
|
+
* dataset. Currently surfaces:
|
|
16
|
+
* - Datetime range slider with histogram (when `facets.datetime` set)
|
|
17
|
+
* - Numeric range sliders for cloud cover / GSD (when present)
|
|
18
|
+
* - Enum chip lists for collection / platform / constellation /
|
|
19
|
+
* instruments / asset roles (when ≥2 distinct values)
|
|
20
|
+
*
|
|
21
|
+
* Mode awareness: this component does NOT push down to the API itself,
|
|
22
|
+
* the parent decides whether the current `state` should be applied
|
|
23
|
+
* client-side via `applyFacets` or translated to native query params via
|
|
24
|
+
* `toNativeQuery`. We just edit `state` and emit `onChange`.
|
|
25
|
+
*/
|
|
26
|
+
let {
|
|
27
|
+
facets,
|
|
28
|
+
state,
|
|
29
|
+
onChange,
|
|
30
|
+
onClose,
|
|
31
|
+
onReset,
|
|
32
|
+
footer
|
|
33
|
+
}: {
|
|
34
|
+
facets: FacetSet;
|
|
35
|
+
state: FacetState;
|
|
36
|
+
onChange: (next: FacetState) => void;
|
|
37
|
+
onClose: () => void;
|
|
38
|
+
onReset: () => void;
|
|
39
|
+
/** Optional footer slot for fetch options (timeRange, itemLimit, mode label). */
|
|
40
|
+
footer?: Snippet;
|
|
41
|
+
} = $props();
|
|
42
|
+
|
|
43
|
+
const NUMERIC_LABEL_KEYS: Record<string, string> = {
|
|
44
|
+
cloudCover: 'stac.cloudCover',
|
|
45
|
+
gsd: 'stac.gsd'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ENUM_LABEL_KEYS: Record<string, string> = {
|
|
49
|
+
collection: 'stac.collection',
|
|
50
|
+
platform: 'stac.platform',
|
|
51
|
+
constellation: 'stac.constellation',
|
|
52
|
+
instruments: 'stac.instruments',
|
|
53
|
+
assetRoles: 'stac.assetRoles'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function setDatetime(next: [number, number]): void {
|
|
57
|
+
if (!facets.datetime) return;
|
|
58
|
+
const minMs = Date.parse(facets.datetime.min);
|
|
59
|
+
const maxMs = Date.parse(facets.datetime.max);
|
|
60
|
+
const lo = next[0] <= minMs ? undefined : new Date(next[0]).toISOString();
|
|
61
|
+
const hi = next[1] >= maxMs ? undefined : new Date(next[1]).toISOString();
|
|
62
|
+
onChange({
|
|
63
|
+
...state,
|
|
64
|
+
datetime: lo || hi ? { min: lo, max: hi } : undefined
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function setNumeric(
|
|
69
|
+
field: string,
|
|
70
|
+
next: [number, number],
|
|
71
|
+
facetMin: number,
|
|
72
|
+
facetMax: number
|
|
73
|
+
): void {
|
|
74
|
+
const lo = next[0] <= facetMin ? undefined : next[0];
|
|
75
|
+
const hi = next[1] >= facetMax ? undefined : next[1];
|
|
76
|
+
const numeric = { ...(state.numeric ?? {}) };
|
|
77
|
+
if (lo == null && hi == null) {
|
|
78
|
+
delete (numeric as Record<string, unknown>)[field];
|
|
79
|
+
} else {
|
|
80
|
+
(numeric as Record<string, unknown>)[field] = { min: lo, max: hi };
|
|
81
|
+
}
|
|
82
|
+
onChange({
|
|
83
|
+
...state,
|
|
84
|
+
numeric: Object.keys(numeric).length > 0 ? numeric : undefined
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toggleEnum(field: string, value: string): void {
|
|
89
|
+
const enums = { ...(state.enums ?? {}) };
|
|
90
|
+
const current = (enums as Record<string, string[] | undefined>)[field] ?? [];
|
|
91
|
+
const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value];
|
|
92
|
+
if (next.length === 0) {
|
|
93
|
+
delete (enums as Record<string, unknown>)[field];
|
|
94
|
+
} else {
|
|
95
|
+
(enums as Record<string, unknown>)[field] = next;
|
|
96
|
+
}
|
|
97
|
+
onChange({
|
|
98
|
+
...state,
|
|
99
|
+
enums: Object.keys(enums).length > 0 ? enums : undefined
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isEnumActive(field: string, value: string): boolean {
|
|
104
|
+
const list = (state.enums as Record<string, string[] | undefined> | undefined)?.[field];
|
|
105
|
+
return Array.isArray(list) && list.includes(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const datetimeBounds = $derived(
|
|
109
|
+
facets.datetime
|
|
110
|
+
? ([Date.parse(facets.datetime.min), Date.parse(facets.datetime.max)] as [number, number])
|
|
111
|
+
: null
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const datetimeValue = $derived.by((): [number, number] | null => {
|
|
115
|
+
if (!datetimeBounds) return null;
|
|
116
|
+
const [lo, hi] = datetimeBounds;
|
|
117
|
+
const stateLo = state.datetime?.min ? Date.parse(state.datetime.min) : lo;
|
|
118
|
+
const stateHi = state.datetime?.max ? Date.parse(state.datetime.max) : hi;
|
|
119
|
+
return [Number.isFinite(stateLo) ? stateLo : lo, Number.isFinite(stateHi) ? stateHi : hi];
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function fmtDate(ms: number): string {
|
|
123
|
+
if (!Number.isFinite(ms)) return '-';
|
|
124
|
+
return formatDate(ms);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function fmtNumber(n: number): string {
|
|
128
|
+
if (!Number.isFinite(n)) return '-';
|
|
129
|
+
if (Math.abs(n) >= 100) return Math.round(n).toString();
|
|
130
|
+
return n.toFixed(2);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const active = $derived(hasActiveFilters(state));
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<div
|
|
137
|
+
class="pointer-events-auto absolute inset-x-0 bottom-0 z-20 flex max-h-[70vh] flex-col gap-3 overflow-hidden rounded-t-xl border border-border bg-card/95 p-3 text-xs text-card-foreground shadow-lg backdrop-blur-sm sm:inset-x-auto sm:bottom-auto sm:end-2 sm:top-12 sm:max-h-[calc(100%-3.5rem)] sm:w-[min(360px,calc(100%-1rem))] sm:rounded-md"
|
|
138
|
+
>
|
|
139
|
+
<header class="flex items-center justify-between gap-2">
|
|
140
|
+
<div class="flex items-center gap-2">
|
|
141
|
+
<span class="font-medium">{t('stac.filters')}</span>
|
|
142
|
+
<span class="text-[10px] text-muted-foreground tabular-nums">
|
|
143
|
+
{t('stac.facetTotal', { count: facets.total })}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="flex items-center gap-1">
|
|
147
|
+
{#if active}
|
|
148
|
+
<button
|
|
149
|
+
class="rounded border border-input px-1.5 py-0.5 text-[10px] hover:bg-accent"
|
|
150
|
+
onclick={onReset}
|
|
151
|
+
>
|
|
152
|
+
{t('stac.resetFilters')}
|
|
153
|
+
</button>
|
|
154
|
+
{/if}
|
|
155
|
+
<button
|
|
156
|
+
class="inline-flex min-h-9 min-w-9 items-center justify-center rounded p-0.5 text-base text-muted-foreground hover:bg-accent hover:text-card-foreground sm:min-h-0 sm:min-w-0 sm:text-xs"
|
|
157
|
+
onclick={onClose}
|
|
158
|
+
aria-label={t('stac.close')}
|
|
159
|
+
style="touch-action: manipulation;"
|
|
160
|
+
>
|
|
161
|
+
×
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
</header>
|
|
165
|
+
|
|
166
|
+
<div class="overflow-y-auto pr-1">
|
|
167
|
+
{#if !facets.datetime && facets.numeric.length === 0 && facets.enums.length === 0}
|
|
168
|
+
<div class="text-[10px] text-muted-foreground">{t('stac.facetNoneAvailable')}</div>
|
|
169
|
+
{/if}
|
|
170
|
+
|
|
171
|
+
{#if facets.datetime && datetimeBounds && datetimeValue}
|
|
172
|
+
<section class="mb-3">
|
|
173
|
+
<div class="mb-1 flex items-baseline justify-between">
|
|
174
|
+
<span class="text-muted-foreground">{t('stac.filterDatetime')}</span>
|
|
175
|
+
<span class="text-[10px] tabular-nums text-muted-foreground">
|
|
176
|
+
{facets.datetime.count}
|
|
177
|
+
</span>
|
|
178
|
+
</div>
|
|
179
|
+
<RangeSlider
|
|
180
|
+
min={datetimeBounds[0]}
|
|
181
|
+
max={datetimeBounds[1]}
|
|
182
|
+
value={datetimeValue}
|
|
183
|
+
step={86_400_000}
|
|
184
|
+
histogram={facets.datetime.bins}
|
|
185
|
+
formatLabel={fmtDate}
|
|
186
|
+
onValueCommit={setDatetime}
|
|
187
|
+
/>
|
|
188
|
+
</section>
|
|
189
|
+
{/if}
|
|
190
|
+
|
|
191
|
+
{#each facets.numeric as facet (facet.field)}
|
|
192
|
+
{@const stateRange = state.numeric?.[facet.field]}
|
|
193
|
+
{@const lo = stateRange?.min ?? facet.min}
|
|
194
|
+
{@const hi = stateRange?.max ?? facet.max}
|
|
195
|
+
<section class="mb-3">
|
|
196
|
+
<div class="mb-1 flex items-baseline justify-between">
|
|
197
|
+
<span class="text-muted-foreground">{t(NUMERIC_LABEL_KEYS[facet.field] ?? facet.field)}</span>
|
|
198
|
+
<span class="text-[10px] tabular-nums text-muted-foreground">{facet.count}</span>
|
|
199
|
+
</div>
|
|
200
|
+
<RangeSlider
|
|
201
|
+
min={facet.min}
|
|
202
|
+
max={facet.max}
|
|
203
|
+
value={[lo, hi]}
|
|
204
|
+
step={Math.max((facet.max - facet.min) / 200, 0.01)}
|
|
205
|
+
formatLabel={fmtNumber}
|
|
206
|
+
onValueCommit={(next) => setNumeric(facet.field, next, facet.min, facet.max)}
|
|
207
|
+
/>
|
|
208
|
+
</section>
|
|
209
|
+
{/each}
|
|
210
|
+
|
|
211
|
+
{#each facets.enums as facet (facet.field)}
|
|
212
|
+
<section class="mb-3">
|
|
213
|
+
<div class="mb-1 text-muted-foreground">
|
|
214
|
+
{t(ENUM_LABEL_KEYS[facet.field] ?? facet.field)}
|
|
215
|
+
</div>
|
|
216
|
+
<div class="flex flex-wrap gap-1">
|
|
217
|
+
{#each facet.values as entry (entry.value)}
|
|
218
|
+
{@const on = isEnumActive(facet.field, entry.value)}
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
class="rounded-full border px-2 py-0.5 text-[10px] transition-colors"
|
|
222
|
+
class:border-primary={on}
|
|
223
|
+
class:bg-primary={on}
|
|
224
|
+
class:text-primary-foreground={on}
|
|
225
|
+
class:border-input={!on}
|
|
226
|
+
class:hover:bg-accent={!on}
|
|
227
|
+
onclick={() => toggleEnum(facet.field, entry.value)}
|
|
228
|
+
>
|
|
229
|
+
{entry.value}
|
|
230
|
+
<span class="ms-1 text-[9px] opacity-70 tabular-nums">{entry.count}</span>
|
|
231
|
+
</button>
|
|
232
|
+
{/each}
|
|
233
|
+
</div>
|
|
234
|
+
</section>
|
|
235
|
+
{/each}
|
|
236
|
+
|
|
237
|
+
{#if footer}
|
|
238
|
+
<div class="mt-2 border-t border-border pt-3">
|
|
239
|
+
{@render footer()}
|
|
240
|
+
</div>
|
|
241
|
+
{/if}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type FacetSet, type FacetState } from '@walkthru-earth/objex-utils';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
facets: FacetSet;
|
|
5
|
+
state: FacetState;
|
|
6
|
+
onChange: (next: FacetState) => void;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onReset: () => void;
|
|
9
|
+
/** Optional footer slot for fetch options (timeRange, itemLimit, mode label). */
|
|
10
|
+
footer?: Snippet;
|
|
11
|
+
};
|
|
12
|
+
declare const StacFilterPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type StacFilterPanel = ReturnType<typeof StacFilterPanel>;
|
|
14
|
+
export default StacFilterPanel;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { StacItemView } from '@walkthru-earth/objex-utils';
|
|
3
|
+
import { copyToClipboard, formatDate, jsonReplacerBigInt } from '@walkthru-earth/objex-utils';
|
|
4
|
+
import { onDestroy } from 'svelte';
|
|
5
|
+
import { t } from '../../../i18n/index.svelte.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Right-side slide-over showing a single STAC item's metadata, asset
|
|
9
|
+
* table, and (collapsible) raw JSON. Rendered when the parent has a
|
|
10
|
+
* non-null `selectedView`. The "x" button calls `onClose` so the parent
|
|
11
|
+
* can clear `selectedId` and trigger a footprint-layer refresh.
|
|
12
|
+
*
|
|
13
|
+
* Asset hrefs are presigned via the parent's `presign` callback before
|
|
14
|
+
* being shown to the user (the "Open" link). Without that, a click on
|
|
15
|
+
* an `s3://` href on a private bucket would 403, and an absolute
|
|
16
|
+
* `https://` href that belongs to the user's own bucket would lose its
|
|
17
|
+
* SigV4 query string. Same helper the strip and mosaic use, so the
|
|
18
|
+
* presign cache is shared and warm.
|
|
19
|
+
*/
|
|
20
|
+
let {
|
|
21
|
+
view,
|
|
22
|
+
presign,
|
|
23
|
+
onClose,
|
|
24
|
+
onFlyTo
|
|
25
|
+
}: {
|
|
26
|
+
view: StacItemView;
|
|
27
|
+
presign: (href: string) => Promise<string>;
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
onFlyTo?: () => void;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
let showRaw = $state(false);
|
|
33
|
+
let copyLabel = $state<string | null>(null);
|
|
34
|
+
// Per-href resolved URL for the asset Open links, fetched on click. We
|
|
35
|
+
// don't pre-resolve every asset because most users only open one or two
|
|
36
|
+
// per item, and presigning is async + sometimes signs a remote endpoint.
|
|
37
|
+
let resolved = $state<Record<string, string>>({});
|
|
38
|
+
const inflight = new Set<string>();
|
|
39
|
+
|
|
40
|
+
function formatDt(iso: string | null): string {
|
|
41
|
+
if (!iso) return '-';
|
|
42
|
+
const t = Date.parse(iso);
|
|
43
|
+
return Number.isFinite(t) ? formatDate(t) : iso;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function openAsset(href: string): Promise<void> {
|
|
47
|
+
let url = resolved[href];
|
|
48
|
+
if (!url && !inflight.has(href)) {
|
|
49
|
+
inflight.add(href);
|
|
50
|
+
try {
|
|
51
|
+
url = await presign(href);
|
|
52
|
+
resolved = { ...resolved, [href]: url };
|
|
53
|
+
} catch {
|
|
54
|
+
url = href;
|
|
55
|
+
} finally {
|
|
56
|
+
inflight.delete(href);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (url) window.open(url, '_blank', 'noopener,noreferrer');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function copyId(): Promise<void> {
|
|
63
|
+
if (await copyToClipboard(view.id)) {
|
|
64
|
+
copyLabel = t('stac.copied');
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
copyLabel = null;
|
|
67
|
+
}, 1200);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function copyJson(): Promise<void> {
|
|
72
|
+
const json = JSON.stringify(view.raw, jsonReplacerBigInt, 2);
|
|
73
|
+
if (await copyToClipboard(json)) {
|
|
74
|
+
copyLabel = t('stac.copied');
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
copyLabel = null;
|
|
77
|
+
}, 1200);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
onDestroy(() => {
|
|
82
|
+
resolved = {};
|
|
83
|
+
copyLabel = null;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const assets = $derived(Object.entries(view.raw.assets ?? {}));
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<aside
|
|
90
|
+
class="pointer-events-auto absolute inset-x-0 bottom-0 z-20 flex max-h-[65vh] flex-col gap-2 overflow-hidden rounded-t-xl border border-border bg-card/95 p-3 text-xs text-card-foreground shadow-lg backdrop-blur-sm sm:inset-x-auto sm:bottom-auto sm:end-2 sm:top-12 sm:max-h-[calc(100%-3.5rem)] sm:w-[min(360px,calc(100%-1rem))] sm:rounded-md"
|
|
91
|
+
>
|
|
92
|
+
<header class="flex items-start justify-between gap-2">
|
|
93
|
+
<div class="min-w-0 flex-1">
|
|
94
|
+
<div class="truncate font-medium" title={view.id}>{view.id}</div>
|
|
95
|
+
{#if view.collection}
|
|
96
|
+
<div class="truncate text-[10px] text-muted-foreground">{view.collection}</div>
|
|
97
|
+
{/if}
|
|
98
|
+
</div>
|
|
99
|
+
<div class="flex items-center gap-1">
|
|
100
|
+
{#if onFlyTo && view.bbox}
|
|
101
|
+
<button
|
|
102
|
+
class="rounded border border-input px-1.5 py-0.5 text-[10px] hover:bg-accent"
|
|
103
|
+
onclick={onFlyTo}
|
|
104
|
+
title={t('stac.flyTo')}
|
|
105
|
+
>
|
|
106
|
+
{t('stac.flyTo')}
|
|
107
|
+
</button>
|
|
108
|
+
{/if}
|
|
109
|
+
<button
|
|
110
|
+
class="rounded border border-input px-1.5 py-0.5 text-[10px] hover:bg-accent"
|
|
111
|
+
onclick={copyId}
|
|
112
|
+
>
|
|
113
|
+
{copyLabel ?? t('stac.copyId')}
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
class="inline-flex min-h-9 min-w-9 items-center justify-center rounded p-0.5 text-base text-muted-foreground hover:bg-accent hover:text-card-foreground sm:min-h-0 sm:min-w-0 sm:text-xs"
|
|
117
|
+
onclick={onClose}
|
|
118
|
+
aria-label={t('stac.close')}
|
|
119
|
+
style="touch-action: manipulation;"
|
|
120
|
+
>
|
|
121
|
+
×
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
</header>
|
|
125
|
+
|
|
126
|
+
<div class="overflow-y-auto">
|
|
127
|
+
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1">
|
|
128
|
+
<dt class="text-muted-foreground">{t('stac.datetime')}</dt>
|
|
129
|
+
<dd class="tabular-nums">{formatDt(view.datetime)}</dd>
|
|
130
|
+
{#if view.endDatetime}
|
|
131
|
+
<dt class="text-muted-foreground">{t('stac.endDatetime')}</dt>
|
|
132
|
+
<dd class="tabular-nums">{formatDt(view.endDatetime)}</dd>
|
|
133
|
+
{/if}
|
|
134
|
+
{#if view.cloudCover != null}
|
|
135
|
+
<dt class="text-muted-foreground">{t('stac.cloudCover')}</dt>
|
|
136
|
+
<dd class="tabular-nums">{view.cloudCover.toFixed(1)}%</dd>
|
|
137
|
+
{/if}
|
|
138
|
+
{#if view.gsd != null}
|
|
139
|
+
<dt class="text-muted-foreground">{t('stac.gsd')}</dt>
|
|
140
|
+
<dd class="tabular-nums">{view.gsd} m</dd>
|
|
141
|
+
{/if}
|
|
142
|
+
{#if view.platform}
|
|
143
|
+
<dt class="text-muted-foreground">{t('stac.platform')}</dt>
|
|
144
|
+
<dd>{view.platform}</dd>
|
|
145
|
+
{/if}
|
|
146
|
+
{#if view.constellation}
|
|
147
|
+
<dt class="text-muted-foreground">{t('stac.constellation')}</dt>
|
|
148
|
+
<dd>{view.constellation}</dd>
|
|
149
|
+
{/if}
|
|
150
|
+
{#if view.instruments.length > 0}
|
|
151
|
+
<dt class="text-muted-foreground">{t('stac.instruments')}</dt>
|
|
152
|
+
<dd>{view.instruments.join(', ')}</dd>
|
|
153
|
+
{/if}
|
|
154
|
+
{#if view.epsg != null}
|
|
155
|
+
<dt class="text-muted-foreground">{t('stac.epsg')}</dt>
|
|
156
|
+
<dd class="tabular-nums">EPSG:{view.epsg}</dd>
|
|
157
|
+
{/if}
|
|
158
|
+
{#if view.bbox}
|
|
159
|
+
<dt class="text-muted-foreground">{t('mapInfo.bounds')}</dt>
|
|
160
|
+
<dd class="tabular-nums text-[10px]">
|
|
161
|
+
W {view.bbox[0].toFixed(3)}, S {view.bbox[1].toFixed(3)}<br />
|
|
162
|
+
E {view.bbox[2].toFixed(3)}, N {view.bbox[3].toFixed(3)}
|
|
163
|
+
</dd>
|
|
164
|
+
{/if}
|
|
165
|
+
</dl>
|
|
166
|
+
|
|
167
|
+
{#if assets.length > 0}
|
|
168
|
+
<div class="mt-3">
|
|
169
|
+
<div class="mb-1 text-muted-foreground">
|
|
170
|
+
{t('stac.assets', { count: assets.length })}
|
|
171
|
+
</div>
|
|
172
|
+
<ul class="space-y-1">
|
|
173
|
+
{#each assets as [key, asset] (key)}
|
|
174
|
+
<li class="rounded border border-border px-1.5 py-1">
|
|
175
|
+
<div class="flex items-center justify-between gap-2">
|
|
176
|
+
<span class="truncate font-medium">{key}</span>
|
|
177
|
+
<button
|
|
178
|
+
class="rounded border border-input px-1.5 py-0.5 text-[10px] hover:bg-accent"
|
|
179
|
+
onclick={() => void openAsset(asset.href)}
|
|
180
|
+
>
|
|
181
|
+
{t('stac.assetOpen')}
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
{#if asset.title}
|
|
185
|
+
<div class="truncate text-[10px] text-muted-foreground">{asset.title}</div>
|
|
186
|
+
{/if}
|
|
187
|
+
{#if asset.type}
|
|
188
|
+
<div class="truncate text-[10px] text-muted-foreground">{asset.type}</div>
|
|
189
|
+
{/if}
|
|
190
|
+
{#if Array.isArray(asset.roles) && asset.roles.length > 0}
|
|
191
|
+
<div class="mt-0.5 flex flex-wrap gap-1">
|
|
192
|
+
{#each asset.roles as role (role)}
|
|
193
|
+
<span class="rounded bg-muted px-1 text-[9px] text-muted-foreground">{role}</span>
|
|
194
|
+
{/each}
|
|
195
|
+
</div>
|
|
196
|
+
{/if}
|
|
197
|
+
</li>
|
|
198
|
+
{/each}
|
|
199
|
+
</ul>
|
|
200
|
+
</div>
|
|
201
|
+
{/if}
|
|
202
|
+
|
|
203
|
+
<div class="mt-3 border-t border-border pt-2">
|
|
204
|
+
<button
|
|
205
|
+
class="text-[10px] text-muted-foreground hover:text-card-foreground"
|
|
206
|
+
onclick={() => (showRaw = !showRaw)}
|
|
207
|
+
>
|
|
208
|
+
{showRaw ? t('stac.hideRaw') : t('stac.showRaw')}
|
|
209
|
+
</button>
|
|
210
|
+
{#if showRaw}
|
|
211
|
+
<div class="mt-1 flex justify-end">
|
|
212
|
+
<button
|
|
213
|
+
class="rounded border border-input px-1.5 py-0.5 text-[10px] hover:bg-accent"
|
|
214
|
+
onclick={copyJson}
|
|
215
|
+
>
|
|
216
|
+
{copyLabel ?? t('stac.copyJson')}
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
<pre class="mt-1 max-h-72 overflow-auto rounded bg-muted p-2 font-mono text-[10px] leading-tight">{JSON.stringify(view.raw, jsonReplacerBigInt, 2)}</pre>
|
|
220
|
+
{/if}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</aside>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { StacItemView } from '@walkthru-earth/objex-utils';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
view: StacItemView;
|
|
4
|
+
presign: (href: string) => Promise<string>;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
onFlyTo?: () => void;
|
|
7
|
+
};
|
|
8
|
+
declare const StacItemInspector: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type StacItemInspector = ReturnType<typeof StacItemInspector>;
|
|
10
|
+
export default StacItemInspector;
|