@walkthru-earth/objex 0.1.0 → 1.1.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/README.md +9 -2
- package/dist/components/browser/FileBrowser.svelte +53 -41
- package/dist/components/browser/FileRow.svelte +8 -3
- package/dist/components/browser/FileTreeSidebar.svelte +2 -4
- package/dist/components/layout/AboutSheet.svelte +126 -0
- package/dist/components/layout/AboutSheet.svelte.d.ts +6 -0
- package/dist/components/layout/ConnectionDialog.svelte +186 -138
- package/dist/components/layout/ConnectionDialog.svelte.d.ts +1 -0
- package/dist/components/layout/Sidebar.svelte +19 -3
- package/dist/components/layout/TabBar.svelte +4 -7
- package/dist/components/viewers/CodeViewer.svelte +17 -9
- package/dist/components/viewers/ImageViewer.svelte +6 -16
- package/dist/components/viewers/MarkdownViewer.svelte +8 -16
- package/dist/components/viewers/MediaViewer.svelte +6 -17
- package/dist/components/viewers/ModelViewer.svelte +4 -2
- package/dist/components/viewers/NotebookViewer.svelte +90 -40
- package/dist/components/viewers/PdfViewer.svelte +5 -3
- package/dist/components/viewers/RawViewer.svelte +4 -2
- package/dist/components/viewers/TableGrid.svelte +3 -2
- package/dist/components/viewers/ZarrMapViewer.svelte +334 -40
- package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +3 -8
- package/dist/components/viewers/ZarrViewer.svelte +459 -178
- package/dist/components/viewers/map/AttributeTable.svelte +1 -6
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +2 -6
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +96 -22
- package/dist/constants.d.ts +28 -0
- package/dist/constants.js +34 -0
- package/dist/file-icons/index.js +6 -0
- package/dist/i18n/ar.js +34 -0
- package/dist/i18n/en.js +34 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +16 -1
- package/dist/query/wasm.js +5 -4
- package/dist/storage/browser-cloud.d.ts +7 -0
- package/dist/storage/browser-cloud.js +74 -7
- package/dist/storage/providers.d.ts +53 -0
- package/dist/storage/providers.js +318 -0
- package/dist/stores/connections.svelte.js +8 -34
- package/dist/stores/files.svelte.d.ts +1 -6
- package/dist/stores/files.svelte.js +4 -36
- package/dist/stores/query-history.svelte.js +5 -28
- package/dist/stores/settings.svelte.d.ts +1 -0
- package/dist/stores/settings.svelte.js +11 -31
- package/dist/types.d.ts +2 -2
- package/dist/utils/clipboard.d.ts +13 -0
- package/dist/utils/clipboard.js +38 -0
- package/dist/utils/cloud-url.d.ts +27 -0
- package/dist/utils/cloud-url.js +61 -0
- package/dist/utils/error.d.ts +8 -0
- package/dist/utils/error.js +12 -0
- package/dist/utils/export.d.ts +22 -2
- package/dist/utils/export.js +35 -10
- package/dist/utils/file-sort.d.ts +20 -0
- package/dist/utils/file-sort.js +41 -0
- package/dist/utils/format.d.ts +10 -0
- package/dist/utils/format.js +22 -0
- package/dist/utils/host-detection.js +78 -18
- package/dist/utils/local-storage.d.ts +16 -0
- package/dist/utils/local-storage.js +37 -0
- package/dist/utils/notebook.d.ts +59 -0
- package/dist/utils/notebook.js +211 -0
- package/dist/utils/parquet-metadata.js +1 -1
- package/dist/utils/pmtiles-tile.js +2 -1
- package/dist/utils/pmtiles.js +2 -1
- package/dist/utils/storage-url.d.ts +1 -1
- package/dist/utils/storage-url.js +82 -24
- package/dist/utils/url-state.js +2 -7
- package/dist/utils/url.d.ts +0 -2
- package/dist/utils/url.js +3 -29
- package/dist/utils/zarr.d.ts +60 -20
- package/dist/utils/zarr.js +450 -103
- package/package.json +66 -54
- package/dist/assets/favicon.svg +0 -17
- package/dist/components/CLAUDE.md +0 -44
- package/dist/components/viewers/CLAUDE.md +0 -60
- package/dist/file-icons/CLAUDE.md +0 -21
- package/dist/i18n/CLAUDE.md +0 -19
- package/dist/query/CLAUDE.md +0 -22
- package/dist/storage/CLAUDE.md +0 -23
- package/dist/stores/CLAUDE.md +0 -29
- package/dist/types/notebookjs.d.ts +0 -14
- package/dist/utils/CLAUDE.md +0 -54
- package/dist/utils/analytics.d.ts +0 -10
- package/dist/utils/analytics.js +0 -38
|
@@ -1,28 +1,50 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type maplibregl from 'maplibre-gl';
|
|
3
|
+
import maplibreModule from 'maplibre-gl';
|
|
3
4
|
import { onDestroy, untrack } from 'svelte';
|
|
4
5
|
import { t } from '../../i18n/index.svelte.js';
|
|
5
6
|
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
6
7
|
import type { Tab } from '../../types';
|
|
7
8
|
import { buildHttpsUrl } from '../../utils/url.js';
|
|
9
|
+
import {
|
|
10
|
+
ensureCodecsRegistered,
|
|
11
|
+
extractZarrStoreUrl,
|
|
12
|
+
inferDims,
|
|
13
|
+
type ZarrNode
|
|
14
|
+
} from '../../utils/zarr.js';
|
|
8
15
|
import MapContainer from './map/MapContainer.svelte';
|
|
9
16
|
|
|
10
|
-
|
|
17
|
+
/** Enriched selector dimension with coordinate metadata. */
|
|
18
|
+
interface SelectorDim {
|
|
11
19
|
name: string;
|
|
12
|
-
|
|
13
|
-
dtype: string;
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
size: number;
|
|
21
|
+
dtype: string | null;
|
|
22
|
+
units: string | null;
|
|
23
|
+
longName: string | null;
|
|
24
|
+
min: string | null;
|
|
25
|
+
max: string | null;
|
|
26
|
+
isDatetime: boolean;
|
|
27
|
+
minDate: Date | null;
|
|
28
|
+
maxDate: Date | null;
|
|
29
|
+
/** True when estimated step size < 1 day (e.g. 6-hourly forecasts). */
|
|
30
|
+
subDaily: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Get the relative path for a ZarrNode (strip leading slash). */
|
|
34
|
+
function varPath(node: ZarrNode): string {
|
|
35
|
+
return node.path.replace(/^\//, '');
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
let {
|
|
19
39
|
tab,
|
|
20
40
|
variables,
|
|
41
|
+
coords = [],
|
|
21
42
|
spatialRefAttrs,
|
|
22
43
|
zarrVersion = null
|
|
23
44
|
}: {
|
|
24
45
|
tab: Tab;
|
|
25
|
-
variables:
|
|
46
|
+
variables: ZarrNode[];
|
|
47
|
+
coords?: ZarrNode[];
|
|
26
48
|
spatialRefAttrs: Record<string, any> | null;
|
|
27
49
|
zarrVersion?: number | null;
|
|
28
50
|
} = $props();
|
|
@@ -32,20 +54,24 @@ let error = $state<string | null>(null);
|
|
|
32
54
|
let selectedVar = $state('');
|
|
33
55
|
let zarrLayer: any = null;
|
|
34
56
|
let mapRef: maplibregl.Map | null = null;
|
|
57
|
+
let inspectPopup: maplibregl.Popup | null = null;
|
|
35
58
|
|
|
36
59
|
// Extract proj4 from spatial_ref if available
|
|
37
60
|
const proj4String = $derived(extractProj4(spatialRefAttrs));
|
|
38
61
|
|
|
39
|
-
// Initialize selectedVar from variables prop
|
|
62
|
+
// Initialize selectedVar from variables prop (store as relative path)
|
|
40
63
|
$effect(() => {
|
|
41
64
|
if (!selectedVar && variables.length > 0) {
|
|
42
|
-
selectedVar = variables[0]
|
|
65
|
+
selectedVar = varPath(variables[0]);
|
|
43
66
|
}
|
|
44
67
|
});
|
|
45
68
|
|
|
69
|
+
// Build coord lookup: dimension name → coordinate variable metadata
|
|
70
|
+
const coordByName = $derived(new Map(coords.map((c) => [c.name, c])));
|
|
71
|
+
|
|
46
72
|
// Identify non-spatial selector dimensions for the selected variable
|
|
47
|
-
const selectedMeta = $derived(variables.find((v) => v
|
|
48
|
-
const selectorDims = $derived(getSelectorDims(selectedMeta));
|
|
73
|
+
const selectedMeta = $derived(variables.find((v) => varPath(v) === selectedVar));
|
|
74
|
+
const selectorDims = $derived(getSelectorDims(selectedMeta, coordByName));
|
|
49
75
|
|
|
50
76
|
// Dimension slider state
|
|
51
77
|
let selectorValues = $state<Record<string, number>>({});
|
|
@@ -104,33 +130,232 @@ function buildProj4FromCrsWkt(crsWkt: string | undefined): string | null {
|
|
|
104
130
|
}
|
|
105
131
|
}
|
|
106
132
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
const DATETIME_DIM_NAMES = new Set(['time', 'init_time', 'valid_time', 'date', 'datetime']);
|
|
134
|
+
|
|
135
|
+
/** Detect timedelta/duration dimension (forecast lead time, etc.). */
|
|
136
|
+
function isTimedeltaDim(attrs: Record<string, any>): boolean {
|
|
137
|
+
if (attrs.standard_name === 'forecast_period') return true;
|
|
138
|
+
if (typeof attrs.dtype === 'string' && attrs.dtype.includes('timedelta')) return true;
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Detect temporal dimension via CF-convention signals. */
|
|
143
|
+
function isDatetimeDim(name: string, attrs: Record<string, any>): boolean {
|
|
144
|
+
if (isTimedeltaDim(attrs)) return false;
|
|
145
|
+
if (attrs.axis === 'T') return true;
|
|
146
|
+
if (attrs.standard_name === 'time' || attrs.standard_name === 'forecast_reference_time')
|
|
147
|
+
return true;
|
|
148
|
+
if (typeof attrs.units === 'string' && /\bsince\b/i.test(attrs.units)) return true;
|
|
149
|
+
if (DATETIME_DIM_NAMES.has(name.toLowerCase())) return true;
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Parse a date string, treating "present"/"now" as today's date. */
|
|
154
|
+
function parseDateOrSentinel(value: string): Date | null {
|
|
155
|
+
const lower = value.trim().toLowerCase();
|
|
156
|
+
if (lower === 'present' || lower === 'now') return new Date();
|
|
157
|
+
const d = new Date(value);
|
|
158
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Linear interpolation: index → estimated date/datetime string.
|
|
162
|
+
* Sub-daily dims return "YYYY-MM-DDTHH:mm", daily+ return "YYYY-MM-DD". */
|
|
163
|
+
function indexToDateStr(index: number, dim: SelectorDim): string | null {
|
|
164
|
+
if (!dim.minDate || !dim.maxDate || dim.size < 2) return null;
|
|
165
|
+
const t = index / (dim.size - 1);
|
|
166
|
+
const ms = dim.minDate.getTime() + t * (dim.maxDate.getTime() - dim.minDate.getTime());
|
|
167
|
+
const iso = new Date(ms).toISOString();
|
|
168
|
+
return dim.subDaily ? iso.slice(0, 16) : iso.slice(0, 10);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Inverse: date string → nearest integer index, clamped to [0, size-1]. */
|
|
172
|
+
function dateToIndex(dateStr: string, dim: SelectorDim): number {
|
|
173
|
+
if (!dim.minDate || !dim.maxDate || dim.size < 2) return 0;
|
|
174
|
+
const range = dim.maxDate.getTime() - dim.minDate.getTime();
|
|
175
|
+
if (range === 0) return 0;
|
|
176
|
+
const target = new Date(dateStr).getTime();
|
|
177
|
+
const t = (target - dim.minDate.getTime()) / range;
|
|
178
|
+
return Math.round(Math.max(0, Math.min(dim.size - 1, t * (dim.size - 1))));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Spatial dimension name aliases → canonical ZarrLayer keys. */
|
|
182
|
+
const SPATIAL_ALIASES: Record<string, 'lat' | 'lon'> = {
|
|
183
|
+
x: 'lon',
|
|
184
|
+
y: 'lat',
|
|
185
|
+
lat: 'lat',
|
|
186
|
+
lon: 'lon',
|
|
187
|
+
latitude: 'lat',
|
|
188
|
+
longitude: 'lon'
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/** Detect spatial dimension mapping for @carbonplan/zarr-layer. */
|
|
192
|
+
function detectSpatialDims(meta: ZarrNode | undefined): { lat: string; lon: string } | null {
|
|
193
|
+
if (!meta?.shape) return null;
|
|
194
|
+
const dimNames = meta.dims?.length ? meta.dims : inferDims(meta.name, meta.shape);
|
|
195
|
+
let lat: string | null = null;
|
|
196
|
+
let lon: string | null = null;
|
|
197
|
+
for (const d of dimNames) {
|
|
198
|
+
const role = SPATIAL_ALIASES[d.toLowerCase()];
|
|
199
|
+
if (role === 'lat' && !lat) lat = d;
|
|
200
|
+
else if (role === 'lon' && !lon) lon = d;
|
|
201
|
+
}
|
|
202
|
+
return lat && lon ? { lat, lon } : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getSelectorDims(
|
|
206
|
+
meta: ZarrNode | undefined,
|
|
207
|
+
coordMap: Map<string, ZarrNode>
|
|
208
|
+
): SelectorDim[] {
|
|
209
|
+
if (!meta?.shape) return [];
|
|
210
|
+
const shape = meta.shape;
|
|
211
|
+
// Use real dim names when available, fall back to inferDims
|
|
212
|
+
const dimNames = meta.dims?.length ? meta.dims : inferDims(meta.name, shape);
|
|
213
|
+
const dims: SelectorDim[] = [];
|
|
214
|
+
for (let i = 0; i < dimNames.length; i++) {
|
|
215
|
+
const d = dimNames[i];
|
|
216
|
+
if (SPATIAL_ALIASES[d.toLowerCase()]) continue;
|
|
217
|
+
|
|
218
|
+
const coord = coordMap.get(d);
|
|
219
|
+
const attrs = coord?.attributes ?? {};
|
|
220
|
+
|
|
221
|
+
// Extract min/max from statistics_approximate if available
|
|
222
|
+
let min: string | null = null;
|
|
223
|
+
let max: string | null = null;
|
|
224
|
+
const stats = attrs.statistics_approximate ?? attrs.statistics;
|
|
225
|
+
if (stats && typeof stats === 'object') {
|
|
226
|
+
if (stats.min != null) min = String(stats.min);
|
|
227
|
+
if (stats.max != null) max = String(stats.max);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Detect datetime dimension and parse date range
|
|
231
|
+
const datetime = isDatetimeDim(d, attrs);
|
|
232
|
+
let minDate: Date | null = null;
|
|
233
|
+
let maxDate: Date | null = null;
|
|
234
|
+
if (datetime && min != null && max != null) {
|
|
235
|
+
const dMin = parseDateOrSentinel(min);
|
|
236
|
+
const dMax = parseDateOrSentinel(max);
|
|
237
|
+
if (dMin && dMax) {
|
|
238
|
+
minDate = dMin;
|
|
239
|
+
maxDate = dMax;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Sub-daily: estimated step < 1 day (e.g. 6-hourly forecasts)
|
|
244
|
+
let subDaily = false;
|
|
245
|
+
if (minDate && maxDate && shape[i] >= 2) {
|
|
246
|
+
const stepMs = (maxDate.getTime() - minDate.getTime()) / (shape[i] - 1);
|
|
247
|
+
subDaily = stepMs < 86_400_000;
|
|
115
248
|
}
|
|
249
|
+
|
|
250
|
+
dims.push({
|
|
251
|
+
name: d,
|
|
252
|
+
size: shape[i],
|
|
253
|
+
dtype: coord?.dtype ?? null,
|
|
254
|
+
units: attrs.units ?? null,
|
|
255
|
+
longName: attrs.long_name ?? null,
|
|
256
|
+
min,
|
|
257
|
+
max,
|
|
258
|
+
isDatetime: datetime,
|
|
259
|
+
minDate,
|
|
260
|
+
maxDate,
|
|
261
|
+
subDaily
|
|
262
|
+
});
|
|
116
263
|
}
|
|
117
264
|
return dims;
|
|
118
265
|
}
|
|
119
266
|
|
|
267
|
+
/** Format a dimension label: show long_name or name, with dtype. */
|
|
268
|
+
function dimLabel(dim: SelectorDim): string {
|
|
269
|
+
const label = dim.longName ?? dim.name;
|
|
270
|
+
return dim.dtype ? `${label} (${dim.dtype})` : label;
|
|
271
|
+
}
|
|
272
|
+
|
|
120
273
|
// Initialize selector values when variable changes
|
|
121
274
|
$effect(() => {
|
|
122
275
|
const dims = selectorDims;
|
|
123
276
|
const prev = untrack(() => selectorValues);
|
|
124
277
|
const newVals: Record<string, number> = {};
|
|
125
278
|
for (const d of dims) {
|
|
126
|
-
newVals[d.name] = prev[d.name] ?? 0;
|
|
279
|
+
newVals[d.name] = prev[d.name] ?? (d.isDatetime ? d.size - 1 : 0);
|
|
127
280
|
}
|
|
128
281
|
selectorValues = newVals;
|
|
129
282
|
});
|
|
130
283
|
|
|
284
|
+
function getOrCreatePopup(): maplibregl.Popup {
|
|
285
|
+
if (!inspectPopup) {
|
|
286
|
+
inspectPopup = new maplibreModule.Popup({
|
|
287
|
+
closeButton: true,
|
|
288
|
+
closeOnClick: false,
|
|
289
|
+
maxWidth: '240px',
|
|
290
|
+
className: 'zarr-inspect-popup'
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return inspectPopup;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function formatPopupHtml(value: number | null | undefined, lngLat: maplibregl.LngLat): string {
|
|
297
|
+
const varName = selectedVar;
|
|
298
|
+
const units = selectedMeta?.attributes?.units;
|
|
299
|
+
const noData = value == null || Number.isNaN(value);
|
|
300
|
+
|
|
301
|
+
let valueStr: string;
|
|
302
|
+
if (noData) {
|
|
303
|
+
valueStr = t('map.noValue');
|
|
304
|
+
} else {
|
|
305
|
+
valueStr = Number.isInteger(value) ? String(value) : value.toPrecision(4);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Hide units when no data, or when units is "1" (CF dimensionless)
|
|
309
|
+
const showUnits = !noData && units && units !== '1';
|
|
310
|
+
|
|
311
|
+
const lat = lngLat.lat.toFixed(4);
|
|
312
|
+
const lon = lngLat.lng.toFixed(4);
|
|
313
|
+
|
|
314
|
+
return `<div class="text-xs space-y-0.5">
|
|
315
|
+
<div class="font-medium text-zinc-300">${varName}</div>
|
|
316
|
+
<div>${valueStr}${showUnits ? ` <span class="text-zinc-500">${units}</span>` : ''}</div>
|
|
317
|
+
<div class="text-zinc-500">${lat}, ${lon}</div>
|
|
318
|
+
</div>`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function handleMapClick(e: maplibregl.MapMouseEvent) {
|
|
322
|
+
if (!zarrLayer) return;
|
|
323
|
+
|
|
324
|
+
const popup = getOrCreatePopup();
|
|
325
|
+
popup
|
|
326
|
+
.setLngLat(e.lngLat)
|
|
327
|
+
.setHTML(`<span class="text-xs">${t('map.loadingZarr')}</span>`)
|
|
328
|
+
.addTo(mapRef!);
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const result = await zarrLayer.queryData({
|
|
332
|
+
type: 'Point',
|
|
333
|
+
coordinates: [e.lngLat.lng, e.lngLat.lat]
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const raw = result?.[selectedVar];
|
|
337
|
+
// queryData may return Array, TypedArray (Float32Array), or scalar
|
|
338
|
+
const value = raw != null && typeof raw === 'object' && 'length' in raw ? raw[0] : raw;
|
|
339
|
+
popup.setHTML(formatPopupHtml(value, e.lngLat));
|
|
340
|
+
} catch {
|
|
341
|
+
popup.setHTML(`<span class="text-xs">${t('map.noValue')}</span>`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Build the current selector object from selectorDims state. */
|
|
346
|
+
function buildSelector(): Record<string, any> {
|
|
347
|
+
const selector: Record<string, any> = {};
|
|
348
|
+
for (const d of selectorDims) {
|
|
349
|
+
const fallback = d.isDatetime ? d.size - 1 : 0;
|
|
350
|
+
selector[d.name] = { selected: selectorValues[d.name] ?? fallback, type: 'index' };
|
|
351
|
+
}
|
|
352
|
+
return selector;
|
|
353
|
+
}
|
|
354
|
+
|
|
131
355
|
async function onMapReady(map: maplibregl.Map) {
|
|
132
356
|
mapRef = map;
|
|
133
357
|
await addZarrLayer(map);
|
|
358
|
+
map.on('click', handleMapClick);
|
|
134
359
|
}
|
|
135
360
|
|
|
136
361
|
async function addZarrLayer(map: maplibregl.Map) {
|
|
@@ -143,13 +368,12 @@ async function addZarrLayer(map: maplibregl.Map) {
|
|
|
143
368
|
map.removeLayer(zarrLayer.id);
|
|
144
369
|
}
|
|
145
370
|
|
|
371
|
+
// Ensure numcodecs codecs (shuffle, zlib, etc.) are registered before zarr-layer uses zarrita
|
|
372
|
+
await ensureCodecsRegistered();
|
|
146
373
|
const { ZarrLayer } = await import('@carbonplan/zarr-layer');
|
|
147
374
|
|
|
148
375
|
const storeUrl = buildStoreUrl();
|
|
149
|
-
const selector
|
|
150
|
-
for (const [dim, val] of Object.entries(selectorValues)) {
|
|
151
|
-
selector[dim] = { selected: val, type: 'index' };
|
|
152
|
-
}
|
|
376
|
+
const selector = buildSelector();
|
|
153
377
|
|
|
154
378
|
const opts: any = {
|
|
155
379
|
id: 'zarr-data',
|
|
@@ -163,15 +387,32 @@ async function addZarrLayer(map: maplibregl.Map) {
|
|
|
163
387
|
onLoadingStateChange: (state: any) => {
|
|
164
388
|
if (state.error) {
|
|
165
389
|
error = state.error.message;
|
|
390
|
+
loading = false;
|
|
391
|
+
// Immediately remove failed layer to prevent WebGL context corruption
|
|
392
|
+
try {
|
|
393
|
+
if (map.getLayer('zarr-data')) map.removeLayer('zarr-data');
|
|
394
|
+
} catch {
|
|
395
|
+
/* map may already be destroyed */
|
|
396
|
+
}
|
|
397
|
+
zarrLayer = null;
|
|
398
|
+
return;
|
|
166
399
|
}
|
|
167
400
|
loading = state.loading;
|
|
168
401
|
}
|
|
169
402
|
};
|
|
170
403
|
|
|
171
|
-
//
|
|
404
|
+
// Map spatial dimension names for @carbonplan/zarr-layer
|
|
405
|
+
const spatial = detectSpatialDims(selectedMeta);
|
|
406
|
+
if (!spatial && !proj4String) {
|
|
407
|
+
error = 'Cannot map this variable: no spatial dimensions (lat/lon, y/x) detected';
|
|
408
|
+
loading = false;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
172
411
|
if (proj4String) {
|
|
173
412
|
opts.proj4 = proj4String;
|
|
174
|
-
opts.spatialDimensions = { lat: 'y', lon: 'x' };
|
|
413
|
+
opts.spatialDimensions = spatial ? spatial : { lat: 'y', lon: 'x' };
|
|
414
|
+
} else if (spatial) {
|
|
415
|
+
opts.spatialDimensions = spatial;
|
|
175
416
|
}
|
|
176
417
|
|
|
177
418
|
zarrLayer = new ZarrLayer(opts);
|
|
@@ -183,20 +424,16 @@ async function addZarrLayer(map: maplibregl.Map) {
|
|
|
183
424
|
}
|
|
184
425
|
|
|
185
426
|
function buildStoreUrl(): string {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
return url.replace(/\/zarr\.json$/, '').replace(/\/+$/, '');
|
|
427
|
+
const rawUrl = buildHttpsUrl(tab).replace(/\/+$/, '');
|
|
428
|
+
return extractZarrStoreUrl(rawUrl) ?? rawUrl;
|
|
189
429
|
}
|
|
190
430
|
|
|
191
431
|
// Re-render when selector changes
|
|
192
432
|
async function updateSelector() {
|
|
193
433
|
if (!zarrLayer) return;
|
|
194
|
-
|
|
195
|
-
for (const [dim, val] of Object.entries(selectorValues)) {
|
|
196
|
-
selector[dim] = { selected: val, type: 'index' };
|
|
197
|
-
}
|
|
434
|
+
inspectPopup?.remove();
|
|
198
435
|
try {
|
|
199
|
-
await zarrLayer.setSelector(
|
|
436
|
+
await zarrLayer.setSelector(buildSelector());
|
|
200
437
|
} catch (err) {
|
|
201
438
|
error = err instanceof Error ? err.message : String(err);
|
|
202
439
|
}
|
|
@@ -205,11 +442,15 @@ async function updateSelector() {
|
|
|
205
442
|
// Re-render when variable changes
|
|
206
443
|
async function changeVariable() {
|
|
207
444
|
if (!mapRef) return;
|
|
445
|
+
inspectPopup?.remove();
|
|
208
446
|
await addZarrLayer(mapRef);
|
|
209
447
|
}
|
|
210
448
|
|
|
211
449
|
function cleanup() {
|
|
450
|
+
inspectPopup?.remove();
|
|
451
|
+
inspectPopup = null;
|
|
212
452
|
try {
|
|
453
|
+
mapRef?.off('click', handleMapClick);
|
|
213
454
|
if (zarrLayer && mapRef?.getLayer('zarr-data')) {
|
|
214
455
|
mapRef.removeLayer('zarr-data');
|
|
215
456
|
}
|
|
@@ -231,7 +472,7 @@ onDestroy(cleanup);
|
|
|
231
472
|
<div class="flex h-full w-full flex-col overflow-hidden">
|
|
232
473
|
<!-- Controls bar -->
|
|
233
474
|
<div
|
|
234
|
-
class="flex items-center gap-
|
|
475
|
+
class="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-zinc-200 px-3 py-1.5 dark:border-zinc-800"
|
|
235
476
|
>
|
|
236
477
|
<label class="flex items-center gap-1 text-xs text-zinc-400">
|
|
237
478
|
{t('map.variable')}
|
|
@@ -241,27 +482,57 @@ onDestroy(cleanup);
|
|
|
241
482
|
onchange={changeVariable}
|
|
242
483
|
>
|
|
243
484
|
{#each variables as v}
|
|
244
|
-
<option value={v
|
|
485
|
+
<option value={varPath(v)}>{v.name}</option>
|
|
245
486
|
{/each}
|
|
246
487
|
</select>
|
|
247
488
|
</label>
|
|
248
489
|
|
|
249
490
|
{#each selectorDims as dim}
|
|
250
|
-
<label
|
|
251
|
-
|
|
491
|
+
<label
|
|
492
|
+
class="flex shrink-0 items-center gap-1.5 rounded border border-zinc-200 px-2 py-0.5 text-xs text-zinc-400 dark:border-zinc-700"
|
|
493
|
+
title={dimLabel(dim)}
|
|
494
|
+
>
|
|
495
|
+
<span class="shrink-0 font-medium text-zinc-500 dark:text-zinc-400">{dim.name}</span>
|
|
252
496
|
<input
|
|
253
497
|
type="range"
|
|
254
498
|
min="0"
|
|
255
499
|
max={dim.size - 1}
|
|
256
|
-
|
|
500
|
+
value={selectorValues[dim.name] ?? 0}
|
|
501
|
+
oninput={(e) => {
|
|
502
|
+
selectorValues[dim.name] = +e.currentTarget.value;
|
|
503
|
+
}}
|
|
257
504
|
onchange={updateSelector}
|
|
258
505
|
class="h-1 w-16"
|
|
259
506
|
/>
|
|
260
|
-
|
|
507
|
+
{#if dim.isDatetime && dim.minDate && dim.maxDate}
|
|
508
|
+
{@const dateVal = indexToDateStr(selectorValues[dim.name] ?? 0, dim)}
|
|
509
|
+
<span class="shrink-0 tabular-nums text-zinc-500">
|
|
510
|
+
{dateVal ? (dim.subDaily ? dateVal.replace('T', ' ') : dateVal) : (selectorValues[dim.name] ?? 0)}
|
|
511
|
+
</span>
|
|
512
|
+
<input
|
|
513
|
+
type={dim.subDaily ? 'datetime-local' : 'date'}
|
|
514
|
+
min={dim.minDate.toISOString().slice(0, dim.subDaily ? 16 : 10)}
|
|
515
|
+
max={dim.maxDate.toISOString().slice(0, dim.subDaily ? 16 : 10)}
|
|
516
|
+
value={dateVal ?? ''}
|
|
517
|
+
onchange={(e) => {
|
|
518
|
+
const val = /** @type {HTMLInputElement} */ (e.currentTarget).value;
|
|
519
|
+
if (val) {
|
|
520
|
+
selectorValues[dim.name] = dateToIndex(val, dim);
|
|
521
|
+
updateSelector();
|
|
522
|
+
}
|
|
523
|
+
}}
|
|
524
|
+
class="h-5 rounded border border-zinc-300 bg-white px-1 text-[10px] text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-400"
|
|
525
|
+
/>
|
|
526
|
+
{:else}
|
|
527
|
+
<span class="shrink-0 tabular-nums text-zinc-500">{selectorValues[dim.name] ?? 0}<span class="text-zinc-500/60">/{dim.size - 1}</span></span>
|
|
528
|
+
{#if dim.dtype}
|
|
529
|
+
<span class="shrink-0 text-[10px] text-zinc-400/70">{dim.dtype}</span>
|
|
530
|
+
{/if}
|
|
531
|
+
{/if}
|
|
261
532
|
</label>
|
|
262
533
|
{/each}
|
|
263
534
|
|
|
264
|
-
{#if selectedMeta}
|
|
535
|
+
{#if selectedMeta?.shape}
|
|
265
536
|
<span class="ms-auto text-xs text-zinc-400">
|
|
266
537
|
{selectedMeta.dtype} [{selectedMeta.shape.join(', ')}]
|
|
267
538
|
</span>
|
|
@@ -286,3 +557,26 @@ onDestroy(cleanup);
|
|
|
286
557
|
{/if}
|
|
287
558
|
</div>
|
|
288
559
|
</div>
|
|
560
|
+
|
|
561
|
+
<style>
|
|
562
|
+
:global(.zarr-inspect-popup .maplibregl-popup-content) {
|
|
563
|
+
background: rgba(24, 24, 27, 0.92);
|
|
564
|
+
color: #e4e4e7;
|
|
565
|
+
border-radius: 6px;
|
|
566
|
+
padding: 6px 8px;
|
|
567
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
568
|
+
border: 1px solid rgba(63, 63, 70, 0.5);
|
|
569
|
+
}
|
|
570
|
+
:global(.zarr-inspect-popup .maplibregl-popup-tip) {
|
|
571
|
+
border-top-color: rgba(24, 24, 27, 0.92);
|
|
572
|
+
}
|
|
573
|
+
:global(.zarr-inspect-popup .maplibregl-popup-close-button) {
|
|
574
|
+
color: #a1a1aa;
|
|
575
|
+
font-size: 14px;
|
|
576
|
+
padding: 2px 4px;
|
|
577
|
+
}
|
|
578
|
+
:global(.zarr-inspect-popup .maplibregl-popup-close-button:hover) {
|
|
579
|
+
color: #e4e4e7;
|
|
580
|
+
background: transparent;
|
|
581
|
+
}
|
|
582
|
+
</style>
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import type { Tab } from '../../types';
|
|
2
|
-
|
|
3
|
-
name: string;
|
|
4
|
-
shape: number[];
|
|
5
|
-
dtype: string;
|
|
6
|
-
dims: string[];
|
|
7
|
-
attributes: Record<string, any>;
|
|
8
|
-
}
|
|
2
|
+
import { type ZarrNode } from '../../utils/zarr.js';
|
|
9
3
|
type $$ComponentProps = {
|
|
10
4
|
tab: Tab;
|
|
11
|
-
variables:
|
|
5
|
+
variables: ZarrNode[];
|
|
6
|
+
coords?: ZarrNode[];
|
|
12
7
|
spatialRefAttrs: Record<string, any> | null;
|
|
13
8
|
zarrVersion?: number | null;
|
|
14
9
|
};
|