@walkthru-earth/objex 1.3.1 → 1.5.0

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