@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,9 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
ChannelComposite,
|
|
4
|
+
ChannelRef,
|
|
5
|
+
CogAsset,
|
|
6
|
+
PresetDef
|
|
7
|
+
} from '@walkthru-earth/objex-utils';
|
|
2
8
|
import { t } from '../../i18n/index.svelte.js';
|
|
3
9
|
import {
|
|
4
10
|
type BandConfig,
|
|
5
11
|
type ColorRampId,
|
|
12
|
+
DEFAULT_NODATA_CONFIG,
|
|
6
13
|
DEFAULT_RESCALE,
|
|
14
|
+
type NodataConfig,
|
|
15
|
+
type NodataMode,
|
|
7
16
|
type RescaleConfig
|
|
8
17
|
} from '../../utils/cog.js';
|
|
9
18
|
import {
|
|
@@ -12,33 +21,46 @@ import {
|
|
|
12
21
|
COLORMAP_SPRITE_LAYERS,
|
|
13
22
|
COLORMAP_SPRITE_URL
|
|
14
23
|
} from '../../utils/colormap-sprite.js';
|
|
24
|
+
import { RangeSlider } from '../ui/slider/index.js';
|
|
25
|
+
import ChannelPicker from './cog/ChannelPicker.svelte';
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
type Props = {
|
|
28
|
+
/** All raster-COG-ish assets on the current item (or `[selfAsset]` for plain CogViewer). */
|
|
29
|
+
assets: CogAsset[];
|
|
30
|
+
/** Current RGB composite. Always present. */
|
|
31
|
+
composite: ChannelComposite;
|
|
32
|
+
onCompositeChange: (next: ChannelComposite) => void;
|
|
33
|
+
/** Presets that resolve on this item. Empty when no preset applies. */
|
|
34
|
+
presets: PresetDef[];
|
|
35
|
+
activePresetId: string;
|
|
36
|
+
onPresetChange: (id: string) => void;
|
|
37
|
+
/** Rendering mode toggle: 'rgb' uses the channel pickers; 'single' the band+ramp picker. */
|
|
38
|
+
mode: 'rgb' | 'single';
|
|
39
|
+
onModeChange: (m: 'rgb' | 'single') => void;
|
|
40
|
+
/** Band/ramp config used when mode === 'single'. Optional for RGB-only callers. */
|
|
41
|
+
bandConfig?: BandConfig | null;
|
|
42
|
+
bandCount?: number;
|
|
43
|
+
onBandConfigChange?: (next: BandConfig) => void;
|
|
30
44
|
rescale: RescaleConfig;
|
|
31
45
|
rescaleApplicable: boolean;
|
|
32
|
-
onRescaleChange: (
|
|
33
|
-
/** Optional histogram bins (normalized, single-band only) for the slider overlay. */
|
|
46
|
+
onRescaleChange: (next: RescaleConfig) => void;
|
|
34
47
|
histogram?: Uint32Array | null;
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
/** Optional 4th channel UI affordance (alpha). When false, alpha row is hidden. */
|
|
49
|
+
showAlpha?: boolean;
|
|
50
|
+
/** User-selected nodata config. Default `{ mode: 'auto' }`. */
|
|
51
|
+
nodata?: NodataConfig;
|
|
52
|
+
/**
|
|
53
|
+
* Value resolved by the viewer for Auto mode (typically the GeoTIFF's
|
|
54
|
+
* GDAL_NODATA tag). Surfaced as a hint pill next to the segmented control.
|
|
55
|
+
* `null` means the file has no GDAL_NODATA tag.
|
|
56
|
+
*/
|
|
57
|
+
autoNodata?: number | null;
|
|
58
|
+
/** Fired when the user changes nodata mode or value. */
|
|
59
|
+
onNodataChange?: (next: NodataConfig) => void;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const props: Props = $props();
|
|
37
63
|
|
|
38
|
-
// ─── Ramp picker state ──────────────────────────────────────────
|
|
39
|
-
// Keep a curated set pinned at the top for familiarity; the full set of
|
|
40
|
-
// 107 is searchable underneath. Pinned names match the old UI exactly so
|
|
41
|
-
// existing muscle memory holds.
|
|
42
64
|
const PINNED_RAMPS: ColorRampId[] = [
|
|
43
65
|
'gray',
|
|
44
66
|
'terrain',
|
|
@@ -60,36 +82,36 @@ const filteredRamps = $derived.by(() => {
|
|
|
60
82
|
return COLORMAP_NAMES.filter((name) => name.toLowerCase().includes(q));
|
|
61
83
|
});
|
|
62
84
|
|
|
63
|
-
|
|
85
|
+
function setChannel(channel: 'r' | 'g' | 'b' | 'a', next: ChannelRef): void {
|
|
86
|
+
if (channel === 'a') {
|
|
87
|
+
const c = { ...props.composite, a: next.assetKey ? next : undefined };
|
|
88
|
+
props.onCompositeChange(c);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
props.onCompositeChange({ ...props.composite, [channel]: next });
|
|
92
|
+
}
|
|
64
93
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
value: i,
|
|
68
|
-
label: `${t('cog.band')} ${i + 1}`
|
|
69
|
-
}));
|
|
94
|
+
function setMode(m: 'rgb' | 'single'): void {
|
|
95
|
+
props.onModeChange(m);
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
function
|
|
73
|
-
if (!bandConfig) return;
|
|
74
|
-
|
|
98
|
+
function setBand(value: number): void {
|
|
99
|
+
if (!props.bandConfig || !props.onBandConfigChange) return;
|
|
100
|
+
props.onBandConfigChange({ ...props.bandConfig, band: value });
|
|
75
101
|
}
|
|
76
102
|
|
|
77
|
-
function
|
|
78
|
-
if (!bandConfig) return;
|
|
79
|
-
|
|
103
|
+
function setRamp(id: ColorRampId): void {
|
|
104
|
+
if (!props.bandConfig || !props.onBandConfigChange) return;
|
|
105
|
+
props.onBandConfigChange({ ...props.bandConfig, colorRamp: id });
|
|
80
106
|
}
|
|
81
107
|
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
function bandOptions(count: number): { value: number; label: string }[] {
|
|
109
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
110
|
+
value: i,
|
|
111
|
+
label: `${t('cog.band')} ${i + 1}`
|
|
112
|
+
}));
|
|
85
113
|
}
|
|
86
114
|
|
|
87
|
-
/**
|
|
88
|
-
* CSS `background` declaration that renders one sprite row at the
|
|
89
|
-
* container's full height. Sprite is 256 wide × 107 tall (one 1px row per
|
|
90
|
-
* ramp); we scale it vertically by the target height and offset to land on
|
|
91
|
-
* the requested layer.
|
|
92
|
-
*/
|
|
93
115
|
function rampBg(name: ColorRampId, heightPx: number): string {
|
|
94
116
|
const index = COLORMAP_INDEX[name];
|
|
95
117
|
if (index === undefined) return '';
|
|
@@ -103,104 +125,204 @@ function rampBg(name: ColorRampId, heightPx: number): string {
|
|
|
103
125
|
].join('; ');
|
|
104
126
|
}
|
|
105
127
|
|
|
106
|
-
// ─── Rescale / histogram ────────────────────────────────────────
|
|
107
|
-
|
|
108
128
|
function clamp01(v: number): number {
|
|
109
129
|
return Math.max(0, Math.min(1, v));
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
function setRescaleMin(value: number) {
|
|
132
|
+
function setRescaleMin(value: number): void {
|
|
113
133
|
const clamped = clamp01(value);
|
|
114
|
-
const next = Math.min(clamped, rescale.max - 0.001);
|
|
115
|
-
onRescaleChange({ min: Number.isFinite(next) ? next : 0, max: rescale.max });
|
|
134
|
+
const next = Math.min(clamped, props.rescale.max - 0.001);
|
|
135
|
+
props.onRescaleChange({ min: Number.isFinite(next) ? next : 0, max: props.rescale.max });
|
|
116
136
|
}
|
|
117
137
|
|
|
118
|
-
function setRescaleMax(value: number) {
|
|
138
|
+
function setRescaleMax(value: number): void {
|
|
119
139
|
const clamped = clamp01(value);
|
|
120
|
-
const next = Math.max(clamped, rescale.min + 0.001);
|
|
121
|
-
onRescaleChange({ min: rescale.min, max: Number.isFinite(next) ? next : 1 });
|
|
140
|
+
const next = Math.max(clamped, props.rescale.min + 0.001);
|
|
141
|
+
props.onRescaleChange({ min: props.rescale.min, max: Number.isFinite(next) ? next : 1 });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function setRescaleRange(next: [number, number]): void {
|
|
145
|
+
const lo = clamp01(next[0]);
|
|
146
|
+
const hi = clamp01(next[1]);
|
|
147
|
+
props.onRescaleChange({ min: Math.min(lo, hi), max: Math.max(lo, hi) });
|
|
122
148
|
}
|
|
123
149
|
|
|
124
|
-
function resetRescale() {
|
|
125
|
-
onRescaleChange({ ...DEFAULT_RESCALE });
|
|
150
|
+
function resetRescale(): void {
|
|
151
|
+
props.onRescaleChange({ ...DEFAULT_RESCALE });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function fmtRescale(n: number): string {
|
|
155
|
+
return n.toFixed(2);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const nodataCfg = $derived(props.nodata ?? DEFAULT_NODATA_CONFIG);
|
|
159
|
+
|
|
160
|
+
function fmtAutoNodata(v: number | null | undefined): string {
|
|
161
|
+
if (v === null || v === undefined) return '';
|
|
162
|
+
if (Number.isNaN(v)) return 'NaN';
|
|
163
|
+
return String(v);
|
|
126
164
|
}
|
|
127
165
|
|
|
166
|
+
function setNodataMode(mode: NodataMode): void {
|
|
167
|
+
if (!props.onNodataChange) return;
|
|
168
|
+
if (mode === nodataCfg.mode) return;
|
|
169
|
+
if (mode === 'value') {
|
|
170
|
+
const seed =
|
|
171
|
+
typeof nodataCfg.value === 'number'
|
|
172
|
+
? nodataCfg.value
|
|
173
|
+
: typeof props.autoNodata === 'number' && Number.isFinite(props.autoNodata)
|
|
174
|
+
? props.autoNodata
|
|
175
|
+
: 0;
|
|
176
|
+
props.onNodataChange({ mode: 'value', value: seed });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
props.onNodataChange({ mode });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function setNodataValue(raw: string): void {
|
|
183
|
+
if (!props.onNodataChange) return;
|
|
184
|
+
const trimmed = raw.trim().toLowerCase();
|
|
185
|
+
if (trimmed === 'nan') {
|
|
186
|
+
props.onNodataChange({ mode: 'value', value: Number.NaN });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const parsed = Number(raw);
|
|
190
|
+
if (!Number.isFinite(parsed) && !Number.isNaN(parsed)) return;
|
|
191
|
+
props.onNodataChange({ mode: 'value', value: parsed });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// B4: Track viewport width to downsample the histogram on narrow phones.
|
|
195
|
+
// We only flip when crossing the `sm` breakpoint (640px) so the $derived
|
|
196
|
+
// below only re-runs on rotate/resize-across-threshold, not on every px.
|
|
197
|
+
let narrowViewport = $state(false);
|
|
198
|
+
|
|
199
|
+
$effect(() => {
|
|
200
|
+
if (typeof window === 'undefined') return;
|
|
201
|
+
const compute = () => {
|
|
202
|
+
narrowViewport = window.innerWidth < 640;
|
|
203
|
+
};
|
|
204
|
+
compute();
|
|
205
|
+
window.addEventListener('resize', compute);
|
|
206
|
+
return () => window.removeEventListener('resize', compute);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Fold 128-bin histogram down to 64 on narrow viewports so each bar gets
|
|
210
|
+
// at least ~2px of width inside the slimmed-down panel.
|
|
211
|
+
const effectiveHistogram = $derived.by<Uint32Array | null | undefined>(() => {
|
|
212
|
+
const h = props.histogram;
|
|
213
|
+
if (!h) return h;
|
|
214
|
+
if (!narrowViewport || h.length !== 128) return h;
|
|
215
|
+
const out = new Uint32Array(64);
|
|
216
|
+
for (let i = 0; i < 64; i++) out[i] = h[2 * i] + h[2 * i + 1];
|
|
217
|
+
return out;
|
|
218
|
+
});
|
|
219
|
+
|
|
128
220
|
const histogramBars = $derived.by(() => {
|
|
129
|
-
|
|
221
|
+
const h = effectiveHistogram;
|
|
222
|
+
if (!h || h.length === 0) return null;
|
|
130
223
|
let max = 0;
|
|
131
|
-
for (const v of
|
|
224
|
+
for (const v of h) if (v > max) max = v;
|
|
132
225
|
if (max === 0) return null;
|
|
133
|
-
|
|
134
|
-
return bins;
|
|
226
|
+
return Array.from(h, (count) => count / max);
|
|
135
227
|
});
|
|
136
228
|
</script>
|
|
137
229
|
|
|
138
230
|
<div
|
|
139
|
-
class="absolute right-2 top-10 z-10 w-
|
|
231
|
+
class="absolute right-2 top-10 z-10 w-[min(18rem,calc(100vw-1rem))] max-h-[calc(100vh-6rem)] overflow-y-auto rounded bg-card/90 p-2.5 text-xs text-card-foreground backdrop-blur-sm sm:w-72"
|
|
140
232
|
>
|
|
141
|
-
{#if mode === '
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
233
|
+
{#if props.presets.length > 0 && props.mode === 'rgb'}
|
|
234
|
+
<div class="mb-2 flex items-center gap-2">
|
|
235
|
+
<span class="text-muted-foreground">{t('map.multiCogPreset.label')}</span>
|
|
236
|
+
<select
|
|
237
|
+
class="flex-1 rounded border border-border bg-background px-1.5 py-0.5 text-xs"
|
|
238
|
+
value={props.activePresetId}
|
|
239
|
+
onchange={(e) => props.onPresetChange((e.target as HTMLSelectElement).value)}
|
|
240
|
+
>
|
|
241
|
+
{#if !props.activePresetId}
|
|
242
|
+
<option value="">{t('map.multiCogPreset.custom')}</option>
|
|
243
|
+
{/if}
|
|
244
|
+
{#each props.presets as p (p.id)}
|
|
245
|
+
<option value={p.id}>{t(p.labelKey)}</option>
|
|
246
|
+
{/each}
|
|
247
|
+
</select>
|
|
248
|
+
</div>
|
|
249
|
+
{/if}
|
|
250
|
+
|
|
251
|
+
{#if props.bandConfig && props.onBandConfigChange}
|
|
252
|
+
<div class="mb-2 flex gap-1">
|
|
253
|
+
<button
|
|
254
|
+
class="flex-1 rounded px-2 py-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
|
255
|
+
class:bg-primary={props.mode === 'rgb'}
|
|
256
|
+
class:text-primary-foreground={props.mode === 'rgb'}
|
|
257
|
+
class:bg-muted={props.mode !== 'rgb'}
|
|
258
|
+
onclick={() => setMode('rgb')}
|
|
259
|
+
>
|
|
260
|
+
RGB
|
|
261
|
+
</button>
|
|
262
|
+
<button
|
|
263
|
+
class="flex-1 rounded px-2 py-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
|
264
|
+
class:bg-primary={props.mode === 'single'}
|
|
265
|
+
class:text-primary-foreground={props.mode === 'single'}
|
|
266
|
+
class:bg-muted={props.mode !== 'single'}
|
|
267
|
+
onclick={() => setMode('single')}
|
|
268
|
+
>
|
|
269
|
+
{t('cog.singleBand')}
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
{/if}
|
|
273
|
+
|
|
274
|
+
{#if props.mode === 'rgb'}
|
|
166
275
|
<div class="space-y-1">
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
276
|
+
<ChannelPicker
|
|
277
|
+
channel="r"
|
|
278
|
+
label="R"
|
|
279
|
+
colorClass="text-red-400"
|
|
280
|
+
assets={props.assets}
|
|
281
|
+
value={props.composite.r}
|
|
282
|
+
onChange={(next) => setChannel('r', next)}
|
|
283
|
+
/>
|
|
284
|
+
<ChannelPicker
|
|
285
|
+
channel="g"
|
|
286
|
+
label="G"
|
|
287
|
+
colorClass="text-green-400"
|
|
288
|
+
assets={props.assets}
|
|
289
|
+
value={props.composite.g}
|
|
290
|
+
onChange={(next) => setChannel('g', next)}
|
|
291
|
+
/>
|
|
292
|
+
<ChannelPicker
|
|
293
|
+
channel="b"
|
|
294
|
+
label="B"
|
|
295
|
+
colorClass="text-blue-400"
|
|
296
|
+
assets={props.assets}
|
|
297
|
+
value={props.composite.b}
|
|
298
|
+
onChange={(next) => setChannel('b', next)}
|
|
299
|
+
/>
|
|
300
|
+
{#if props.showAlpha}
|
|
301
|
+
<ChannelPicker
|
|
302
|
+
channel="a"
|
|
303
|
+
label="A"
|
|
304
|
+
colorClass="text-muted-foreground"
|
|
305
|
+
assets={props.assets}
|
|
306
|
+
value={props.composite.a ?? { assetKey: '', bandIndex: 0 }}
|
|
307
|
+
onChange={(next) => setChannel('a', next)}
|
|
308
|
+
allowNone
|
|
309
|
+
/>
|
|
310
|
+
{/if}
|
|
186
311
|
</div>
|
|
187
|
-
{:else}
|
|
188
|
-
<!-- Single band selector -->
|
|
312
|
+
{:else if props.bandConfig && typeof props.bandCount === 'number'}
|
|
189
313
|
<div class="mb-2 flex items-center gap-2">
|
|
190
314
|
<span class="text-muted-foreground">{t('cog.band')}</span>
|
|
191
315
|
<select
|
|
192
316
|
class="flex-1 rounded border border-border bg-background px-1.5 py-0.5 text-xs"
|
|
193
|
-
value={bandConfig.band}
|
|
194
|
-
onchange={(e) =>
|
|
195
|
-
setBand('band', Number((e.target as HTMLSelectElement).value))}
|
|
317
|
+
value={props.bandConfig.band}
|
|
318
|
+
onchange={(e) => setBand(Number((e.target as HTMLSelectElement).value))}
|
|
196
319
|
>
|
|
197
|
-
{#each bandOptions(bandCount) as opt}
|
|
320
|
+
{#each bandOptions(props.bandCount) as opt (opt.value)}
|
|
198
321
|
<option value={opt.value}>{opt.label}</option>
|
|
199
322
|
{/each}
|
|
200
323
|
</select>
|
|
201
324
|
</div>
|
|
202
325
|
|
|
203
|
-
<!-- Color ramp picker -->
|
|
204
326
|
<div class="space-y-1">
|
|
205
327
|
<div class="flex items-center justify-between">
|
|
206
328
|
<span class="text-muted-foreground">{t('cog.colorRamp')}</span>
|
|
@@ -209,12 +331,11 @@ const histogramBars = $derived.by(() => {
|
|
|
209
331
|
</span>
|
|
210
332
|
</div>
|
|
211
333
|
|
|
212
|
-
<!-- Pinned quick-access (only when no search active) -->
|
|
213
334
|
{#if !rampQuery}
|
|
214
335
|
<div class="grid grid-cols-2 gap-1">
|
|
215
|
-
{#each PINNED_RAMPS as id}
|
|
336
|
+
{#each PINNED_RAMPS as id (id)}
|
|
216
337
|
<button
|
|
217
|
-
class="flex flex-col items-stretch rounded border px-1 py-0.5 transition-colors {bandConfig.colorRamp === id ? 'border-primary bg-muted' : 'border-transparent hover:border-border'}"
|
|
338
|
+
class="flex flex-col items-stretch rounded border px-1 py-0.5 transition-colors {props.bandConfig.colorRamp === id ? 'border-primary bg-muted' : 'border-transparent hover:border-border'}"
|
|
218
339
|
onclick={() => setRamp(id)}
|
|
219
340
|
title={id}
|
|
220
341
|
>
|
|
@@ -227,7 +348,6 @@ const histogramBars = $derived.by(() => {
|
|
|
227
348
|
</div>
|
|
228
349
|
{/if}
|
|
229
350
|
|
|
230
|
-
<!-- Search + all-ramps scroll list -->
|
|
231
351
|
<input
|
|
232
352
|
type="search"
|
|
233
353
|
placeholder={t('cog.colorRampSearch')}
|
|
@@ -236,9 +356,9 @@ const histogramBars = $derived.by(() => {
|
|
|
236
356
|
oninput={(e) => (rampQuery = (e.target as HTMLInputElement).value)}
|
|
237
357
|
/>
|
|
238
358
|
<div class="max-h-40 overflow-y-auto rounded border border-border">
|
|
239
|
-
{#each filteredRamps as id}
|
|
359
|
+
{#each filteredRamps as id (id)}
|
|
240
360
|
<button
|
|
241
|
-
class="flex w-full items-center gap-2 px-1.5 py-0.5 text-left text-[11px] transition-colors {bandConfig.colorRamp === id ? 'bg-muted' : 'hover:bg-muted/60'}"
|
|
361
|
+
class="flex w-full items-center gap-2 px-1.5 py-0.5 text-left text-[11px] transition-colors {props.bandConfig.colorRamp === id ? 'bg-muted' : 'hover:bg-muted/60'}"
|
|
242
362
|
onclick={() => setRamp(id)}
|
|
243
363
|
title={id}
|
|
244
364
|
>
|
|
@@ -249,89 +369,123 @@ const histogramBars = $derived.by(() => {
|
|
|
249
369
|
</div>
|
|
250
370
|
</div>
|
|
251
371
|
{/if}
|
|
252
|
-
{/if}
|
|
253
372
|
|
|
254
|
-
{#if rescaleApplicable}
|
|
255
|
-
<!-- GPU LinearRescale slider with histogram overlay. -->
|
|
373
|
+
{#if props.rescaleApplicable}
|
|
256
374
|
<div class="mt-2 space-y-1 border-t border-border pt-2">
|
|
257
375
|
<div class="flex items-center justify-between">
|
|
258
|
-
<span class="text-muted-foreground">{t('cog.rescale')}</span>
|
|
376
|
+
<span class="text-muted-foreground">{t('cog.rescale.label')}</span>
|
|
259
377
|
<button
|
|
260
|
-
class="text-[10px] text-muted-foreground hover:text-card-foreground"
|
|
378
|
+
class="rounded text-[10px] text-muted-foreground hover:text-card-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
|
261
379
|
onclick={resetRescale}
|
|
262
380
|
>
|
|
263
|
-
{t('cog.
|
|
381
|
+
{t('cog.rescale.reset')}
|
|
264
382
|
</button>
|
|
265
383
|
</div>
|
|
266
384
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
>
|
|
277
|
-
{#each histogramBars as h, i}
|
|
278
|
-
<rect
|
|
279
|
-
x={(i * 100) / histogramBars.length}
|
|
280
|
-
y={100 - h * 100}
|
|
281
|
-
width={100 / histogramBars.length}
|
|
282
|
-
height={h * 100}
|
|
283
|
-
class="fill-primary/40"
|
|
284
|
-
/>
|
|
285
|
-
{/each}
|
|
286
|
-
</svg>
|
|
287
|
-
<!-- Active rescale window -->
|
|
288
|
-
<div
|
|
289
|
-
class="pointer-events-none absolute inset-y-0 border-x border-primary bg-primary/10"
|
|
290
|
-
style="left: {rescale.min * 100}%; right: {(1 - rescale.max) * 100}%;"
|
|
291
|
-
></div>
|
|
292
|
-
</div>
|
|
293
|
-
{/if}
|
|
385
|
+
<RangeSlider
|
|
386
|
+
min={0}
|
|
387
|
+
max={1}
|
|
388
|
+
step={0.01}
|
|
389
|
+
value={[props.rescale.min, props.rescale.max]}
|
|
390
|
+
histogram={histogramBars}
|
|
391
|
+
formatLabel={fmtRescale}
|
|
392
|
+
onValueChange={setRescaleRange}
|
|
393
|
+
/>
|
|
294
394
|
|
|
295
395
|
<div class="flex items-center gap-1.5">
|
|
296
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
class="
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
396
|
+
<label class="flex flex-1 items-center gap-1 text-[10px] text-muted-foreground">
|
|
397
|
+
<span class="w-6">min</span>
|
|
398
|
+
<input
|
|
399
|
+
type="number"
|
|
400
|
+
inputmode="decimal"
|
|
401
|
+
min="0"
|
|
402
|
+
max="1"
|
|
403
|
+
step="0.01"
|
|
404
|
+
class="min-h-11 w-full rounded border border-border bg-background px-2 py-1.5 text-sm tabular-nums sm:min-h-0 sm:px-1 sm:py-0.5 sm:text-[11px]"
|
|
405
|
+
value={props.rescale.min}
|
|
406
|
+
oninput={(e) => setRescaleMin(Number((e.target as HTMLInputElement).value))}
|
|
407
|
+
/>
|
|
408
|
+
</label>
|
|
409
|
+
<label class="flex flex-1 items-center gap-1 text-[10px] text-muted-foreground">
|
|
410
|
+
<span class="w-6">max</span>
|
|
411
|
+
<input
|
|
412
|
+
type="number"
|
|
413
|
+
inputmode="decimal"
|
|
414
|
+
min="0"
|
|
415
|
+
max="1"
|
|
416
|
+
step="0.01"
|
|
417
|
+
class="min-h-11 w-full rounded border border-border bg-background px-2 py-1.5 text-sm tabular-nums sm:min-h-0 sm:px-1 sm:py-0.5 sm:text-[11px]"
|
|
418
|
+
value={props.rescale.max}
|
|
419
|
+
oninput={(e) => setRescaleMax(Number((e.target as HTMLInputElement).value))}
|
|
420
|
+
/>
|
|
421
|
+
</label>
|
|
314
422
|
</div>
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
423
|
+
</div>
|
|
424
|
+
{/if}
|
|
425
|
+
|
|
426
|
+
{#if props.onNodataChange}
|
|
427
|
+
<div class="mt-2 space-y-1 border-t border-border pt-2">
|
|
428
|
+
<div class="flex items-center justify-between">
|
|
429
|
+
<span class="text-muted-foreground">{t('cog.nodata.label')}</span>
|
|
430
|
+
{#if nodataCfg.mode === 'auto'}
|
|
431
|
+
<span class="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
|
432
|
+
{props.autoNodata === null || props.autoNodata === undefined
|
|
433
|
+
? t('cog.nodata.autoNone')
|
|
434
|
+
: t('cog.nodata.autoHint', { value: fmtAutoNodata(props.autoNodata) })}
|
|
435
|
+
</span>
|
|
436
|
+
{/if}
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<div
|
|
440
|
+
class="flex w-full gap-1"
|
|
441
|
+
role="radiogroup"
|
|
442
|
+
aria-label={t('cog.nodata.label')}
|
|
443
|
+
tabindex={-1}
|
|
444
|
+
onkeydown={(e) => {
|
|
445
|
+
const modes = ['auto', 'value', 'off'] as const;
|
|
446
|
+
const i = modes.indexOf(nodataCfg.mode);
|
|
447
|
+
let next: NodataMode | null = null;
|
|
448
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = modes[(i + 1) % 3];
|
|
449
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = modes[(i + 2) % 3];
|
|
450
|
+
else if (e.key === 'Home') next = modes[0];
|
|
451
|
+
else if (e.key === 'End') next = modes[2];
|
|
452
|
+
if (next) {
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
setNodataMode(next);
|
|
455
|
+
const buttons = (e.currentTarget as HTMLElement).querySelectorAll<HTMLButtonElement>(
|
|
456
|
+
'button[role="radio"]'
|
|
457
|
+
);
|
|
458
|
+
buttons[modes.indexOf(next)]?.focus();
|
|
459
|
+
}
|
|
460
|
+
}}
|
|
461
|
+
>
|
|
462
|
+
{#each ['auto', 'value', 'off'] as const as mode (mode)}
|
|
463
|
+
<button
|
|
464
|
+
type="button"
|
|
465
|
+
role="radio"
|
|
466
|
+
aria-checked={nodataCfg.mode === mode}
|
|
467
|
+
tabindex={nodataCfg.mode === mode ? 0 : -1}
|
|
468
|
+
class="min-h-11 flex-1 rounded px-2 py-1 text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 sm:min-h-0 sm:py-1.5"
|
|
469
|
+
class:bg-primary={nodataCfg.mode === mode}
|
|
470
|
+
class:text-primary-foreground={nodataCfg.mode === mode}
|
|
471
|
+
class:bg-muted={nodataCfg.mode !== mode}
|
|
472
|
+
onclick={() => setNodataMode(mode)}
|
|
473
|
+
>
|
|
474
|
+
{t(`cog.nodata.${mode}`)}
|
|
475
|
+
</button>
|
|
476
|
+
{/each}
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
{#if nodataCfg.mode === 'value'}
|
|
325
480
|
<input
|
|
326
|
-
type="
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
oninput={(e) => setRescaleMax(Number((e.target as HTMLInputElement).value))}
|
|
481
|
+
type="text"
|
|
482
|
+
inputmode="decimal"
|
|
483
|
+
placeholder={t('cog.nodata.valuePlaceholder')}
|
|
484
|
+
class="min-h-11 w-full rounded border border-border bg-background px-2 py-1.5 text-sm tabular-nums sm:min-h-0 sm:px-1.5 sm:py-1 sm:text-[11px]"
|
|
485
|
+
value={Number.isNaN(nodataCfg.value as number) ? 'NaN' : (nodataCfg.value ?? '')}
|
|
486
|
+
oninput={(e) => setNodataValue((e.target as HTMLInputElement).value)}
|
|
333
487
|
/>
|
|
334
|
-
|
|
488
|
+
{/if}
|
|
335
489
|
</div>
|
|
336
490
|
{/if}
|
|
337
491
|
</div>
|