@walkthru-earth/objex 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +28 -20
  3. package/dist/components/browser/FileTreeSidebar.svelte +32 -17
  4. package/dist/components/layout/AboutSheet.svelte +5 -2
  5. package/dist/components/layout/ConnectionDialog.svelte +7 -2
  6. package/dist/components/layout/SettingsSheet.svelte +238 -0
  7. package/dist/components/layout/SettingsSheet.svelte.d.ts +6 -0
  8. package/dist/components/layout/Sidebar.svelte +73 -6
  9. package/dist/components/layout/Sidebar.svelte.d.ts +4 -1
  10. package/dist/components/layout/StatusBar.svelte +17 -14
  11. package/dist/components/layout/TabBar.svelte +4 -4
  12. package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
  13. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
  14. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
  15. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  16. package/dist/components/ui/resizable/index.d.ts +1 -1
  17. package/dist/components/ui/resizable/index.js +2 -2
  18. package/dist/components/ui/slider/index.d.ts +3 -0
  19. package/dist/components/ui/slider/index.js +5 -0
  20. package/dist/components/ui/slider/range-slider.svelte +94 -0
  21. package/dist/components/ui/slider/range-slider.svelte.d.ts +21 -0
  22. package/dist/components/ui/slider/slider.svelte +83 -0
  23. package/dist/components/ui/slider/slider.svelte.d.ts +7 -0
  24. package/dist/components/viewers/ArchiveViewer.svelte +140 -113
  25. package/dist/components/viewers/CodeViewer.svelte +45 -48
  26. package/dist/components/viewers/CodeViewer.svelte.d.ts +1 -1
  27. package/dist/components/viewers/CogControls.svelte +338 -184
  28. package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
  29. package/dist/components/viewers/CogViewer.svelte +269 -116
  30. package/dist/components/viewers/CopcViewer.svelte +8 -15
  31. package/dist/components/viewers/DatabaseViewer.svelte +22 -21
  32. package/dist/components/viewers/FileInfo.svelte +16 -16
  33. package/dist/components/viewers/FlatGeobufViewer.svelte +16 -46
  34. package/dist/components/viewers/GeoParquetMapViewer.svelte +11 -9
  35. package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
  36. package/dist/components/viewers/ImageViewer.svelte +12 -14
  37. package/dist/components/viewers/LoadProgress.svelte +6 -6
  38. package/dist/components/viewers/MarkdownViewer.svelte +29 -30
  39. package/dist/components/viewers/MediaViewer.svelte +13 -14
  40. package/dist/components/viewers/ModelViewer.svelte +18 -21
  41. package/dist/components/viewers/MultiCogViewer.svelte +474 -106
  42. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
  43. package/dist/components/viewers/NotebookViewer.svelte +28 -29
  44. package/dist/components/viewers/PdfViewer.svelte +24 -33
  45. package/dist/components/viewers/PmtilesViewer.svelte +13 -15
  46. package/dist/components/viewers/QueryHistoryPanel.svelte +18 -18
  47. package/dist/components/viewers/RawViewer.svelte +27 -21
  48. package/dist/components/viewers/StacMapViewer.svelte +6 -13
  49. package/dist/components/viewers/StacMosaicViewer.svelte +1764 -410
  50. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
  51. package/dist/components/viewers/StacTabViewer.svelte +26 -15
  52. package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
  53. package/dist/components/viewers/TableGrid.svelte +38 -34
  54. package/dist/components/viewers/TableStatusBar.svelte +7 -7
  55. package/dist/components/viewers/TableToolbar.svelte +10 -9
  56. package/dist/components/viewers/TableViewer.svelte +47 -30
  57. package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
  58. package/dist/components/viewers/ViewerHeader.svelte +18 -0
  59. package/dist/components/viewers/ViewerHeader.svelte.d.ts +10 -0
  60. package/dist/components/viewers/ViewerRouter.svelte +16 -8
  61. package/dist/components/viewers/ViewerStatus.svelte +19 -0
  62. package/dist/components/viewers/ViewerStatus.svelte.d.ts +7 -0
  63. package/dist/components/viewers/ZarrMapViewer.svelte +24 -21
  64. package/dist/components/viewers/ZarrViewer.svelte +98 -65
  65. package/dist/components/viewers/cog/ChannelPicker.svelte +83 -0
  66. package/dist/components/viewers/cog/ChannelPicker.svelte.d.ts +13 -0
  67. package/dist/components/viewers/cog/PixelInspectorPanel.svelte +87 -0
  68. package/dist/components/viewers/cog/PixelInspectorPanel.svelte.d.ts +17 -0
  69. package/dist/components/viewers/cog/buildRgbLayer.d.ts +78 -0
  70. package/dist/components/viewers/cog/buildRgbLayer.js +176 -0
  71. package/dist/components/viewers/map/AttributeTable.svelte +7 -7
  72. package/dist/components/viewers/map/MapContainer.svelte +38 -12
  73. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +109 -83
  74. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +16 -16
  75. package/dist/components/viewers/stac/StacDatetimeBar.svelte +175 -0
  76. package/dist/components/viewers/stac/StacDatetimeBar.svelte.d.ts +10 -0
  77. package/dist/components/viewers/stac/StacFilterPanel.svelte +243 -0
  78. package/dist/components/viewers/stac/StacFilterPanel.svelte.d.ts +14 -0
  79. package/dist/components/viewers/stac/StacItemInspector.svelte +223 -0
  80. package/dist/components/viewers/stac/StacItemInspector.svelte.d.ts +10 -0
  81. package/dist/components/viewers/stac/StacItemStrip.svelte +228 -0
  82. package/dist/components/viewers/stac/StacItemStrip.svelte.d.ts +12 -0
  83. package/dist/constants.d.ts +6 -0
  84. package/dist/constants.js +8 -0
  85. package/dist/file-icons/index.d.ts +1 -1
  86. package/dist/file-icons/index.js +1 -1
  87. package/dist/i18n/ar.js +113 -2
  88. package/dist/i18n/en.js +113 -2
  89. package/dist/index.d.ts +2 -28
  90. package/dist/index.js +7 -23
  91. package/dist/query/engine.d.ts +10 -0
  92. package/dist/query/source.js +1 -1
  93. package/dist/query/stac-source-factory.d.ts +65 -0
  94. package/dist/query/stac-source-factory.js +77 -0
  95. package/dist/query/stac-source-parquet.d.ts +135 -0
  96. package/dist/query/stac-source-parquet.js +468 -0
  97. package/dist/query/wasm.d.ts +8 -0
  98. package/dist/query/wasm.js +310 -65
  99. package/dist/storage/presign.js +3 -2
  100. package/dist/storage/providers.js +7 -6
  101. package/dist/stores/config.svelte.d.ts +15 -0
  102. package/dist/stores/config.svelte.js +46 -0
  103. package/dist/stores/connections.svelte.d.ts +2 -2
  104. package/dist/stores/connections.svelte.js +1 -2
  105. package/dist/stores/files.svelte.d.ts +1 -1
  106. package/dist/stores/files.svelte.js +1 -1
  107. package/dist/stores/query-history.svelte.js +1 -1
  108. package/dist/stores/settings.svelte.d.ts +16 -1
  109. package/dist/stores/settings.svelte.js +104 -48
  110. package/dist/stores/tabs.svelte.d.ts +3 -0
  111. package/dist/stores/tabs.svelte.js +17 -0
  112. package/dist/utils/cog-histogram.d.ts +121 -0
  113. package/dist/utils/cog-histogram.js +424 -0
  114. package/dist/utils/cog.d.ts +177 -20
  115. package/dist/utils/cog.js +361 -76
  116. package/dist/utils/colormap-sprite.d.ts +0 -9
  117. package/dist/utils/colormap-sprite.js +0 -21
  118. package/dist/utils/deck.d.ts +18 -12
  119. package/dist/utils/deck.js +15 -7
  120. package/dist/utils/media-query.svelte.d.ts +14 -0
  121. package/dist/utils/media-query.svelte.js +29 -0
  122. package/dist/utils/pmtiles-tile.js +2 -2
  123. package/dist/utils/signed-url-effect.d.ts +7 -0
  124. package/dist/utils/signed-url-effect.js +19 -0
  125. package/dist/utils/{url.d.ts → signed-url.d.ts} +15 -1
  126. package/dist/utils/{url.js → signed-url.js} +32 -10
  127. package/dist/utils/url-state.d.ts +36 -0
  128. package/dist/utils/url-state.js +72 -2
  129. package/dist/utils/zarr-tab.d.ts +1 -2
  130. package/dist/utils/zarr-tab.js +1 -2
  131. package/dist/utils/zarr.d.ts +0 -17
  132. package/dist/utils/zarr.js +1 -45
  133. package/package.json +55 -84
  134. package/dist/components/browser/Breadcrumb.svelte +0 -50
  135. package/dist/components/browser/Breadcrumb.svelte.d.ts +0 -7
  136. package/dist/components/browser/CreateFolderDialog.svelte +0 -98
  137. package/dist/components/browser/CreateFolderDialog.svelte.d.ts +0 -6
  138. package/dist/components/browser/DeleteConfirmDialog.svelte +0 -90
  139. package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +0 -8
  140. package/dist/components/browser/DropZone.svelte +0 -83
  141. package/dist/components/browser/DropZone.svelte.d.ts +0 -7
  142. package/dist/components/browser/FileBrowser.svelte +0 -252
  143. package/dist/components/browser/FileBrowser.svelte.d.ts +0 -3
  144. package/dist/components/browser/FileRow.svelte +0 -117
  145. package/dist/components/browser/FileRow.svelte.d.ts +0 -9
  146. package/dist/components/browser/RenameDialog.svelte +0 -101
  147. package/dist/components/browser/RenameDialog.svelte.d.ts +0 -8
  148. package/dist/components/browser/SearchBar.svelte +0 -40
  149. package/dist/components/browser/SearchBar.svelte.d.ts +0 -6
  150. package/dist/components/browser/UploadButton.svelte +0 -65
  151. package/dist/components/browser/UploadButton.svelte.d.ts +0 -3
  152. package/dist/query/stac-geoparquet.d.ts +0 -31
  153. package/dist/query/stac-geoparquet.js +0 -136
  154. package/dist/utils/clipboard.d.ts +0 -13
  155. package/dist/utils/clipboard.js +0 -38
  156. package/dist/utils/cloud-url.d.ts +0 -27
  157. package/dist/utils/cloud-url.js +0 -61
  158. package/dist/utils/cog-pure.d.ts +0 -25
  159. package/dist/utils/cog-pure.js +0 -35
  160. package/dist/utils/column-types.d.ts +0 -5
  161. package/dist/utils/column-types.js +0 -137
  162. package/dist/utils/connection-identity.d.ts +0 -51
  163. package/dist/utils/connection-identity.js +0 -97
  164. package/dist/utils/error.d.ts +0 -8
  165. package/dist/utils/error.js +0 -12
  166. package/dist/utils/evidence-context.d.ts +0 -22
  167. package/dist/utils/evidence-context.js +0 -56
  168. package/dist/utils/export.d.ts +0 -22
  169. package/dist/utils/export.js +0 -76
  170. package/dist/utils/file-sort.d.ts +0 -20
  171. package/dist/utils/file-sort.js +0 -41
  172. package/dist/utils/format.d.ts +0 -24
  173. package/dist/utils/format.js +0 -78
  174. package/dist/utils/geoarrow.d.ts +0 -32
  175. package/dist/utils/geoarrow.js +0 -672
  176. package/dist/utils/geometry-type.d.ts +0 -52
  177. package/dist/utils/geometry-type.js +0 -76
  178. package/dist/utils/hex.d.ts +0 -10
  179. package/dist/utils/hex.js +0 -27
  180. package/dist/utils/host-detection.d.ts +0 -23
  181. package/dist/utils/host-detection.js +0 -95
  182. package/dist/utils/local-storage.d.ts +0 -16
  183. package/dist/utils/local-storage.js +0 -37
  184. package/dist/utils/markdown-sql.d.ts +0 -30
  185. package/dist/utils/markdown-sql.js +0 -72
  186. package/dist/utils/notebook.d.ts +0 -59
  187. package/dist/utils/notebook.js +0 -211
  188. package/dist/utils/parquet-metadata.d.ts +0 -64
  189. package/dist/utils/parquet-metadata.js +0 -262
  190. package/dist/utils/stac-geoparquet.d.ts +0 -90
  191. package/dist/utils/stac-geoparquet.js +0 -223
  192. package/dist/utils/stac-hydrate.d.ts +0 -38
  193. package/dist/utils/stac-hydrate.js +0 -243
  194. package/dist/utils/stac.d.ts +0 -136
  195. package/dist/utils/stac.js +0 -176
  196. package/dist/utils/storage-url.d.ts +0 -90
  197. package/dist/utils/storage-url.js +0 -568
  198. package/dist/utils/wkb.d.ts +0 -43
  199. package/dist/utils/wkb.js +0 -359
@@ -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
+ &times;
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
+ &times;
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;