@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
package/dist/utils/zarr.js
CHANGED
|
@@ -1,11 +1,123 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zarr metadata parsing utilities.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Builds a hierarchical tree of groups and arrays from consolidated metadata
|
|
5
|
+
* (Zarr v2 .zmetadata, v3 zarr.json), with zarrita fallback for non-consolidated stores.
|
|
6
6
|
*/
|
|
7
|
+
import { formatFileSize } from './format.js';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Register numcodecs-wrapped codecs with zarrita's codec registry.
|
|
10
|
+
// Zarr v3 stores produced by Python's zarr-python may wrap codecs with the
|
|
11
|
+
// "numcodecs." prefix (e.g. "numcodecs.zlib", "numcodecs.shuffle").
|
|
12
|
+
// zarrita only registers the bare names, so we add aliases + a byte shuffle.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/**
|
|
15
|
+
* Byte shuffle codec (HDF5 / numcodecs byte shuffle).
|
|
16
|
+
* Rearranges bytes within elements to improve downstream compression ratios.
|
|
17
|
+
* Operates as a bytes_to_bytes filter in the codec pipeline.
|
|
18
|
+
*/
|
|
19
|
+
const ShuffleCodec = {
|
|
20
|
+
kind: 'bytes_to_bytes',
|
|
21
|
+
fromConfig(config) {
|
|
22
|
+
const elementsize = config?.elementsize ?? 4;
|
|
23
|
+
return {
|
|
24
|
+
kind: 'bytes_to_bytes',
|
|
25
|
+
encode(data) {
|
|
26
|
+
const count = data.length / elementsize;
|
|
27
|
+
const out = new Uint8Array(data.length);
|
|
28
|
+
for (let i = 0; i < count; i++) {
|
|
29
|
+
for (let j = 0; j < elementsize; j++) {
|
|
30
|
+
out[j * count + i] = data[i * elementsize + j];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
},
|
|
35
|
+
decode(data) {
|
|
36
|
+
const count = data.length / elementsize;
|
|
37
|
+
const out = new Uint8Array(data.length);
|
|
38
|
+
for (let i = 0; i < count; i++) {
|
|
39
|
+
for (let j = 0; j < elementsize; j++) {
|
|
40
|
+
out[i * elementsize + j] = data[j * count + i];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/** Register numcodecs-prefixed aliases with zarrita's codec registry. */
|
|
49
|
+
async function registerNumcodecs() {
|
|
50
|
+
try {
|
|
51
|
+
const { registry } = await import('zarrita');
|
|
52
|
+
// Alias existing codecs with numcodecs. prefix
|
|
53
|
+
for (const name of ['zlib', 'gzip', 'blosc', 'lz4', 'zstd']) {
|
|
54
|
+
const existing = registry.get(name);
|
|
55
|
+
if (existing)
|
|
56
|
+
registry.set(`numcodecs.${name}`, existing);
|
|
57
|
+
}
|
|
58
|
+
// Register shuffle codec
|
|
59
|
+
registry.set('numcodecs.shuffle', () => Promise.resolve(ShuffleCodec));
|
|
60
|
+
registry.set('shuffle', () => Promise.resolve(ShuffleCodec));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// zarrita not available — skip registration
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
let _codecsPromise = null;
|
|
67
|
+
/** Ensure numcodecs codecs are registered. Await before creating ZarrLayer. */
|
|
68
|
+
export function ensureCodecsRegistered() {
|
|
69
|
+
if (!_codecsPromise)
|
|
70
|
+
_codecsPromise = registerNumcodecs();
|
|
71
|
+
return _codecsPromise;
|
|
72
|
+
}
|
|
73
|
+
// Start registration immediately at module load
|
|
74
|
+
ensureCodecsRegistered();
|
|
75
|
+
/** Zarr store marker files — presence of any indicates a Zarr store. */
|
|
76
|
+
export const ZARR_MARKER_FILES = new Set([
|
|
77
|
+
'zarr.json',
|
|
78
|
+
'.zmetadata',
|
|
79
|
+
'.zgroup',
|
|
80
|
+
'.zarray',
|
|
81
|
+
'.zattrs'
|
|
82
|
+
]);
|
|
83
|
+
/** Zarr marker file suffixes used in URLs (with leading slash). */
|
|
84
|
+
const ZARR_MARKER_SUFFIXES = ['/zarr.json', '/.zmetadata', '/.zgroup', '/.zarray', '/.zattrs'];
|
|
85
|
+
/**
|
|
86
|
+
* Detect whether a set of file names contains Zarr marker files.
|
|
87
|
+
* Returns the detected version (2 or 3) or null if not detected.
|
|
88
|
+
*/
|
|
89
|
+
export function detectZarrMarkers(fileNames) {
|
|
90
|
+
let hasV3 = false;
|
|
91
|
+
let hasV2 = false;
|
|
92
|
+
for (const name of fileNames) {
|
|
93
|
+
if (name === 'zarr.json')
|
|
94
|
+
hasV3 = true;
|
|
95
|
+
else if (ZARR_MARKER_FILES.has(name))
|
|
96
|
+
hasV2 = true;
|
|
97
|
+
}
|
|
98
|
+
if (hasV3)
|
|
99
|
+
return { detected: true, version: 3 };
|
|
100
|
+
if (hasV2)
|
|
101
|
+
return { detected: true, version: 2 };
|
|
102
|
+
return { detected: false, version: null };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* If a URL points to a Zarr marker file, strip the marker suffix and return the store URL.
|
|
106
|
+
* Returns null if the URL doesn't end with a known marker suffix.
|
|
107
|
+
*/
|
|
108
|
+
export function extractZarrStoreUrl(url) {
|
|
109
|
+
for (const suffix of ZARR_MARKER_SUFFIXES) {
|
|
110
|
+
if (url.endsWith(suffix)) {
|
|
111
|
+
return url.slice(0, -suffix.length);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Kept helpers
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
7
119
|
/** Dimension-like variable names treated as coordinates. */
|
|
8
|
-
const DIM_LIKE_NAMES = new Set([
|
|
120
|
+
export const DIM_LIKE_NAMES = new Set([
|
|
9
121
|
'x',
|
|
10
122
|
'y',
|
|
11
123
|
'lat',
|
|
@@ -36,110 +148,279 @@ export function formatShape(shape) {
|
|
|
36
148
|
return 'scalar';
|
|
37
149
|
return `[${shape.join(' × ')}]`;
|
|
38
150
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Metadata display helpers
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
/** Byte size of a dtype string. Handles v2 (`<f4`, `|u1`) and v3 (`float32`, `uint8`). */
|
|
155
|
+
export function dtypeByteSize(dtype) {
|
|
156
|
+
// v2 style: last char(s) are the byte width e.g. "<f4" → 4, "|u1" → 1
|
|
157
|
+
const v2 = /^[<>|]?[a-zA-Z](\d+)$/.exec(dtype);
|
|
158
|
+
if (v2)
|
|
159
|
+
return Number(v2[1]);
|
|
160
|
+
// v3 style: "float32" → 4, "int16" → 2, "uint8" → 1, "complex128" → 16
|
|
161
|
+
const v3 = /(\d+)$/.exec(dtype);
|
|
162
|
+
if (v3)
|
|
163
|
+
return Number(v3[1]) / 8;
|
|
164
|
+
// bool
|
|
165
|
+
if (dtype === 'bool')
|
|
166
|
+
return 1;
|
|
167
|
+
return 1; // fallback
|
|
168
|
+
}
|
|
169
|
+
/** Format chunk count: `"36 [6 × 6]"` */
|
|
170
|
+
export function computeChunkCount(shape, chunks) {
|
|
171
|
+
if (!shape?.length || !chunks?.length || shape.length !== chunks.length)
|
|
172
|
+
return null;
|
|
173
|
+
const dims = shape.map((s, i) => Math.ceil(s / chunks[i]));
|
|
174
|
+
const total = dims.reduce((a, b) => a * b, 1);
|
|
175
|
+
return `${total.toLocaleString()} [${dims.join(' × ')}]`;
|
|
176
|
+
}
|
|
177
|
+
/** Format chunk size in bytes: `"817.6 KB"` */
|
|
178
|
+
export function computeChunkSize(chunks, dtype) {
|
|
179
|
+
if (!chunks?.length || !dtype)
|
|
180
|
+
return null;
|
|
181
|
+
const elements = chunks.reduce((a, b) => a * b, 1);
|
|
182
|
+
return formatFileSize(elements * dtypeByteSize(dtype));
|
|
183
|
+
}
|
|
184
|
+
/** Format uncompressed size: `"28.7 MB"` */
|
|
185
|
+
export function computeUncompressed(shape, dtype) {
|
|
186
|
+
if (!shape?.length || !dtype)
|
|
187
|
+
return null;
|
|
188
|
+
const elements = shape.reduce((a, b) => a * b, 1);
|
|
189
|
+
return formatFileSize(elements * dtypeByteSize(dtype));
|
|
190
|
+
}
|
|
191
|
+
/** Format codec pipeline for display. */
|
|
192
|
+
export function formatCodecs(node) {
|
|
193
|
+
// v3: codecs array
|
|
194
|
+
if (node.codecs?.length) {
|
|
195
|
+
return node.codecs
|
|
196
|
+
.map((c) => {
|
|
197
|
+
const name = c.name ?? c.codec_id ?? (typeof c === 'string' ? c : '');
|
|
198
|
+
if (!name)
|
|
199
|
+
return JSON.stringify(c);
|
|
200
|
+
const cfg = c.configuration ?? c.codec_config ?? {};
|
|
201
|
+
const parts = [];
|
|
202
|
+
if (cfg.cname)
|
|
203
|
+
parts.push(cfg.cname);
|
|
204
|
+
const level = cfg.clevel ?? cfg.level;
|
|
205
|
+
if (level != null)
|
|
206
|
+
parts.push(`level ${level}`);
|
|
207
|
+
if (cfg.shuffle != null)
|
|
208
|
+
parts.push(cfg.shuffle ? 'shuffle' : 'no shuffle');
|
|
209
|
+
if (cfg.typesize)
|
|
210
|
+
parts.push(`typesize ${cfg.typesize}`);
|
|
211
|
+
if (cfg.elementsize)
|
|
212
|
+
parts.push(`elementsize ${cfg.elementsize}`);
|
|
213
|
+
return parts.length ? `${name} (${parts.join(', ')})` : name;
|
|
214
|
+
})
|
|
215
|
+
.join(' → ');
|
|
216
|
+
}
|
|
217
|
+
// v2: compressor + filters
|
|
218
|
+
if (node.compressor) {
|
|
219
|
+
const c = node.compressor;
|
|
220
|
+
const name = c.id ?? c.codec ?? 'unknown';
|
|
221
|
+
const parts = [];
|
|
222
|
+
if (c.cname)
|
|
223
|
+
parts.push(c.cname);
|
|
224
|
+
if (c.clevel != null)
|
|
225
|
+
parts.push(`level ${c.clevel}`);
|
|
226
|
+
if (c.shuffle != null)
|
|
227
|
+
parts.push(c.shuffle ? 'shuffle' : 'no shuffle');
|
|
228
|
+
const base = parts.length ? `${name} (${parts.join(', ')})` : name;
|
|
229
|
+
if (node.filters?.length) {
|
|
230
|
+
const filterStr = node.filters.map((f) => f.id ?? JSON.stringify(f)).join(', ');
|
|
231
|
+
return `${filterStr} → ${base}`;
|
|
47
232
|
}
|
|
233
|
+
return base;
|
|
48
234
|
}
|
|
49
|
-
return
|
|
235
|
+
return null;
|
|
50
236
|
}
|
|
51
|
-
/**
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
237
|
+
/** Format chunk_key_encoding for display: `"default (sep: "/")"` */
|
|
238
|
+
export function formatChunkKeys(node) {
|
|
239
|
+
return node.chunkKeyEncoding ?? null;
|
|
240
|
+
}
|
|
241
|
+
/** Find a node by slash-delimited path. */
|
|
242
|
+
export function findNodeByPath(root, path) {
|
|
243
|
+
if (path === '/' || path === '')
|
|
244
|
+
return root;
|
|
245
|
+
const parts = path.replace(/^\//, '').split('/');
|
|
246
|
+
let current = root;
|
|
247
|
+
for (const part of parts) {
|
|
248
|
+
const child = current.children.find((c) => c.name === part);
|
|
249
|
+
if (!child)
|
|
250
|
+
return null;
|
|
251
|
+
current = child;
|
|
252
|
+
}
|
|
253
|
+
return current;
|
|
254
|
+
}
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Tree builders
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
function makeNode(path, kind, attrs) {
|
|
259
|
+
const name = path === '/' ? '/' : path.split('/').pop();
|
|
260
|
+
return { path, name, kind, children: [], attributes: attrs };
|
|
261
|
+
}
|
|
262
|
+
/** Ensure all intermediate groups exist and return the parent for `path`. */
|
|
263
|
+
function ensureParent(root, path) {
|
|
264
|
+
const parts = path.replace(/^\//, '').split('/');
|
|
265
|
+
parts.pop(); // remove leaf
|
|
266
|
+
let current = root;
|
|
267
|
+
let currentPath = '';
|
|
268
|
+
for (const part of parts) {
|
|
269
|
+
currentPath += `/${part}`;
|
|
270
|
+
let child = current.children.find((c) => c.name === part);
|
|
271
|
+
if (!child) {
|
|
272
|
+
child = makeNode(currentPath, 'group', {});
|
|
273
|
+
current.children.push(child);
|
|
73
274
|
}
|
|
74
|
-
|
|
75
|
-
|
|
275
|
+
current = child;
|
|
276
|
+
}
|
|
277
|
+
return current;
|
|
278
|
+
}
|
|
279
|
+
/** Build tree from Zarr v3 consolidated metadata (zarr.json). */
|
|
280
|
+
export function buildV3Tree(data) {
|
|
281
|
+
const meta = data.consolidated_metadata?.metadata ?? {};
|
|
282
|
+
const rootAttrs = data.attributes ?? {};
|
|
283
|
+
const root = makeNode('/', 'group', rootAttrs);
|
|
284
|
+
let totalNodes = 1;
|
|
285
|
+
let spatialRefAttrs = null;
|
|
286
|
+
for (const [key, info] of Object.entries(meta)) {
|
|
287
|
+
const path = `/${key}`;
|
|
288
|
+
const attrs = info.attributes ?? {};
|
|
289
|
+
if (info.node_type === 'group' || (!info.shape && !info.data_type)) {
|
|
290
|
+
// Group node
|
|
291
|
+
const node = makeNode(path, 'group', attrs);
|
|
292
|
+
const parent = ensureParent(root, path);
|
|
293
|
+
// Avoid duplicating if ensureParent already created this node
|
|
294
|
+
const existing = parent.children.find((c) => c.name === node.name);
|
|
295
|
+
if (existing) {
|
|
296
|
+
existing.attributes = { ...existing.attributes, ...attrs };
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
parent.children.push(node);
|
|
300
|
+
}
|
|
301
|
+
totalNodes++;
|
|
76
302
|
}
|
|
77
303
|
else {
|
|
78
|
-
|
|
304
|
+
// Array node
|
|
305
|
+
const node = makeNode(path, 'array', attrs);
|
|
306
|
+
node.shape = info.shape;
|
|
307
|
+
node.dtype = info.data_type ?? 'unknown';
|
|
308
|
+
node.dims =
|
|
309
|
+
info.dimension_names ?? attrs._ARRAY_DIMENSIONS ?? inferDims(node.name, node.shape ?? []);
|
|
310
|
+
node.chunks = info.chunk_grid?.configuration?.chunk_shape ?? [];
|
|
311
|
+
node.fillValue = info.fill_value;
|
|
312
|
+
node.codecs = info.codecs ?? [];
|
|
313
|
+
// Parse chunk_key_encoding for v3
|
|
314
|
+
const cke = info.chunk_key_encoding;
|
|
315
|
+
if (cke) {
|
|
316
|
+
const sep = cke.configuration?.separator ?? '/';
|
|
317
|
+
node.chunkKeyEncoding = `${cke.name ?? 'default'} (sep: "${sep}")`;
|
|
318
|
+
}
|
|
319
|
+
const parent = ensureParent(root, path);
|
|
320
|
+
parent.children.push(node);
|
|
321
|
+
totalNodes++;
|
|
322
|
+
if (node.name === 'spatial_ref') {
|
|
323
|
+
spatialRefAttrs = attrs;
|
|
324
|
+
}
|
|
79
325
|
}
|
|
80
326
|
}
|
|
81
|
-
|
|
327
|
+
// Sort children alphabetically, groups first
|
|
328
|
+
sortTree(root);
|
|
329
|
+
return {
|
|
330
|
+
root,
|
|
331
|
+
zarrVersion: 3,
|
|
332
|
+
totalNodes,
|
|
333
|
+
storeAttrs: rootAttrs,
|
|
334
|
+
spatialRefAttrs
|
|
335
|
+
};
|
|
82
336
|
}
|
|
83
|
-
/**
|
|
84
|
-
export function
|
|
337
|
+
/** Build tree from Zarr v2 consolidated metadata (.zmetadata). */
|
|
338
|
+
export function buildV2Tree(data) {
|
|
85
339
|
const meta = data.metadata ?? {};
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
let
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
340
|
+
const rootAttrs = meta['.zattrs'] ?? {};
|
|
341
|
+
const root = makeNode('/', 'group', rootAttrs);
|
|
342
|
+
let totalNodes = 1;
|
|
343
|
+
let spatialRefAttrs = null;
|
|
344
|
+
// Collect group paths
|
|
345
|
+
const groupKeys = Object.keys(meta).filter((k) => k.endsWith('/.zgroup'));
|
|
346
|
+
for (const key of groupKeys) {
|
|
347
|
+
const name = key.replace('/.zgroup', '');
|
|
348
|
+
if (!name)
|
|
349
|
+
continue; // root
|
|
350
|
+
const path = `/${name}`;
|
|
351
|
+
const attrs = meta[`${name}/.zattrs`] ?? {};
|
|
352
|
+
const parent = ensureParent(root, path);
|
|
353
|
+
const existing = parent.children.find((c) => c.name === name.split('/').pop());
|
|
354
|
+
if (existing) {
|
|
355
|
+
existing.attributes = { ...existing.attributes, ...attrs };
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
parent.children.push(makeNode(path, 'group', attrs));
|
|
100
359
|
}
|
|
360
|
+
totalNodes++;
|
|
101
361
|
}
|
|
362
|
+
// Collect array paths
|
|
363
|
+
const arrayKeys = Object.keys(meta).filter((k) => k.endsWith('/.zarray'));
|
|
102
364
|
for (const key of arrayKeys) {
|
|
103
365
|
const name = key.replace('/.zarray', '');
|
|
366
|
+
const path = `/${name}`;
|
|
104
367
|
const zarray = meta[key];
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
vars.push(v);
|
|
368
|
+
const attrs = meta[`${name}/.zattrs`] ?? {};
|
|
369
|
+
const shape = zarray.shape ?? [];
|
|
370
|
+
const node = makeNode(path, 'array', attrs);
|
|
371
|
+
node.shape = shape;
|
|
372
|
+
node.dtype = zarray.dtype ?? 'unknown';
|
|
373
|
+
node.dims = attrs._ARRAY_DIMENSIONS ?? inferDims(node.name, shape);
|
|
374
|
+
node.chunks = zarray.chunks ?? [];
|
|
375
|
+
node.fillValue = zarray.fill_value;
|
|
376
|
+
node.compressor = zarray.compressor ?? null;
|
|
377
|
+
node.filters = zarray.filters ?? [];
|
|
378
|
+
const parent = ensureParent(root, path);
|
|
379
|
+
parent.children.push(node);
|
|
380
|
+
totalNodes++;
|
|
381
|
+
if (node.name === 'spatial_ref') {
|
|
382
|
+
spatialRefAttrs = attrs;
|
|
123
383
|
}
|
|
124
384
|
}
|
|
125
|
-
|
|
385
|
+
sortTree(root);
|
|
386
|
+
return {
|
|
387
|
+
root,
|
|
388
|
+
zarrVersion: 2,
|
|
389
|
+
totalNodes,
|
|
390
|
+
storeAttrs: rootAttrs,
|
|
391
|
+
spatialRefAttrs
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/** Recursively sort children: groups first, then alphabetically. */
|
|
395
|
+
function sortTree(node) {
|
|
396
|
+
node.children.sort((a, b) => {
|
|
397
|
+
if (a.kind !== b.kind)
|
|
398
|
+
return a.kind === 'group' ? -1 : 1;
|
|
399
|
+
return a.name.localeCompare(b.name);
|
|
400
|
+
});
|
|
401
|
+
for (const child of node.children) {
|
|
402
|
+
sortTree(child);
|
|
403
|
+
}
|
|
126
404
|
}
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// Fetchers
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
127
408
|
/**
|
|
128
|
-
* Fetch
|
|
129
|
-
* Tries v3 (zarr.json) first, then v2 (.zmetadata).
|
|
409
|
+
* Fetch hierarchy from a Zarr store URL.
|
|
410
|
+
* Tries v3 (zarr.json) first, then v2 (.zmetadata), then zarrita fallback.
|
|
130
411
|
*/
|
|
131
|
-
export async function
|
|
412
|
+
export async function fetchHierarchy(storeUrl, storeName, signal) {
|
|
132
413
|
// Try Zarr v3 zarr.json
|
|
133
414
|
try {
|
|
134
|
-
const res = await fetch(`${storeUrl}/zarr.json
|
|
415
|
+
const res = await fetch(`${storeUrl}/zarr.json`, { signal });
|
|
135
416
|
if (res.ok) {
|
|
136
417
|
const data = await res.json();
|
|
137
418
|
if (data.zarr_format === 3) {
|
|
138
419
|
if (data.consolidated_metadata) {
|
|
139
|
-
return
|
|
420
|
+
return buildV3Tree(data);
|
|
140
421
|
}
|
|
141
|
-
//
|
|
142
|
-
return
|
|
422
|
+
// V3 without consolidated metadata — discover children
|
|
423
|
+
return discoverV3Children(storeUrl, data, signal);
|
|
143
424
|
}
|
|
144
425
|
}
|
|
145
426
|
}
|
|
@@ -148,53 +429,119 @@ export async function fetchConsolidated(storeUrl) {
|
|
|
148
429
|
}
|
|
149
430
|
// Try Zarr v2 .zmetadata
|
|
150
431
|
try {
|
|
151
|
-
const res = await fetch(`${storeUrl}/.zmetadata
|
|
432
|
+
const res = await fetch(`${storeUrl}/.zmetadata`, { signal });
|
|
152
433
|
if (res.ok) {
|
|
153
434
|
const data = await res.json();
|
|
154
435
|
if (data.metadata) {
|
|
155
|
-
return
|
|
436
|
+
return buildV2Tree(data);
|
|
156
437
|
}
|
|
157
438
|
}
|
|
158
439
|
}
|
|
159
440
|
catch {
|
|
160
441
|
/* ignore */
|
|
161
442
|
}
|
|
162
|
-
|
|
443
|
+
// Fallback: zarrita probe
|
|
444
|
+
return probeHierarchy(storeUrl, storeName);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Discover children of a v3 store without consolidated metadata.
|
|
448
|
+
* Probes child paths from zarr.json attributes (e.g. multiscales convention)
|
|
449
|
+
* and by fetching individual zarr.json files for discovered paths.
|
|
450
|
+
*/
|
|
451
|
+
async function discoverV3Children(storeUrl, rootData, signal) {
|
|
452
|
+
const rootAttrs = rootData.attributes ?? {};
|
|
453
|
+
const root = makeNode('/', 'group', rootAttrs);
|
|
454
|
+
let totalNodes = 1;
|
|
455
|
+
// Collect candidate child names from conventions and common patterns
|
|
456
|
+
const candidates = new Set();
|
|
457
|
+
// Multiscales convention: layout[].asset lists child array names
|
|
458
|
+
const multiscales = rootAttrs.multiscales;
|
|
459
|
+
if (multiscales?.layout && Array.isArray(multiscales.layout)) {
|
|
460
|
+
for (const entry of multiscales.layout) {
|
|
461
|
+
if (entry.asset)
|
|
462
|
+
candidates.add(String(entry.asset));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Probe each candidate path for zarr.json
|
|
466
|
+
const probes = [...candidates].map(async (name) => {
|
|
467
|
+
try {
|
|
468
|
+
const res = await fetch(`${storeUrl}/${name}/zarr.json`, { signal });
|
|
469
|
+
if (!res.ok)
|
|
470
|
+
return null;
|
|
471
|
+
const data = await res.json();
|
|
472
|
+
if (data.node_type === 'array' && data.shape) {
|
|
473
|
+
const node = makeNode(`/${name}`, 'array', data.attributes ?? {});
|
|
474
|
+
node.shape = data.shape;
|
|
475
|
+
node.dtype = data.data_type ?? 'unknown';
|
|
476
|
+
node.dims = data.dimension_names ?? inferDims(name, data.shape);
|
|
477
|
+
node.chunks = data.chunk_grid?.configuration?.chunk_shape ?? [];
|
|
478
|
+
node.fillValue = data.fill_value;
|
|
479
|
+
node.codecs = data.codecs ?? [];
|
|
480
|
+
const cke = data.chunk_key_encoding;
|
|
481
|
+
if (cke) {
|
|
482
|
+
const sep = cke.configuration?.separator ?? '/';
|
|
483
|
+
node.chunkKeyEncoding = `${cke.name ?? 'default'} (sep: "${sep}")`;
|
|
484
|
+
}
|
|
485
|
+
return node;
|
|
486
|
+
}
|
|
487
|
+
if (data.node_type === 'group') {
|
|
488
|
+
return makeNode(`/${name}`, 'group', data.attributes ?? {});
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
const results = await Promise.all(probes);
|
|
497
|
+
for (const node of results) {
|
|
498
|
+
if (node) {
|
|
499
|
+
root.children.push(node);
|
|
500
|
+
totalNodes++;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
sortTree(root);
|
|
504
|
+
return {
|
|
505
|
+
root,
|
|
506
|
+
zarrVersion: 3,
|
|
507
|
+
totalNodes,
|
|
508
|
+
storeAttrs: rootAttrs,
|
|
509
|
+
spatialRefAttrs: null
|
|
510
|
+
};
|
|
163
511
|
}
|
|
164
512
|
/**
|
|
165
513
|
* Fallback: probe a Zarr store using zarrita when consolidated metadata is unavailable.
|
|
166
|
-
* @param storeName - Display name for the root array (e.g. file name without .zarr)
|
|
167
514
|
*/
|
|
168
|
-
export async function
|
|
515
|
+
export async function probeHierarchy(storeUrl, storeName) {
|
|
169
516
|
const zarrita = await import('zarrita');
|
|
170
517
|
const store = new zarrita.FetchStore(storeUrl);
|
|
171
518
|
try {
|
|
172
519
|
const arr = await zarrita.open(store, { kind: 'array' });
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
};
|
|
520
|
+
const root = makeNode('/', 'group', {});
|
|
521
|
+
const node = makeNode(`/${storeName}`, 'array', arr.attrs ?? {});
|
|
522
|
+
node.shape = arr.shape ?? [];
|
|
523
|
+
node.dtype = String(arr.dtype ?? 'unknown');
|
|
524
|
+
node.dims = arr.attrs?._ARRAY_DIMENSIONS ?? [];
|
|
525
|
+
node.chunks = arr.chunks ?? [];
|
|
526
|
+
root.children.push(node);
|
|
181
527
|
return {
|
|
528
|
+
root,
|
|
529
|
+
zarrVersion: null,
|
|
530
|
+
totalNodes: 2,
|
|
182
531
|
storeAttrs: {},
|
|
183
|
-
|
|
184
|
-
coords: [],
|
|
185
|
-
spatialRefAttrs: null,
|
|
186
|
-
zarrVersion: null
|
|
532
|
+
spatialRefAttrs: null
|
|
187
533
|
};
|
|
188
534
|
}
|
|
189
535
|
catch {
|
|
190
536
|
try {
|
|
191
|
-
await zarrita.open(store, { kind: 'group' });
|
|
537
|
+
const grp = await zarrita.open(store, { kind: 'group' });
|
|
538
|
+
const attrs = grp.attrs ?? {};
|
|
192
539
|
return {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
540
|
+
root: makeNode('/', 'group', attrs),
|
|
541
|
+
zarrVersion: null,
|
|
542
|
+
totalNodes: 1,
|
|
543
|
+
storeAttrs: attrs,
|
|
544
|
+
spatialRefAttrs: null
|
|
198
545
|
};
|
|
199
546
|
}
|
|
200
547
|
catch {
|