@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.
- package/LICENSE +5 -0
- package/README.md +28 -20
- package/dist/components/browser/FileTreeSidebar.svelte +32 -17
- package/dist/components/layout/AboutSheet.svelte +5 -2
- package/dist/components/layout/ConnectionDialog.svelte +7 -2
- package/dist/components/layout/SettingsSheet.svelte +238 -0
- package/dist/components/layout/SettingsSheet.svelte.d.ts +6 -0
- package/dist/components/layout/Sidebar.svelte +73 -6
- package/dist/components/layout/Sidebar.svelte.d.ts +4 -1
- package/dist/components/layout/StatusBar.svelte +17 -14
- package/dist/components/layout/TabBar.svelte +4 -4
- package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/input/input.svelte.d.ts +1 -1
- package/dist/components/ui/resizable/index.d.ts +1 -1
- package/dist/components/ui/resizable/index.js +2 -2
- package/dist/components/ui/slider/index.d.ts +3 -0
- package/dist/components/ui/slider/index.js +5 -0
- package/dist/components/ui/slider/range-slider.svelte +94 -0
- package/dist/components/ui/slider/range-slider.svelte.d.ts +21 -0
- package/dist/components/ui/slider/slider.svelte +83 -0
- package/dist/components/ui/slider/slider.svelte.d.ts +7 -0
- package/dist/components/viewers/ArchiveViewer.svelte +140 -113
- package/dist/components/viewers/CodeViewer.svelte +45 -48
- package/dist/components/viewers/CodeViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/CogControls.svelte +338 -184
- package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
- package/dist/components/viewers/CogViewer.svelte +269 -116
- package/dist/components/viewers/CopcViewer.svelte +8 -15
- package/dist/components/viewers/DatabaseViewer.svelte +22 -21
- package/dist/components/viewers/FileInfo.svelte +16 -16
- package/dist/components/viewers/FlatGeobufViewer.svelte +16 -46
- package/dist/components/viewers/GeoParquetMapViewer.svelte +11 -9
- package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/ImageViewer.svelte +12 -14
- package/dist/components/viewers/LoadProgress.svelte +6 -6
- package/dist/components/viewers/MarkdownViewer.svelte +29 -30
- package/dist/components/viewers/MediaViewer.svelte +13 -14
- package/dist/components/viewers/ModelViewer.svelte +18 -21
- package/dist/components/viewers/MultiCogViewer.svelte +474 -106
- package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/NotebookViewer.svelte +28 -29
- package/dist/components/viewers/PdfViewer.svelte +24 -33
- package/dist/components/viewers/PmtilesViewer.svelte +13 -15
- package/dist/components/viewers/QueryHistoryPanel.svelte +18 -18
- package/dist/components/viewers/RawViewer.svelte +27 -21
- package/dist/components/viewers/StacMapViewer.svelte +6 -13
- package/dist/components/viewers/StacMosaicViewer.svelte +1764 -410
- package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/StacTabViewer.svelte +26 -15
- package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/TableGrid.svelte +38 -34
- package/dist/components/viewers/TableStatusBar.svelte +7 -7
- package/dist/components/viewers/TableToolbar.svelte +10 -9
- package/dist/components/viewers/TableViewer.svelte +47 -30
- package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
- package/dist/components/viewers/ViewerHeader.svelte +18 -0
- package/dist/components/viewers/ViewerHeader.svelte.d.ts +10 -0
- package/dist/components/viewers/ViewerRouter.svelte +16 -8
- package/dist/components/viewers/ViewerStatus.svelte +19 -0
- package/dist/components/viewers/ViewerStatus.svelte.d.ts +7 -0
- package/dist/components/viewers/ZarrMapViewer.svelte +24 -21
- package/dist/components/viewers/ZarrViewer.svelte +98 -65
- package/dist/components/viewers/cog/ChannelPicker.svelte +83 -0
- package/dist/components/viewers/cog/ChannelPicker.svelte.d.ts +13 -0
- package/dist/components/viewers/cog/PixelInspectorPanel.svelte +87 -0
- package/dist/components/viewers/cog/PixelInspectorPanel.svelte.d.ts +17 -0
- package/dist/components/viewers/cog/buildRgbLayer.d.ts +78 -0
- package/dist/components/viewers/cog/buildRgbLayer.js +176 -0
- package/dist/components/viewers/map/AttributeTable.svelte +7 -7
- package/dist/components/viewers/map/MapContainer.svelte +38 -12
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +109 -83
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +16 -16
- package/dist/components/viewers/stac/StacDatetimeBar.svelte +175 -0
- package/dist/components/viewers/stac/StacDatetimeBar.svelte.d.ts +10 -0
- package/dist/components/viewers/stac/StacFilterPanel.svelte +243 -0
- package/dist/components/viewers/stac/StacFilterPanel.svelte.d.ts +14 -0
- package/dist/components/viewers/stac/StacItemInspector.svelte +223 -0
- package/dist/components/viewers/stac/StacItemInspector.svelte.d.ts +10 -0
- package/dist/components/viewers/stac/StacItemStrip.svelte +228 -0
- package/dist/components/viewers/stac/StacItemStrip.svelte.d.ts +12 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +8 -0
- package/dist/file-icons/index.d.ts +1 -1
- package/dist/file-icons/index.js +1 -1
- package/dist/i18n/ar.js +113 -2
- package/dist/i18n/en.js +113 -2
- package/dist/index.d.ts +2 -28
- package/dist/index.js +7 -23
- package/dist/query/engine.d.ts +10 -0
- package/dist/query/source.js +1 -1
- package/dist/query/stac-source-factory.d.ts +65 -0
- package/dist/query/stac-source-factory.js +77 -0
- package/dist/query/stac-source-parquet.d.ts +135 -0
- package/dist/query/stac-source-parquet.js +468 -0
- package/dist/query/wasm.d.ts +8 -0
- package/dist/query/wasm.js +310 -65
- package/dist/storage/presign.js +3 -2
- package/dist/storage/providers.js +7 -6
- package/dist/stores/config.svelte.d.ts +15 -0
- package/dist/stores/config.svelte.js +46 -0
- package/dist/stores/connections.svelte.d.ts +2 -2
- package/dist/stores/connections.svelte.js +1 -2
- package/dist/stores/files.svelte.d.ts +1 -1
- package/dist/stores/files.svelte.js +1 -1
- package/dist/stores/query-history.svelte.js +1 -1
- package/dist/stores/settings.svelte.d.ts +16 -1
- package/dist/stores/settings.svelte.js +104 -48
- package/dist/stores/tabs.svelte.d.ts +3 -0
- package/dist/stores/tabs.svelte.js +17 -0
- package/dist/utils/cog-histogram.d.ts +121 -0
- package/dist/utils/cog-histogram.js +424 -0
- package/dist/utils/cog.d.ts +177 -20
- package/dist/utils/cog.js +361 -76
- package/dist/utils/colormap-sprite.d.ts +0 -9
- package/dist/utils/colormap-sprite.js +0 -21
- package/dist/utils/deck.d.ts +18 -12
- package/dist/utils/deck.js +15 -7
- package/dist/utils/media-query.svelte.d.ts +14 -0
- package/dist/utils/media-query.svelte.js +29 -0
- package/dist/utils/pmtiles-tile.js +2 -2
- package/dist/utils/signed-url-effect.d.ts +7 -0
- package/dist/utils/signed-url-effect.js +19 -0
- package/dist/utils/{url.d.ts → signed-url.d.ts} +15 -1
- package/dist/utils/{url.js → signed-url.js} +32 -10
- package/dist/utils/url-state.d.ts +36 -0
- package/dist/utils/url-state.js +72 -2
- package/dist/utils/zarr-tab.d.ts +1 -2
- package/dist/utils/zarr-tab.js +1 -2
- package/dist/utils/zarr.d.ts +0 -17
- package/dist/utils/zarr.js +1 -45
- package/package.json +55 -84
- package/dist/components/browser/Breadcrumb.svelte +0 -50
- package/dist/components/browser/Breadcrumb.svelte.d.ts +0 -7
- package/dist/components/browser/CreateFolderDialog.svelte +0 -98
- package/dist/components/browser/CreateFolderDialog.svelte.d.ts +0 -6
- package/dist/components/browser/DeleteConfirmDialog.svelte +0 -90
- package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +0 -8
- package/dist/components/browser/DropZone.svelte +0 -83
- package/dist/components/browser/DropZone.svelte.d.ts +0 -7
- package/dist/components/browser/FileBrowser.svelte +0 -252
- package/dist/components/browser/FileBrowser.svelte.d.ts +0 -3
- package/dist/components/browser/FileRow.svelte +0 -117
- package/dist/components/browser/FileRow.svelte.d.ts +0 -9
- package/dist/components/browser/RenameDialog.svelte +0 -101
- package/dist/components/browser/RenameDialog.svelte.d.ts +0 -8
- package/dist/components/browser/SearchBar.svelte +0 -40
- package/dist/components/browser/SearchBar.svelte.d.ts +0 -6
- package/dist/components/browser/UploadButton.svelte +0 -65
- package/dist/components/browser/UploadButton.svelte.d.ts +0 -3
- package/dist/query/stac-geoparquet.d.ts +0 -31
- package/dist/query/stac-geoparquet.js +0 -136
- package/dist/utils/clipboard.d.ts +0 -13
- package/dist/utils/clipboard.js +0 -38
- package/dist/utils/cloud-url.d.ts +0 -27
- package/dist/utils/cloud-url.js +0 -61
- package/dist/utils/cog-pure.d.ts +0 -25
- package/dist/utils/cog-pure.js +0 -35
- package/dist/utils/column-types.d.ts +0 -5
- package/dist/utils/column-types.js +0 -137
- package/dist/utils/connection-identity.d.ts +0 -51
- package/dist/utils/connection-identity.js +0 -97
- package/dist/utils/error.d.ts +0 -8
- package/dist/utils/error.js +0 -12
- package/dist/utils/evidence-context.d.ts +0 -22
- package/dist/utils/evidence-context.js +0 -56
- package/dist/utils/export.d.ts +0 -22
- package/dist/utils/export.js +0 -76
- package/dist/utils/file-sort.d.ts +0 -20
- package/dist/utils/file-sort.js +0 -41
- package/dist/utils/format.d.ts +0 -24
- package/dist/utils/format.js +0 -78
- package/dist/utils/geoarrow.d.ts +0 -32
- package/dist/utils/geoarrow.js +0 -672
- package/dist/utils/geometry-type.d.ts +0 -52
- package/dist/utils/geometry-type.js +0 -76
- package/dist/utils/hex.d.ts +0 -10
- package/dist/utils/hex.js +0 -27
- package/dist/utils/host-detection.d.ts +0 -23
- package/dist/utils/host-detection.js +0 -95
- package/dist/utils/local-storage.d.ts +0 -16
- package/dist/utils/local-storage.js +0 -37
- package/dist/utils/markdown-sql.d.ts +0 -30
- package/dist/utils/markdown-sql.js +0 -72
- package/dist/utils/notebook.d.ts +0 -59
- package/dist/utils/notebook.js +0 -211
- package/dist/utils/parquet-metadata.d.ts +0 -64
- package/dist/utils/parquet-metadata.js +0 -262
- package/dist/utils/stac-geoparquet.d.ts +0 -90
- package/dist/utils/stac-geoparquet.js +0 -223
- package/dist/utils/stac-hydrate.d.ts +0 -38
- package/dist/utils/stac-hydrate.js +0 -243
- package/dist/utils/stac.d.ts +0 -136
- package/dist/utils/stac.js +0 -176
- package/dist/utils/storage-url.d.ts +0 -90
- package/dist/utils/storage-url.js +0 -568
- package/dist/utils/wkb.d.ts +0 -43
- package/dist/utils/wkb.js +0 -359
|
@@ -1,16 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
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: (
|
|
10
|
-
/** Optional histogram bins (normalized, single-band only) for the slider overlay. */
|
|
22
|
+
onRescaleChange: (next: RescaleConfig) => void;
|
|
11
23
|
histogram?: Uint32Array | null;
|
|
12
|
-
|
|
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
|
|
37
|
+
declare const CogControls: import("svelte").Component<Props, {}, "">;
|
|
15
38
|
type CogControls = ReturnType<typeof CogControls>;
|
|
16
39
|
export default CogControls;
|
|
@@ -2,6 +2,15 @@
|
|
|
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
|
+
handleLoadError,
|
|
10
|
+
isAbortError,
|
|
11
|
+
smokeTestHref,
|
|
12
|
+
syntheticSelfAsset
|
|
13
|
+
} from '@walkthru-earth/objex-utils';
|
|
5
14
|
import type maplibregl from 'maplibre-gl';
|
|
6
15
|
import { onDestroy, untrack } from 'svelte';
|
|
7
16
|
import { t } from '../../i18n/index.svelte.js';
|
|
@@ -9,28 +18,39 @@ import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
|
9
18
|
import type { Tab } from '../../types.js';
|
|
10
19
|
import {
|
|
11
20
|
type BandConfig,
|
|
21
|
+
buildCustomRenderTile,
|
|
12
22
|
buildDataTypeLabel,
|
|
13
23
|
type CogInfo,
|
|
24
|
+
type ConfigurableTileLoader,
|
|
14
25
|
type CustomTileData,
|
|
15
26
|
clampBounds,
|
|
16
27
|
cleanupNativeBitmap,
|
|
28
|
+
createConfigurableGetTileData,
|
|
17
29
|
createEpsgResolver,
|
|
30
|
+
DEFAULT_NODATA_CONFIG,
|
|
18
31
|
DEFAULT_RESCALE,
|
|
19
32
|
defaultBandConfig,
|
|
20
33
|
fitCogBounds,
|
|
21
34
|
HISTOGRAM_BIN_COUNT,
|
|
22
35
|
inspectCogTags,
|
|
36
|
+
loadGeoTIFF,
|
|
37
|
+
mapResolutionMetersPerPixel,
|
|
38
|
+
type NodataConfig,
|
|
23
39
|
needsCustomPipelineForConfig,
|
|
24
40
|
normalizeCogGeotiff,
|
|
25
41
|
type PixelValue,
|
|
26
42
|
type RescaleConfig,
|
|
43
|
+
readGdalNodata,
|
|
27
44
|
readPixelAtLngLat,
|
|
28
45
|
renderNonTiledBitmap,
|
|
29
46
|
resolveProj4Def,
|
|
30
|
-
selectCogPipeline
|
|
47
|
+
selectCogPipeline,
|
|
48
|
+
selectOverviewForResolution
|
|
31
49
|
} from '../../utils/cog.js';
|
|
32
|
-
import {
|
|
50
|
+
import { seedRescaleFromGeotiff } from '../../utils/cog-histogram.js';
|
|
51
|
+
import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
|
|
33
52
|
import CogControls from './CogControls.svelte';
|
|
53
|
+
import PixelInspectorPanel, { type PixelInspectorRow } from './cog/PixelInspectorPanel.svelte';
|
|
34
54
|
import MapContainer from './map/MapContainer.svelte';
|
|
35
55
|
|
|
36
56
|
// ─── State ───────────────────────────────────────────────────────
|
|
@@ -43,24 +63,69 @@ let showControls = $state(false);
|
|
|
43
63
|
let bounds = $state<[number, number, number, number] | undefined>();
|
|
44
64
|
let cogInfo = $state<CogInfo | null>(null);
|
|
45
65
|
let bandConfig = $state<BandConfig | null>(null);
|
|
66
|
+
let resolvedHrefForControls = $state<string | null>(null);
|
|
67
|
+
let probedBandCount = $state<number | null>(null);
|
|
68
|
+
|
|
69
|
+
const cogControlsAssets = $derived.by<CogAsset[]>(() => {
|
|
70
|
+
const href = resolvedHrefForControls;
|
|
71
|
+
if (!href) return [];
|
|
72
|
+
return [syntheticSelfAsset(href, probedBandCount ?? undefined)];
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const cogControlsComposite = $derived.by<ChannelComposite>(() => {
|
|
76
|
+
const bc = bandConfig;
|
|
77
|
+
if (!bc) {
|
|
78
|
+
return {
|
|
79
|
+
r: { assetKey: 'self', bandIndex: 0 },
|
|
80
|
+
g: { assetKey: 'self', bandIndex: 0 },
|
|
81
|
+
b: { assetKey: 'self', bandIndex: 0 }
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (bc.mode === 'rgb') {
|
|
85
|
+
return {
|
|
86
|
+
r: { assetKey: 'self', bandIndex: bc.rBand ?? 0 },
|
|
87
|
+
g: { assetKey: 'self', bandIndex: bc.gBand ?? 0 },
|
|
88
|
+
b: { assetKey: 'self', bandIndex: bc.bBand ?? 0 }
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const i = bc.band ?? 0;
|
|
92
|
+
return {
|
|
93
|
+
r: { assetKey: 'self', bandIndex: i },
|
|
94
|
+
g: { assetKey: 'self', bandIndex: i },
|
|
95
|
+
b: { assetKey: 'self', bandIndex: i }
|
|
96
|
+
};
|
|
97
|
+
});
|
|
46
98
|
let histogram = $state.raw<Uint32Array | null>(null);
|
|
47
99
|
let histogramTick = $state(0);
|
|
48
100
|
let rescale = $state<RescaleConfig>({ ...DEFAULT_RESCALE });
|
|
101
|
+
// User-facing nodata override (Auto/Value/Off). Auto resolves at read time
|
|
102
|
+
// from the GeoTIFF's GDAL_NODATA tag, surfaced as a hint pill in CogControls.
|
|
103
|
+
let nodataConfig = $state<NodataConfig>({ ...DEFAULT_NODATA_CONFIG });
|
|
104
|
+
let autoNodata = $state<number | null>(null);
|
|
49
105
|
// Palette-indexed COGs render through the library's Colormap module; a GPU
|
|
50
106
|
// rescale at that stage is cosmetic and would confuse the legend. Keep the
|
|
51
107
|
// slider hidden when a ColorMap tag is present.
|
|
52
108
|
let isPaletteIndexed = $state(false);
|
|
53
109
|
let pixelValue = $state<PixelValue | null>(null);
|
|
54
110
|
let inspecting = $state(false);
|
|
111
|
+
// Storage smoke-test result for the primary asset href. Inspired by
|
|
112
|
+
// lazycogs `_smoketest_store`, surfaces auth / CORS / bucket failures at
|
|
113
|
+
// open time as a small amber pill, never blocks the layer mount.
|
|
114
|
+
let smokeWarning = $state<string | null>(null);
|
|
115
|
+
let smokeProbed = false;
|
|
55
116
|
|
|
56
117
|
let abortController = new AbortController();
|
|
57
118
|
let mapRef: maplibregl.Map | null = null;
|
|
58
119
|
let overlayRef: MapboxOverlay | null = null;
|
|
59
120
|
let geotiffRef: GeoTIFF | null = null;
|
|
121
|
+
// Identity-stable tile loader for the configurable CPU path. Lives for the
|
|
122
|
+
// duration of the current GeoTIFF identity, so deck.gl's TileLayer cache
|
|
123
|
+
// survives band/ramp swaps (a fresh getTileData reference would invalidate it).
|
|
124
|
+
let tileLoaderRef: ConfigurableTileLoader | null = null;
|
|
60
125
|
let proj4DefRef: string | null = null;
|
|
61
126
|
let sampleFormatRef = 1;
|
|
62
127
|
let isTiledRef = true;
|
|
63
|
-
let
|
|
128
|
+
let detachInspector: (() => void) | null = null;
|
|
64
129
|
let resolvedHttpsUrl: string | null = null;
|
|
65
130
|
// LinearRescale operates on a 0..1 scalar. Two cases expose a meaningful
|
|
66
131
|
// slider: (1) the library-default uint RGB pipeline (scales `color.rgb`
|
|
@@ -111,8 +176,11 @@ $effect(() => {
|
|
|
111
176
|
}
|
|
112
177
|
overlayRef = null;
|
|
113
178
|
geotiffRef = null;
|
|
179
|
+
tileLoaderRef = null;
|
|
114
180
|
proj4DefRef = null;
|
|
115
181
|
resolvedHttpsUrl = null;
|
|
182
|
+
resolvedHrefForControls = null;
|
|
183
|
+
probedBandCount = null;
|
|
116
184
|
loading = true;
|
|
117
185
|
error = null;
|
|
118
186
|
cogInfo = null;
|
|
@@ -120,8 +188,12 @@ $effect(() => {
|
|
|
120
188
|
histogram = null;
|
|
121
189
|
histogramTick = 0;
|
|
122
190
|
rescale = { ...DEFAULT_RESCALE };
|
|
191
|
+
nodataConfig = { ...DEFAULT_NODATA_CONFIG };
|
|
192
|
+
autoNodata = null;
|
|
123
193
|
isPaletteIndexed = false;
|
|
124
194
|
pixelValue = null;
|
|
195
|
+
smokeWarning = null;
|
|
196
|
+
smokeProbed = false;
|
|
125
197
|
bounds = undefined;
|
|
126
198
|
hasFittedOnce = false;
|
|
127
199
|
showControls = false;
|
|
@@ -140,34 +212,32 @@ function onMapReady(map: maplibregl.Map) {
|
|
|
140
212
|
// ─── Click handler for pixel inspection ──────────────────────────
|
|
141
213
|
|
|
142
214
|
function removeClickHandler() {
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
215
|
+
if (detachInspector) {
|
|
216
|
+
detachInspector();
|
|
217
|
+
detachInspector = null;
|
|
146
218
|
}
|
|
147
219
|
}
|
|
148
220
|
|
|
149
221
|
function setupClickHandler(map: maplibregl.Map) {
|
|
150
222
|
removeClickHandler();
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
223
|
+
detachInspector = attachPixelInspector<PixelValue>(map, {
|
|
224
|
+
probe: async ({ lng, lat, signal }) => {
|
|
225
|
+
if (!geotiffRef) return null;
|
|
226
|
+
// matches overview shown on screen
|
|
227
|
+
const targetRes = mapResolutionMetersPerPixel(map.getZoom(), lat);
|
|
228
|
+
const overview = selectOverviewForResolution(geotiffRef, targetRes);
|
|
229
|
+
return readPixelAtLngLat(geotiffRef, lng, lat, proj4DefRef, pool, signal, {
|
|
230
|
+
overview
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
onStart: () => {
|
|
234
|
+
inspecting = true;
|
|
235
|
+
},
|
|
236
|
+
onResult: (result) => {
|
|
163
237
|
pixelValue = result;
|
|
164
|
-
} catch {
|
|
165
|
-
pixelValue = null;
|
|
166
|
-
} finally {
|
|
167
238
|
inspecting = false;
|
|
168
239
|
}
|
|
169
|
-
};
|
|
170
|
-
map.on('click', clickHandlerRef);
|
|
240
|
+
});
|
|
171
241
|
}
|
|
172
242
|
|
|
173
243
|
// ─── Core load function ──────────────────────────────────────────
|
|
@@ -179,12 +249,30 @@ async function loadCog(map: maplibregl.Map) {
|
|
|
179
249
|
const url = await buildHttpsUrlAsync(tab);
|
|
180
250
|
if (signal.aborted) return;
|
|
181
251
|
resolvedHttpsUrl = url;
|
|
252
|
+
resolvedHrefForControls = url;
|
|
253
|
+
|
|
254
|
+
// One-shot storage smoke-test. lazycogs-style probe surfaces auth /
|
|
255
|
+
// CORS / bucket failures at open time as an amber pill, never blocks
|
|
256
|
+
// the layer mount. Aborts via the viewer's existing controller.
|
|
257
|
+
if (!smokeProbed) {
|
|
258
|
+
smokeProbed = true;
|
|
259
|
+
void (async () => {
|
|
260
|
+
try {
|
|
261
|
+
const result = await smokeTestHref(url, signal);
|
|
262
|
+
if (signal.aborted) return;
|
|
263
|
+
if (!result.ok) smokeWarning = result.reason;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (isAbortError(err)) return;
|
|
266
|
+
smokeWarning = handleLoadError(err);
|
|
267
|
+
}
|
|
268
|
+
})();
|
|
269
|
+
}
|
|
182
270
|
|
|
183
271
|
// Pre-flight: read first IFD to check if tiled (single range request).
|
|
184
272
|
let isTiled = true;
|
|
185
273
|
let preflightGeotiff: GeoTIFF | undefined;
|
|
186
274
|
try {
|
|
187
|
-
preflightGeotiff = await
|
|
275
|
+
preflightGeotiff = await loadGeoTIFF(url);
|
|
188
276
|
if (signal.aborted) return;
|
|
189
277
|
isTiled = preflightGeotiff.isTiled;
|
|
190
278
|
|
|
@@ -193,7 +281,7 @@ async function loadCog(map: maplibregl.Map) {
|
|
|
193
281
|
const _crs = preflightGeotiff.crs;
|
|
194
282
|
void _crs;
|
|
195
283
|
} catch (crsErr) {
|
|
196
|
-
const msg = crsErr
|
|
284
|
+
const msg = handleLoadError(crsErr) ?? String(crsErr);
|
|
197
285
|
error = `Unsupported CRS: ${msg}`;
|
|
198
286
|
loading = false;
|
|
199
287
|
return;
|
|
@@ -207,7 +295,7 @@ async function loadCog(map: maplibregl.Map) {
|
|
|
207
295
|
// recognized as being in a supported file format" on the same file).
|
|
208
296
|
// Surface a clear message and bail — COGLayer would re-invoke the
|
|
209
297
|
// same loader and throw the identical error uncaught during update.
|
|
210
|
-
const msg = preflightErr
|
|
298
|
+
const msg = handleLoadError(preflightErr) ?? String(preflightErr);
|
|
211
299
|
if (/Only tiff supported version|not a tiff|Invalid.*magic/i.test(msg)) {
|
|
212
300
|
error = t('map.cogInvalidTiff');
|
|
213
301
|
loading = false;
|
|
@@ -233,6 +321,22 @@ async function loadCog(map: maplibregl.Map) {
|
|
|
233
321
|
|
|
234
322
|
// Set default band config
|
|
235
323
|
bandConfig = defaultBandConfig(preflightGeotiff.count, sampleFormatRef);
|
|
324
|
+
probedBandCount = preflightGeotiff.count;
|
|
325
|
+
|
|
326
|
+
// Surface GDAL_NODATA + a shader-space rescale seed (when present) so
|
|
327
|
+
// the nodata hint pill and rescale slider have meaningful defaults
|
|
328
|
+
// before the first tile decodes — matches source-cooperative/cog-viewer
|
|
329
|
+
// UX. The slider operates in normalized [0, 1] shader space, so
|
|
330
|
+
// `seedRescaleFromGeotiff` divides GDAL STATISTICS_MIN/MAX by the
|
|
331
|
+
// sample-format factor and falls back to a p2/p98 histogram + the
|
|
332
|
+
// bit-depth-aware default.
|
|
333
|
+
autoNodata = readGdalNodata(preflightGeotiff);
|
|
334
|
+
try {
|
|
335
|
+
rescale = await seedRescaleFromGeotiff(preflightGeotiff, { signal });
|
|
336
|
+
} catch {
|
|
337
|
+
// fall through, defaults remain
|
|
338
|
+
}
|
|
339
|
+
if (signal.aborted) return;
|
|
236
340
|
}
|
|
237
341
|
|
|
238
342
|
if (!isTiled && preflightGeotiff) {
|
|
@@ -259,33 +363,56 @@ async function loadCog(map: maplibregl.Map) {
|
|
|
259
363
|
buildAndAddLayer(map, preflightGeotiff, signal);
|
|
260
364
|
} catch (err) {
|
|
261
365
|
if (signal.aborted) return;
|
|
262
|
-
if (err
|
|
263
|
-
error =
|
|
366
|
+
if (isAbortError(err)) return;
|
|
367
|
+
error = handleLoadError(err);
|
|
264
368
|
loading = false;
|
|
265
369
|
}
|
|
266
370
|
}
|
|
267
371
|
|
|
268
372
|
// ─── Build & add COGLayer ────────────────────────────────────────
|
|
269
373
|
|
|
270
|
-
|
|
374
|
+
// Build the pipeline props (getTileData/renderTile/etc) for the current state.
|
|
375
|
+
// When the configurable CPU path applies, the tile loader is created once per
|
|
376
|
+
// GeoTIFF identity and its `getTileData` reference is reused across rebuilds so
|
|
377
|
+
// deck.gl's TileLayer cache survives band/ramp swaps. Only `renderTile` and
|
|
378
|
+
// downstream uniforms vary across style changes.
|
|
379
|
+
function buildPipelineProps(geotiff: GeoTIFF | undefined): Record<string, unknown> {
|
|
380
|
+
if (!geotiff || !bandConfig) {
|
|
381
|
+
return geotiff ? selectCogPipeline(geotiff, { bandConfig, rescale }) : {};
|
|
382
|
+
}
|
|
383
|
+
if (needsCustomPipelineForConfig(geotiff, bandConfig)) {
|
|
384
|
+
if (!tileLoaderRef) {
|
|
385
|
+
tileLoaderRef = createConfigurableGetTileData(geotiff, bandConfig);
|
|
386
|
+
} else {
|
|
387
|
+
tileLoaderRef.updateConfig(bandConfig);
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
getTileData: tileLoaderRef.getTileData,
|
|
391
|
+
renderTile: buildCustomRenderTile(bandConfig, rescale)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
// Library-default or rescaled-only path. The loader (if previously seeded)
|
|
395
|
+
// is harmless to keep, but the upcoming rebuild won't reference it.
|
|
396
|
+
return selectCogPipeline(geotiff, { bandConfig, rescale });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function buildCogLayer(
|
|
271
400
|
map: maplibregl.Map,
|
|
272
401
|
preflightGeotiff: GeoTIFF | undefined,
|
|
273
402
|
signal: AbortSignal
|
|
274
|
-
) {
|
|
403
|
+
): COGLayer {
|
|
275
404
|
// Pick the library-default or one of three custom pipelines. Empty when the
|
|
276
405
|
// library-default uint path runs unchanged.
|
|
277
|
-
const customProps = preflightGeotiff
|
|
278
|
-
? selectCogPipeline(preflightGeotiff, { bandConfig, rescale })
|
|
279
|
-
: {};
|
|
406
|
+
const customProps = buildPipelineProps(preflightGeotiff);
|
|
280
407
|
|
|
281
408
|
// Apply upstream-bug workarounds in place (overview filter, 4326 bbox clamp).
|
|
282
409
|
if (preflightGeotiff) normalizeCogGeotiff(preflightGeotiff);
|
|
283
410
|
|
|
284
411
|
const cogInput = preflightGeotiff ?? resolvedHttpsUrl ?? '';
|
|
285
412
|
|
|
286
|
-
// Cast: `onViewportLoad` is forwarded by
|
|
287
|
-
//
|
|
288
|
-
//
|
|
413
|
+
// Cast: `onViewportLoad` is forwarded natively by COGLayer's RasterTileLayer
|
|
414
|
+
// base in 0.7.0 (deck.gl-raster PR #546), but COGLayer's generated .d.ts does
|
|
415
|
+
// not surface it.
|
|
289
416
|
const cogProps: any = {
|
|
290
417
|
// Stable id per tab so rebuilds on band/style change don't force deck.gl
|
|
291
418
|
// to treat this as a brand-new layer and drop cached tile state.
|
|
@@ -351,7 +478,18 @@ function buildAndAddLayer(
|
|
|
351
478
|
loading = false;
|
|
352
479
|
}
|
|
353
480
|
};
|
|
354
|
-
|
|
481
|
+
return new COGLayer(cogProps);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// First-mount: create the MapboxOverlay once and attach via addControl.
|
|
485
|
+
// Subsequent style changes go through pushLayer() which only calls setProps,
|
|
486
|
+
// preserving deck.gl's WebGL context and tile cache.
|
|
487
|
+
function buildAndAddLayer(
|
|
488
|
+
map: maplibregl.Map,
|
|
489
|
+
preflightGeotiff: GeoTIFF | undefined,
|
|
490
|
+
signal: AbortSignal
|
|
491
|
+
) {
|
|
492
|
+
const layer = buildCogLayer(map, preflightGeotiff, signal);
|
|
355
493
|
|
|
356
494
|
const overlay = new MapboxOverlay({
|
|
357
495
|
interleaved: false,
|
|
@@ -368,6 +506,15 @@ function buildAndAddLayer(
|
|
|
368
506
|
map.addControl(overlay as unknown as maplibregl.IControl);
|
|
369
507
|
}
|
|
370
508
|
|
|
509
|
+
// Style-change update path: swap layers in place via setProps. Identity of the
|
|
510
|
+
// COGLayer's `id` and `getTileData` is preserved so deck.gl reconciles the
|
|
511
|
+
// existing layer instance and keeps its tile cache.
|
|
512
|
+
function pushLayer() {
|
|
513
|
+
if (!mapRef || !geotiffRef || !overlayRef) return;
|
|
514
|
+
const layer = buildCogLayer(mapRef, geotiffRef, abortController.signal);
|
|
515
|
+
overlayRef.setProps({ layers: [layer] });
|
|
516
|
+
}
|
|
517
|
+
|
|
371
518
|
// ─── Viewport-scoped histogram aggregation ───────────────────────
|
|
372
519
|
|
|
373
520
|
/**
|
|
@@ -418,35 +565,41 @@ function handleConfigChange(newConfig: BandConfig) {
|
|
|
418
565
|
histogramTick = 0;
|
|
419
566
|
if (!mapRef || !geotiffRef || !isTiledRef) return;
|
|
420
567
|
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
} catch {
|
|
426
|
-
/* already removed */
|
|
427
|
-
}
|
|
428
|
-
overlayRef = null;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Rebuild with new config
|
|
432
|
-
buildAndAddLayer(mapRef, geotiffRef, abortController.signal);
|
|
568
|
+
// Swap layers in place: deck.gl diffs on layer id and reuses the stable
|
|
569
|
+
// `getTileData` reference held by `tileLoaderRef`, so the tile cache and
|
|
570
|
+
// in-flight fetches survive band/style changes.
|
|
571
|
+
pushLayer();
|
|
433
572
|
}
|
|
434
573
|
|
|
435
574
|
function handleRescaleChange(next: RescaleConfig) {
|
|
436
575
|
rescale = next;
|
|
437
576
|
if (!mapRef || !geotiffRef || !isTiledRef) return;
|
|
577
|
+
pushLayer();
|
|
578
|
+
}
|
|
438
579
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
580
|
+
// ─── Unified picker change handlers ──────────────────────────────
|
|
581
|
+
|
|
582
|
+
function handleCompositeChange(next: ChannelComposite): void {
|
|
583
|
+
if (!bandConfig) return;
|
|
584
|
+
if (bandConfig.mode === 'rgb') {
|
|
585
|
+
handleConfigChange({
|
|
586
|
+
...bandConfig,
|
|
587
|
+
rBand: next.r.bandIndex,
|
|
588
|
+
gBand: next.g.bandIndex,
|
|
589
|
+
bBand: next.b.bandIndex
|
|
590
|
+
});
|
|
591
|
+
} else {
|
|
592
|
+
handleConfigChange({ ...bandConfig, band: next.r.bandIndex });
|
|
448
593
|
}
|
|
449
|
-
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function handleModeChange(m: 'rgb' | 'single'): void {
|
|
597
|
+
if (!bandConfig) return;
|
|
598
|
+
handleConfigChange({ ...bandConfig, mode: m });
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function handleBandConfigChange(next: BandConfig): void {
|
|
602
|
+
handleConfigChange(next);
|
|
450
603
|
}
|
|
451
604
|
|
|
452
605
|
// ─── Cleanup ─────────────────────────────────────────────────────
|
|
@@ -465,9 +618,12 @@ function cleanup() {
|
|
|
465
618
|
mapRef = null;
|
|
466
619
|
overlayRef = null;
|
|
467
620
|
geotiffRef = null;
|
|
621
|
+
tileLoaderRef = null;
|
|
468
622
|
proj4DefRef = null;
|
|
469
623
|
pixelValue = null;
|
|
470
624
|
resolvedHttpsUrl = null;
|
|
625
|
+
resolvedHrefForControls = null;
|
|
626
|
+
probedBandCount = null;
|
|
471
627
|
}
|
|
472
628
|
|
|
473
629
|
$effect(() => {
|
|
@@ -484,7 +640,7 @@ onDestroy(cleanup);
|
|
|
484
640
|
</div>
|
|
485
641
|
|
|
486
642
|
<!-- Top-left: Loading + metadata badges -->
|
|
487
|
-
<div class="pointer-events-none absolute left-2 top-2 z-10 flex flex-col gap-1">
|
|
643
|
+
<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
644
|
{#if loading}
|
|
489
645
|
<div
|
|
490
646
|
class="rounded bg-card/80 px-2 py-1 text-xs text-card-foreground backdrop-blur-sm"
|
|
@@ -512,14 +668,23 @@ onDestroy(cleanup);
|
|
|
512
668
|
{error}
|
|
513
669
|
</div>
|
|
514
670
|
{/if}
|
|
671
|
+
|
|
672
|
+
{#if smokeWarning && !error}
|
|
673
|
+
<div
|
|
674
|
+
class="pointer-events-auto max-w-sm rounded bg-amber-900/80 px-2 py-1 text-xs text-amber-100"
|
|
675
|
+
title={t('stac.smokeWarningHint')}
|
|
676
|
+
>
|
|
677
|
+
{t('stac.smokeWarning', { reason: smokeWarning })}
|
|
678
|
+
</div>
|
|
679
|
+
{/if}
|
|
515
680
|
</div>
|
|
516
681
|
|
|
517
682
|
<!-- Top-right: Info + Style buttons -->
|
|
518
683
|
{#if cogInfo}
|
|
519
|
-
<div class="absolute right-2 top-2 z-10 flex gap-1">
|
|
684
|
+
<div class="absolute right-2 top-2 z-10 flex gap-1" style="touch-action: manipulation;">
|
|
520
685
|
{#if bandConfig}
|
|
521
686
|
<button
|
|
522
|
-
class="rounded bg-card/80 px-
|
|
687
|
+
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
688
|
class:ring-1={showControls}
|
|
524
689
|
class:ring-primary={showControls}
|
|
525
690
|
onclick={() => {
|
|
@@ -531,7 +696,7 @@ onDestroy(cleanup);
|
|
|
531
696
|
</button>
|
|
532
697
|
{/if}
|
|
533
698
|
<button
|
|
534
|
-
class="rounded bg-card/80 px-
|
|
699
|
+
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
700
|
class:ring-1={showInfo}
|
|
536
701
|
class:ring-primary={showInfo}
|
|
537
702
|
onclick={() => {
|
|
@@ -543,23 +708,39 @@ onDestroy(cleanup);
|
|
|
543
708
|
</button>
|
|
544
709
|
</div>
|
|
545
710
|
|
|
546
|
-
<!-- Band/Color controls panel
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
711
|
+
<!-- Band/Color controls panel. Kept mounted so slider drag state and focus
|
|
712
|
+
survive every visibility toggle; only the `hidden` class is flipped. -->
|
|
713
|
+
{#if bandConfig}
|
|
714
|
+
<div class={showControls ? 'contents' : 'hidden'}>
|
|
715
|
+
<CogControls
|
|
716
|
+
assets={cogControlsAssets}
|
|
717
|
+
composite={cogControlsComposite}
|
|
718
|
+
onCompositeChange={handleCompositeChange}
|
|
719
|
+
presets={[]}
|
|
720
|
+
activePresetId=""
|
|
721
|
+
onPresetChange={() => {}}
|
|
722
|
+
mode={bandConfig?.mode ?? 'rgb'}
|
|
723
|
+
onModeChange={handleModeChange}
|
|
724
|
+
{bandConfig}
|
|
725
|
+
bandCount={probedBandCount ?? cogInfo.bandCount}
|
|
726
|
+
onBandConfigChange={handleBandConfigChange}
|
|
727
|
+
{rescale}
|
|
728
|
+
rescaleApplicable={rescaleApplicable}
|
|
729
|
+
onRescaleChange={handleRescaleChange}
|
|
730
|
+
{histogram}
|
|
731
|
+
nodata={nodataConfig}
|
|
732
|
+
{autoNodata}
|
|
733
|
+
onNodataChange={(next) => {
|
|
734
|
+
nodataConfig = next;
|
|
735
|
+
}}
|
|
736
|
+
/>
|
|
737
|
+
</div>
|
|
557
738
|
{/if}
|
|
558
739
|
|
|
559
740
|
<!-- Info panel -->
|
|
560
741
|
{#if showInfo}
|
|
561
742
|
<div
|
|
562
|
-
class="absolute
|
|
743
|
+
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
744
|
>
|
|
564
745
|
<h3 class="mb-2 font-medium">{t('map.cogInfo')}</h3>
|
|
565
746
|
<dl class="space-y-1.5">
|
|
@@ -579,45 +760,17 @@ onDestroy(cleanup);
|
|
|
579
760
|
{/if}
|
|
580
761
|
|
|
581
762
|
<!-- Bottom-left: Pixel value on click -->
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
<div class="space-y-0.5 text-muted-foreground">
|
|
596
|
-
<div>
|
|
597
|
-
{pixelValue.lat.toFixed(6)}°, {pixelValue.lng.toFixed(6)}°
|
|
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}
|
|
763
|
+
<PixelInspectorPanel
|
|
764
|
+
lng={pixelValue?.lng ?? null}
|
|
765
|
+
lat={pixelValue?.lat ?? null}
|
|
766
|
+
rows={pixelValue
|
|
767
|
+
? (pixelValue.values.map((v, i) => ({
|
|
768
|
+
label: `${t('cog.band')} ${i + 1}`,
|
|
769
|
+
value: v
|
|
770
|
+
})) satisfies PixelInspectorRow[])
|
|
771
|
+
: null}
|
|
772
|
+
footnote={pixelValue ? `px (${pixelValue.col}, ${pixelValue.row})` : undefined}
|
|
773
|
+
onClose={() => (pixelValue = null)}
|
|
774
|
+
{inspecting}
|
|
775
|
+
/>
|
|
623
776
|
</div>
|