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