@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.
- package/LICENSE +5 -0
- package/README.md +28 -20
- package/dist/components/browser/FileTreeSidebar.svelte +32 -17
- package/dist/components/layout/AboutSheet.svelte +5 -2
- package/dist/components/layout/ConnectionDialog.svelte +7 -2
- package/dist/components/layout/SettingsSheet.svelte +238 -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 +17 -14
- package/dist/components/layout/TabBar.svelte +4 -4
- 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 +140 -113
- package/dist/components/viewers/CodeViewer.svelte +45 -48
- package/dist/components/viewers/CodeViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/CogControls.svelte +338 -184
- package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
- package/dist/components/viewers/CogViewer.svelte +269 -116
- package/dist/components/viewers/CopcViewer.svelte +8 -15
- package/dist/components/viewers/DatabaseViewer.svelte +22 -21
- package/dist/components/viewers/FileInfo.svelte +16 -16
- package/dist/components/viewers/FlatGeobufViewer.svelte +16 -46
- package/dist/components/viewers/GeoParquetMapViewer.svelte +11 -9
- package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/ImageViewer.svelte +12 -14
- package/dist/components/viewers/LoadProgress.svelte +6 -6
- package/dist/components/viewers/MarkdownViewer.svelte +29 -30
- package/dist/components/viewers/MediaViewer.svelte +13 -14
- package/dist/components/viewers/ModelViewer.svelte +18 -21
- package/dist/components/viewers/MultiCogViewer.svelte +474 -106
- package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/NotebookViewer.svelte +28 -29
- package/dist/components/viewers/PdfViewer.svelte +24 -33
- package/dist/components/viewers/PmtilesViewer.svelte +13 -15
- package/dist/components/viewers/QueryHistoryPanel.svelte +18 -18
- package/dist/components/viewers/RawViewer.svelte +27 -21
- package/dist/components/viewers/StacMapViewer.svelte +6 -13
- package/dist/components/viewers/StacMosaicViewer.svelte +1764 -410
- package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/StacTabViewer.svelte +26 -15
- package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/TableGrid.svelte +38 -34
- package/dist/components/viewers/TableStatusBar.svelte +7 -7
- package/dist/components/viewers/TableToolbar.svelte +10 -9
- package/dist/components/viewers/TableViewer.svelte +47 -30
- package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
- package/dist/components/viewers/ViewerHeader.svelte +18 -0
- package/dist/components/viewers/ViewerHeader.svelte.d.ts +10 -0
- package/dist/components/viewers/ViewerRouter.svelte +16 -8
- package/dist/components/viewers/ViewerStatus.svelte +19 -0
- package/dist/components/viewers/ViewerStatus.svelte.d.ts +7 -0
- package/dist/components/viewers/ZarrMapViewer.svelte +24 -21
- package/dist/components/viewers/ZarrViewer.svelte +98 -65
- 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 +7 -7
- package/dist/components/viewers/map/MapContainer.svelte +38 -12
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +109 -83
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +16 -16
- 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/constants.d.ts +6 -0
- package/dist/constants.js +8 -0
- package/dist/file-icons/index.d.ts +1 -1
- package/dist/file-icons/index.js +1 -1
- package/dist/i18n/ar.js +113 -2
- package/dist/i18n/en.js +113 -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 +468 -0
- package/dist/query/wasm.d.ts +8 -0
- package/dist/query/wasm.js +310 -65
- package/dist/storage/presign.js +3 -2
- package/dist/storage/providers.js +7 -6
- 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 +18 -12
- package/dist/utils/deck.js +15 -7
- package/dist/utils/media-query.svelte.d.ts +14 -0
- package/dist/utils/media-query.svelte.js +29 -0
- package/dist/utils/pmtiles-tile.js +2 -2
- package/dist/utils/signed-url-effect.d.ts +7 -0
- package/dist/utils/signed-url-effect.js +19 -0
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
|
3
|
+
import { formatFileSize, handleLoadError } from '@walkthru-earth/objex-utils';
|
|
3
4
|
import type { PMTiles } from 'pmtiles';
|
|
4
5
|
import { tileIdToZxy } from 'pmtiles';
|
|
5
6
|
import {
|
|
@@ -8,9 +9,9 @@ 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
|
+
import { useIsWide } from '../../../utils/media-query.svelte.js';
|
|
14
15
|
|
|
15
16
|
let {
|
|
16
17
|
metadata,
|
|
@@ -22,6 +23,8 @@ let {
|
|
|
22
23
|
onOpenInspector?: (z: number, x: number, y: number) => void;
|
|
23
24
|
} = $props();
|
|
24
25
|
|
|
26
|
+
const isWide = useIsWide();
|
|
27
|
+
|
|
25
28
|
interface ZoomSummary {
|
|
26
29
|
zoom: number;
|
|
27
30
|
count: number;
|
|
@@ -126,7 +129,7 @@ async function selectZoom(zoom: number) {
|
|
|
126
129
|
if (result.length > 5000) break;
|
|
127
130
|
}
|
|
128
131
|
} catch (err) {
|
|
129
|
-
errorMsg = err
|
|
132
|
+
errorMsg = handleLoadError(err) ?? '';
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
} else {
|
|
@@ -165,7 +168,7 @@ const dedupRatio = $derived(
|
|
|
165
168
|
{#snippet entryDetails()}
|
|
166
169
|
{#if selectedEntry}
|
|
167
170
|
<div
|
|
168
|
-
class="shrink-0 border-b border-
|
|
171
|
+
class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
169
172
|
>
|
|
170
173
|
{t('pmtiles.entryDetails')}
|
|
171
174
|
</div>
|
|
@@ -212,7 +215,7 @@ const dedupRatio = $derived(
|
|
|
212
215
|
<div class="flex h-full flex-col overflow-hidden">
|
|
213
216
|
<!-- Stats grid -->
|
|
214
217
|
<div
|
|
215
|
-
class="shrink-0 border-b border-
|
|
218
|
+
class="shrink-0 border-b border-border px-3 py-3 sm:px-4"
|
|
216
219
|
>
|
|
217
220
|
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-xs sm:grid-cols-3 lg:grid-cols-6">
|
|
218
221
|
<div>
|
|
@@ -278,96 +281,119 @@ const dedupRatio = $derived(
|
|
|
278
281
|
{/if}
|
|
279
282
|
</div>
|
|
280
283
|
|
|
281
|
-
<!-- Column browser (resizable) -->
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
<!-- Column browser (resizable or stacked) -->
|
|
285
|
+
{#snippet zoomLevels()}
|
|
286
|
+
<div class="flex h-full flex-col">
|
|
287
|
+
<div
|
|
288
|
+
class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
289
|
+
>
|
|
290
|
+
{t('pmtiles.zoomLevels')}
|
|
291
|
+
</div>
|
|
292
|
+
<div class="flex-1 overflow-auto">
|
|
293
|
+
{#each zoomSummaries as s}
|
|
294
|
+
<button
|
|
295
|
+
class="flex w-full items-center gap-2 px-3 py-1.5 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
|
|
296
|
+
class:bg-muted={selectedZoom === s.zoom}
|
|
297
|
+
onclick={() => selectZoom(s.zoom)}
|
|
298
|
+
>
|
|
299
|
+
<span class="w-7 shrink-0 font-mono text-muted-foreground">z{s.zoom}</span>
|
|
300
|
+
<div class="min-w-0 flex-1">
|
|
301
|
+
<div
|
|
302
|
+
class="h-1.5 rounded-full bg-blue-500/60"
|
|
303
|
+
style="width: {Math.max(2, (s.count / maxCount) * 100)}%"
|
|
304
|
+
></div>
|
|
305
|
+
</div>
|
|
306
|
+
<span class="shrink-0 text-[10px] tabular-nums text-muted-foreground">
|
|
307
|
+
{s.count.toLocaleString()}
|
|
308
|
+
</span>
|
|
309
|
+
<ChevronRightIcon class="size-3 shrink-0 text-muted-foreground" />
|
|
310
|
+
</button>
|
|
311
|
+
{/each}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
{/snippet}
|
|
315
|
+
|
|
316
|
+
{#snippet zoomEntryList()}
|
|
317
|
+
<div class="flex h-full flex-col">
|
|
318
|
+
{#if selectedZoom !== null}
|
|
286
319
|
<div
|
|
287
|
-
class="shrink-0 border-b border-
|
|
320
|
+
class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
288
321
|
>
|
|
289
|
-
{t('pmtiles.
|
|
322
|
+
{t('pmtiles.tilesAtZoom').replace('{zoom}', String(selectedZoom))}
|
|
323
|
+
<span class="ms-1 normal-case tracking-normal">({zoomEntries.length.toLocaleString()})</span>
|
|
290
324
|
</div>
|
|
291
325
|
<div class="flex-1 overflow-auto">
|
|
292
|
-
{#
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
326
|
+
{#if loadingEntries}
|
|
327
|
+
<div class="p-4 text-center text-xs text-muted-foreground">Loading...</div>
|
|
328
|
+
{:else if errorMsg}
|
|
329
|
+
<div class="p-4 text-center text-xs text-destructive">{errorMsg}</div>
|
|
330
|
+
{:else if zoomEntries.length === 0}
|
|
331
|
+
<div class="p-4 text-center text-xs text-muted-foreground">{t('pmtiles.noEntries')}</div>
|
|
332
|
+
{:else}
|
|
333
|
+
{#each zoomEntries as entry}
|
|
334
|
+
<button
|
|
335
|
+
class="flex w-full items-center gap-2 px-3 py-1 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
|
|
336
|
+
class:bg-muted={selectedEntry?.tileId === entry.tileId}
|
|
337
|
+
onclick={() => (selectedEntry = entry)}
|
|
338
|
+
>
|
|
339
|
+
<span class="shrink-0 truncate font-mono text-[11px]">
|
|
340
|
+
{entry.z}/{entry.x}/{entry.y}
|
|
341
|
+
</span>
|
|
342
|
+
<span class="ms-auto shrink-0 text-[10px] tabular-nums text-muted-foreground">
|
|
343
|
+
{formatBytes(entry.length)}
|
|
344
|
+
</span>
|
|
345
|
+
<ChevronRightIcon class="size-3 shrink-0 text-muted-foreground" />
|
|
346
|
+
</button>
|
|
347
|
+
{/each}
|
|
348
|
+
{/if}
|
|
312
349
|
</div>
|
|
313
|
-
|
|
314
|
-
|
|
350
|
+
{:else}
|
|
351
|
+
<div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">
|
|
352
|
+
Select a zoom level
|
|
353
|
+
</div>
|
|
354
|
+
{/if}
|
|
355
|
+
</div>
|
|
356
|
+
{/snippet}
|
|
315
357
|
|
|
316
|
-
|
|
358
|
+
{#if isWide.value}
|
|
359
|
+
<ResizablePaneGroup direction="horizontal" class="min-h-0 flex-1">
|
|
360
|
+
<!-- Column 1: Zoom levels -->
|
|
361
|
+
<ResizablePane defaultSize={28} minSize={15}>
|
|
362
|
+
{@render zoomLevels()}
|
|
363
|
+
</ResizablePane>
|
|
317
364
|
|
|
318
|
-
|
|
319
|
-
<ResizablePane defaultSize={42} minSize={20}>
|
|
320
|
-
<div class="flex h-full flex-col">
|
|
321
|
-
{#if selectedZoom !== null}
|
|
322
|
-
<div
|
|
323
|
-
class="shrink-0 border-b border-zinc-200 px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground dark:border-zinc-800"
|
|
324
|
-
>
|
|
325
|
-
{t('pmtiles.tilesAtZoom').replace('{zoom}', String(selectedZoom))}
|
|
326
|
-
<span class="ms-1 normal-case tracking-normal">({zoomEntries.length.toLocaleString()})</span>
|
|
327
|
-
</div>
|
|
328
|
-
<div class="flex-1 overflow-auto">
|
|
329
|
-
{#if loadingEntries}
|
|
330
|
-
<div class="p-4 text-center text-xs text-muted-foreground">Loading...</div>
|
|
331
|
-
{:else if errorMsg}
|
|
332
|
-
<div class="p-4 text-center text-xs text-red-400">{errorMsg}</div>
|
|
333
|
-
{:else if zoomEntries.length === 0}
|
|
334
|
-
<div class="p-4 text-center text-xs text-muted-foreground">{t('pmtiles.noEntries')}</div>
|
|
335
|
-
{:else}
|
|
336
|
-
{#each zoomEntries as entry}
|
|
337
|
-
<button
|
|
338
|
-
class="flex w-full items-center gap-2 px-3 py-1 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
|
|
339
|
-
class:bg-zinc-100={selectedEntry?.tileId === entry.tileId}
|
|
340
|
-
class:dark:bg-zinc-800={selectedEntry?.tileId === entry.tileId}
|
|
341
|
-
onclick={() => (selectedEntry = entry)}
|
|
342
|
-
>
|
|
343
|
-
<span class="shrink-0 truncate font-mono text-[11px]">
|
|
344
|
-
{entry.z}/{entry.x}/{entry.y}
|
|
345
|
-
</span>
|
|
346
|
-
<span class="ms-auto shrink-0 text-[10px] tabular-nums text-muted-foreground">
|
|
347
|
-
{formatBytes(entry.length)}
|
|
348
|
-
</span>
|
|
349
|
-
<ChevronRightIcon class="size-3 shrink-0 text-muted-foreground" />
|
|
350
|
-
</button>
|
|
351
|
-
{/each}
|
|
352
|
-
{/if}
|
|
353
|
-
</div>
|
|
354
|
-
{:else}
|
|
355
|
-
<div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">
|
|
356
|
-
Select a zoom level
|
|
357
|
-
</div>
|
|
358
|
-
{/if}
|
|
359
|
-
</div>
|
|
360
|
-
</ResizablePane>
|
|
365
|
+
<ResizableHandle />
|
|
361
366
|
|
|
362
|
-
|
|
367
|
+
<!-- Column 2: Entries at zoom -->
|
|
368
|
+
<ResizablePane defaultSize={42} minSize={20}>
|
|
369
|
+
{@render zoomEntryList()}
|
|
370
|
+
</ResizablePane>
|
|
363
371
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
372
|
+
<ResizableHandle />
|
|
373
|
+
|
|
374
|
+
<!-- Column 3: Entry details -->
|
|
375
|
+
<ResizablePane defaultSize={30} minSize={15}>
|
|
376
|
+
<div class="flex h-full flex-col">
|
|
377
|
+
{@render entryDetails()}
|
|
378
|
+
</div>
|
|
379
|
+
</ResizablePane>
|
|
380
|
+
</ResizablePaneGroup>
|
|
381
|
+
{:else}
|
|
382
|
+
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
|
383
|
+
<!-- Zoom level list: compact fixed height -->
|
|
384
|
+
<div class="max-h-48 shrink-0 border-b border-border">
|
|
385
|
+
{@render zoomLevels()}
|
|
386
|
+
</div>
|
|
387
|
+
<!-- Entries at selected zoom: fixed height -->
|
|
388
|
+
<div class="max-h-56 shrink-0 border-b border-border">
|
|
389
|
+
{@render zoomEntryList()}
|
|
390
|
+
</div>
|
|
391
|
+
<!-- Entry details: grows to fill remaining space -->
|
|
392
|
+
<div class="flex flex-1 flex-col">
|
|
367
393
|
{@render entryDetails()}
|
|
368
394
|
</div>
|
|
369
|
-
</
|
|
370
|
-
|
|
395
|
+
</div>
|
|
396
|
+
{/if}
|
|
371
397
|
</div>
|
|
372
398
|
|
|
373
399
|
<style>
|
|
@@ -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, handleLoadError } 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,
|
|
@@ -121,7 +121,7 @@ async function fetchTile() {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
} catch (e) {
|
|
124
|
-
error =
|
|
124
|
+
error = handleLoadError(e);
|
|
125
125
|
} finally {
|
|
126
126
|
loading = false;
|
|
127
127
|
}
|
|
@@ -180,7 +180,7 @@ function formatValue(v: unknown): string {
|
|
|
180
180
|
<div class="flex h-full flex-col overflow-hidden">
|
|
181
181
|
<!-- Navigation bar -->
|
|
182
182
|
<div
|
|
183
|
-
class="flex shrink-0 flex-wrap items-center gap-2 border-b border-
|
|
183
|
+
class="flex shrink-0 flex-wrap items-center gap-2 border-b border-border px-3 py-2"
|
|
184
184
|
>
|
|
185
185
|
<!-- Z/X/Y inputs -->
|
|
186
186
|
<div class="flex items-center gap-1 text-xs">
|
|
@@ -190,7 +190,7 @@ function formatValue(v: unknown): string {
|
|
|
190
190
|
bind:value={inputZ}
|
|
191
191
|
min={0}
|
|
192
192
|
max={30}
|
|
193
|
-
class="w-12 rounded border border-
|
|
193
|
+
class="w-12 rounded border border-border bg-transparent px-1.5 py-0.5 text-center font-mono text-xs"
|
|
194
194
|
onkeydown={handleKeydown}
|
|
195
195
|
/>
|
|
196
196
|
<span class="text-muted-foreground">x</span>
|
|
@@ -198,7 +198,7 @@ function formatValue(v: unknown): string {
|
|
|
198
198
|
type="number"
|
|
199
199
|
bind:value={inputX}
|
|
200
200
|
min={0}
|
|
201
|
-
class="w-16 rounded border border-
|
|
201
|
+
class="w-16 rounded border border-border bg-transparent px-1.5 py-0.5 text-center font-mono text-xs"
|
|
202
202
|
onkeydown={handleKeydown}
|
|
203
203
|
/>
|
|
204
204
|
<span class="text-muted-foreground">y</span>
|
|
@@ -206,7 +206,7 @@ function formatValue(v: unknown): string {
|
|
|
206
206
|
type="number"
|
|
207
207
|
bind:value={inputY}
|
|
208
208
|
min={0}
|
|
209
|
-
class="w-16 rounded border border-
|
|
209
|
+
class="w-16 rounded border border-border bg-transparent px-1.5 py-0.5 text-center font-mono text-xs"
|
|
210
210
|
onkeydown={handleKeydown}
|
|
211
211
|
/>
|
|
212
212
|
</div>
|
|
@@ -253,13 +253,13 @@ function formatValue(v: unknown): string {
|
|
|
253
253
|
</div>
|
|
254
254
|
|
|
255
255
|
<!-- Main content -->
|
|
256
|
-
<div class="flex min-h-0 flex-1 overflow-hidden">
|
|
256
|
+
<div class="flex min-h-0 flex-1 flex-col overflow-hidden sm:flex-row">
|
|
257
257
|
{#if loading}
|
|
258
258
|
<div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">
|
|
259
259
|
Loading tile...
|
|
260
260
|
</div>
|
|
261
261
|
{:else if error}
|
|
262
|
-
<div class="flex flex-1 items-center justify-center text-xs text-
|
|
262
|
+
<div class="flex flex-1 items-center justify-center text-xs text-destructive">
|
|
263
263
|
{error}
|
|
264
264
|
</div>
|
|
265
265
|
{:else if tile}
|
|
@@ -312,16 +312,16 @@ function formatValue(v: unknown): string {
|
|
|
312
312
|
|
|
313
313
|
<!-- Feature properties panel -->
|
|
314
314
|
<div
|
|
315
|
-
class="flex w-56 shrink-0
|
|
315
|
+
class="flex w-full flex-col border-t border-border sm:w-56 sm:shrink-0 sm:border-s sm:border-t-0 lg:w-64"
|
|
316
316
|
>
|
|
317
317
|
<div
|
|
318
|
-
class="shrink-0 border-b border-
|
|
318
|
+
class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
319
319
|
>
|
|
320
320
|
{t('pmtiles.featureProperties')}
|
|
321
321
|
</div>
|
|
322
322
|
{#if selectedFeature && selectedLayerName !== null}
|
|
323
323
|
<div class="flex-1 overflow-auto">
|
|
324
|
-
<div class="border-b border-
|
|
324
|
+
<div class="border-b border-border px-3 py-2">
|
|
325
325
|
<div class="flex items-center gap-1.5 text-xs">
|
|
326
326
|
<span
|
|
327
327
|
class="inline-block size-2 rounded-sm"
|
|
@@ -337,14 +337,14 @@ function formatValue(v: unknown): string {
|
|
|
337
337
|
· #{selectedFeatureIdx}
|
|
338
338
|
</div>
|
|
339
339
|
</div>
|
|
340
|
-
<div class="divide-y divide-
|
|
340
|
+
<div class="divide-y divide-border">
|
|
341
341
|
{#each Object.entries(selectedFeature.properties) as [key, value]}
|
|
342
342
|
<div class="px-3 py-1.5">
|
|
343
|
-
<div class="text-[10px] font-medium text-
|
|
343
|
+
<div class="text-[10px] font-medium text-muted-foreground">
|
|
344
344
|
{key}
|
|
345
345
|
</div>
|
|
346
346
|
<div
|
|
347
|
-
class="break-all text-xs text-
|
|
347
|
+
class="break-all text-xs text-foreground"
|
|
348
348
|
title={formatValue(value)}
|
|
349
349
|
>
|
|
350
350
|
{formatValue(value)}
|
|
@@ -402,10 +402,10 @@ function formatValue(v: unknown): string {
|
|
|
402
402
|
|
|
403
403
|
<!-- Raster tile info panel -->
|
|
404
404
|
<div
|
|
405
|
-
class="flex w-56 shrink-0
|
|
405
|
+
class="flex w-full flex-col border-t border-border sm:w-56 sm:shrink-0 sm:border-s sm:border-t-0 lg:w-64"
|
|
406
406
|
>
|
|
407
407
|
<div
|
|
408
|
-
class="shrink-0 border-b border-
|
|
408
|
+
class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
409
409
|
>
|
|
410
410
|
{t('pmtiles.tileInfo')}
|
|
411
411
|
</div>
|
|
@@ -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;
|