@walkthru-earth/objex 1.4.0 → 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/README.md +9 -9
- package/dist/components/layout/ConnectionDialog.svelte +7 -2
- package/dist/components/layout/SettingsSheet.svelte +2 -1
- package/dist/components/layout/StatusBar.svelte +16 -13
- package/dist/components/layout/TabBar.svelte +2 -2
- package/dist/components/viewers/ArchiveViewer.svelte +139 -112
- package/dist/components/viewers/CodeViewer.svelte +15 -27
- package/dist/components/viewers/CodeViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/CogViewer.svelte +8 -6
- 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 +15 -45
- package/dist/components/viewers/GeoParquetMapViewer.svelte +5 -3
- package/dist/components/viewers/ImageViewer.svelte +10 -12
- package/dist/components/viewers/LoadProgress.svelte +6 -6
- package/dist/components/viewers/MarkdownViewer.svelte +17 -21
- package/dist/components/viewers/MediaViewer.svelte +11 -12
- package/dist/components/viewers/ModelViewer.svelte +17 -20
- package/dist/components/viewers/MultiCogViewer.svelte +11 -8
- package/dist/components/viewers/NotebookViewer.svelte +22 -26
- package/dist/components/viewers/PdfViewer.svelte +22 -31
- package/dist/components/viewers/PmtilesViewer.svelte +10 -9
- package/dist/components/viewers/QueryHistoryPanel.svelte +18 -18
- package/dist/components/viewers/RawViewer.svelte +21 -18
- package/dist/components/viewers/StacMapViewer.svelte +6 -13
- package/dist/components/viewers/StacMosaicViewer.svelte +9 -7
- package/dist/components/viewers/StacTabViewer.svelte +2 -2
- package/dist/components/viewers/TableGrid.svelte +34 -30
- package/dist/components/viewers/TableStatusBar.svelte +6 -6
- package/dist/components/viewers/TableToolbar.svelte +9 -8
- package/dist/components/viewers/TableViewer.svelte +22 -13
- package/dist/components/viewers/ViewerHeader.svelte +18 -0
- package/dist/components/viewers/ViewerHeader.svelte.d.ts +10 -0
- package/dist/components/viewers/ViewerStatus.svelte +19 -0
- package/dist/components/viewers/ViewerStatus.svelte.d.ts +7 -0
- package/dist/components/viewers/ZarrMapViewer.svelte +13 -12
- package/dist/components/viewers/ZarrViewer.svelte +94 -61
- package/dist/components/viewers/map/AttributeTable.svelte +6 -6
- package/dist/components/viewers/map/MapContainer.svelte +2 -2
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +109 -83
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +16 -16
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +8 -0
- package/dist/i18n/ar.js +3 -0
- package/dist/i18n/en.js +3 -0
- package/dist/query/stac-source-parquet.js +8 -5
- package/dist/query/wasm.js +6 -63
- package/dist/storage/presign.js +2 -1
- package/dist/storage/providers.js +2 -1
- package/dist/stores/settings.svelte.js +3 -3
- package/dist/utils/deck.d.ts +2 -0
- package/dist/utils/deck.js +5 -3
- package/dist/utils/media-query.svelte.d.ts +14 -0
- package/dist/utils/media-query.svelte.js +29 -0
- package/dist/utils/signed-url-effect.d.ts +7 -0
- package/dist/utils/signed-url-effect.js +19 -0
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { MapboxOverlay } from '@deck.gl/mapbox';
|
|
3
|
+
import { handleLoadError } from '@walkthru-earth/objex-utils';
|
|
3
4
|
import type maplibregl from 'maplibre-gl';
|
|
4
5
|
import maplibreModule from 'maplibre-gl';
|
|
5
6
|
import { onDestroy, untrack } from 'svelte';
|
|
@@ -491,7 +492,7 @@ async function addZarrLayer(map: maplibregl.Map) {
|
|
|
491
492
|
zarrLayer = new ZarrLayer(opts);
|
|
492
493
|
map.addLayer(zarrLayer);
|
|
493
494
|
} catch (err) {
|
|
494
|
-
error =
|
|
495
|
+
error = handleLoadError(err);
|
|
495
496
|
loading = false;
|
|
496
497
|
}
|
|
497
498
|
}
|
|
@@ -599,7 +600,7 @@ async function updateSelector() {
|
|
|
599
600
|
try {
|
|
600
601
|
await zarrLayer.setSelector(buildSelector());
|
|
601
602
|
} catch (err) {
|
|
602
|
-
error =
|
|
603
|
+
error = handleLoadError(err);
|
|
603
604
|
}
|
|
604
605
|
}
|
|
605
606
|
|
|
@@ -641,12 +642,12 @@ onDestroy(cleanup);
|
|
|
641
642
|
<div class="flex h-full w-full flex-col overflow-hidden">
|
|
642
643
|
<!-- Controls bar -->
|
|
643
644
|
<div
|
|
644
|
-
class="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-
|
|
645
|
+
class="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-border px-3 py-1.5"
|
|
645
646
|
>
|
|
646
|
-
<label class="flex items-center gap-1 text-xs text-
|
|
647
|
+
<label class="flex items-center gap-1 text-xs text-muted-foreground">
|
|
647
648
|
{t('map.variable')}
|
|
648
649
|
<select
|
|
649
|
-
class="rounded border border-
|
|
650
|
+
class="rounded border border-border bg-background px-1.5 py-0.5 text-xs text-foreground"
|
|
650
651
|
bind:value={selectedVar}
|
|
651
652
|
onchange={changeVariable}
|
|
652
653
|
>
|
|
@@ -658,10 +659,10 @@ onDestroy(cleanup);
|
|
|
658
659
|
|
|
659
660
|
{#each selectorDims as dim}
|
|
660
661
|
<label
|
|
661
|
-
class="flex shrink-0 items-center gap-1.5 rounded border border-
|
|
662
|
+
class="flex shrink-0 items-center gap-1.5 rounded border border-border px-2 py-0.5 text-xs text-muted-foreground"
|
|
662
663
|
title={dimLabel(dim)}
|
|
663
664
|
>
|
|
664
|
-
<span class="shrink-0 font-medium text-
|
|
665
|
+
<span class="shrink-0 font-medium text-muted-foreground">{dim.name}</span>
|
|
665
666
|
<Slider
|
|
666
667
|
type="single"
|
|
667
668
|
min={0}
|
|
@@ -676,7 +677,7 @@ onDestroy(cleanup);
|
|
|
676
677
|
/>
|
|
677
678
|
{#if dim.isDatetime && dim.minDate && dim.maxDate}
|
|
678
679
|
{@const dateVal = indexToDateStr(selectorValues[dim.name] ?? 0, dim)}
|
|
679
|
-
<span class="shrink-0 tabular-nums text-
|
|
680
|
+
<span class="shrink-0 tabular-nums text-muted-foreground">
|
|
680
681
|
{dateVal ? (dim.subDaily ? dateVal.replace('T', ' ') : dateVal) : (selectorValues[dim.name] ?? 0)}
|
|
681
682
|
</span>
|
|
682
683
|
<input
|
|
@@ -691,10 +692,10 @@ onDestroy(cleanup);
|
|
|
691
692
|
updateSelector();
|
|
692
693
|
}
|
|
693
694
|
}}
|
|
694
|
-
class="h-5 rounded border border-
|
|
695
|
+
class="h-5 rounded border border-border bg-background px-1 text-[10px] text-muted-foreground"
|
|
695
696
|
/>
|
|
696
697
|
{:else}
|
|
697
|
-
<span class="shrink-0 tabular-nums text-
|
|
698
|
+
<span class="shrink-0 tabular-nums text-muted-foreground">{selectorValues[dim.name] ?? 0}<span class="text-muted-foreground/60">/{dim.size - 1}</span></span>
|
|
698
699
|
{#if dim.dtype}
|
|
699
700
|
<span class="shrink-0 text-[10px] text-zinc-400/70">{dim.dtype}</span>
|
|
700
701
|
{/if}
|
|
@@ -703,7 +704,7 @@ onDestroy(cleanup);
|
|
|
703
704
|
{/each}
|
|
704
705
|
|
|
705
706
|
{#if selectedMeta?.shape}
|
|
706
|
-
<span class="ms-auto text-xs text-
|
|
707
|
+
<span class="ms-auto text-xs text-muted-foreground">
|
|
707
708
|
{selectedMeta.dtype} [{selectedMeta.shape.join(', ')}]
|
|
708
709
|
</span>
|
|
709
710
|
{/if}
|
|
@@ -713,7 +714,7 @@ onDestroy(cleanup);
|
|
|
713
714
|
<div class="relative min-h-0 flex-1">
|
|
714
715
|
{#if error && !loading}
|
|
715
716
|
<div class="flex h-full items-center justify-center">
|
|
716
|
-
<p class="max-w-md text-center text-sm text-
|
|
717
|
+
<p class="max-w-md text-center text-sm text-destructive">{error}</p>
|
|
717
718
|
</div>
|
|
718
719
|
{:else}
|
|
719
720
|
<MapContainer {onMapReady} bounds={[-130, 20, -60, 55]} />
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { handleLoadError } from '@walkthru-earth/objex-utils';
|
|
3
|
+
import { onDestroy, untrack } from 'svelte';
|
|
3
4
|
import { Badge } from '../ui/badge/index.js';
|
|
4
5
|
import { Button } from '../ui/button/index.js';
|
|
5
6
|
import {
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
ResizablePaneGroup
|
|
9
10
|
} from '../ui/resizable/index.js';
|
|
10
11
|
import { t } from '../../i18n/index.svelte.js';
|
|
12
|
+
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
11
13
|
import type { Tab } from '../../types';
|
|
12
14
|
import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
|
|
13
15
|
import { pickViewMode, updateUrlView } from '../../utils/url-state.js';
|
|
@@ -25,9 +27,12 @@ import {
|
|
|
25
27
|
type ZarrHierarchy,
|
|
26
28
|
type ZarrNode
|
|
27
29
|
} from '../../utils/zarr.js';
|
|
30
|
+
import { useIsWide } from '../../utils/media-query.svelte.js';
|
|
28
31
|
|
|
29
32
|
let { tab }: { tab: Tab } = $props();
|
|
30
33
|
|
|
34
|
+
const isWide = useIsWide();
|
|
35
|
+
|
|
31
36
|
let loading = $state(true);
|
|
32
37
|
let error = $state<string | null>(null);
|
|
33
38
|
type ZarrViewMode = 'inspect' | 'map';
|
|
@@ -110,6 +115,19 @@ $effect(() => {
|
|
|
110
115
|
});
|
|
111
116
|
});
|
|
112
117
|
|
|
118
|
+
function cleanup() {
|
|
119
|
+
hierarchy = null;
|
|
120
|
+
selectedNode = null;
|
|
121
|
+
expanded = new Set();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
$effect(() => {
|
|
125
|
+
const id = tab.id;
|
|
126
|
+
const unregister = tabResources.register(id, cleanup);
|
|
127
|
+
return unregister;
|
|
128
|
+
});
|
|
129
|
+
onDestroy(cleanup);
|
|
130
|
+
|
|
113
131
|
function setViewMode(mode: 'inspect' | 'map') {
|
|
114
132
|
viewMode = mode;
|
|
115
133
|
updateUrlView(viewMode);
|
|
@@ -133,7 +151,7 @@ async function loadHierarchy() {
|
|
|
133
151
|
expanded = new Set(['/']);
|
|
134
152
|
}
|
|
135
153
|
} catch (err) {
|
|
136
|
-
error =
|
|
154
|
+
error = handleLoadError(err);
|
|
137
155
|
} finally {
|
|
138
156
|
loading = false;
|
|
139
157
|
updateUrlView(viewMode);
|
|
@@ -182,7 +200,7 @@ function selectStoreAttrs() {
|
|
|
182
200
|
{#if hasChildren}
|
|
183
201
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
184
202
|
<span
|
|
185
|
-
class="flex size-4 shrink-0 items-center justify-center rounded hover:bg-
|
|
203
|
+
class="flex size-4 shrink-0 items-center justify-center rounded hover:bg-accent"
|
|
186
204
|
role="button"
|
|
187
205
|
tabindex="-1"
|
|
188
206
|
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
|
@@ -192,7 +210,7 @@ function selectStoreAttrs() {
|
|
|
192
210
|
}}
|
|
193
211
|
>
|
|
194
212
|
<svg
|
|
195
|
-
class="size-3 text-
|
|
213
|
+
class="size-3 text-muted-foreground transition-transform"
|
|
196
214
|
class:rotate-90={isExpanded}
|
|
197
215
|
viewBox="0 0 16 16"
|
|
198
216
|
fill="currentColor"
|
|
@@ -229,10 +247,8 @@ function selectStoreAttrs() {
|
|
|
229
247
|
<span
|
|
230
248
|
class="truncate"
|
|
231
249
|
class:font-medium={node.kind === 'array'}
|
|
232
|
-
class:text-
|
|
233
|
-
class:
|
|
234
|
-
class:text-zinc-600={node.kind === 'group'}
|
|
235
|
-
class:dark:text-zinc-400={node.kind === 'group'}
|
|
250
|
+
class:text-foreground={node.kind === 'array'}
|
|
251
|
+
class:text-muted-foreground={node.kind === 'group'}
|
|
236
252
|
>
|
|
237
253
|
{node.path === '/' ? '/ (root)' : node.name}
|
|
238
254
|
</span>
|
|
@@ -259,18 +275,18 @@ function selectStoreAttrs() {
|
|
|
259
275
|
{#snippet nodeDetails()}
|
|
260
276
|
{#if showingStoreAttrs && hierarchy}
|
|
261
277
|
<div
|
|
262
|
-
class="shrink-0 border-b border-
|
|
278
|
+
class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
263
279
|
>
|
|
264
280
|
{t('zarr.storeAttributes')}
|
|
265
281
|
</div>
|
|
266
282
|
<div class="flex-1 overflow-auto p-3">
|
|
267
283
|
<div
|
|
268
|
-
class="rounded border border-
|
|
284
|
+
class="rounded border border-border bg-muted p-2 text-xs"
|
|
269
285
|
>
|
|
270
286
|
{#each Object.entries(hierarchy.storeAttrs) as [key, value]}
|
|
271
287
|
<div class="flex gap-2 py-0.5">
|
|
272
288
|
<span class="shrink-0 font-medium text-muted-foreground">{key}:</span>
|
|
273
|
-
<span class="break-all text-
|
|
289
|
+
<span class="break-all text-foreground">
|
|
274
290
|
{typeof value === 'string' ? value : JSON.stringify(value)}
|
|
275
291
|
</span>
|
|
276
292
|
</div>
|
|
@@ -279,7 +295,7 @@ function selectStoreAttrs() {
|
|
|
279
295
|
</div>
|
|
280
296
|
{:else if selectedNode}
|
|
281
297
|
<div
|
|
282
|
-
class="shrink-0 border-b border-
|
|
298
|
+
class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
283
299
|
>
|
|
284
300
|
{selectedNode.path}
|
|
285
301
|
</div>
|
|
@@ -389,12 +405,12 @@ function selectStoreAttrs() {
|
|
|
389
405
|
<dt class="text-muted-foreground">{t('zarr.attributes')}</dt>
|
|
390
406
|
<dd>
|
|
391
407
|
<div
|
|
392
|
-
class="mt-1 rounded border border-
|
|
408
|
+
class="mt-1 rounded border border-border bg-muted p-2"
|
|
393
409
|
>
|
|
394
410
|
{#each Object.entries(selectedNode.attributes) as [key, value]}
|
|
395
411
|
<div class="flex gap-2 py-0.5">
|
|
396
412
|
<span class="shrink-0 font-medium text-muted-foreground">{key}:</span>
|
|
397
|
-
<span class="break-all text-
|
|
413
|
+
<span class="break-all text-foreground">
|
|
398
414
|
{typeof value === 'string' ? value : JSON.stringify(value)}
|
|
399
415
|
</span>
|
|
400
416
|
</div>
|
|
@@ -414,10 +430,10 @@ function selectStoreAttrs() {
|
|
|
414
430
|
|
|
415
431
|
<div class="flex h-full flex-col">
|
|
416
432
|
<!-- Header bar -->
|
|
417
|
-
<div class="shrink-0 border-b border-
|
|
433
|
+
<div class="shrink-0 border-b border-border px-3 py-2 sm:px-4">
|
|
418
434
|
<div class="flex items-center gap-1.5 sm:gap-2">
|
|
419
435
|
<span
|
|
420
|
-
class="max-w-[140px] truncate text-sm font-medium text-
|
|
436
|
+
class="max-w-[140px] truncate text-sm font-medium text-foreground sm:max-w-none"
|
|
421
437
|
>{tab.name}</span
|
|
422
438
|
>
|
|
423
439
|
<Badge
|
|
@@ -459,11 +475,11 @@ function selectStoreAttrs() {
|
|
|
459
475
|
<!-- Content -->
|
|
460
476
|
{#if loading}
|
|
461
477
|
<div class="flex flex-1 items-center justify-center">
|
|
462
|
-
<p class="text-sm text-
|
|
478
|
+
<p class="text-sm text-muted-foreground">{t('zarr.loading')}</p>
|
|
463
479
|
</div>
|
|
464
480
|
{:else if error}
|
|
465
481
|
<div class="flex flex-1 items-center justify-center">
|
|
466
|
-
<p class="max-w-md text-center text-sm text-
|
|
482
|
+
<p class="max-w-md text-center text-sm text-destructive">{error}</p>
|
|
467
483
|
</div>
|
|
468
484
|
{:else if viewMode === 'map' && hasMapVars}
|
|
469
485
|
{#key viewMode}
|
|
@@ -480,56 +496,73 @@ function selectStoreAttrs() {
|
|
|
480
496
|
{/key}
|
|
481
497
|
{:else if hierarchy}
|
|
482
498
|
<!-- Inspect mode (tree + detail panel) -->
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
499
|
+
{#snippet zarrTree()}
|
|
500
|
+
<div class="flex h-full flex-col">
|
|
501
|
+
<div
|
|
502
|
+
class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
503
|
+
>
|
|
504
|
+
{t('zarr.contents')}
|
|
505
|
+
<span class="ms-1 normal-case tracking-normal"
|
|
506
|
+
>({hierarchy!.totalNodes})</span
|
|
489
507
|
>
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
508
|
+
</div>
|
|
509
|
+
<div class="flex-1 overflow-auto">
|
|
510
|
+
{#if hasStoreAttrs}
|
|
511
|
+
<button
|
|
512
|
+
class="flex w-full items-center gap-2 border-b border-zinc-100 px-3 py-1 text-xs hover:bg-zinc-100 dark:border-zinc-800/50 dark:hover:bg-zinc-800/50"
|
|
513
|
+
class:bg-blue-50={showingStoreAttrs}
|
|
514
|
+
class:dark:bg-blue-950={showingStoreAttrs}
|
|
515
|
+
onclick={selectStoreAttrs}
|
|
493
516
|
>
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
class:bg-blue-50={showingStoreAttrs}
|
|
500
|
-
class:dark:bg-blue-950={showingStoreAttrs}
|
|
501
|
-
onclick={selectStoreAttrs}
|
|
517
|
+
<span class="size-4 shrink-0"></span>
|
|
518
|
+
<svg
|
|
519
|
+
class="size-3.5 shrink-0 text-muted-foreground"
|
|
520
|
+
viewBox="0 0 16 16"
|
|
521
|
+
fill="currentColor"
|
|
502
522
|
>
|
|
503
|
-
<
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
<span class="truncate font-medium text-muted-foreground">
|
|
516
|
-
{t('zarr.storeAttributes')}
|
|
517
|
-
</span>
|
|
518
|
-
</button>
|
|
519
|
-
{/if}
|
|
520
|
-
{@render treeNode(hierarchy.root, 0)}
|
|
521
|
-
</div>
|
|
523
|
+
<path
|
|
524
|
+
fill-rule="evenodd"
|
|
525
|
+
d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H3a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-1.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
|
|
526
|
+
clip-rule="evenodd"
|
|
527
|
+
/>
|
|
528
|
+
</svg>
|
|
529
|
+
<span class="truncate font-medium text-muted-foreground">
|
|
530
|
+
{t('zarr.storeAttributes')}
|
|
531
|
+
</span>
|
|
532
|
+
</button>
|
|
533
|
+
{/if}
|
|
534
|
+
{@render treeNode(hierarchy!.root, 0)}
|
|
522
535
|
</div>
|
|
523
|
-
</
|
|
536
|
+
</div>
|
|
537
|
+
{/snippet}
|
|
538
|
+
|
|
539
|
+
{#if isWide.value}
|
|
540
|
+
<ResizablePaneGroup direction="horizontal" class="min-h-0 flex-1">
|
|
541
|
+
<!-- Left: Tree view -->
|
|
542
|
+
<ResizablePane defaultSize={40} minSize={20}>
|
|
543
|
+
{@render zarrTree()}
|
|
544
|
+
</ResizablePane>
|
|
524
545
|
|
|
525
|
-
|
|
546
|
+
<ResizableHandle />
|
|
526
547
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
548
|
+
<!-- Right: Detail panel -->
|
|
549
|
+
<ResizablePane defaultSize={60} minSize={30}>
|
|
550
|
+
<div class="flex h-full flex-col">
|
|
551
|
+
{@render nodeDetails()}
|
|
552
|
+
</div>
|
|
553
|
+
</ResizablePane>
|
|
554
|
+
</ResizablePaneGroup>
|
|
555
|
+
{:else}
|
|
556
|
+
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
|
557
|
+
<!-- Tree pane: fixed height so it doesn't crowd the detail section -->
|
|
558
|
+
<div class="max-h-64 shrink-0 border-b border-border">
|
|
559
|
+
{@render zarrTree()}
|
|
560
|
+
</div>
|
|
561
|
+
<!-- Detail panel: grows to fill remaining space -->
|
|
562
|
+
<div class="flex flex-1 flex-col">
|
|
530
563
|
{@render nodeDetails()}
|
|
531
564
|
</div>
|
|
532
|
-
</
|
|
533
|
-
|
|
565
|
+
</div>
|
|
566
|
+
{/if}
|
|
534
567
|
{/if}
|
|
535
568
|
</div>
|
|
@@ -18,24 +18,24 @@ let {
|
|
|
18
18
|
class="absolute bottom-2 end-2 top-10 z-10 flex w-64 flex-col overflow-hidden rounded bg-card/95 text-card-foreground shadow-lg backdrop-blur-sm sm:w-72"
|
|
19
19
|
>
|
|
20
20
|
<div
|
|
21
|
-
class="flex items-center justify-between border-b border-
|
|
21
|
+
class="flex items-center justify-between border-b border-border px-3 py-2"
|
|
22
22
|
>
|
|
23
|
-
<h3 class="text-xs font-medium text-
|
|
23
|
+
<h3 class="text-xs font-medium text-muted-foreground">Feature Attributes</h3>
|
|
24
24
|
{#if onClose}
|
|
25
25
|
<button
|
|
26
|
-
class="rounded p-0.5 text-
|
|
26
|
+
class="rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
27
27
|
onclick={onClose}
|
|
28
28
|
>
|
|
29
29
|
<XIcon class="size-3.5" />
|
|
30
30
|
</button>
|
|
31
31
|
{/if}
|
|
32
32
|
</div>
|
|
33
|
-
<div class="flex-1 divide-y divide-
|
|
33
|
+
<div class="flex-1 divide-y divide-border overflow-auto">
|
|
34
34
|
{#each Object.entries(feature) as [key, value]}
|
|
35
35
|
<div class="px-3 py-1.5">
|
|
36
|
-
<div class="text-[10px] font-medium text-
|
|
36
|
+
<div class="text-[10px] font-medium text-muted-foreground">{key}</div>
|
|
37
37
|
<div
|
|
38
|
-
class="break-all text-xs text-
|
|
38
|
+
class="break-all text-xs text-foreground"
|
|
39
39
|
title={formatValue(value)}
|
|
40
40
|
>
|
|
41
41
|
{formatValue(value)}
|
|
@@ -162,9 +162,9 @@ onDestroy(() => {
|
|
|
162
162
|
<div bind:this={containerEl} class="h-full w-full" style="touch-action: none;"></div>
|
|
163
163
|
<!-- Zoom level indicator — positioned above nav controls -->
|
|
164
164
|
<div
|
|
165
|
-
class="pointer-events-none absolute bottom-[7rem] right-[10px] z-10 flex size-[29px] items-center justify-center rounded-full border border-
|
|
165
|
+
class="pointer-events-none absolute bottom-[7rem] right-[10px] z-10 flex size-[29px] items-center justify-center rounded-full border border-border bg-background shadow-sm sm:bottom-[10rem]"
|
|
166
166
|
>
|
|
167
|
-
<span class="text-[10px] font-semibold tabular-nums text-
|
|
167
|
+
<span class="text-[10px] font-semibold tabular-nums text-foreground">
|
|
168
168
|
{currentZoom.toFixed(1)}
|
|
169
169
|
</span>
|
|
170
170
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
|
3
|
-
import { formatFileSize } from '@walkthru-earth/objex-utils';
|
|
3
|
+
import { formatFileSize, handleLoadError } from '@walkthru-earth/objex-utils';
|
|
4
4
|
import type { PMTiles } from 'pmtiles';
|
|
5
5
|
import { tileIdToZxy } from 'pmtiles';
|
|
6
6
|
import {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { t } from '../../../i18n/index.svelte.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>
|