@walkthru-earth/objex 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +20 -12
  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 +1 -1
  6. package/dist/components/layout/SettingsSheet.svelte +237 -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 +1 -1
  11. package/dist/components/layout/TabBar.svelte +2 -2
  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 +2 -2
  25. package/dist/components/viewers/CodeViewer.svelte +31 -22
  26. package/dist/components/viewers/CogControls.svelte +338 -184
  27. package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
  28. package/dist/components/viewers/CogViewer.svelte +263 -112
  29. package/dist/components/viewers/CopcViewer.svelte +1 -1
  30. package/dist/components/viewers/FlatGeobufViewer.svelte +1 -1
  31. package/dist/components/viewers/GeoParquetMapViewer.svelte +6 -6
  32. package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
  33. package/dist/components/viewers/ImageViewer.svelte +2 -2
  34. package/dist/components/viewers/MarkdownViewer.svelte +12 -9
  35. package/dist/components/viewers/MediaViewer.svelte +2 -2
  36. package/dist/components/viewers/ModelViewer.svelte +1 -1
  37. package/dist/components/viewers/MultiCogViewer.svelte +467 -102
  38. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
  39. package/dist/components/viewers/NotebookViewer.svelte +6 -3
  40. package/dist/components/viewers/PdfViewer.svelte +2 -2
  41. package/dist/components/viewers/PmtilesViewer.svelte +3 -6
  42. package/dist/components/viewers/RawViewer.svelte +6 -3
  43. package/dist/components/viewers/StacMapViewer.svelte +1 -1
  44. package/dist/components/viewers/StacMosaicViewer.svelte +1760 -408
  45. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
  46. package/dist/components/viewers/StacTabViewer.svelte +24 -13
  47. package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
  48. package/dist/components/viewers/TableGrid.svelte +4 -4
  49. package/dist/components/viewers/TableStatusBar.svelte +1 -1
  50. package/dist/components/viewers/TableToolbar.svelte +1 -1
  51. package/dist/components/viewers/TableViewer.svelte +25 -17
  52. package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
  53. package/dist/components/viewers/ViewerRouter.svelte +16 -8
  54. package/dist/components/viewers/ZarrMapViewer.svelte +11 -9
  55. package/dist/components/viewers/ZarrViewer.svelte +4 -4
  56. package/dist/components/viewers/cog/ChannelPicker.svelte +83 -0
  57. package/dist/components/viewers/cog/ChannelPicker.svelte.d.ts +13 -0
  58. package/dist/components/viewers/cog/PixelInspectorPanel.svelte +87 -0
  59. package/dist/components/viewers/cog/PixelInspectorPanel.svelte.d.ts +17 -0
  60. package/dist/components/viewers/cog/buildRgbLayer.d.ts +78 -0
  61. package/dist/components/viewers/cog/buildRgbLayer.js +176 -0
  62. package/dist/components/viewers/map/AttributeTable.svelte +1 -1
  63. package/dist/components/viewers/map/MapContainer.svelte +37 -11
  64. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +1 -1
  65. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +1 -1
  66. package/dist/components/viewers/stac/StacDatetimeBar.svelte +175 -0
  67. package/dist/components/viewers/stac/StacDatetimeBar.svelte.d.ts +10 -0
  68. package/dist/components/viewers/stac/StacFilterPanel.svelte +243 -0
  69. package/dist/components/viewers/stac/StacFilterPanel.svelte.d.ts +14 -0
  70. package/dist/components/viewers/stac/StacItemInspector.svelte +223 -0
  71. package/dist/components/viewers/stac/StacItemInspector.svelte.d.ts +10 -0
  72. package/dist/components/viewers/stac/StacItemStrip.svelte +228 -0
  73. package/dist/components/viewers/stac/StacItemStrip.svelte.d.ts +12 -0
  74. package/dist/file-icons/index.d.ts +1 -1
  75. package/dist/file-icons/index.js +1 -1
  76. package/dist/i18n/ar.js +110 -2
  77. package/dist/i18n/en.js +110 -2
  78. package/dist/index.d.ts +2 -28
  79. package/dist/index.js +7 -23
  80. package/dist/query/engine.d.ts +10 -0
  81. package/dist/query/source.js +1 -1
  82. package/dist/query/stac-source-factory.d.ts +65 -0
  83. package/dist/query/stac-source-factory.js +77 -0
  84. package/dist/query/stac-source-parquet.d.ts +135 -0
  85. package/dist/query/stac-source-parquet.js +465 -0
  86. package/dist/query/wasm.d.ts +8 -0
  87. package/dist/query/wasm.js +304 -2
  88. package/dist/storage/presign.js +1 -1
  89. package/dist/storage/providers.js +5 -5
  90. package/dist/stores/config.svelte.d.ts +15 -0
  91. package/dist/stores/config.svelte.js +46 -0
  92. package/dist/stores/connections.svelte.d.ts +2 -2
  93. package/dist/stores/connections.svelte.js +1 -2
  94. package/dist/stores/files.svelte.d.ts +1 -1
  95. package/dist/stores/files.svelte.js +1 -1
  96. package/dist/stores/query-history.svelte.js +1 -1
  97. package/dist/stores/settings.svelte.d.ts +16 -1
  98. package/dist/stores/settings.svelte.js +104 -48
  99. package/dist/stores/tabs.svelte.d.ts +3 -0
  100. package/dist/stores/tabs.svelte.js +17 -0
  101. package/dist/utils/cog-histogram.d.ts +121 -0
  102. package/dist/utils/cog-histogram.js +424 -0
  103. package/dist/utils/cog.d.ts +177 -20
  104. package/dist/utils/cog.js +361 -76
  105. package/dist/utils/colormap-sprite.d.ts +0 -9
  106. package/dist/utils/colormap-sprite.js +0 -21
  107. package/dist/utils/deck.d.ts +16 -12
  108. package/dist/utils/deck.js +10 -4
  109. package/dist/utils/pmtiles-tile.js +2 -2
  110. package/dist/utils/{url.d.ts → signed-url.d.ts} +15 -1
  111. package/dist/utils/{url.js → signed-url.js} +32 -10
  112. package/dist/utils/url-state.d.ts +36 -0
  113. package/dist/utils/url-state.js +72 -2
  114. package/dist/utils/zarr-tab.d.ts +1 -2
  115. package/dist/utils/zarr-tab.js +1 -2
  116. package/dist/utils/zarr.d.ts +0 -17
  117. package/dist/utils/zarr.js +1 -45
  118. package/package.json +55 -84
  119. package/dist/components/browser/Breadcrumb.svelte +0 -50
  120. package/dist/components/browser/Breadcrumb.svelte.d.ts +0 -7
  121. package/dist/components/browser/CreateFolderDialog.svelte +0 -98
  122. package/dist/components/browser/CreateFolderDialog.svelte.d.ts +0 -6
  123. package/dist/components/browser/DeleteConfirmDialog.svelte +0 -90
  124. package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +0 -8
  125. package/dist/components/browser/DropZone.svelte +0 -83
  126. package/dist/components/browser/DropZone.svelte.d.ts +0 -7
  127. package/dist/components/browser/FileBrowser.svelte +0 -252
  128. package/dist/components/browser/FileBrowser.svelte.d.ts +0 -3
  129. package/dist/components/browser/FileRow.svelte +0 -117
  130. package/dist/components/browser/FileRow.svelte.d.ts +0 -9
  131. package/dist/components/browser/RenameDialog.svelte +0 -101
  132. package/dist/components/browser/RenameDialog.svelte.d.ts +0 -8
  133. package/dist/components/browser/SearchBar.svelte +0 -40
  134. package/dist/components/browser/SearchBar.svelte.d.ts +0 -6
  135. package/dist/components/browser/UploadButton.svelte +0 -65
  136. package/dist/components/browser/UploadButton.svelte.d.ts +0 -3
  137. package/dist/query/stac-geoparquet.d.ts +0 -31
  138. package/dist/query/stac-geoparquet.js +0 -136
  139. package/dist/utils/clipboard.d.ts +0 -13
  140. package/dist/utils/clipboard.js +0 -38
  141. package/dist/utils/cloud-url.d.ts +0 -27
  142. package/dist/utils/cloud-url.js +0 -61
  143. package/dist/utils/cog-pure.d.ts +0 -25
  144. package/dist/utils/cog-pure.js +0 -35
  145. package/dist/utils/column-types.d.ts +0 -5
  146. package/dist/utils/column-types.js +0 -137
  147. package/dist/utils/connection-identity.d.ts +0 -51
  148. package/dist/utils/connection-identity.js +0 -97
  149. package/dist/utils/error.d.ts +0 -8
  150. package/dist/utils/error.js +0 -12
  151. package/dist/utils/evidence-context.d.ts +0 -22
  152. package/dist/utils/evidence-context.js +0 -56
  153. package/dist/utils/export.d.ts +0 -22
  154. package/dist/utils/export.js +0 -76
  155. package/dist/utils/file-sort.d.ts +0 -20
  156. package/dist/utils/file-sort.js +0 -41
  157. package/dist/utils/format.d.ts +0 -24
  158. package/dist/utils/format.js +0 -78
  159. package/dist/utils/geoarrow.d.ts +0 -32
  160. package/dist/utils/geoarrow.js +0 -672
  161. package/dist/utils/geometry-type.d.ts +0 -52
  162. package/dist/utils/geometry-type.js +0 -76
  163. package/dist/utils/hex.d.ts +0 -10
  164. package/dist/utils/hex.js +0 -27
  165. package/dist/utils/host-detection.d.ts +0 -23
  166. package/dist/utils/host-detection.js +0 -95
  167. package/dist/utils/local-storage.d.ts +0 -16
  168. package/dist/utils/local-storage.js +0 -37
  169. package/dist/utils/markdown-sql.d.ts +0 -30
  170. package/dist/utils/markdown-sql.js +0 -72
  171. package/dist/utils/notebook.d.ts +0 -59
  172. package/dist/utils/notebook.js +0 -211
  173. package/dist/utils/parquet-metadata.d.ts +0 -64
  174. package/dist/utils/parquet-metadata.js +0 -262
  175. package/dist/utils/stac-geoparquet.d.ts +0 -90
  176. package/dist/utils/stac-geoparquet.js +0 -223
  177. package/dist/utils/stac-hydrate.d.ts +0 -38
  178. package/dist/utils/stac-hydrate.js +0 -243
  179. package/dist/utils/stac.d.ts +0 -136
  180. package/dist/utils/stac.js +0 -176
  181. package/dist/utils/storage-url.d.ts +0 -90
  182. package/dist/utils/storage-url.js +0 -568
  183. package/dist/utils/wkb.d.ts +0 -43
  184. package/dist/utils/wkb.js +0 -359
