@walkthru-earth/objex 1.3.0 → 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.
- package/LICENSE +5 -0
- package/README.md +20 -12
- package/dist/components/browser/FileTreeSidebar.svelte +32 -17
- package/dist/components/layout/AboutSheet.svelte +5 -2
- package/dist/components/layout/ConnectionDialog.svelte +1 -1
- package/dist/components/layout/SettingsSheet.svelte +237 -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 +1 -1
- package/dist/components/layout/TabBar.svelte +2 -2
- 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 +2 -2
- package/dist/components/viewers/CodeViewer.svelte +31 -22
- package/dist/components/viewers/CogControls.svelte +338 -184
- package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
- package/dist/components/viewers/CogViewer.svelte +320 -119
- package/dist/components/viewers/CopcViewer.svelte +1 -1
- package/dist/components/viewers/FlatGeobufViewer.svelte +1 -1
- package/dist/components/viewers/GeoParquetMapViewer.svelte +6 -6
- package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/ImageViewer.svelte +2 -2
- package/dist/components/viewers/MarkdownViewer.svelte +12 -9
- package/dist/components/viewers/MediaViewer.svelte +2 -2
- package/dist/components/viewers/ModelViewer.svelte +1 -1
- package/dist/components/viewers/MultiCogViewer.svelte +467 -102
- package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/NotebookViewer.svelte +6 -3
- package/dist/components/viewers/PdfViewer.svelte +2 -2
- package/dist/components/viewers/PmtilesViewer.svelte +3 -6
- package/dist/components/viewers/RawViewer.svelte +6 -3
- package/dist/components/viewers/StacMapViewer.svelte +10 -2
- package/dist/components/viewers/StacMosaicViewer.svelte +1800 -362
- package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/StacTabViewer.svelte +24 -13
- package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/TableGrid.svelte +4 -4
- package/dist/components/viewers/TableStatusBar.svelte +1 -1
- package/dist/components/viewers/TableToolbar.svelte +1 -1
- package/dist/components/viewers/TableViewer.svelte +25 -17
- package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
- package/dist/components/viewers/ViewerRouter.svelte +16 -8
- package/dist/components/viewers/ZarrMapViewer.svelte +11 -9
- package/dist/components/viewers/ZarrViewer.svelte +4 -4
- 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 +1 -1
- package/dist/components/viewers/map/MapContainer.svelte +37 -11
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +1 -1
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +1 -1
- 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/file-icons/index.d.ts +1 -1
- package/dist/file-icons/index.js +1 -1
- package/dist/i18n/ar.js +110 -2
- package/dist/i18n/en.js +110 -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 +465 -0
- package/dist/query/wasm.d.ts +8 -0
- package/dist/query/wasm.js +304 -2
- package/dist/storage/presign.js +1 -1
- package/dist/storage/providers.js +5 -5
- 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 +200 -60
- package/dist/utils/cog.js +377 -114
- package/dist/utils/colormap-sprite.d.ts +0 -9
- package/dist/utils/colormap-sprite.js +0 -21
- package/dist/utils/deck.d.ts +16 -12
- package/dist/utils/deck.js +10 -4
- package/dist/utils/pmtiles-tile.js +2 -2
- 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/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,7 +1,24 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { MapboxOverlay } from '@deck.gl/mapbox';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { DecoderPool, GeoTIFF } from '@developmentseed/geotiff';
|
|
4
|
+
import {
|
|
5
|
+
applyPreset,
|
|
6
|
+
attachPixelInspector,
|
|
7
|
+
availablePresets,
|
|
8
|
+
type ChannelComposite,
|
|
9
|
+
type CogAsset,
|
|
10
|
+
compositeFromUrl,
|
|
11
|
+
compositeToUrl,
|
|
12
|
+
extractCogAssets,
|
|
13
|
+
isSingleAssetComposite,
|
|
14
|
+
isStacItem,
|
|
15
|
+
PRESETS,
|
|
16
|
+
pickNaturalColorComposite,
|
|
17
|
+
presetMatchesComposite,
|
|
18
|
+
type StacItem,
|
|
19
|
+
type StacRoutableKind,
|
|
20
|
+
smokeTestHref
|
|
21
|
+
} from '@walkthru-earth/objex-utils';
|
|
5
22
|
import type maplibregl from 'maplibre-gl';
|
|
6
23
|
import { onDestroy, untrack } from 'svelte';
|
|
7
24
|
import { t } from '../../i18n/index.svelte.js';
|
|
@@ -11,78 +28,93 @@ import { connectionStore } from '../../stores/connections.svelte.js';
|
|
|
11
28
|
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
12
29
|
import type { Tab } from '../../types.js';
|
|
13
30
|
import {
|
|
14
|
-
|
|
31
|
+
buildHistogramFromGeotiff,
|
|
15
32
|
clampBounds,
|
|
16
33
|
cleanupNativeBitmap,
|
|
17
34
|
createEpsgResolver,
|
|
35
|
+
DEFAULT_NODATA_CONFIG,
|
|
36
|
+
defaultRescaleForGeotiff,
|
|
18
37
|
fitCogBounds,
|
|
19
|
-
|
|
38
|
+
loadGeoTIFF,
|
|
39
|
+
mapResolutionMetersPerPixel,
|
|
40
|
+
type NodataConfig,
|
|
41
|
+
normalizeCogGeotiff,
|
|
42
|
+
type PixelValue,
|
|
43
|
+
percentileFromHistogram,
|
|
44
|
+
type RescaleConfig,
|
|
45
|
+
readGdalNodata,
|
|
46
|
+
readPixelAtLngLat,
|
|
47
|
+
resolveNodata,
|
|
48
|
+
resolveProj4Def,
|
|
49
|
+
selectOverviewForResolution
|
|
20
50
|
} from '../../utils/cog.js';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
type BandSlot,
|
|
24
|
-
extractSentinelBandAssets,
|
|
25
|
-
hasRgbBands,
|
|
26
|
-
isStacItem,
|
|
27
|
-
type StacItem,
|
|
28
|
-
type StacRoutableKind
|
|
29
|
-
} from '../../utils/stac.js';
|
|
30
|
-
import { buildHttpsUrlAsync } from '../../utils/url.js';
|
|
51
|
+
import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
|
|
52
|
+
import { getUrlViewParams, updateUrlViewParams } from '../../utils/url-state.js';
|
|
31
53
|
import CogControls from './CogControls.svelte';
|
|
54
|
+
import { buildRgbLayer } from './cog/buildRgbLayer.js';
|
|
55
|
+
import PixelInspectorPanel, { type PixelInspectorRow } from './cog/PixelInspectorPanel.svelte';
|
|
32
56
|
import MapContainer from './map/MapContainer.svelte';
|
|
33
57
|
|
|
34
|
-
interface Preset {
|
|
35
|
-
id: string;
|
|
36
|
-
labelKey: string;
|
|
37
|
-
composite: { r: BandSlot; g: BandSlot; b: BandSlot };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const PRESETS: Preset[] = [
|
|
41
|
-
{
|
|
42
|
-
id: 'true-color',
|
|
43
|
-
labelKey: 'map.multiCogPreset.trueColor',
|
|
44
|
-
composite: { r: 'red', g: 'green', b: 'blue' }
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
id: 'false-color-ir',
|
|
48
|
-
labelKey: 'map.multiCogPreset.falseColorIR',
|
|
49
|
-
composite: { r: 'nir', g: 'red', b: 'green' }
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: 'swir',
|
|
53
|
-
labelKey: 'map.multiCogPreset.swir',
|
|
54
|
-
composite: { r: 'swir2', g: 'swir1', b: 'red' }
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
id: 'vegetation',
|
|
58
|
-
labelKey: 'map.multiCogPreset.vegetation',
|
|
59
|
-
composite: { r: 'nir', g: 'swir1', b: 'red' }
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
id: 'agriculture',
|
|
63
|
-
labelKey: 'map.multiCogPreset.agriculture',
|
|
64
|
-
composite: { r: 'swir1', g: 'nir', b: 'blue' }
|
|
65
|
-
}
|
|
66
|
-
];
|
|
67
|
-
|
|
68
58
|
let { tab, classified }: { tab: Tab; classified?: StacRoutableKind } = $props();
|
|
69
59
|
|
|
70
60
|
let loading = $state(true);
|
|
71
61
|
let error = $state<string | null>(null);
|
|
72
62
|
let showControls = $state(false);
|
|
73
63
|
let bounds = $state<[number, number, number, number] | undefined>();
|
|
74
|
-
let activePresetId = $state<string>('
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
64
|
+
let activePresetId = $state<string>('natural-color');
|
|
65
|
+
// Default rescale is bit-depth aware: uint8 visual COGs (Sentinel-2 `visual`,
|
|
66
|
+
// NAIP `image`) want max=0.3, uint16 reflectance bands (S2 raw `nir`/`swir`/`red`)
|
|
67
|
+
// want ~0.05 because r16unorm divides raw values by 65535, leaving typical
|
|
68
|
+
// reflectance ~0.012-0.046 in shader space. The actual default is reseeded
|
|
69
|
+
// from the first preflighted GeoTIFF in `buildAndAddLayer`. Until the user
|
|
70
|
+
// drags the slider, preset/composite swaps continue to refresh the default
|
|
71
|
+
// so switching from a uint8 visual to a uint16 multi-asset preset doesn't
|
|
72
|
+
// render near-black tiles.
|
|
78
73
|
let rescale = $state<RescaleConfig>({ min: 0, max: 0.3 });
|
|
74
|
+
let userTouchedRescale = false;
|
|
75
|
+
// Single-band histogram baked once from the R-channel preflight's smallest
|
|
76
|
+
// overview, in the same shader-space [0,1] domain the rescale slider uses.
|
|
77
|
+
// Backs the histogram overlay in CogControls. Recomputed when the R-channel
|
|
78
|
+
// asset changes (tracked by histogramAssetKey) so swapping bands gives the
|
|
79
|
+
// user an accurate distribution to scrub against.
|
|
80
|
+
let histogram = $state.raw<Uint32Array | null>(null);
|
|
81
|
+
let histogramAssetKey: string | null = null;
|
|
82
|
+
// User-facing nodata override (Auto/Value/Off). `autoNodata` is the GDAL_NODATA
|
|
83
|
+
// value read from the R-channel preflight; Auto mode resolves to it via
|
|
84
|
+
// `resolveNodata()` at layer-build time.
|
|
85
|
+
let nodataConfig = $state<NodataConfig>({ ...DEFAULT_NODATA_CONFIG });
|
|
86
|
+
let autoNodata = $state<number | null>(null);
|
|
79
87
|
|
|
80
|
-
let
|
|
88
|
+
let assets = $state.raw<CogAsset[]>([]);
|
|
89
|
+
let composite = $state.raw<ChannelComposite | null>(null);
|
|
81
90
|
let abortController = new AbortController();
|
|
82
91
|
let mapRef: maplibregl.Map | null = null;
|
|
83
92
|
let overlayRef: MapboxOverlay | null = null;
|
|
84
93
|
let hasFittedOnce = false;
|
|
85
94
|
let presignCache = new Map<string, Promise<string>>();
|
|
95
|
+
|
|
96
|
+
// Pixel inspection: same UX as CogViewer / StacMosaicViewer. Click → read one
|
|
97
|
+
// pixel from each active composite channel's GeoTIFF and show channel/asset/value.
|
|
98
|
+
type MultiPixelEntry = {
|
|
99
|
+
channel: 'R' | 'G' | 'B' | 'A';
|
|
100
|
+
assetKey: string;
|
|
101
|
+
bandIndex: number;
|
|
102
|
+
value: number | null;
|
|
103
|
+
};
|
|
104
|
+
type MultiPixelValue = { lng: number; lat: number; entries: MultiPixelEntry[] };
|
|
105
|
+
let pixelValue = $state<MultiPixelValue | null>(null);
|
|
106
|
+
let inspecting = $state(false);
|
|
107
|
+
let proj4DefRef: string | null = null;
|
|
108
|
+
// Storage smoke-test result for the primary R-channel asset.
|
|
109
|
+
let smokeWarning = $state<string | null>(null);
|
|
110
|
+
let smokeProbed = false;
|
|
111
|
+
let detachInspector: (() => void) | null = null;
|
|
112
|
+
// Per-asset-key GeoTIFF cache. Opening the GeoTIFF up-front lets buildRgbLayer
|
|
113
|
+
// run selectCogPipeline (which inspects sampleFormat / band count) and emit a
|
|
114
|
+
// custom getTileData/renderTile pair that honors per-channel bandIndex picks.
|
|
115
|
+
// Without this, the single-asset multi-band path (e.g. Sentinel-2 `visual`,
|
|
116
|
+
// NAIP `image`) silently falls back to bands 0/1/2 regardless of the picker.
|
|
117
|
+
let geotiffCache = new Map<string, Promise<GeoTIFF>>();
|
|
86
118
|
let loadGen = 0;
|
|
87
119
|
let layerVersion = 0;
|
|
88
120
|
let rebuildTimer: number | null = null;
|
|
@@ -96,10 +128,7 @@ const REBUILD_INTERVAL_MS = 750;
|
|
|
96
128
|
let pool: DecoderPool | null = new DecoderPool();
|
|
97
129
|
const epsgResolver = createEpsgResolver();
|
|
98
130
|
|
|
99
|
-
const
|
|
100
|
-
const availablePresets = $derived(
|
|
101
|
-
PRESETS.filter((p) => bandMap[p.composite.r] && bandMap[p.composite.g] && bandMap[p.composite.b])
|
|
102
|
-
);
|
|
131
|
+
const presetsForItem = $derived(availablePresets(assets));
|
|
103
132
|
|
|
104
133
|
$effect(() => {
|
|
105
134
|
if (!tab) return;
|
|
@@ -127,16 +156,115 @@ function resetViewer(): void {
|
|
|
127
156
|
/* already destroyed */
|
|
128
157
|
}
|
|
129
158
|
}
|
|
159
|
+
removeClickHandler();
|
|
130
160
|
overlayRef = null;
|
|
131
|
-
|
|
161
|
+
assets = [];
|
|
162
|
+
composite = null;
|
|
132
163
|
presignCache = new Map();
|
|
164
|
+
geotiffCache = new Map();
|
|
133
165
|
loading = true;
|
|
134
166
|
error = null;
|
|
135
167
|
bounds = undefined;
|
|
136
|
-
activePresetId = '
|
|
168
|
+
activePresetId = 'natural-color';
|
|
137
169
|
rescale = { min: 0, max: 0.3 };
|
|
170
|
+
userTouchedRescale = false;
|
|
171
|
+
histogram = null;
|
|
172
|
+
histogramAssetKey = null;
|
|
173
|
+
nodataConfig = { ...DEFAULT_NODATA_CONFIG };
|
|
174
|
+
autoNodata = null;
|
|
138
175
|
hasFittedOnce = false;
|
|
139
176
|
showControls = false;
|
|
177
|
+
pixelValue = null;
|
|
178
|
+
inspecting = false;
|
|
179
|
+
proj4DefRef = null;
|
|
180
|
+
smokeWarning = null;
|
|
181
|
+
smokeProbed = false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function removeClickHandler(): void {
|
|
185
|
+
if (detachInspector) {
|
|
186
|
+
detachInspector();
|
|
187
|
+
detachInspector = null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function ensureGeotiff(assetKey: string): Promise<GeoTIFF | null> {
|
|
192
|
+
const asset = assets.find((a) => a.key === assetKey);
|
|
193
|
+
if (!asset) return null;
|
|
194
|
+
let promise = geotiffCache.get(assetKey);
|
|
195
|
+
if (!promise) {
|
|
196
|
+
promise = (async () => {
|
|
197
|
+
const url = await presignHref(asset.href);
|
|
198
|
+
const g = await loadGeoTIFF(url);
|
|
199
|
+
normalizeCogGeotiff(g);
|
|
200
|
+
return g;
|
|
201
|
+
})();
|
|
202
|
+
geotiffCache.set(assetKey, promise);
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
return await promise;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.warn('[MultiCogViewer] ensureGeotiff failed', { assetKey, err });
|
|
208
|
+
geotiffCache.delete(assetKey);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function setupClickHandler(map: maplibregl.Map): void {
|
|
214
|
+
removeClickHandler();
|
|
215
|
+
detachInspector = attachPixelInspector<MultiPixelValue>(map, {
|
|
216
|
+
probe: async ({ lng, lat, signal }) => {
|
|
217
|
+
const c = composite;
|
|
218
|
+
if (!c) return null;
|
|
219
|
+
// Match the overview that's currently on screen so the pixel readout
|
|
220
|
+
// reflects the visible decimation level. Computed once per click and
|
|
221
|
+
// re-picked per-channel because per-asset COGs may have different
|
|
222
|
+
// pyramids (Sentinel-2 SWIR is 20 m native, true color is 10 m).
|
|
223
|
+
const targetRes = mapResolutionMetersPerPixel(map.getZoom(), lat);
|
|
224
|
+
const channels: { channel: 'R' | 'G' | 'B' | 'A'; ref: typeof c.r | undefined }[] = [
|
|
225
|
+
{ channel: 'R', ref: c.r },
|
|
226
|
+
{ channel: 'G', ref: c.g },
|
|
227
|
+
{ channel: 'B', ref: c.b },
|
|
228
|
+
{ channel: 'A', ref: c.a }
|
|
229
|
+
];
|
|
230
|
+
const active = channels.filter(
|
|
231
|
+
(x): x is { channel: 'R' | 'G' | 'B' | 'A'; ref: NonNullable<typeof c.r> } => Boolean(x.ref)
|
|
232
|
+
);
|
|
233
|
+
const entries = await Promise.all(
|
|
234
|
+
active.map(async ({ channel, ref }): Promise<MultiPixelEntry> => {
|
|
235
|
+
const geotiff = await ensureGeotiff(ref.assetKey);
|
|
236
|
+
if (!geotiff || signal.aborted) {
|
|
237
|
+
return { channel, assetKey: ref.assetKey, bandIndex: ref.bandIndex, value: null };
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const overview = selectOverviewForResolution(geotiff, targetRes);
|
|
241
|
+
const result: PixelValue | null = await readPixelAtLngLat(
|
|
242
|
+
geotiff,
|
|
243
|
+
lng,
|
|
244
|
+
lat,
|
|
245
|
+
proj4DefRef,
|
|
246
|
+
pool,
|
|
247
|
+
signal,
|
|
248
|
+
{ overview }
|
|
249
|
+
);
|
|
250
|
+
const v = result?.values?.[ref.bandIndex] ?? null;
|
|
251
|
+
return { channel, assetKey: ref.assetKey, bandIndex: ref.bandIndex, value: v };
|
|
252
|
+
} catch {
|
|
253
|
+
return { channel, assetKey: ref.assetKey, bandIndex: ref.bandIndex, value: null };
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
if (signal.aborted) return null;
|
|
258
|
+
return { lng, lat, entries };
|
|
259
|
+
},
|
|
260
|
+
onStart: () => {
|
|
261
|
+
inspecting = true;
|
|
262
|
+
},
|
|
263
|
+
onResult: (result) => {
|
|
264
|
+
pixelValue = result;
|
|
265
|
+
inspecting = false;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
140
268
|
}
|
|
141
269
|
|
|
142
270
|
function scheduleLayerRebuild(map: maplibregl.Map, signal: AbortSignal): void {
|
|
@@ -210,7 +338,7 @@ async function loadItem(map: maplibregl.Map): Promise<void> {
|
|
|
210
338
|
loading = false;
|
|
211
339
|
return;
|
|
212
340
|
}
|
|
213
|
-
item = parsed
|
|
341
|
+
item = parsed;
|
|
214
342
|
}
|
|
215
343
|
if (!item) {
|
|
216
344
|
error = t('map.multiCogMissingBands');
|
|
@@ -218,13 +346,62 @@ async function loadItem(map: maplibregl.Map): Promise<void> {
|
|
|
218
346
|
return;
|
|
219
347
|
}
|
|
220
348
|
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
349
|
+
const next = extractCogAssets(item);
|
|
350
|
+
if (next.length < 1) {
|
|
223
351
|
error = t('map.multiCogMissingBands');
|
|
224
352
|
loading = false;
|
|
225
353
|
return;
|
|
226
354
|
}
|
|
227
|
-
|
|
355
|
+
assets = next;
|
|
356
|
+
|
|
357
|
+
// Hydrate composite: URL params first, then natural-color default.
|
|
358
|
+
const params = getUrlViewParams();
|
|
359
|
+
const fromUrl = compositeFromUrl(params, next);
|
|
360
|
+
console.debug('[MultiCogViewer] loadItem', {
|
|
361
|
+
assetKeys: next.map((a) => `${a.key}(bands=${a.bandCount},common=${a.eoCommon[0] ?? ''})`),
|
|
362
|
+
urlParams: Object.fromEntries(params.entries()),
|
|
363
|
+
fromUrl
|
|
364
|
+
});
|
|
365
|
+
if (fromUrl) {
|
|
366
|
+
composite = fromUrl;
|
|
367
|
+
const presetId = params.get('preset');
|
|
368
|
+
if (presetId && PRESETS.find((p) => p.id === presetId)) activePresetId = presetId;
|
|
369
|
+
else activePresetId = '';
|
|
370
|
+
} else {
|
|
371
|
+
const picked = pickNaturalColorComposite(next);
|
|
372
|
+
composite = picked?.composite ?? null;
|
|
373
|
+
activePresetId = picked?.source === 'rgb-bands' ? 'natural-color' : '';
|
|
374
|
+
}
|
|
375
|
+
console.debug('[MultiCogViewer] composite seeded', { composite, activePresetId });
|
|
376
|
+
|
|
377
|
+
if (!composite) {
|
|
378
|
+
error = t('map.multiCogMissingBands');
|
|
379
|
+
loading = false;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// One-shot storage smoke-test against the R-channel asset. lazycogs-style
|
|
384
|
+
// probe surfaces auth / CORS / bucket failures at open time as an amber
|
|
385
|
+
// pill, fires in parallel with the rest of the load. Aborts via the
|
|
386
|
+
// viewer's existing controller.
|
|
387
|
+
if (!smokeProbed) {
|
|
388
|
+
smokeProbed = true;
|
|
389
|
+
const rAsset = next.find((a) => a.key === composite!.r.assetKey);
|
|
390
|
+
if (rAsset) {
|
|
391
|
+
void (async () => {
|
|
392
|
+
try {
|
|
393
|
+
const url = await presignHref(rAsset.href);
|
|
394
|
+
const result = await smokeTestHref(url, signal);
|
|
395
|
+
if (gen !== loadGen || signal.aborted) return;
|
|
396
|
+
if (!result.ok) smokeWarning = result.reason;
|
|
397
|
+
} catch (err) {
|
|
398
|
+
if (err instanceof DOMException && err.name === 'AbortError') return;
|
|
399
|
+
if (gen !== loadGen) return;
|
|
400
|
+
smokeWarning = err instanceof Error ? err.message : String(err);
|
|
401
|
+
}
|
|
402
|
+
})();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
228
405
|
|
|
229
406
|
if (Array.isArray(item.bbox) && item.bbox.length >= 4) {
|
|
230
407
|
const clamped = clampBounds({
|
|
@@ -255,37 +432,149 @@ async function buildAndAddLayer(
|
|
|
255
432
|
version: number,
|
|
256
433
|
signal: AbortSignal
|
|
257
434
|
): Promise<void> {
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
435
|
+
const c = composite;
|
|
436
|
+
if (!c) return;
|
|
437
|
+
|
|
438
|
+
console.debug('[MultiCogViewer] buildAndAddLayer start', {
|
|
439
|
+
version,
|
|
440
|
+
composite: c,
|
|
441
|
+
rescale: { ...rescale }
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Pre-open the R-channel GeoTIFF on every path. For single-asset composites
|
|
445
|
+
// this lets buildRgbLayer run selectCogPipeline and honor per-channel
|
|
446
|
+
// bandIndex. For multi-asset composites (MultiCOGLayer) the GeoTIFF object
|
|
447
|
+
// is not consumed by the layer, but inspecting its tags lets us pick a
|
|
448
|
+
// bit-depth-appropriate default rescale so uint16 reflectance bands don't
|
|
449
|
+
// render near-black against a slider tuned for uint8 visuals.
|
|
450
|
+
let preflightGeotiff: GeoTIFF | null = null;
|
|
451
|
+
const rChannelKey = c.r.assetKey;
|
|
452
|
+
const rAsset = assets.find((a) => a.key === rChannelKey);
|
|
453
|
+
if (rAsset) {
|
|
454
|
+
let promise = geotiffCache.get(rChannelKey);
|
|
455
|
+
if (!promise) {
|
|
456
|
+
promise = (async () => {
|
|
457
|
+
const url = await presignHref(rAsset.href);
|
|
458
|
+
const g = await loadGeoTIFF(url);
|
|
459
|
+
normalizeCogGeotiff(g);
|
|
460
|
+
return g;
|
|
461
|
+
})();
|
|
462
|
+
geotiffCache.set(rChannelKey, promise);
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
preflightGeotiff = await promise;
|
|
466
|
+
} catch (err) {
|
|
467
|
+
console.warn('[MultiCogViewer] preflight GeoTIFF open failed', {
|
|
468
|
+
assetKey: rChannelKey,
|
|
469
|
+
err
|
|
470
|
+
});
|
|
471
|
+
geotiffCache.delete(rChannelKey);
|
|
472
|
+
preflightGeotiff = null;
|
|
473
|
+
}
|
|
474
|
+
if (signal.aborted) return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (preflightGeotiff && !userTouchedRescale) {
|
|
478
|
+
const next = defaultRescaleForGeotiff(preflightGeotiff);
|
|
479
|
+
if (next.min !== rescale.min || next.max !== rescale.max) {
|
|
480
|
+
console.debug('[MultiCogViewer] reseeding rescale from preflight', {
|
|
481
|
+
assetKey: rChannelKey,
|
|
482
|
+
prev: { ...rescale },
|
|
483
|
+
next
|
|
484
|
+
});
|
|
485
|
+
rescale = next;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Surface GDAL_NODATA from the R-channel preflight so the CogControls nodata
|
|
490
|
+
// hint pill and the `Auto` resolved value have a real number to show before
|
|
491
|
+
// the first tile decodes.
|
|
492
|
+
if (preflightGeotiff) {
|
|
493
|
+
autoNodata = readGdalNodata(preflightGeotiff);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Bake the histogram once per R-channel asset. Cheap (one overview tile),
|
|
497
|
+
// and gives CogControls a real distribution to overlay behind the slider.
|
|
498
|
+
// When the user hasn't touched the slider, also reseed rescale to the
|
|
499
|
+
// p2/p98 of that distribution so the thumbs land where the data actually
|
|
500
|
+
// lives instead of at the bit-depth-aware default. This is what gives a
|
|
501
|
+
// preset switch (e.g. true-color → vegetation, red → swir16) auto-contrast
|
|
502
|
+
// without the user having to re-drag the slider every time.
|
|
503
|
+
if (preflightGeotiff && histogramAssetKey !== rChannelKey) {
|
|
504
|
+
histogramAssetKey = rChannelKey;
|
|
505
|
+
void (async () => {
|
|
506
|
+
const bins = await buildHistogramFromGeotiff(preflightGeotiff, signal);
|
|
507
|
+
if (signal.aborted) return;
|
|
508
|
+
if (histogramAssetKey !== rChannelKey) return; // user swapped while baking
|
|
509
|
+
histogram = bins;
|
|
510
|
+
if (!userTouchedRescale && bins) {
|
|
511
|
+
const lo = percentileFromHistogram(bins, 0.02);
|
|
512
|
+
const hi = percentileFromHistogram(bins, 0.98);
|
|
513
|
+
if (lo !== null && hi !== null && hi > lo) {
|
|
514
|
+
console.debug('[MultiCogViewer] reseeding rescale from histogram p2/p98', {
|
|
515
|
+
assetKey: rChannelKey,
|
|
516
|
+
prev: { ...rescale },
|
|
517
|
+
next: { min: lo, max: hi }
|
|
518
|
+
});
|
|
519
|
+
rescale = { min: lo, max: hi };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
})();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Resolve proj4 once for pixel inspection. All band assets in a STAC Item
|
|
526
|
+
// share the same source CRS so the R-channel preflight is sufficient.
|
|
527
|
+
if (preflightGeotiff && proj4DefRef === null) {
|
|
528
|
+
try {
|
|
529
|
+
proj4DefRef = await resolveProj4Def(preflightGeotiff.crs, signal);
|
|
530
|
+
} catch {
|
|
531
|
+
proj4DefRef = null;
|
|
532
|
+
}
|
|
533
|
+
if (signal.aborted) return;
|
|
266
534
|
}
|
|
267
535
|
|
|
268
|
-
|
|
536
|
+
// Multi-asset path doesn't consume the GeoTIFF object; only single-asset
|
|
537
|
+
// flows it through to selectCogPipeline. Drop the reference so buildRgbLayer
|
|
538
|
+
// doesn't try to translate per-channel bandIndex on a path that can't honor it.
|
|
539
|
+
const preflightForLayer = isSingleAssetComposite(c) ? preflightGeotiff : null;
|
|
540
|
+
|
|
541
|
+
const resolvedNodata = resolveNodata(nodataConfig, autoNodata);
|
|
542
|
+
const { layer, kind } = await buildRgbLayer({
|
|
269
543
|
id: `multicog-${tab.id}-v${version}`,
|
|
270
|
-
|
|
271
|
-
composite:
|
|
272
|
-
|
|
273
|
-
|
|
544
|
+
assets,
|
|
545
|
+
composite: c,
|
|
546
|
+
rescale: { ...rescale },
|
|
547
|
+
resolveHref: presignHref,
|
|
548
|
+
pool,
|
|
274
549
|
epsgResolver,
|
|
275
550
|
signal,
|
|
276
|
-
|
|
551
|
+
preflightGeotiff: preflightForLayer,
|
|
552
|
+
noDataVal: resolvedNodata,
|
|
553
|
+
onLoad: ({ bounds: nextBounds }) => {
|
|
277
554
|
if (version !== layerVersion || signal.aborted) return;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
555
|
+
if (nextBounds) {
|
|
556
|
+
const clamped = clampBounds(nextBounds);
|
|
557
|
+
if (!hasFittedOnce) {
|
|
558
|
+
bounds = [clamped.west, clamped.south, clamped.east, clamped.north];
|
|
559
|
+
fitCogBounds(map, clamped);
|
|
560
|
+
hasFittedOnce = true;
|
|
561
|
+
}
|
|
283
562
|
}
|
|
284
563
|
loading = false;
|
|
285
564
|
}
|
|
286
565
|
});
|
|
287
566
|
|
|
567
|
+
console.debug('[MultiCogViewer] buildAndAddLayer built', {
|
|
568
|
+
version,
|
|
569
|
+
kind,
|
|
570
|
+
layerId: (layer as { id?: string }).id,
|
|
571
|
+
hasOverlay: !!overlayRef
|
|
572
|
+
});
|
|
288
573
|
if (overlayRef) {
|
|
574
|
+
console.debug('[MultiCogViewer] overlayRef.setProps swapping layer', {
|
|
575
|
+
version,
|
|
576
|
+
layerId: (layer as { id?: string }).id
|
|
577
|
+
});
|
|
289
578
|
overlayRef.setProps({ layers: [layer] });
|
|
290
579
|
return;
|
|
291
580
|
}
|
|
@@ -295,6 +584,12 @@ async function buildAndAddLayer(
|
|
|
295
584
|
layers: [layer],
|
|
296
585
|
onError: (err: Error) => {
|
|
297
586
|
if (signal.aborted) return;
|
|
587
|
+
console.error('[MultiCogViewer] MapboxOverlay error', {
|
|
588
|
+
name: err?.name,
|
|
589
|
+
message: err?.message,
|
|
590
|
+
stack: err?.stack,
|
|
591
|
+
err
|
|
592
|
+
});
|
|
298
593
|
if (!error) {
|
|
299
594
|
error = err?.message || String(err);
|
|
300
595
|
loading = false;
|
|
@@ -302,16 +597,55 @@ async function buildAndAddLayer(
|
|
|
302
597
|
}
|
|
303
598
|
});
|
|
304
599
|
overlayRef = overlay;
|
|
600
|
+
console.debug('[MultiCogViewer] addControl initial overlay', {
|
|
601
|
+
version,
|
|
602
|
+
layerId: (layer as { id?: string }).id
|
|
603
|
+
});
|
|
305
604
|
map.addControl(overlay as unknown as maplibregl.IControl);
|
|
605
|
+
setupClickHandler(map);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function syncCompositeToUrl(c: ChannelComposite | null, presetId: string | null): void {
|
|
609
|
+
if (!c) {
|
|
610
|
+
updateUrlViewParams('map', null);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
updateUrlViewParams('map', compositeToUrl(c, presetId));
|
|
306
614
|
}
|
|
307
615
|
|
|
308
616
|
function setPreset(id: string): void {
|
|
617
|
+
const preset = PRESETS.find((p) => p.id === id);
|
|
618
|
+
if (!preset) return;
|
|
619
|
+
const next = applyPreset(assets, preset);
|
|
620
|
+
if (!next) return;
|
|
621
|
+
const a = composite?.a;
|
|
622
|
+
composite = a ? { ...next, a } : next;
|
|
309
623
|
activePresetId = id;
|
|
624
|
+
// Asset references changed: let the next preflight reseed a bit-depth-
|
|
625
|
+
// appropriate default rescale (uint8 visual → 0.3, uint16 reflectance → 0.05).
|
|
626
|
+
userTouchedRescale = false;
|
|
627
|
+
console.debug('[MultiCogViewer] setPreset', { id, composite });
|
|
628
|
+
syncCompositeToUrl(composite, id);
|
|
629
|
+
if (mapRef) scheduleLayerRebuild(mapRef, abortController.signal);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function setComposite(next: ChannelComposite): void {
|
|
633
|
+
const rAssetChanged = composite?.r.assetKey !== next.r.assetKey;
|
|
634
|
+
composite = next;
|
|
635
|
+
const matching = PRESETS.find((p) => presetMatchesComposite(p, next, assets));
|
|
636
|
+
activePresetId = matching?.id ?? '';
|
|
637
|
+
// Only reseed the rescale default when the R-channel asset changed, because
|
|
638
|
+
// that's the asset we preflight; band-index-only swaps shouldn't stomp the
|
|
639
|
+
// user's slider.
|
|
640
|
+
if (rAssetChanged) userTouchedRescale = false;
|
|
641
|
+
console.debug('[MultiCogViewer] setComposite', { next, activePresetId, rAssetChanged });
|
|
642
|
+
syncCompositeToUrl(next, activePresetId || null);
|
|
310
643
|
if (mapRef) scheduleLayerRebuild(mapRef, abortController.signal);
|
|
311
644
|
}
|
|
312
645
|
|
|
313
646
|
function handleRescaleChange(next: RescaleConfig): void {
|
|
314
647
|
rescale = next;
|
|
648
|
+
userTouchedRescale = true;
|
|
315
649
|
if (mapRef) scheduleLayerRebuild(mapRef, abortController.signal);
|
|
316
650
|
}
|
|
317
651
|
|
|
@@ -321,6 +655,7 @@ function cleanup(): void {
|
|
|
321
655
|
clearTimeout(rebuildTimer);
|
|
322
656
|
rebuildTimer = null;
|
|
323
657
|
}
|
|
658
|
+
removeClickHandler();
|
|
324
659
|
if (mapRef && overlayRef) {
|
|
325
660
|
try {
|
|
326
661
|
mapRef.removeControl(overlayRef as unknown as maplibregl.IControl);
|
|
@@ -331,8 +666,15 @@ function cleanup(): void {
|
|
|
331
666
|
if (mapRef) cleanupNativeBitmap(mapRef);
|
|
332
667
|
mapRef = null;
|
|
333
668
|
overlayRef = null;
|
|
334
|
-
|
|
669
|
+
assets = [];
|
|
670
|
+
composite = null;
|
|
335
671
|
presignCache.clear();
|
|
672
|
+
geotiffCache.clear();
|
|
673
|
+
pixelValue = null;
|
|
674
|
+
inspecting = false;
|
|
675
|
+
proj4DefRef = null;
|
|
676
|
+
histogram = null;
|
|
677
|
+
histogramAssetKey = null;
|
|
336
678
|
const maybeDestroy = pool as unknown as { destroy?: () => void; terminate?: () => void } | null;
|
|
337
679
|
if (maybeDestroy?.destroy) {
|
|
338
680
|
try {
|
|
@@ -363,7 +705,7 @@ onDestroy(cleanup);
|
|
|
363
705
|
<MapContainer {onMapReady} {bounds} />
|
|
364
706
|
</div>
|
|
365
707
|
|
|
366
|
-
<div class="pointer-events-none absolute left-2 top-2 z-10 flex flex-col gap-1">
|
|
708
|
+
<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">
|
|
367
709
|
{#if loading}
|
|
368
710
|
<div class="rounded bg-card/80 px-2 py-1 text-xs text-card-foreground backdrop-blur-sm">
|
|
369
711
|
{t('map.loadingCog')}
|
|
@@ -374,24 +716,20 @@ onDestroy(cleanup);
|
|
|
374
716
|
{error}
|
|
375
717
|
</div>
|
|
376
718
|
{/if}
|
|
719
|
+
{#if smokeWarning && !error}
|
|
720
|
+
<div
|
|
721
|
+
class="pointer-events-auto max-w-sm rounded bg-amber-900/80 px-2 py-1 text-xs text-amber-100"
|
|
722
|
+
title={t('stac.smokeWarningHint')}
|
|
723
|
+
>
|
|
724
|
+
{t('stac.smokeWarning', { reason: smokeWarning })}
|
|
725
|
+
</div>
|
|
726
|
+
{/if}
|
|
377
727
|
</div>
|
|
378
728
|
|
|
379
|
-
{#if !error &&
|
|
380
|
-
<div class="absolute right-2 top-2 z-10 flex items-center gap-1">
|
|
381
|
-
<label class="flex items-center gap-1 rounded bg-card/80 px-2 py-1 text-xs text-card-foreground backdrop-blur-sm">
|
|
382
|
-
<span class="text-muted-foreground">{t('map.multiCogPreset.label')}</span>
|
|
383
|
-
<select
|
|
384
|
-
class="rounded border border-border bg-background px-1 py-0.5 text-xs"
|
|
385
|
-
value={activePresetId}
|
|
386
|
-
onchange={(e) => setPreset((e.target as HTMLSelectElement).value)}
|
|
387
|
-
>
|
|
388
|
-
{#each availablePresets as p}
|
|
389
|
-
<option value={p.id}>{t(p.labelKey)}</option>
|
|
390
|
-
{/each}
|
|
391
|
-
</select>
|
|
392
|
-
</label>
|
|
729
|
+
{#if !error && assets.length > 0 && composite}
|
|
730
|
+
<div class="absolute right-2 top-2 z-10 flex items-center gap-1" style="touch-action: manipulation;">
|
|
393
731
|
<button
|
|
394
|
-
class="rounded bg-card/80 px-
|
|
732
|
+
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"
|
|
395
733
|
class:ring-1={showControls}
|
|
396
734
|
class:ring-primary={showControls}
|
|
397
735
|
onclick={() => {
|
|
@@ -404,13 +742,40 @@ onDestroy(cleanup);
|
|
|
404
742
|
|
|
405
743
|
{#if showControls}
|
|
406
744
|
<CogControls
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
745
|
+
{assets}
|
|
746
|
+
composite={composite}
|
|
747
|
+
onCompositeChange={setComposite}
|
|
748
|
+
presets={presetsForItem}
|
|
749
|
+
{activePresetId}
|
|
750
|
+
onPresetChange={setPreset}
|
|
751
|
+
mode="rgb"
|
|
752
|
+
onModeChange={() => {}}
|
|
410
753
|
{rescale}
|
|
411
754
|
rescaleApplicable={true}
|
|
412
755
|
onRescaleChange={handleRescaleChange}
|
|
756
|
+
showAlpha={assets.length >= 4}
|
|
757
|
+
{histogram}
|
|
758
|
+
nodata={nodataConfig}
|
|
759
|
+
{autoNodata}
|
|
760
|
+
onNodataChange={(next) => {
|
|
761
|
+
nodataConfig = next;
|
|
762
|
+
if (mapRef) scheduleLayerRebuild(mapRef, abortController.signal);
|
|
763
|
+
}}
|
|
413
764
|
/>
|
|
414
765
|
{/if}
|
|
415
766
|
{/if}
|
|
767
|
+
|
|
768
|
+
<PixelInspectorPanel
|
|
769
|
+
lng={pixelValue?.lng ?? null}
|
|
770
|
+
lat={pixelValue?.lat ?? null}
|
|
771
|
+
rows={pixelValue
|
|
772
|
+
? (pixelValue.entries.map((e) => ({
|
|
773
|
+
label: e.channel,
|
|
774
|
+
sublabel: e.assetKey,
|
|
775
|
+
value: e.value
|
|
776
|
+
})) satisfies PixelInspectorRow[])
|
|
777
|
+
: null}
|
|
778
|
+
onClose={() => (pixelValue = null)}
|
|
779
|
+
{inspecting}
|
|
780
|
+
/>
|
|
416
781
|
</div>
|