@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
@@ -0,0 +1,176 @@
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 { allChannelsBand0, isSingleAssetComposite } from '@walkthru-earth/objex-utils';
25
+ import { buildBandRenderPipeline, selectCogPipeline } from '../../../utils/cog.js';
26
+ /**
27
+ * Build the appropriate deck.gl layer for an RGB composite.
28
+ *
29
+ * For single-asset composites the band indices flow through `selectCogPipeline`
30
+ * (when `preflightGeotiff` is provided) into a custom `getTileData` /
31
+ * `renderTile` pair that honors the requested R/G/B band order. Without a
32
+ * preflight GeoTIFF the layer uses the library's default pipeline (bands 0/1/2).
33
+ * For multi-asset composites a warning is logged (once per call) when any
34
+ * non-band-0 index is requested, since MultiCOGLayer cannot honor it today.
35
+ */
36
+ export async function buildRgbLayer(opts) {
37
+ const assetByKey = new Map(opts.assets.map((a) => [a.key, a]));
38
+ const c = opts.composite;
39
+ console.debug('[buildRgbLayer]', {
40
+ id: opts.id,
41
+ composite: c,
42
+ single: isSingleAssetComposite(c),
43
+ assetKeys: opts.assets.map((a) => a.key),
44
+ hasPreflightGeotiff: !!opts.preflightGeotiff
45
+ });
46
+ if (isSingleAssetComposite(c)) {
47
+ const asset = assetByKey.get(c.r.assetKey);
48
+ if (!asset)
49
+ throw new Error(`unknown asset key: ${c.r.assetKey}`);
50
+ const url = await opts.resolveHref(asset.href);
51
+ if (opts.signal.aborted)
52
+ throw new DOMException('Aborted', 'AbortError');
53
+ const onGeoTIFFLoad = (_g, info) => {
54
+ opts.onLoad?.({
55
+ kind: 'cog',
56
+ bounds: info.geographicBounds
57
+ });
58
+ };
59
+ // Branch on whether we have a pre-opened GeoTIFF.
60
+ // - Present: build a per-channel BandConfig from the composite, hand
61
+ // it to selectCogPipeline (which inspects sampleFormat / bandCount)
62
+ // and spread the resolved {getTileData?, renderTile?} into COGLayer.
63
+ // This is the only path that honors a non-default per-channel
64
+ // bandIndex on a single-asset multi-band COG (e.g. NAIP NIR-R-G).
65
+ // - Absent: fall back to the library's default render pipeline. Bands
66
+ // 0/1/2 are read in that order, which is correct for single-band
67
+ // per-asset COGs (every bandIndex is 0 anyway) and for the default
68
+ // natural-color order on pre-baked multi-band visuals.
69
+ if (opts.preflightGeotiff) {
70
+ const bandConfig = {
71
+ mode: 'rgb',
72
+ rBand: c.r.bandIndex,
73
+ gBand: c.g.bandIndex,
74
+ bBand: c.b.bandIndex,
75
+ band: 0,
76
+ colorRamp: 'viridis'
77
+ };
78
+ console.debug('[buildRgbLayer] cog single-asset with preflight', {
79
+ id: opts.id,
80
+ bandConfig,
81
+ url
82
+ });
83
+ const pipeline = selectCogPipeline(opts.preflightGeotiff, {
84
+ bandConfig,
85
+ rescale: opts.rescale
86
+ });
87
+ const layer = new COGLayer({
88
+ id: opts.id,
89
+ geotiff: url,
90
+ ...pipeline,
91
+ pool: opts.pool ?? undefined,
92
+ epsgResolver: opts.epsgResolver,
93
+ signal: opts.signal,
94
+ onGeoTIFFLoad
95
+ });
96
+ return { kind: 'cog', layer };
97
+ }
98
+ console.debug('[buildRgbLayer] cog single-asset (library default pipeline)', {
99
+ id: opts.id,
100
+ url
101
+ });
102
+ // Fallback: no preflight GeoTIFF supplied. COGLayer's typed prop surface
103
+ // does not include `renderPipeline` (only `getTileData` + `renderTile`),
104
+ // so we cannot apply the band render pipeline statically here. Without
105
+ // a preflight to feed `selectCogPipeline`, we have no way to inspect
106
+ // sample format / band count up front, so we let the library infer its
107
+ // own pipeline from the GeoTIFF metadata at load time. Bands 0/1/2 are
108
+ // read in that order, which is correct for single-band per-asset COGs
109
+ // (every bandIndex is 0 anyway) and for the default natural-color order
110
+ // on pre-baked multi-band visuals (NAIP `image`, S2 `visual`).
111
+ const layer = new COGLayer({
112
+ id: opts.id,
113
+ geotiff: url,
114
+ pool: opts.pool ?? undefined,
115
+ epsgResolver: opts.epsgResolver,
116
+ signal: opts.signal,
117
+ onGeoTIFFLoad
118
+ });
119
+ return { kind: 'cog', layer };
120
+ }
121
+ if (!allChannelsBand0(c)) {
122
+ // Library limitation: MultiCOGLayer always reads band 0. Surface a
123
+ // console warning once per call so the consumer sees that the user's
124
+ // per-channel band index was dropped.
125
+ console.warn('[buildRgbLayer] multi-asset composite with non-band-0 indices, band index ignored on multi-asset path');
126
+ }
127
+ const sources = {};
128
+ for (const ref of [c.r, c.g, c.b, c.a].filter((x) => Boolean(x))) {
129
+ if (sources[ref.assetKey])
130
+ continue;
131
+ const asset = assetByKey.get(ref.assetKey);
132
+ if (!asset) {
133
+ console.warn('[buildRgbLayer] missing asset for ref', ref);
134
+ continue;
135
+ }
136
+ const url = await opts.resolveHref(asset.href);
137
+ if (opts.signal.aborted)
138
+ throw new DOMException('Aborted', 'AbortError');
139
+ sources[ref.assetKey] = { url };
140
+ }
141
+ const compositeSpec = {
142
+ r: c.r.assetKey,
143
+ g: c.g.assetKey,
144
+ b: c.b.assetKey
145
+ };
146
+ if (c.a && sources[c.a.assetKey])
147
+ compositeSpec.a = c.a.assetKey;
148
+ console.debug('[buildRgbLayer] multicog sources resolved', {
149
+ sourceKeys: Object.keys(sources),
150
+ composite: compositeSpec,
151
+ urls: Object.fromEntries(Object.entries(sources).map(([k, v]) => [k, v.url]))
152
+ });
153
+ const layer = new MultiCOGLayer({
154
+ id: opts.id,
155
+ sources,
156
+ composite: compositeSpec,
157
+ renderPipeline: buildBandRenderPipeline({
158
+ noDataVal: opts.noDataVal ?? null,
159
+ rescale: { ...opts.rescale }
160
+ }),
161
+ pool: opts.pool ?? undefined,
162
+ epsgResolver: opts.epsgResolver,
163
+ signal: opts.signal,
164
+ onGeoTIFFLoad: (_tiffs, info) => {
165
+ console.debug('[buildRgbLayer] MultiCOG onGeoTIFFLoad', {
166
+ id: opts.id,
167
+ bounds: info.geographicBounds
168
+ });
169
+ opts.onLoad?.({
170
+ kind: 'multicog',
171
+ bounds: info.geographicBounds
172
+ });
173
+ }
174
+ });
175
+ return { kind: 'multicog', layer };
176
+ }
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import XIcon from '@lucide/svelte/icons/x';
3
- import { formatValue } from '../../../utils/format.js';
3
+ import { formatValue } from '@walkthru-earth/objex-utils';
4
4
 