@@ -1,5 +1,5 @@
1
+ import { type StacRoutableKind } from '@walkthru-earth/objex-utils';
1
2
  import type { Tab } from '../../types.js';
2
- import { type StacRoutableKind } from '../../utils/stac.js';
3
3
  type $$ComponentProps = {
4
4
  tab: Tab;
5
5
  classified?: StacRoutableKind;
@@ -3,12 +3,12 @@ import CodeIcon from '@lucide/svelte/icons/file-code';
3
3
  import GlobeIcon from '@lucide/svelte/icons/globe';
4
4
  import LayersIcon from '@lucide/svelte/icons/layers';
5
5
  import MapIcon from '@lucide/svelte/icons/map';
6
+ import type { StacRoutableKind } from '@walkthru-earth/objex-utils';
6
7
  import { t } from '../../i18n/index.svelte.js';
7
8
  import { connectionStore } from '../../stores/connections.svelte.js';
8
9
  import type { Tab } from '../../types.js';
9
- import type { StacRoutableKind } from '../../utils/stac.js';
10
- import { canStreamDirectly } from '../../utils/url.js';
11
- import { getUrlView, updateUrlView } from '../../utils/url-state.js';
10
+ import { canStreamDirectly } from '../../utils/signed-url.js';
11
+ import { getUrlView, pickViewMode, updateUrlView } from '../../utils/url-state.js';
12
12
  import { Badge } from '../ui/badge/index.js';
13
13
  import { Button } from '../ui/button/index.js';
14
14
  import * as Tooltip from '../ui/tooltip/index.js';
@@ -28,7 +28,11 @@ interface Props {
28
28
 
29
29
  let { tab, mapKind, classified }: Props = $props();
30
30
 
31
- type ViewMode = 'map' | 'stac-map' | 'stac-browser' | 'code';
31
+ // 'code' is the URL token for raw-content view on JSON tabs; 'table' is the
32
+ // equivalent token on stac-geoparquet tabs. Both render the nested viewer
33
+ // (CodeViewer for JSON, TableViewer for parquet) but the URL stays
34
+ // semantically meaningful per filetype.
35
+ type ViewMode = 'map' | 'stac-map' | 'stac-browser' | 'code' | 'table';
32
36
 
33
37
  interface CodeActions {
34
38
  toggleFormat: () => Promise<void>;
@@ -70,13 +74,16 @@ const stacBadgeKey = $derived.by(() => {
70
74
  });
71
75
 
72
76
  function initialView(): ViewMode {
77
+ // `'map'` is conditional on `mapKind`, so it's not in the static vocabulary.
78
+ // Both `'code'` and `'table'` are accepted regardless of filetype so a URL
79
+ // shared from one type still resolves; the render branch dispatches to the
80
+ // appropriate inner viewer (`TableViewer` / `CodeViewer`).
81
+ const picked = pickViewMode<ViewMode>(['stac-map', 'stac-browser', 'code', 'table'], 'stac-map');
73
82
  const urlView = getUrlView();
74
83
  if (urlView === 'map' && mapKind) return 'map';
75
- if (urlView === 'stac-map') return 'stac-map';
76
- if (urlView === 'stac-browser') return 'stac-browser';
77
- if (urlView === 'code') return 'code';
78
- if (mapKind) return 'map';
79
- return 'stac-map';
84
+ // Hash was unknown or empty: prefer the rich map view when available.
85
+ if (mapKind && !urlView) return 'map';
86
+ return picked;
80
87
  }
81
88
 
82
89
  let viewMode = $state<ViewMode>(initialView());
@@ -86,8 +93,12 @@ let codeActions = $state<CodeActions | null>(null);
86
93
  function setView(next: ViewMode) {
87
94
  if (viewMode === next) return;
88
95
  viewMode = next;
89
- updateUrlView(next === 'map' ? 'map' : next);
96
+ updateUrlView(next);
90
97
  }
98
+
99
+ // The "Table" / "JSON" button in the navbar writes a filetype-aware token so
100
+ // the URL stays semantically meaningful (parquet → `#table`, json → `#code`).
101
+ const rawContentMode: ViewMode = $derived(isParquet ? 'table' : 'code');
91
102
  </script>
92
103
 
93
104
  <Tooltip.Provider>
@@ -191,9 +202,9 @@ function setView(next: ViewMode) {
191
202
  {/if}
192
203
  <Button
193
204
  size="sm"
194
- variant={viewMode === 'code' ? 'default' : 'ghost'}
205
+ variant={viewMode === rawContentMode ? 'default' : 'ghost'}
195
206
  class="h-7 gap-1 px-2"
196
- onclick={() => setView('code')}
207
+ onclick={() => setView(rawContentMode)}
197
208
  >
198
209
  <CodeIcon class="size-3.5" />
199
210
  {isParquet ? t('stac.viewTable') : t('stac.viewJson')}
@@ -244,7 +255,7 @@ function setView(next: ViewMode) {
244
255
  {:else if viewMode === 'stac-browser'}
245
256
  <StacMapViewer {tab} variant="stac-browser" />
246
257
  {:else if isParquet}
247
- <TableViewer {tab} />
258
+ <TableViewer {tab} nested />
248
259
  {:else}
249
260
  <CodeViewer {tab} nested bind:wordWrap bind:actions={codeActions} />
250
261
  {/if}
@@ -1,5 +1,5 @@
1
+ import type { StacRoutableKind } from '@walkthru-earth/objex-utils';
1
2
  import type { Tab } from '../../types.js';
2
- import type { StacRoutableKind } from '../../utils/stac.js';
3
3
  type MapKind = 'mosaic' | 'multicog' | null;
4
4
  interface Props {
5
5
  tab: Tab;
@@ -7,15 +7,15 @@ import ColumnsIcon from '@lucide/svelte/icons/columns-3';
7
7
  import CopyIcon from '@lucide/svelte/icons/copy';
8
8
  import RowsIcon from '@lucide/svelte/icons/rows-3';
9
9
  import XIcon from '@lucide/svelte/icons/x';
10
- import { onDestroy } from 'svelte';
11
- import { t } from '../../i18n/index.svelte.js';
12
10
  import {
13
11
  classifyType,
12
+ jsonReplacerBigInt,
14
13
  type TypeCategory,
15
14
  typeBadgeClass,
16
15
  typeLabel
17
- } from '../../utils/column-types.js';
18
- import { jsonReplacerBigInt } from '../../utils/format.js';
16
+ } from '@walkthru-earth/objex-utils';
17
+ import { onDestroy } from 'svelte';
18
+ import { t } from '../../i18n/index.svelte.js';
19
19
 
20
20
  const INITIAL_ROWS = 100;
21
21
  const BATCH_SIZE = 100;
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
2
  import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
3
3
  import DownloadIcon from '@lucide/svelte/icons/download';
4
+ import { exportToCsv, exportToJson } from '@walkthru-earth/objex-utils';
4
5
  import { t } from '../../i18n/index.svelte.js';
5
- import { exportToCsv, exportToJson } from '../../utils/export.js';
6
6
 
7
7
  let {
8
8
  rowCount = 0,
@@ -16,7 +16,7 @@ import { Separator } from '../ui/separator/index.js';
16
16
  import { t } from '../../i18n/index.svelte.js';
17
17
  import { connections } from '../../stores/connections.svelte.js';
18
18
  import type { Tab } from '../../types';
19
- import { buildHttpsUrl, buildStorageUrl } from '../../utils/url.js';
19
+ import { buildHttpsUrl, buildStorageUrl } from '../../utils/signed-url.js';
20
20
 
21
21
  const PROVIDER_LABELS: Record<string, string> = {
22
22
  s3: 'S3',
@@ -1,4 +1,17 @@
1
1
  <script lang="ts">
2
+ import type { GeoArrowGeomType } from '@walkthru-earth/objex-utils';
3
+ import {
4
+ buildTransformExpr,
5
+ extractBounds,
6
+ extractEpsgFromGeoMeta,
7
+ extractGeometryTypes,
8
+ findGeoColumn,
9
+ findGeoColumnFromRows,
10
+ parseWKB,
11
+ readParquetMetadata,
12
+ toBinary,
13
+ wrapWkbWithCrs
14
+ } from '@walkthru-earth/objex-utils';
2
15
  import { format as formatSql } from 'sql-formatter';
3
16
  import { untrack } from 'svelte';
4
17
  import CodeMirrorEditor from '../editor/CodeMirrorEditor.svelte';
@@ -18,22 +31,13 @@ import { queryHistory } from '../../stores/query-history.svelte.js';
18
31
  import { settings } from '../../stores/settings.svelte.js';
19
32
  import { tabResources } from '../../stores/tab-resources.svelte.js';
20
33
  import type { Tab } from '../../types';
21
- import type { GeoArrowGeomType } from '../../utils/geoarrow.js';
22
- import { buildTransformExpr, wrapWkbWithCrs } from '../../utils/geometry-type.js';
23
- import {
24
- extractBounds,
25
- extractEpsgFromGeoMeta,
26
- extractGeometryTypes,
27
- readParquetMetadata
28
- } from '../../utils/parquet-metadata.js';
29
34
  import {
30
35
  buildDuckDbUrl,
31
36
  buildHttpsUrl,
32
37
  buildStorageUrl,
33
38
  canStreamDirectly
34
- } from '../../utils/url.js';
35
- import { getUrlView, updateUrlView } from '../../utils/url-state.js';
36
- import { findGeoColumn, findGeoColumnFromRows, parseWKB, toBinary } from '../../utils/wkb.js';
39
+ } from '../../utils/signed-url.js';
40
+ import { pickViewMode, updateUrlView } from '../../utils/url-state.js';
37
41
  import FileInfo from './FileInfo.svelte';
38
42
  import LoadProgress, { type ProgressEntry } from './LoadProgress.svelte';
39
43
  import QueryHistoryPanel from './QueryHistoryPanel.svelte';
@@ -41,7 +45,11 @@ import TableGrid from './TableGrid.svelte';
41
45
  import TableStatusBar from './TableStatusBar.svelte';
42
46
  import TableToolbar from './TableToolbar.svelte';
43
47
 
44
- let { tab }: { tab: Tab } = $props();
48
+ // `nested = true` when mounted inside `StacTabViewer` for a stac-geoparquet
49
+ // tab. The outer wrapper already exposes `stac-map` / `STAC Browser` buttons,
50
+ // so the inner `TableToolbar` hides its own STAC Map toggle to avoid
51
+ // duplicating the same `StacMapViewer` mount.
52
+ let { tab, nested = false }: { tab: Tab; nested?: boolean } = $props();
45
53
 
46
54
  let pageSize = $state(settings.featureLimit);
47
55
 
@@ -56,9 +64,9 @@ let historyVisible = $state(false);
56
64
  let hasGeo = $state(false);
57
65
  let isStac = $state(false);
58
66
  // Restore view mode from URL hash if present
59
- const urlView = getUrlView();
60
- let viewMode = $state<'table' | 'map' | 'stac' | 'info'>(
61
- urlView === 'map' ? 'map' : urlView === 'stac' ? 'stac' : urlView === 'info' ? 'info' : 'table'
67
+ type TableViewMode = 'table' | 'map' | 'stac' | 'info';
68
+ let viewMode = $state<TableViewMode>(
69
+ pickViewMode<TableViewMode>(['table', 'map', 'stac', 'info'], 'table')
62
70
  );
63
71
  let sqlQuery = $state('');
64
72
  let customSql = $state('');
@@ -908,7 +916,7 @@ function setStacView() {
908
916
  {pageSize}
909
917
  {historyVisible}
910
918
  {hasGeo}
911
- {isStac}
919
+ isStac={isStac && !nested}
912
920
  {viewMode}
913
921
  onPrevPage={prevPage}
914
922
  onNextPage={nextPage}
@@ -916,7 +924,7 @@ function setStacView() {
916
924
  onToggleInfo={toggleInfo}
917
925
  onToggleHistory={toggleHistory}
918
926
  onToggleView={toggleView}
919
- onToggleStac={setStacView}
927
+ onToggleStac={nested ? undefined : setStacView}
920
928
  onPageSizeChange={handlePageSizeChange}
921
929
  />
922
930
 
@@ -1,6 +1,7 @@
1
1
  import type { Tab } from '../../types';
2
2
  type $$ComponentProps = {
3
3
  tab: Tab;
4
+ nested?: boolean;
4
5
  };
5
6
  declare const TableViewer: import("svelte").Component<$$ComponentProps, {}, "">;
6
7
  type TableViewer = ReturnType<typeof TableViewer>;
@@ -1,17 +1,17 @@
1
1
  <script lang="ts">
2
- import { getViewerKind } from '../../file-icons/index.js';
3
- import { getAdapter } from '../../storage/index.js';
4
- import type { Tab } from '../../types.js';
5
- import { readParquetMetadata } from '../../utils/parquet-metadata.js';
6
2
  import {
7
3
  classifyStac,
8
4
  detectMosaicCapable,
9
5
  detectMultiCogCapable,
6
+ isStacGeoparquetSchema,
7
+ readParquetMetadata,
8
+ STAC_API_PATH_RE,
10
9
  type StacRoutableKind
11
- } from '../../utils/stac.js';
12
- import { isStacGeoparquetSchema } from '../../utils/stac-geoparquet.js';
13
- import { STAC_API_PATH_RE } from '../../utils/storage-url.js';
14
- import { buildHttpsUrlAsync } from '../../utils/url.js';
10
+ } from '@walkthru-earth/objex-utils';
11
+ import { getViewerKind } from '../../file-icons/index.js';
12
+ import { getAdapter } from '../../storage/index.js';
13
+ import type { Tab } from '../../types.js';
14
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
15
15
  import CodeViewer from './CodeViewer.svelte';
16
16
  import ImageViewer from './ImageViewer.svelte';
17
17
  import MediaViewer from './MediaViewer.svelte';
@@ -164,6 +164,14 @@ function pickMapKind(classified: StacRoutableKind): 'mosaic' | 'multicog' | null
164
164
 
165
165
  {#if stacRoute.kind === 'stac' && viewerKind === 'table'}
166
166
  <StacTabViewer {tab} mapKind={stacRoute.mapKind} classified={stacRoute.classified} />
167
+ {:else if stacRoute.kind === 'pending' && (viewerKind === 'table' || viewerKind === 'code' || viewerKind === 'raw')}
168
+ <!-- STAC detection (sniff parquet schema or peek 256KB JSON) is in flight.
169
+ Mounting TableViewer / CodeViewer here would let them read the URL hash,
170
+ pick a default viewMode, and potentially write back over an explicit
171
+ hash that StacTabViewer would otherwise own (e.g. `#map` on a STAC
172
+ collection JSON). The pending window is short — render an empty pane
173
+ until detection resolves and the right viewer takes over. -->
174
+ <div class="h-full"></div>
167
175
  {:else if viewerKind === 'table'}
168
176
  <TableViewer {tab} />
169
177
  {:else if viewerKind === 'image'}
@@ -7,7 +7,7 @@ import { t } from '../../i18n/index.svelte.js';
7
7
  import { tabResources } from '../../stores/tab-resources.svelte.js';
8
8
  import type { Tab } from '../../types.js';
9
9
  import { createEpsgResolver } from '../../utils/cog.js';
10
- import { buildHttpsUrlAsync } from '../../utils/url.js';
10
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
11
11
  import {
12
12
  detectGeoZarr,
13
13
  ensureCodecsRegistered,
@@ -17,6 +17,7 @@ import {
17
17
  type ZarrHierarchy,
18
18
  type ZarrNode
19
19
  } from '../../utils/zarr.js';
20
+ import { Slider } from '../ui/slider/index.js';
20
21
  import MapContainer from './map/MapContainer.svelte';
21
22
 
22
23
  /** Enriched selector dimension with coordinate metadata. */
@@ -526,7 +527,7 @@ async function tryAddGeoZarrLayer(
526
527
  const zarrInfoSnapshot = $state.snapshot(geoZarrInfo) as GeoZarrInfo;
527
528
  const layer = new ZarrLayer({
528
529
  id: `geozarr-${tab.id}`,
529
- source: group,
530
+ node: group,
530
531
  variable: zarrInfoSnapshot.variantPath || undefined,
531
532
  selection: {},
532
533
  epsgResolver: dsZarrEpsg,
@@ -661,16 +662,17 @@ onDestroy(cleanup);
661
662
  title={dimLabel(dim)}
662
663
  >
663
664
  <span class="shrink-0 font-medium text-zinc-500 dark:text-zinc-400">{dim.name}</span>
664
- <input
665
- type="range"
666
- min="0"
665
+ <Slider
666
+ type="single"
667
+ min={0}
667
668
  max={dim.size - 1}
669
+ step={1}
668
670
  value={selectorValues[dim.name] ?? 0}
669
- oninput={(e) => {
670
- selectorValues[dim.name] = +e.currentTarget.value;
671
+ onValueChange={(v) => {
672
+ selectorValues[dim.name] = v as number;
671
673
  }}
672
- onchange={updateSelector}
673
- class="h-1 w-16"
674
+ onValueCommit={() => updateSelector()}
675
+ class="w-20"
674
676
  />
675
677
  {#if dim.isDatetime && dim.minDate && dim.maxDate}
676
678
  {@const dateVal = indexToDateStr(selectorValues[dim.name] ?? 0, dim)}
@@ -9,8 +9,8 @@ import {
9
9
  } from '../ui/resizable/index.js';
10
10
  import { t } from '../../i18n/index.svelte.js';
11
11
  import type { Tab } from '../../types';
12
- import { buildHttpsUrlAsync } from '../../utils/url.js';
13
- import { getUrlView, updateUrlView } from '../../utils/url-state.js';
12
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
13
+ import { pickViewMode, updateUrlView } from '../../utils/url-state.js';
14
14
  import {
15
15
  computeChunkCount,
16
16
  computeChunkSize,
@@ -30,8 +30,8 @@ let { tab }: { tab: Tab } = $props();
30
30
 
31
31
  let loading = $state(true);
32
32
  let error = $state<string | null>(null);
33
- const urlView = getUrlView();
34
- let viewMode = $state<'inspect' | 'map'>(urlView === 'map' ? 'map' : 'inspect');
33
+ type ZarrViewMode = 'inspect' | 'map';
34
+ let viewMode = $state<ZarrViewMode>(pickViewMode<ZarrViewMode>(['inspect', 'map'], 'inspect'));
35
35
 
36
36
  let hierarchy = $state.raw<ZarrHierarchy | null>(null);
37
37
  let selectedNode = $state<ZarrNode | null>(null);
@@ -0,0 +1,83 @@
1
+ <script lang="ts">
2
+ import type { ChannelRef, CogAsset } from '@walkthru-earth/objex-utils';
3
+ import { t } from '../../../i18n/index.svelte.js';
4
+
5
+ type Props = {
6
+ channel: 'r' | 'g' | 'b' | 'a';
7
+ label: string;
8
+ colorClass: string;
9
+ assets: CogAsset[];
10
+ value: ChannelRef;
11
+ onChange: (next: ChannelRef) => void;
12
+ allowNone?: boolean;
13
+ };
14
+
15
+ let { channel, label, colorClass, assets, value, onChange, allowNone = false }: Props = $props();
16
+
17
+ const assetByKey = $derived(new Map(assets.map((a) => [a.key, a])));
18
+ const currentAsset = $derived(assetByKey.get(value.assetKey) ?? null);
19
+ const bandCount = $derived(currentAsset?.bandCount ?? 1);
20
+ const bandIndices = $derived(Array.from({ length: bandCount }, (_, i) => i));
21
+
22
+ function assetLabel(a: CogAsset): string {
23
+ const cn = a.eoCommon[0];
24
+ const base = cn ? `${a.key} (${cn})` : a.key;
25
+ return a.bandCount > 1 ? `${base} · ${a.bandCount} bands` : base;
26
+ }
27
+
28
+ function bandLabel(i: number, asset: CogAsset | null): string {
29
+ if (!asset) return `${t('cog.band')} ${i + 1}`;
30
+ const cn = asset.eoCommon[i];
31
+ return cn ? `${t('cog.band')} ${i + 1} (${cn})` : `${t('cog.band')} ${i + 1}`;
32
+ }
33
+
34
+ function setAsset(key: string): void {
35
+ if (channel === 'a' && allowNone && key === '') {
36
+ onChange({ assetKey: '', bandIndex: 0 });
37
+ return;
38
+ }
39
+ const target = assetByKey.get(key);
40
+ const maxIdx = Math.max(0, (target?.bandCount ?? 1) - 1);
41
+ const nextBand = Math.min(value.bandIndex, maxIdx);
42
+ onChange({ assetKey: key, bandIndex: nextBand });
43
+ }
44
+
45
+ function setBand(idx: number): void {
46
+ onChange({ assetKey: value.assetKey, bandIndex: idx });
47
+ }
48
+ </script>
49
+
50
+ <div class="flex items-start gap-2">
51
+ <span class="mt-1 w-3 shrink-0 font-bold {colorClass}">{label}</span>
52
+ <div class="flex min-w-0 flex-1 flex-col gap-1 sm:flex-row sm:items-center">
53
+ <select
54
+ class="min-w-0 flex-1 truncate rounded border border-border bg-background px-1.5 py-0.5 text-xs"
55
+ aria-label={`${label} ${t('cog.asset')}`}
56
+ value={value.assetKey}
57
+ onchange={(e) => setAsset((e.target as HTMLSelectElement).value)}
58
+ >
59
+ {#if allowNone}
60
+ <option value="">{t('map.multiCogChannelNone')}</option>
61
+ {/if}
62
+ {#each assets as a (a.key)}
63
+ <option value={a.key}>{assetLabel(a)}</option>
64
+ {/each}
65
+ </select>
66
+ {#if currentAsset && bandCount > 1}
67
+ <select
68
+ class="min-w-0 flex-1 truncate rounded border border-border bg-background px-1.5 py-0.5 text-xs sm:flex-[0_1_auto] sm:min-w-24"
69
+ aria-label={`${label} ${t('cog.band')}`}
70
+ value={value.bandIndex}
71
+ onchange={(e) => setBand(Number((e.target as HTMLSelectElement).value))}
72
+ >
73
+ {#each bandIndices as i (i)}
74
+ <option value={i}>{bandLabel(i, currentAsset)}</option>
75
+ {/each}
76
+ </select>
77
+ {:else if currentAsset}
78
+ <span class="min-w-0 truncate px-1.5 py-0.5 text-[10px] text-muted-foreground sm:min-w-24">
79
+ {t('cog.band')} 1
80
+ </span>
81
+ {/if}
82
+ </div>
83
+ </div>
@@ -0,0 +1,13 @@
1
+ import type { ChannelRef, CogAsset } from '@walkthru-earth/objex-utils';
2
+ type Props = {
3
+ channel: 'r' | 'g' | 'b' | 'a';
4
+ label: string;
5
+ colorClass: string;
6
+ assets: CogAsset[];
7
+ value: ChannelRef;
8
+ onChange: (next: ChannelRef) => void;
9
+ allowNone?: boolean;
10
+ };
11
+ declare const ChannelPicker: import("svelte").Component<Props, {}, "">;
12
+ type ChannelPicker = ReturnType<typeof ChannelPicker>;
13
+ export default ChannelPicker;
@@ -0,0 +1,87 @@
1
+ <script lang="ts" module>
2
+ export type PixelInspectorRow = {
3
+ label: string;
4
+ sublabel?: string;
5
+ value: number | null;
6
+ };
7
+ </script>
8
+
9
+ <script lang="ts">
10
+ import { t } from '../../../i18n/index.svelte.js';
11
+
12
+ let {
13
+ lng,
14
+ lat,
15
+ rows,
16
+ footnote,
17
+ extraLine,
18
+ onClose,
19
+ inspecting = false
20
+ }: {
21
+ lng: number | null;
22
+ lat: number | null;
23
+ rows: PixelInspectorRow[] | null;
24
+ footnote?: string;
25
+ extraLine?: string;
26
+ onClose: () => void;
27
+ inspecting?: boolean;
28
+ } = $props();
29
+
30
+ // The panel renders whenever we have a coordinate + rows. The "reading" pill
31
+ // renders independently while inspecting is true. Both blocks can render at
32
+ // once during a follow-up click, this matches the existing per-viewer UX.
33
+ const showPanel = $derived(rows !== null && lng !== null && lat !== null);
34
+ const showReading = $derived(inspecting);
35
+
36
+ function formatValue(v: number | null): string {
37
+ if (v === null) return '-';
38
+ return Number.isInteger(v) ? String(v) : v.toFixed(4);
39
+ }
40
+ </script>
41
+
42
+ {#if showPanel && rows && lng !== null && lat !== null}
43
+ <div
44
+ class="absolute bottom-2 left-2 z-10 max-w-[calc(100vw-1rem)] rounded bg-card/90 p-2.5 text-xs text-card-foreground backdrop-blur-sm sm:max-w-none"
45
+ >
46
+ <div class="mb-1 flex items-center justify-between gap-3">
47
+ <span class="font-medium">{t('cog.pixelValue')}</span>
48
+ <button
49
+ class="inline-flex min-h-8 min-w-8 items-center justify-center text-base text-muted-foreground hover:text-card-foreground sm:min-h-0 sm:min-w-0 sm:text-xs"
50
+ style="touch-action: manipulation;"
51
+ onclick={onClose}
52
+ aria-label={t('stac.close')}
53
+ >
54
+ &times;
55
+ </button>
56
+ </div>
57
+ <div class="space-y-0.5 text-muted-foreground">
58
+ <div>{lat.toFixed(6)}&deg;, {lng.toFixed(6)}&deg;</div>
59
+ {#if footnote}
60
+ <div class="text-[10px]">{footnote}</div>
61
+ {/if}
62
+ {#if extraLine}
63
+ <div class="truncate text-[10px]" title={extraLine}>{extraLine}</div>
64
+ {/if}
65
+ </div>
66
+ <div class="mt-1.5 space-y-0.5">
67
+ {#each rows as row}
68
+ <div class="flex justify-between gap-2">
69
+ <span class="text-muted-foreground">
70
+ {row.label}{#if row.sublabel}
71
+ <span class="ml-1 text-[10px] opacity-70">({row.sublabel})</span>
72
+ {/if}
73
+ </span>
74
+ <span class="font-mono tabular-nums">{formatValue(row.value)}</span>
75
+ </div>
76
+ {/each}
77
+ </div>
78
+ </div>
79
+ {/if}
80
+
81
+ {#if showReading}
82
+ <div
83
+ class="pointer-events-none absolute bottom-2 left-2 z-10 rounded bg-card/80 px-2 py-1 text-xs text-card-foreground backdrop-blur-sm"
84
+ >
85
+ {t('cog.reading')}
86
+ </div>
87
+ {/if}
@@ -0,0 +1,17 @@
1
+ export type PixelInspectorRow = {
2
+ label: string;
3
+ sublabel?: string;
4
+ value: number | null;
5
+ };
6
+ type $$ComponentProps = {
7
+ lng: number | null;
8
+ lat: number | null;
9
+ rows: PixelInspectorRow[] | null;
10
+ footnote?: string;
11
+ extraLine?: string;
12
+ onClose: () => void;
13
+ inspecting?: boolean;
14
+ };
15
+ declare const PixelInspectorPanel: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type PixelInspectorPanel = ReturnType<typeof PixelInspectorPanel>;
17
+ export default PixelInspectorPanel;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Layer-construction dispatch for the unified RGB picker.
3
+ *
4
+ * Decision rule:
5
+ * - All three RGB channels point to the SAME asset → COGLayer. When a
6
+ * `preflightGeotiff` is supplied, the per-channel `bandIndex` values are
7
+ * translated into a `BandConfig` and run through `selectCogPipeline`,
8
+ * which returns a custom `getTileData` / `renderTile` pair that swaps
9
+ * bands as requested (the library's COGLayer does not accept a
10
+ * `bandConfig` prop, only the resolved pipeline). Without a preflight
11
+ * GeoTIFF the layer falls back to the library default pipeline, which
12
+ * reads bands 0/1/2 in that order, correct for single-band per-asset
13
+ * COGs and for the default natural-color order on pre-baked multi-band
14
+ * visuals.
15
+ * - Channels point to DIFFERENT assets → MultiCOGLayer with the legacy
16
+ * `composite: { r, g, b }` keyed on asset keys. MultiCOGLayer reads band 0
17
+ * of each source, per-channel band index is silently ignored on this path
18
+ * (library limitation, see spec Known Limitations).
19
+ *
20
+ * `buildRgbLayer` ONLY constructs the layer. It does not add overlays,
21
+ * register cleanup, or touch deck.gl state. Caller owns lifecycle.
22
+ */
23
+ import { COGLayer, MultiCOGLayer } from '@developmentseed/deck.gl-geotiff';
24
+ import type { DecoderPool, GeoTIFF as GeoTIFFType } from '@developmentseed/geotiff';
25
+ import type { EpsgResolver } from '@developmentseed/proj';
26
+ import { type ChannelComposite, type CogAsset } from '@walkthru-earth/objex-utils';
27
+ import { type GeoBounds, type RescaleConfig } from '../../../utils/cog.js';
28
+ export type RgbLayerKind = 'cog' | 'multicog';
29
+ export interface BuildRgbLayerOptions {
30
+ id: string;
31
+ assets: CogAsset[];
32
+ composite: ChannelComposite;
33
+ rescale: RescaleConfig;
34
+ /** href → presigned-or-passthrough URL. */
35
+ resolveHref: (href: string) => Promise<string>;
36
+ pool?: DecoderPool | null;
37
+ epsgResolver: EpsgResolver;
38
+ signal: AbortSignal;
39
+ onLoad?: (info: {
40
+ kind: RgbLayerKind;
41
+ bounds?: GeoBounds;
42
+ }) => void;
43
+ /**
44
+ * Pre-opened GeoTIFF for the single-asset path. When provided, the per-channel
45
+ * `bandIndex` values from the composite are honored via `selectCogPipeline`,
46
+ * which inspects the COG's sample format / band count and returns a custom
47
+ * `getTileData` + `renderTile` pair that swaps bands as requested.
48
+ *
49
+ * When omitted, the layer falls back to the library default render pipeline,
50
+ * which always reads bands 0/1/2 in that order. That is fine for:
51
+ * - single-band per-asset COGs (Sentinel-2, Landsat per-band) where every
52
+ * `bandIndex` is 0 anyway, OR
53
+ * - pre-baked multi-band visuals (NAIP `image`, S2 `visual`) where the
54
+ * natural-color preset wants the default band order.
55
+ */
56
+ preflightGeotiff?: GeoTIFFType | null;
57
+ /**
58
+ * Resolved nodata value threaded into `buildBandRenderPipeline` for the
59
+ * multi-asset `MultiCOGLayer` path. `null` (default) disables the nodata
60
+ * filter so legacy callers preserve their previous behaviour.
61
+ */
62
+ noDataVal?: number | null;
63
+ }
64
+ export interface BuiltRgbLayer {
65
+ kind: RgbLayerKind;
66
+ layer: COGLayer | MultiCOGLayer;
67
+ }
68
+ /**
69
+ * Build the appropriate deck.gl layer for an RGB composite.
70
+ *
71
+ * For single-asset composites the band indices flow through `selectCogPipeline`
72
+ * (when `preflightGeotiff` is provided) into a custom `getTileData` /
73
+ * `renderTile` pair that honors the requested R/G/B band order. Without a
74
+ * preflight GeoTIFF the layer uses the library's default pipeline (bands 0/1/2).
75
+ * For multi-asset composites a warning is logged (once per call) when any
76
+ * non-band-0 index is requested, since MultiCOGLayer cannot honor it today.
77
+ */
78
+ export declare function buildRgbLayer(opts: BuildRgbLayerOptions): Promise<BuiltRgbLayer>;