@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,16 +1,39 @@
1
- import { type BandConfig, type RescaleConfig } from '../../utils/cog.js';
2
- type $$ComponentProps = {
3
- bandCount: number;
4
- /** Required when `mode === 'single'`, ignored when `mode === 'multi'`. */
5
- bandConfig?: BandConfig;
6
- onConfigChange: (config: BandConfig) => void;
1
+ import type { ChannelComposite, CogAsset, PresetDef } from '@walkthru-earth/objex-utils';
2
+ import { type BandConfig, type NodataConfig, type RescaleConfig } from '../../utils/cog.js';
3
+ type Props = {
4
+ /** All raster-COG-ish assets on the current item (or `[selfAsset]` for plain CogViewer). */
5
+ assets: CogAsset[];
6
+ /** Current RGB composite. Always present. */
7
+ composite: ChannelComposite;
8
+ onCompositeChange: (next: ChannelComposite) => void;
9
+ /** Presets that resolve on this item. Empty when no preset applies. */
10
+ presets: PresetDef[];
11
+ activePresetId: string;
12
+ onPresetChange: (id: string) => void;
13
+ /** Rendering mode toggle: 'rgb' uses the channel pickers; 'single' the band+ramp picker. */
14
+ mode: 'rgb' | 'single';
15
+ onModeChange: (m: 'rgb' | 'single') => void;
16
+ /** Band/ramp config used when mode === 'single'. Optional for RGB-only callers. */
17
+ bandConfig?: BandConfig | null;
18
+ bandCount?: number;
19
+ onBandConfigChange?: (next: BandConfig) => void;
7
20
  rescale: RescaleConfig;
8
21
  rescaleApplicable: boolean;
9
- onRescaleChange: (rescale: RescaleConfig) => void;
10
- /** Optional histogram bins (normalized, single-band only) for the slider overlay. */
22
+ onRescaleChange: (next: RescaleConfig) => void;
11
23
  histogram?: Uint32Array | null;
12
- mode?: 'single' | 'multi';
24
+ /** Optional 4th channel UI affordance (alpha). When false, alpha row is hidden. */
25
+ showAlpha?: boolean;
26
+ /** User-selected nodata config. Default `{ mode: 'auto' }`. */
27
+ nodata?: NodataConfig;
28
+ /**
29
+ * Value resolved by the viewer for Auto mode (typically the GeoTIFF's
30
+ * GDAL_NODATA tag). Surfaced as a hint pill next to the segmented control.
31
+ * `null` means the file has no GDAL_NODATA tag.
32
+ */
33
+ autoNodata?: number | null;
34
+ /** Fired when the user changes nodata mode or value. */
35
+ onNodataChange?: (next: NodataConfig) => void;
13
36
  };
14
- declare const CogControls: import("svelte").Component<$$ComponentProps, {}, "">;
37
+ declare const CogControls: import("svelte").Component<Props, {}, "">;
15
38
  type CogControls = ReturnType<typeof CogControls>;
16
39
  export default CogControls;
@@ -2,6 +2,13 @@
2
2
  import { MapboxOverlay } from '@deck.gl/mapbox';
3
3
  import { COGLayer } from '@developmentseed/deck.gl-geotiff';
4
4
  import { DecoderPool, GeoTIFF } from '@developmentseed/geotiff';
5
+ import {
6
+ attachPixelInspector,
7
+ type ChannelComposite,
8
+ type CogAsset,
9
+ smokeTestHref,
10
+ syntheticSelfAsset
11
+ } from '@walkthru-earth/objex-utils';
5
12
  import type maplibregl from 'maplibre-gl';
6
13
  import { onDestroy, untrack } from 'svelte';
7
14
  import { t } from '../../i18n/index.svelte.js';
@@ -9,28 +16,39 @@ import { tabResources } from '../../stores/tab-resources.svelte.js';
9
16
  import type { Tab } from '../../types.js';
10
17
  import {
11
18
  type BandConfig,
19
+ buildCustomRenderTile,
12
20
  buildDataTypeLabel,
13
21
  type CogInfo,
22
+ type ConfigurableTileLoader,
14
23
  type CustomTileData,
15
24
  clampBounds,
16
25
  cleanupNativeBitmap,
26
+ createConfigurableGetTileData,
17
27
  createEpsgResolver,
28
+ DEFAULT_NODATA_CONFIG,
18
29
  DEFAULT_RESCALE,
19
30
  defaultBandConfig,
20
31
  fitCogBounds,
21
32
  HISTOGRAM_BIN_COUNT,
22
33
  inspectCogTags,
34
+ loadGeoTIFF,
35
+ mapResolutionMetersPerPixel,
36
+ type NodataConfig,
23
37
  needsCustomPipelineForConfig,
24
38
  normalizeCogGeotiff,
25
39
  type PixelValue,
26
40
  type RescaleConfig,
41
+ readGdalNodata,
27
42
  readPixelAtLngLat,
28
43
  renderNonTiledBitmap,
29
44
  resolveProj4Def,
30
- selectCogPipeline
45
+ selectCogPipeline,
46
+ selectOverviewForResolution
31
47
  } from '../../utils/cog.js';
32
- import { buildHttpsUrlAsync } from '../../utils/url.js';
48
+ import { seedRescaleFromGeotiff } from '../../utils/cog-histogram.js';
49
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
33
50
  import CogControls from './CogControls.svelte';
51
+ import PixelInspectorPanel, { type PixelInspectorRow } from './cog/PixelInspectorPanel.svelte';
34
52
  import MapContainer from './map/MapContainer.svelte';
35
53
 
36
54
  // ─── State ───────────────────────────────────────────────────────
@@ -43,24 +61,69 @@ let showControls = $state(false);
43
61
  let bounds = $state<[number, number, number, number] | undefined>();
44
62
  let cogInfo = $state<CogInfo | null>(null);
45
63
  let bandConfig = $state<BandConfig | null>(null);
64
+ let resolvedHrefForControls = $state<string | null>(null);
65
+ let probedBandCount = $state<number | null>(null);
66
+
67
+ const cogControlsAssets = $derived.by<CogAsset[]>(() => {
68
+ const href = resolvedHrefForControls;
69
+ if (!href) return [];
70
+ return [syntheticSelfAsset(href, probedBandCount ?? undefined)];
71
+ });
72
+
73
+ const cogControlsComposite = $derived.by<ChannelComposite>(() => {
74
+ const bc = bandConfig;
75
+ if (!bc) {
76
+ return {
77
+ r: { assetKey: 'self', bandIndex: 0 },
78
+ g: { assetKey: 'self', bandIndex: 0 },
79
+ b: { assetKey: 'self', bandIndex: 0 }
80
+ };
81
+ }
82
+ if (bc.mode === 'rgb') {
83
+ return {
84
+ r: { assetKey: 'self', bandIndex: bc.rBand ?? 0 },
85
+ g: { assetKey: 'self', bandIndex: bc.gBand ?? 0 },
86
+ b: { assetKey: 'self', bandIndex: bc.bBand ?? 0 }
87
+ };
88
+ }
89
+ const i = bc.band ?? 0;
90
+ return {
91
+ r: { assetKey: 'self', bandIndex: i },
92
+ g: { assetKey: 'self', bandIndex: i },
93
+ b: { assetKey: 'self', bandIndex: i }
94
+ };
95
+ });
46
96
  let histogram = $state.raw<Uint32Array | null>(null);
47
97
  let histogramTick = $state(0);
48
98
  let rescale = $state<RescaleConfig>({ ...DEFAULT_RESCALE });
99
+ // User-facing nodata override (Auto/Value/Off). Auto resolves at read time
100
+ // from the GeoTIFF's GDAL_NODATA tag, surfaced as a hint pill in CogControls.
101
+ let nodataConfig = $state<NodataConfig>({ ...DEFAULT_NODATA_CONFIG });
102
+ let autoNodata = $state<number | null>(null);
49
103
  // Palette-indexed COGs render through the library's Colormap module; a GPU
50
104
  // rescale at that stage is cosmetic and would confuse the legend. Keep the
51
105
  // slider hidden when a ColorMap tag is present.
52
106
  let isPaletteIndexed = $state(false);
53
107
  let pixelValue = $state<PixelValue | null>(null);
54
108
  let inspecting = $state(false);
109
+ // Storage smoke-test result for the primary asset href. Inspired by
110
+ // lazycogs `_smoketest_store`, surfaces auth / CORS / bucket failures at
111
+ // open time as a small amber pill, never blocks the layer mount.
112
+ let smokeWarning = $state<string | null>(null);
113
+ let smokeProbed = false;
55
114
 
56
115
  let abortController = new AbortController();
57
116
  let mapRef: maplibregl.Map | null = null;
58
117
  let overlayRef: MapboxOverlay | null = null;
59
118
  let geotiffRef: GeoTIFF | null = null;
119
+ // Identity-stable tile loader for the configurable CPU path. Lives for the
120
+ // duration of the current GeoTIFF identity, so deck.gl's TileLayer cache
121
+ // survives band/ramp swaps (a fresh getTileData reference would invalidate it).
122
+ let tileLoaderRef: ConfigurableTileLoader | null = null;
60
123
  let proj4DefRef: string | null = null;
61
124
  let sampleFormatRef = 1;
62
125
  let isTiledRef = true;
63
- let clickHandlerRef: ((e: maplibregl.MapMouseEvent) => void) | null = null;
126
+ let detachInspector: (() => void) | null = null;
64
127
  let resolvedHttpsUrl: string | null = null;
65
128
  // LinearRescale operates on a 0..1 scalar. Two cases expose a meaningful
66
129
  // slider: (1) the library-default uint RGB pipeline (scales `color.rgb`
@@ -111,8 +174,11 @@ $effect(() => {
111
174
  }
112
175
  overlayRef = null;
113
176
  geotiffRef = null;
177
+ tileLoaderRef = null;
114
178
  proj4DefRef = null;
115
179
  resolvedHttpsUrl = null;
180
+ resolvedHrefForControls = null;
181
+ probedBandCount = null;
116
182
  loading = true;
117
183
  error = null;
118
184
  cogInfo = null;
@@ -120,8 +186,12 @@ $effect(() => {
120
186
  histogram = null;
121
187
  histogramTick = 0;
122
188
  rescale = { ...DEFAULT_RESCALE };
189
+ nodataConfig = { ...DEFAULT_NODATA_CONFIG };
190
+ autoNodata = null;
123
191
  isPaletteIndexed = false;
124
192
  pixelValue = null;
193
+ smokeWarning = null;
194
+ smokeProbed = false;
125
195
  bounds = undefined;
126
196
  hasFittedOnce = false;
127
197
  showControls = false;
@@ -140,34 +210,32 @@ function onMapReady(map: maplibregl.Map) {
140
210
  // ─── Click handler for pixel inspection ──────────────────────────
141
211
 
142
212
  function removeClickHandler() {
143
- if (mapRef && clickHandlerRef) {
144
- mapRef.off('click', clickHandlerRef);
145
- clickHandlerRef = null;
213
+ if (detachInspector) {
214
+ detachInspector();
215
+ detachInspector = null;
146
216
  }
147
217
  }
148
218
 
149
219
  function setupClickHandler(map: maplibregl.Map) {
150
220
  removeClickHandler();
151
- clickHandlerRef = async (e: maplibregl.MapMouseEvent) => {
152
- if (!geotiffRef) return;
153
- inspecting = true;
154
- try {
155
- const result = await readPixelAtLngLat(
156
- geotiffRef,
157
- e.lngLat.lng,
158
- e.lngLat.lat,
159
- proj4DefRef,
160
- pool,
161
- abortController.signal
162
- );
221
+ detachInspector = attachPixelInspector<PixelValue>(map, {
222
+ probe: async ({ lng, lat, signal }) => {
223
+ if (!geotiffRef) return null;
224
+ // matches overview shown on screen
225
+ const targetRes = mapResolutionMetersPerPixel(map.getZoom(), lat);
226
+ const overview = selectOverviewForResolution(geotiffRef, targetRes);
227
+ return readPixelAtLngLat(geotiffRef, lng, lat, proj4DefRef, pool, signal, {
228
+ overview
229
+ });
230
+ },
231
+ onStart: () => {
232
+ inspecting = true;
233
+ },
234
+ onResult: (result) => {
163
235
  pixelValue = result;
164
- } catch {
165
- pixelValue = null;
166
- } finally {
167
236
  inspecting = false;
168
237
  }
169
- };
170
- map.on('click', clickHandlerRef);
238
+ });
171
239
  }
172
240
 
173
241
  // ─── Core load function ──────────────────────────────────────────
@@ -179,12 +247,30 @@ async function loadCog(map: maplibregl.Map) {
179
247
  const url = await buildHttpsUrlAsync(tab);
180
248
  if (signal.aborted) return;
181
249
  resolvedHttpsUrl = url;
250
+ resolvedHrefForControls = url;
251
+
252
+ // One-shot storage smoke-test. lazycogs-style probe surfaces auth /
253
+ // CORS / bucket failures at open time as an amber pill, never blocks
254
+ // the layer mount. Aborts via the viewer's existing controller.
255
+ if (!smokeProbed) {
256
+ smokeProbed = true;
257
+ void (async () => {
258
+ try {
259
+ const result = await smokeTestHref(url, signal);
260
+ if (signal.aborted) return;
261
+ if (!result.ok) smokeWarning = result.reason;
262
+ } catch (err) {
263
+ if (err instanceof DOMException && err.name === 'AbortError') return;
264
+ smokeWarning = err instanceof Error ? err.message : String(err);
265
+ }
266
+ })();
267
+ }
182
268
 
183
269
  // Pre-flight: read first IFD to check if tiled (single range request).
184
270
  let isTiled = true;
185
271
  let preflightGeotiff: GeoTIFF | undefined;
186
272
  try {
187
- preflightGeotiff = await GeoTIFF.fromUrl(url);
273
+ preflightGeotiff = await loadGeoTIFF(url);
188
274
  if (signal.aborted) return;
189
275
  isTiled = preflightGeotiff.isTiled;
190
276
 
@@ -233,6 +319,22 @@ async function loadCog(map: maplibregl.Map) {
233
319
 
234
320
  // Set default band config
235
321
  bandConfig = defaultBandConfig(preflightGeotiff.count, sampleFormatRef);
322
+ probedBandCount = preflightGeotiff.count;
323
+
324
+ // Surface GDAL_NODATA + a shader-space rescale seed (when present) so
325
+ // the nodata hint pill and rescale slider have meaningful defaults
326
+ // before the first tile decodes — matches source-cooperative/cog-viewer
327
+ // UX. The slider operates in normalized [0, 1] shader space, so
328
+ // `seedRescaleFromGeotiff` divides GDAL STATISTICS_MIN/MAX by the
329
+ // sample-format factor and falls back to a p2/p98 histogram + the
330
+ // bit-depth-aware default.
331
+ autoNodata = readGdalNodata(preflightGeotiff);
332
+ try {
333
+ rescale = await seedRescaleFromGeotiff(preflightGeotiff, { signal });
334
+ } catch {
335
+ // fall through, defaults remain
336
+ }
337
+ if (signal.aborted) return;
236
338
  }
237
339
 
238
340
  if (!isTiled && preflightGeotiff) {
@@ -267,25 +369,48 @@ async function loadCog(map: maplibregl.Map) {
267
369
 
268
370
  // ─── Build & add COGLayer ────────────────────────────────────────
269
371
 
270
- function buildAndAddLayer(
372
+ // Build the pipeline props (getTileData/renderTile/etc) for the current state.
373
+ // When the configurable CPU path applies, the tile loader is created once per
374
+ // GeoTIFF identity and its `getTileData` reference is reused across rebuilds so
375
+ // deck.gl's TileLayer cache survives band/ramp swaps. Only `renderTile` and
376
+ // downstream uniforms vary across style changes.
377
+ function buildPipelineProps(geotiff: GeoTIFF | undefined): Record<string, unknown> {
378
+ if (!geotiff || !bandConfig) {
379
+ return geotiff ? selectCogPipeline(geotiff, { bandConfig, rescale }) : {};
380
+ }
381
+ if (needsCustomPipelineForConfig(geotiff, bandConfig)) {
382
+ if (!tileLoaderRef) {
383
+ tileLoaderRef = createConfigurableGetTileData(geotiff, bandConfig);
384
+ } else {
385
+ tileLoaderRef.updateConfig(bandConfig);
386
+ }
387
+ return {
388
+ getTileData: tileLoaderRef.getTileData,
389
+ renderTile: buildCustomRenderTile(bandConfig, rescale)
390
+ };
391
+ }
392
+ // Library-default or rescaled-only path. The loader (if previously seeded)
393
+ // is harmless to keep, but the upcoming rebuild won't reference it.
394
+ return selectCogPipeline(geotiff, { bandConfig, rescale });
395
+ }
396
+
397
+ function buildCogLayer(
271
398
  map: maplibregl.Map,
272
399
  preflightGeotiff: GeoTIFF | undefined,
273
400
  signal: AbortSignal
274
- ) {
401
+ ): COGLayer {
275
402
  // Pick the library-default or one of three custom pipelines. Empty when the
276
403
  // library-default uint path runs unchanged.
277
- const customProps = preflightGeotiff
278
- ? selectCogPipeline(preflightGeotiff, { bandConfig, rescale })
279
- : {};
404
+ const customProps = buildPipelineProps(preflightGeotiff);
280
405
 
281
406
  // Apply upstream-bug workarounds in place (overview filter, 4326 bbox clamp).
282
407
  if (preflightGeotiff) normalizeCogGeotiff(preflightGeotiff);
283
408
 
284
409
  const cogInput = preflightGeotiff ?? resolvedHttpsUrl ?? '';
285
410
 
286
- // Cast: `onViewportLoad` is forwarded by our pnpm patch to the inner
287
- // TileLayer, but COGLayer's generated .d.ts does not expose it.
288
- // biome-ignore lint/suspicious/noExplicitAny: upstream prop not yet in types
411
+ // Cast: `onViewportLoad` is forwarded natively by COGLayer's RasterTileLayer
412
+ // base in 0.7.0 (deck.gl-raster PR #546), but COGLayer's generated .d.ts does
413
+ // not surface it.
289
414
  const cogProps: any = {
290
415
  // Stable id per tab so rebuilds on band/style change don't force deck.gl
291
416
  // to treat this as a brand-new layer and drop cached tile state.
@@ -351,7 +476,18 @@ function buildAndAddLayer(
351
476
  loading = false;
352
477
  }
353
478
  };
354
- const layer = new COGLayer(cogProps);
479
+ return new COGLayer(cogProps);
480
+ }
481
+
482
+ // First-mount: create the MapboxOverlay once and attach via addControl.
483
+ // Subsequent style changes go through pushLayer() which only calls setProps,
484
+ // preserving deck.gl's WebGL context and tile cache.
485
+ function buildAndAddLayer(
486
+ map: maplibregl.Map,
487
+ preflightGeotiff: GeoTIFF | undefined,
488
+ signal: AbortSignal
489
+ ) {
490
+ const layer = buildCogLayer(map, preflightGeotiff, signal);
355
491
 
356
492
  const overlay = new MapboxOverlay({
357
493
  interleaved: false,
@@ -368,6 +504,15 @@ function buildAndAddLayer(
368
504
  map.addControl(overlay as unknown as maplibregl.IControl);
369
505
  }
370
506
 
507
+ // Style-change update path: swap layers in place via setProps. Identity of the
508
+ // COGLayer's `id` and `getTileData` is preserved so deck.gl reconciles the
509
+ // existing layer instance and keeps its tile cache.
510
+ function pushLayer() {
511
+ if (!mapRef || !geotiffRef || !overlayRef) return;
512
+ const layer = buildCogLayer(mapRef, geotiffRef, abortController.signal);
513
+ overlayRef.setProps({ layers: [layer] });
514
+ }
515
+
371
516
  // ─── Viewport-scoped histogram aggregation ───────────────────────
372
517
 
373
518
  /**
@@ -418,35 +563,41 @@ function handleConfigChange(newConfig: BandConfig) {
418
563
  histogramTick = 0;
419
564
  if (!mapRef || !geotiffRef || !isTiledRef) return;
420
565
 
421
- // Remove old overlay
422
- if (overlayRef) {
423
- try {
424
- mapRef.removeControl(overlayRef as unknown as maplibregl.IControl);
425
- } catch {
426
- /* already removed */
427
- }
428
- overlayRef = null;
429
- }
430
-
431
- // Rebuild with new config
432
- buildAndAddLayer(mapRef, geotiffRef, abortController.signal);
566
+ // Swap layers in place: deck.gl diffs on layer id and reuses the stable
567
+ // `getTileData` reference held by `tileLoaderRef`, so the tile cache and
568
+ // in-flight fetches survive band/style changes.
569
+ pushLayer();
433
570
  }
434
571
 
435
572
  function handleRescaleChange(next: RescaleConfig) {
436
573
  rescale = next;
437
574
  if (!mapRef || !geotiffRef || !isTiledRef) return;
575
+ pushLayer();
576
+ }
438
577
 
439
- // Remove old overlay and rebuild. deck.gl diffs on layer id, so reusing the
440
- // stable per-tab id keeps tile cache state where possible.
441
- if (overlayRef) {
442
- try {
443
- mapRef.removeControl(overlayRef as unknown as maplibregl.IControl);
444
- } catch {
445
- /* already removed */
446
- }
447
- overlayRef = null;
578
+ // ─── Unified picker change handlers ──────────────────────────────
579
+
580
+ function handleCompositeChange(next: ChannelComposite): void {
581
+ if (!bandConfig) return;
582
+ if (bandConfig.mode === 'rgb') {
583
+ handleConfigChange({
584
+ ...bandConfig,
585
+ rBand: next.r.bandIndex,
586
+ gBand: next.g.bandIndex,
587
+ bBand: next.b.bandIndex
588
+ });
589
+ } else {
590
+ handleConfigChange({ ...bandConfig, band: next.r.bandIndex });
448
591
  }
449
- buildAndAddLayer(mapRef, geotiffRef, abortController.signal);
592
+ }
593
+
594
+ function handleModeChange(m: 'rgb' | 'single'): void {
595
+ if (!bandConfig) return;
596
+ handleConfigChange({ ...bandConfig, mode: m });
597
+ }
598
+
599
+ function handleBandConfigChange(next: BandConfig): void {
600
+ handleConfigChange(next);
450
601
  }
451
602
 
452
603
  // ─── Cleanup ─────────────────────────────────────────────────────
@@ -465,9 +616,12 @@ function cleanup() {
465
616
  mapRef = null;
466
617
  overlayRef = null;
467
618
  geotiffRef = null;
619
+ tileLoaderRef = null;
468
620
  proj4DefRef = null;
469
621
  pixelValue = null;
470
622
  resolvedHttpsUrl = null;
623
+ resolvedHrefForControls = null;
624
+ probedBandCount = null;
471
625
  }
472
626
 
473
627
  $effect(() => {
@@ -484,7 +638,7 @@ onDestroy(cleanup);
484
638
  </div>
485
639
 
486
640
  <!-- Top-left: Loading + metadata badges -->
487
- <div class="pointer-events-none absolute left-2 top-2 z-10 flex flex-col gap-1">
641
+ <div class="pointer-events-none absolute left-2 top-2 z-10 flex max-w-[calc(100vw-7rem)] flex-col gap-1 sm:max-w-none">
488
642
  {#if loading}
489
643
  <div
490
644
  class="rounded bg-card/80 px-2 py-1 text-xs text-card-foreground backdrop-blur-sm"
@@ -512,14 +666,23 @@ onDestroy(cleanup);
512
666
  {error}
513
667
  </div>
514
668
  {/if}
669
+
670
+ {#if smokeWarning && !error}
671
+ <div
672
+ class="pointer-events-auto max-w-sm rounded bg-amber-900/80 px-2 py-1 text-xs text-amber-100"
673
+ title={t('stac.smokeWarningHint')}
674
+ >
675
+ {t('stac.smokeWarning', { reason: smokeWarning })}
676
+ </div>
677
+ {/if}
515
678
  </div>
516
679
 
517
680
  <!-- Top-right: Info + Style buttons -->
518
681
  {#if cogInfo}
519
- <div class="absolute right-2 top-2 z-10 flex gap-1">
682
+ <div class="absolute right-2 top-2 z-10 flex gap-1" style="touch-action: manipulation;">
520
683
  {#if bandConfig}
521
684
  <button
522
- class="rounded bg-card/80 px-2 py-1 text-xs text-card-foreground backdrop-blur-sm hover:bg-card"
685
+ class="inline-flex min-h-11 min-w-11 items-center justify-center rounded bg-card/80 px-3 py-1.5 text-xs text-card-foreground backdrop-blur-sm hover:bg-card sm:min-h-0 sm:min-w-0 sm:px-2 sm:py-1"
523
686
  class:ring-1={showControls}
524
687
  class:ring-primary={showControls}
525
688
  onclick={() => {
@@ -531,7 +694,7 @@ onDestroy(cleanup);
531
694
  </button>
532
695
  {/if}
533
696
  <button
534
- class="rounded bg-card/80 px-2 py-1 text-xs text-card-foreground backdrop-blur-sm hover:bg-card"
697
+ class="inline-flex min-h-11 min-w-11 items-center justify-center rounded bg-card/80 px-3 py-1.5 text-xs text-card-foreground backdrop-blur-sm hover:bg-card sm:min-h-0 sm:min-w-0 sm:px-2 sm:py-1"
535
698
  class:ring-1={showInfo}
536
699
  class:ring-primary={showInfo}
537
700
  onclick={() => {
@@ -543,23 +706,39 @@ onDestroy(cleanup);
543
706
  </button>
544
707
  </div>
545
708
 
546
- <!-- Band/Color controls panel -->
547
- {#if showControls && bandConfig}
548
- <CogControls
549
- bandCount={cogInfo.bandCount}
550
- {bandConfig}
551
- onConfigChange={handleConfigChange}
552
- {rescale}
553
- rescaleApplicable={rescaleApplicable}
554
- onRescaleChange={handleRescaleChange}
555
- {histogram}
556
- />
709
+ <!-- Band/Color controls panel. Kept mounted so slider drag state and focus
710
+ survive every visibility toggle; only the `hidden` class is flipped. -->
711
+ {#if bandConfig}
712
+ <div class={showControls ? 'contents' : 'hidden'}>
713
+ <CogControls
714
+ assets={cogControlsAssets}
715
+ composite={cogControlsComposite}
716
+ onCompositeChange={handleCompositeChange}
717
+ presets={[]}
718
+ activePresetId=""
719
+ onPresetChange={() => {}}
720
+ mode={bandConfig?.mode ?? 'rgb'}
721
+ onModeChange={handleModeChange}
722
+ {bandConfig}
723
+ bandCount={probedBandCount ?? cogInfo.bandCount}
724
+ onBandConfigChange={handleBandConfigChange}
725
+ {rescale}
726
+ rescaleApplicable={rescaleApplicable}
727
+ onRescaleChange={handleRescaleChange}
728
+ {histogram}
729
+ nodata={nodataConfig}
730
+ {autoNodata}
731
+ onNodataChange={(next) => {
732
+ nodataConfig = next;
733
+ }}
734
+ />
735
+ </div>
557
736
  {/if}
558
737
 
559
738
  <!-- Info panel -->
560
739
  {#if showInfo}
561
740
  <div
562
- class="absolute right-2 top-10 z-10 max-h-[70vh] w-64 overflow-auto rounded bg-card/90 p-3 text-xs text-card-foreground backdrop-blur-sm"
741
+ class="absolute inset-x-2 top-16 z-10 max-h-[60vh] overflow-auto rounded bg-card/90 p-3 text-xs text-card-foreground backdrop-blur-sm sm:inset-x-auto sm:right-2 sm:top-10 sm:max-h-[70vh] sm:w-64"
563
742
  >
564
743
  <h3 class="mb-2 font-medium">{t('map.cogInfo')}</h3>
565
744
  <dl class="space-y-1.5">
@@ -579,45 +758,17 @@ onDestroy(cleanup);
579
758
  {/if}
580
759
 
581
760
  <!-- Bottom-left: Pixel value on click -->
582
- {#if pixelValue}
583
- <div
584
- class="absolute bottom-2 left-2 z-10 rounded bg-card/90 p-2.5 text-xs text-card-foreground backdrop-blur-sm"
585
- >
586
- <div class="mb-1 flex items-center justify-between gap-3">
587
- <span class="font-medium">{t('cog.pixelValue')}</span>
588
- <button
589
- class="text-muted-foreground hover:text-card-foreground"
590
- onclick={() => (pixelValue = null)}
591
- >
592
- &times;
593
- </button>
594
- </div>
595
- <div class="space-y-0.5 text-muted-foreground">
596
- <div>
597
- {pixelValue.lat.toFixed(6)}&deg;, {pixelValue.lng.toFixed(6)}&deg;
598
- </div>
599
- <div class="text-[10px]">
600
- px ({pixelValue.col}, {pixelValue.row})
601
- </div>
602
- </div>
603
- <div class="mt-1.5 space-y-0.5">
604
- {#each pixelValue.values as val, i}
605
- <div class="flex justify-between gap-2">
606
- <span class="text-muted-foreground">{t('cog.band')} {i + 1}</span>
607
- <span class="font-mono tabular-nums">
608
- {Number.isInteger(val) ? val : val.toFixed(4)}
609
- </span>
610
- </div>
611
- {/each}
612
- </div>
613
- </div>
614
- {/if}
615
-
616
- {#if inspecting}
617
- <div
618
- 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"
619
- >
620
- {t('cog.reading')}
621
- </div>
622
- {/if}
761
+ <PixelInspectorPanel
762
+ lng={pixelValue?.lng ?? null}
763
+ lat={pixelValue?.lat ?? null}
764
+ rows={pixelValue
765
+ ? (pixelValue.values.map((v, i) => ({
766
+ label: `${t('cog.band')} ${i + 1}`,
767
+ value: v
768
+ })) satisfies PixelInspectorRow[])
769
+ : null}
770
+ footnote={pixelValue ? `px (${pixelValue.col}, ${pixelValue.row})` : undefined}
771
+ onClose={() => (pixelValue = null)}
772
+ {inspecting}
773
+ />
623
774
  </div>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Tab } from '../../types';
3
- import { buildHttpsUrlAsync } from '../../utils/url.js';
3
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
4
4
 
5
5
  let { tab }: { tab: Tab } = $props();
6
6
 
@@ -19,7 +19,7 @@ import {
19
19
  hoverCursor,
20
20
  loadDeckModules
21
21
  } from '../../utils/deck.js';
22
- import { buildHttpsUrlAsync } from '../../utils/url.js';
22
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
23
23
  import AttributeTable from './map/AttributeTable.svelte';
24
24
  import MapContainer from './map/MapContainer.svelte';
25
25
 
@@ -1,5 +1,11 @@
1
1
  <script lang="ts">
2
2
  import LocateIcon from '@lucide/svelte/icons/locate';
3
+ import {
4
+ buildGeoArrowTables,
5
+ type GeoArrowGeomType,
6
+ type GeoArrowResult,
7
+ parseWKB
8
+ } from '@walkthru-earth/objex-utils';
3
9
  import type maplibregl from 'maplibre-gl';
4
10
  import { onDestroy } from 'svelte';
5
11
  import { t } from '../../i18n/index.svelte.js';
@@ -14,12 +20,6 @@ import {
14
20
  hoverCursor,
15
21
  loadGeoArrowModules
16
22
  } from '../../utils/deck.js';
17
- import {
18
- buildGeoArrowTables,
19
- type GeoArrowGeomType,
20
- type GeoArrowResult
21
- } from '../../utils/geoarrow.js';
22
- import { parseWKB } from '../../utils/wkb.js';
23
23
  import LoadProgress, { type ProgressEntry } from './LoadProgress.svelte';
24
24
  import AttributeTable from './map/AttributeTable.svelte';
25
25
  import MapContainer from './map/MapContainer.svelte';