5
5
  let {
6
6
  feature = null,
@@ -1,8 +1,10 @@
1
1
  <script lang="ts">
2
2
  import maplibregl from 'maplibre-gl';
3
3
  import 'maplibre-gl/dist/maplibre-gl.css';
4
+ import { resolveBasemap } from '@walkthru-earth/objex-utils';
4
5
  import { onDestroy } from 'svelte';
5
6
  import { t } from '../../../i18n/index.svelte.js';
7
+ import { appConfig } from '../../../stores/config.svelte.js';
6
8
  import { settings } from '../../../stores/settings.svelte.js';
7
9
 
8
10
  const MAP_STYLES = {
@@ -37,11 +39,34 @@ let {
37
39
  bounds?: [number, number, number, number];
38
40
  } = $props();
39
41
 
40
- const resolvedStyle = $derived(style ?? MAP_STYLES[settings.resolved]);
42
+ function toMapStyle(variant: 'light' | 'dark'): string | maplibregl.StyleSpecification {
43
+ const bm = resolveBasemap(appConfig.value, variant, settings.basemapId);
44
+ if (!bm) return MAP_STYLES[variant];
45
+ if (bm.type === 'raster') {
46
+ return {
47
+ version: 8,
48
+ sources: {
49
+ 'objex-basemap': { type: 'raster', tiles: [bm.url], tileSize: 256 }
50
+ },
51
+ layers: [{ id: 'objex-basemap', type: 'raster', source: 'objex-basemap' }]
52
+ };
53
+ }
54
+ return bm.url;
55
+ }
56
+
57
+ const resolvedBasemap = $derived(
58
+ style ? undefined : resolveBasemap(appConfig.value, settings.resolved, settings.basemapId)
59
+ );
60
+ const resolvedStyle = $derived(style ?? toMapStyle(settings.resolved));
61
+ // Stable identity for style-swap comparison: a raster StyleSpecification is a
62
+ // fresh object on every derive, so compare by basemap id + variant instead.
63
+ const styleKey = $derived(
64
+ style ? 'custom' : `${resolvedBasemap?.id ?? 'fallback'}:${settings.resolved}`
65
+ );
41
66
 
42
67
  let containerEl: HTMLDivElement | undefined = $state();
43
68
  let map: maplibregl.Map | null = null;
44
- let currentStyleUrl: string | maplibregl.StyleSpecification | null = null;
69
+ let currentStyleKey: string | null = null;
45
70
  let currentZoom = $state(2);
46
71
  let webglLost = $state(false);
47
72
 
@@ -56,7 +81,7 @@ function initMap() {
56
81
  zoom
57
82
  });
58
83
 
59
- currentStyleUrl = resolvedStyle;
84
+ currentStyleKey = styleKey;
60
85
 
61
86
  map.addControl(
62
87
  new maplibregl.NavigationControl({ showCompass: true, visualizePitch: true }),
@@ -117,12 +142,13 @@ $effect(() => {
117
142
  }
118
143
  });
119
144
 
120
- // React to theme changes — swap basemap style
145
+ // React to theme / basemap changes — swap basemap style
121
146
  $effect(() => {
122
- const newStyle = resolvedStyle;
123
- if (map && currentStyleUrl !== newStyle && !style) {
124
- currentStyleUrl = newStyle;
125
- map.setStyle(newStyle);
147
+ const nextKey = styleKey;
148
+ const nextStyle = resolvedStyle;
149
+ if (map && currentStyleKey !== nextKey && !style) {
150
+ currentStyleKey = nextKey;
151
+ map.setStyle(nextStyle);
126
152
  }
127
153
  });
128
154
 
@@ -132,11 +158,11 @@ onDestroy(() => {
132
158
  });
133
159
  </script>
134
160
 
135
- <div class="relative h-full w-full">
136
- <div bind:this={containerEl} class="h-full w-full"></div>
161
+ <div class="relative h-full w-full" style="touch-action: pan-x pan-y;">
162
+ <div bind:this={containerEl} class="h-full w-full" style="touch-action: none;"></div>
137
163
  <!-- Zoom level indicator — positioned above nav controls -->
138
164
  <div
139
- class="pointer-events-none absolute bottom-[10rem] right-[10px] z-10 flex size-[29px] items-center justify-center rounded-full border border-zinc-300 bg-white shadow-sm dark:border-zinc-600 dark:bg-zinc-800"
165
+ class="pointer-events-none absolute bottom-[7rem] right-[10px] z-10 flex size-[29px] items-center justify-center rounded-full border border-zinc-300 bg-white shadow-sm dark:border-zinc-600 dark:bg-zinc-800 sm:bottom-[10rem]"
140
166
  >
141
167
  <span class="text-[10px] font-semibold tabular-nums text-zinc-600 dark:text-zinc-300">
142
168
  {currentZoom.toFixed(1)}
@@ -1,5 +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
4
  import type { PMTiles } from 'pmtiles';
4
5
  import { tileIdToZxy } from 'pmtiles';
5
6
  import {
@@ -8,7 +9,6 @@ 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
14
 
@@ -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 } 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,
@@ -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">&rarr;</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;