ol-stac 1.5.1 → 1.7.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.
@@ -0,0 +1,1930 @@
1
+ /**
2
+ * @module ol/source/GeoZarr
3
+ */
4
+ import { FetchStore, NotFoundError, get, open, slice, withRangeCoalescing, } from 'zarrita';
5
+ import { warn } from 'ol/console.js';
6
+ import { getCenter } from 'ol/extent.js';
7
+ import { get as getProjection, toUserCoordinate, toUserExtent } from 'ol/proj.js';
8
+ import { fromProjectionDefinition } from '../shim/proj4.js';
9
+ import { toSize } from 'ol/size.js';
10
+ import WMTSTileGrid from 'ol/tilegrid/WMTS.js';
11
+ import { DEFAULT_TILE_SIZE } from 'ol/tilegrid/common.js';
12
+ import { getUid } from 'ol/util.js';
13
+ import DataTileSource from 'ol/source/DataTile.js';
14
+ import { parseTileMatrixSet } from '../shim/ogcTileUtil.js';
15
+ /**
16
+ * @typedef {'nearest'|'linear'} ResampleMethod
17
+ */
18
+ /**
19
+ * @typedef {Object} Band
20
+ * @property {string} name The band name.
21
+ * @property {string} group The group path relative to the `url`, containing this band
22
+ * (e.g. `'measurements/reflectance'`).
23
+ */
24
+ /**
25
+ * @typedef {Object} GeoZarrStoreOptions
26
+ * @property {Object<string, string>} [headers] additional key-value pairs of headers to be passed with each request. Key is the header name, value the header value.
27
+ * @property {string} [credentials] How credentials shall be handled. See
28
+ * https://developer.mozilla.org/en-US/docs/Web/API/fetch for reference and possible values
29
+ */
30
+ /**
31
+ * @typedef {Object} Options
32
+ * @property {string} url When `bands` contains plain strings, this must be the full URL to the
33
+ * multiscales group (e.g. `'https://example.com/store.zarr/measurements/reflectance'`).
34
+ * When `bands` contains {@link Band} objects, this is the base URL from which each band's
35
+ * `group` path is resolved (e.g. `'https://example.com/store.zarr/satellite/sentinel2'`).
36
+ * @property {Array<string|Band>} [bands] The bands to render, for stores where each
37
+ * band is a separate array. Mutually exclusive with `variable`. Each entry is either a band name
38
+ * string (single-group mode) or a {@link Band} object specifying both the band name and the
39
+ * group it belongs to (multi-group mode). In multi-group mode, the first band's group
40
+ * determines the tile grid and must follow at least the proj: and spatial: conventions.
41
+ * If it also has a multiscales layout (all three conventions), multiple resolution levels are
42
+ * supported. Otherwise a single-resolution tile grid is derived from `spatial:bbox`,
43
+ * `proj:code`, and `spatial:shape` (or the array shape from consolidated metadata).
44
+ * Bands from additional groups do not need to follow any convention; they can be multi-scale
45
+ * (array located at `<matrixId>/<bandName>`) or single-scale (array at the group root).
46
+ * @property {GeoZarrStoreOptions} [storeOptions] Additional options to be passed to
47
+ * [zarrita](https://zarrita.dev/)'s `FetchStore` with each request to the Zarr store.
48
+ * @property {import("ol/proj.js").ProjectionLike} [projection] Source projection.
49
+ * If not provided, the GeoZarr metadata will be read for projection information.
50
+ * @property {number} [transition=250] Duration of the opacity transition for rendering.
51
+ * To disable the opacity transition, pass `transition: 0`.
52
+ * @property {boolean} [wrapX=false] Render tiles beyond the tile grid extent.
53
+ * @property {ResampleMethod} [resample='linear'] Resampling method if bands are not available for all multi-scale levels.
54
+ * @property {Object<string, number|string|Array<number|string>>} [dimensions] How to slice
55
+ * each non-spatial dimension of the band arrays, keyed by dimension name (e.g. `{time: 0}` for
56
+ * the first time step of a `[time, y, x]` cube). Values are 0-based indices (number) or
57
+ * coordinate labels (string); unlisted dimensions default to index 0. Names come from each
58
+ * array's `dimension_names`, or are the axis position as a string when it has none; use the
59
+ * names from {@link getDimensions}. Labels are resolved against the dimension's coordinate
60
+ * array; if that array cannot be read, pass indices instead. With `variable`, at most one
61
+ * dimension may map to an array of values, whose entries are rendered as separate bands in
62
+ * the given order. Change the selection on the fly with
63
+ * {@link module:ol/source/GeoZarr~GeoZarr#updateDimensions}.
64
+ * @property {string} [variable] The name of an n-dimensional data array (variable) to
65
+ * render, for stores where all bands are packed into a single array (e.g. a
66
+ * `(time, band, y, x)` datacube). The array must exist within each multiscale level
67
+ * group (or at the group root for single-scale stores). Mutually exclusive with `bands`,
68
+ * and required to select several bands from one dimension through `dimensions`.
69
+ * @property {import("ol/extent.js").Extent} [extent] Fallback extent of the data, in
70
+ * coordinates of the source projection. Only used when the store neither declares its
71
+ * extent (`spatial:bbox` or `bounds` attributes) nor has coordinate arrays to infer it.
72
+ * @property {boolean} [flipY] Fallback orientation: set to `true` when the data is
73
+ * stored south-up (ascending y). Only used when the orientation can neither be read
74
+ * from the store metadata nor inferred from its coordinate arrays.
75
+ */
76
+ /**
77
+ * Source for GeoZarr stores conforming to the following conventions:
78
+ * - [Zarr multiscales convention](https://github.com/zarr-conventions/multiscales)
79
+ * - [Geospatial projection convention](https://github.com/zarr-conventions/geo-proj)
80
+ * - [Spatial convention](https://github.com/zarr-conventions/spatial)
81
+ *
82
+ * The store is read as a stack of resolution levels, enumerated from the
83
+ * `multiscales` attribute in either its `{layout: [{asset, ...}]}` or its
84
+ * `[{datasets: [{path, ...}]}]` form; a store with neither has the group itself as
85
+ * its only level. The legacy `tile_matrix_set` attribute is also supported, and
86
+ * describes the levels itself.
87
+ *
88
+ * Extent, resolution, projection and y-axis orientation are read from the store
89
+ * metadata (`spatial:bbox`, `spatial:shape`, `spatial:transform`, `proj:code`,
90
+ * ...) where declared, and otherwise inferred from the coordinate arrays. The
91
+ * conventions above are what make that metadata available, but none of them has
92
+ * to be declared for a store that provides the attributes.
93
+ *
94
+ * Two data layouts are supported:
95
+ * - One array per band (`bands` option), addressed by name at `<matrixId>/<bandName>`
96
+ * (multi-scale) or at the group root (single-scale).
97
+ * - A single n-dimensional data array shared by all bands (`variable` + `dimensions`
98
+ * options), e.g. a `(time, band, y, x)` datacube.
99
+ *
100
+ * Both layouts support Zarr v2 and v3.
101
+ */
102
+ export default class GeoZarr extends DataTileSource {
103
+ /**
104
+ * @param {Options} options The options.
105
+ */
106
+ constructor(options) {
107
+ super({
108
+ state: 'loading',
109
+ tileGrid: /** @type {?} */ (null),
110
+ projection: /** @type {import("ol/proj.js").ProjectionLike} */ (options.projection || null),
111
+ transition: options.transition,
112
+ wrapX: options.wrapX,
113
+ hasAlpha: false,
114
+ });
115
+ /**
116
+ * @type {string}
117
+ * @private
118
+ */
119
+ this.url_ = options.url;
120
+ /**
121
+ * @type {GeoZarrStoreOptions|undefined}
122
+ * @private
123
+ */
124
+ this.storeOptions_ = options.storeOptions;
125
+ /**
126
+ * Selection per non-spatial dimension name, from the `dimensions` option.
127
+ * Coordinate labels are replaced by their index once resolved.
128
+ * @type {Object<string, number|string|Array<number|string>>}
129
+ * @private
130
+ */
131
+ this.dimensions_ = options.dimensions || {};
132
+ /**
133
+ * @type {string|undefined}
134
+ * @private
135
+ */
136
+ this.variable_ = options.variable;
137
+ /**
138
+ * @type {import("ol/extent.js").Extent|undefined}
139
+ * @private
140
+ */
141
+ this.fallbackExtent_ = options.extent;
142
+ /**
143
+ * @type {boolean|undefined}
144
+ * @private
145
+ */
146
+ this.fallbackFlipY_ = options.flipY;
147
+ /**
148
+ * The zarrita open function, pinned to the store's Zarr version by
149
+ * `configure_`. Never the unpinned `open`, which probes v2 metadata first
150
+ * and so requests keys that a v3 store does not have.
151
+ * @type {Function}
152
+ * @private
153
+ */
154
+ this.openFn_ = open.v3;
155
+ /**
156
+ * Group path prefix per tile matrix id, for stores whose levels are not
157
+ * addressed by the matrix id itself (empty string for the group root).
158
+ * `null` when the matrix id is the prefix.
159
+ * @type {Object<string, string>|null}
160
+ * @private
161
+ */
162
+ this.levelPaths_ = null;
163
+ /**
164
+ * Row axis information per tile matrix id, for data with non-square
165
+ * pixels or south-up (flipped) rows.
166
+ * @type {Object<string, {rowResolution: number, shapeY: number|undefined, flip: boolean}>|null}
167
+ * @private
168
+ */
169
+ this.levelRowInfo_ = null;
170
+ /**
171
+ * The axis selected as multiple bands through the `dimensions` option,
172
+ * or -1.
173
+ * @type {number}
174
+ * @private
175
+ */
176
+ this.multiAxis_ = -1;
177
+ /**
178
+ * @type {Error|null}
179
+ */
180
+ this.error_ = null;
181
+ /**
182
+ * @type {Array<import('zarrita').Group<any>>}
183
+ * @private
184
+ */
185
+ this.groups_ = [];
186
+ /**
187
+ * @type {Object<string, *>|null}
188
+ * @private
189
+ */
190
+ this.consolidatedMetadata_ = null;
191
+ /**
192
+ * Cache of opened zarrita arrays keyed by path. Caching the Promise
193
+ * (not the resolved value) deduplicates concurrent opens for the same
194
+ * array path across tiles at the same zoom level.
195
+ * @private
196
+ * @type {Map<string, Promise<import('zarrita').Array<import('zarrita').DataType, any>>>}
197
+ */
198
+ this.arrayCache_ = new Map();
199
+ const groupOrder = /** @type {Array<string>} */ ([]);
200
+ const bandGroupIndex = /** @type {Array<number>} */ ([]);
201
+ const bands = (options.bands || []).map((b) => {
202
+ if (typeof b === 'string') {
203
+ bandGroupIndex.push(0);
204
+ return b;
205
+ }
206
+ let gi = groupOrder.indexOf(b.group);
207
+ if (gi === -1) {
208
+ gi = groupOrder.length;
209
+ groupOrder.push(b.group);
210
+ }
211
+ bandGroupIndex.push(gi);
212
+ return b.name;
213
+ });
214
+ /**
215
+ * @type {Array<string>|undefined}
216
+ * @private
217
+ */
218
+ this.groupPaths_ = groupOrder.length > 0 ? groupOrder : undefined;
219
+ /**
220
+ * Maps each band index to the index of the group it belongs to in `this.groups_`.
221
+ * @type {Array<number>}
222
+ * @private
223
+ */
224
+ this.bandGroupIndex_ = bandGroupIndex;
225
+ /**
226
+ * Pixel resolution for single-scale bands. When set, indicates that the
227
+ * band lives directly at its group root (no matrixId subdirectory) and
228
+ * provides the pixel resolution to use for coordinate calculations.
229
+ * Undefined for multi-scale bands.
230
+ * @type {Array<number|undefined>}
231
+ * @private
232
+ */
233
+ this.bandSingleScaleResolution_ = new Array(bands.length).fill(undefined);
234
+ /**
235
+ * @type {Array<string>}
236
+ * @private
237
+ */
238
+ this.bands_ = bands;
239
+ /**
240
+ * Per-band selection along non-spatial dimensions: `undefined` for 2-D
241
+ * arrays, otherwise an array aligned to the array rank with a fixed integer
242
+ * at each extra axis and `null` at the two spatial axes (e.g. `[2, null,
243
+ * null]` for a `[time, y, x]` array with `time: 2`).
244
+ * @type {Array<Array<number|null>|undefined>}
245
+ * @private
246
+ */
247
+ this.bandExtraSelection_ = new Array(bands.length).fill(undefined);
248
+ /**
249
+ * Per-band spatial (y, x) axis positions, as `{row, col}`.
250
+ * @type {Array<{row: number, col: number}>}
251
+ * @private
252
+ */
253
+ this.bandSpatialAxes_ = new Array(bands.length);
254
+ /**
255
+ * The two spatial axis names from the group's `spatial:dimensions` (`[y, x]`).
256
+ * @type {Array<string>|undefined}
257
+ * @private
258
+ */
259
+ this.spatialDimensionNames_;
260
+ /**
261
+ * Non-spatial dimensions of the bands, exposed via {@link getDimensions}.
262
+ * @type {Array<{name: string, size: number}>}
263
+ * @private
264
+ */
265
+ this.extraDimensions_ = [];
266
+ /**
267
+ * @type {Object<string, Array<string>>|null|undefined}
268
+ * @private
269
+ */
270
+ this.bandsByLevel_ = null;
271
+ /**
272
+ * @type {number|undefined}
273
+ * @private
274
+ */
275
+ this.fillValue_;
276
+ /**
277
+ * @type {ResampleMethod}
278
+ * @private
279
+ */
280
+ this.resampleMethod_ = options.resample || 'linear';
281
+ /**
282
+ * Number of bands.
283
+ * @type {number}
284
+ */
285
+ this.bandCount = this.variable_ ? 1 : this.bands_.length;
286
+ // A dimension listing several values renders one band per value. Only
287
+ // `variable` can do that, since `bands` names one array per band.
288
+ let multiName;
289
+ for (const name in this.dimensions_) {
290
+ const value = this.dimensions_[name];
291
+ if (!Array.isArray(value)) {
292
+ continue;
293
+ }
294
+ if (!this.variable_) {
295
+ throw new Error(`GeoZarr: dimension "${name}" selects several bands, which requires ` +
296
+ 'the `variable` option.');
297
+ }
298
+ if (multiName) {
299
+ throw new Error(`GeoZarr: dimensions "${multiName}" and "${name}" both select ` +
300
+ 'several bands; at most one may.');
301
+ }
302
+ multiName = name;
303
+ }
304
+ /**
305
+ * @type {import("ol/tilegrid/WMTS.js").default}
306
+ * @override
307
+ */
308
+ this.tileGrid;
309
+ this.setLoader(this.loadTile_.bind(this));
310
+ this.configure_()
311
+ .then(() => {
312
+ this.setState('ready');
313
+ })
314
+ .catch((err) => {
315
+ // A 403 reads as a missing key, so the two causes are indistinguishable.
316
+ this.error_ =
317
+ err instanceof NotFoundError
318
+ ? new Error(`GeoZarr: could not read ${this.url_}; it is missing, or ` +
319
+ `access to it is denied.`, { cause: err })
320
+ : err;
321
+ this.setState('error');
322
+ });
323
+ }
324
+ async configure_() {
325
+ var _a, _b, _c;
326
+ const overrides = /** @type {RequestInit} */ (this.storeOptions_) || {};
327
+ const store = /** @type {FetchStore} */ (withRangeCoalescing(new FetchStore(this.url_, { overrides, fetch: storeFetch })));
328
+ // Fetch group zarr.json once for opening the group, extracting consolidated
329
+ // metadata, and pinning the detected Zarr version.
330
+ const groupBytes = await probe(store, '/zarr.json');
331
+ this.openFn_ = groupBytes ? open.v3 : open.v2;
332
+ if (groupBytes) {
333
+ try {
334
+ this.consolidatedMetadata_ = JSON.parse(new TextDecoder().decode(groupBytes)).consolidated_metadata.metadata;
335
+ }
336
+ catch (_d) {
337
+ // no consolidated metadata
338
+ }
339
+ }
340
+ /** @type {Object<string, *>|null} */
341
+ let v2Metadata = null;
342
+ if (!groupBytes) {
343
+ // Zarr v2: consolidated metadata lives in .zmetadata
344
+ const v2Bytes = await probe(store, '/.zmetadata');
345
+ if (v2Bytes) {
346
+ try {
347
+ v2Metadata = JSON.parse(new TextDecoder().decode(v2Bytes)).metadata;
348
+ this.consolidatedMetadata_ = normalizeV2Metadata(
349
+ /** @type {Object<string, *>} */ (v2Metadata));
350
+ }
351
+ catch (_e) {
352
+ // no consolidated metadata
353
+ }
354
+ }
355
+ }
356
+ // Wrap the store so that child metadata (groups, arrays) is served from
357
+ // the consolidated metadata instead of making per-child HTTP requests.
358
+ const cachedStore = v2Metadata
359
+ ? createCachedStoreV2(store, v2Metadata)
360
+ : this.consolidatedMetadata_
361
+ ? createCachedStore(store,
362
+ /** @type {Uint8Array} */ (groupBytes), this.consolidatedMetadata_)
363
+ : withChunkCache(store);
364
+ const groupPromises = [];
365
+ if (this.groupPaths_) {
366
+ // Multi-group mode: open root, then each sub-group
367
+ const rootGroup = await this.openFn_(cachedStore, { kind: 'group' });
368
+ for (const groupPath of this.groupPaths_) {
369
+ groupPromises.push(this.openFn_(rootGroup.resolve(groupPath), { kind: 'group' }));
370
+ }
371
+ }
372
+ else {
373
+ // Single group mode
374
+ groupPromises.push(this.openFn_(cachedStore, { kind: 'group' }));
375
+ }
376
+ this.groups_.push(...(await Promise.all(groupPromises)));
377
+ const attributes =
378
+ /** @type {LegacyDatasetAttributes | DatasetAttributes} */ (this.groups_[0].attrs);
379
+ // The spatial: convention names the two spatial axes (`[y, x]`), so they can
380
+ // be located by name in each array's `dimension_names`.
381
+ const spatialDimensions = 'spatial:dimensions' in attributes
382
+ ? /** @type {Array<string>} */ (attributes['spatial:dimensions'])
383
+ : undefined;
384
+ if (Array.isArray(spatialDimensions) && spatialDimensions.length === 2) {
385
+ this.spatialDimensionNames_ = spatialDimensions;
386
+ }
387
+ const multiscales = attributes.multiscales;
388
+ // The two multiscale layouts that enumerate level groups: the GeoZarr
389
+ // `[{datasets: [...]}]` form and the convention's `{layout: [...]}` form.
390
+ const hasMultiscaleLevels = Array.isArray(multiscales) ||
391
+ Array.isArray(/** @type {Multiscales} */ (_a = (multiscales)) === null || _a === void 0 ? void 0 : _a.layout);
392
+ // The legacy tile_matrix_set describes the levels itself, so a store that
393
+ // has nothing else to enumerate them by is configured from it alone.
394
+ const legacyOnly = !!multiscales && 'tile_matrix_set' in multiscales && !hasMultiscaleLevels;
395
+ let hasTileSizes = false;
396
+ if (this.variable_ ||
397
+ (!legacyOnly &&
398
+ (hasMultiscaleLevels ||
399
+ 'spatial:bbox' in attributes ||
400
+ 'bounds' in attributes))) {
401
+ hasTileSizes = await this.configureLevels_(attributes, store);
402
+ }
403
+ if (!hasTileSizes && multiscales && 'tile_matrix_set' in multiscales) {
404
+ // If available, use tile_matrix_set (legacy attributes) to get a tile grid, because it
405
+ // should provide a better mapping of tiles to zarr chunks.
406
+ const { tileGrid, projection } = getTileGridInfoFromLegacyAttributes(
407
+ /** @type {LegacyDatasetAttributes} */ (attributes));
408
+ this.tileGrid = tileGrid;
409
+ // The tile matrix set brings its own matrix ids, which the levels
410
+ // resolved above are not keyed by.
411
+ this.bandsByLevel_ = null;
412
+ this.levelPaths_ = null;
413
+ this.levelRowInfo_ = null;
414
+ if (!this.projection) {
415
+ // Only the tile matrix set declares one for a legacy-only store
416
+ this.projection = projection;
417
+ }
418
+ }
419
+ // For multi-group: determine which group owns each band and supplement
420
+ // bandsByLevel with bands from additional groups.
421
+ if (this.groupPaths_ && this.consolidatedMetadata_ && this.bandsByLevel_) {
422
+ this.resolveBandOwnership_();
423
+ }
424
+ if (this.fillValue_ !== null && this.fillValue_ !== undefined) {
425
+ this.bandCount += 1;
426
+ this.nodataBandIndex = this.bandCount;
427
+ this.hasAlpha = true;
428
+ }
429
+ if (!this.tileGrid) {
430
+ throw new Error('Could not determine tile grid');
431
+ }
432
+ // Replace coordinate labels by their index once, so that the per-band
433
+ // resolution below and `updateDimensions` can stay synchronous.
434
+ // (In `variable` mode, configureLevels_ resolves them per band instead.)
435
+ if (!this.variable_) {
436
+ for (const [name, value] of Object.entries(this.dimensions_)) {
437
+ if (typeof value === 'string') {
438
+ this.dimensions_[name] = await this.resolveCoordinateLabel_(name, value, (_c = (_b = this.coordinateArray_(name)) === null || _b === void 0 ? void 0 : _b.path) !== null && _c !== void 0 ? _c : name);
439
+ }
440
+ }
441
+ }
442
+ // Resolve, per band, the spatial axes and the fixed indices for any
443
+ // non-spatial dimensions, and record the selectable dimensions.
444
+ // (In `variable` mode, configureLevels_ resolves these instead.)
445
+ for (let i = 0, ii = this.variable_ ? 0 : this.bands_.length; i < ii; ++i) {
446
+ const arrayMeta = this.getBandArrayMeta_(this.bands_[i], this.bandGroupIndex_[i]);
447
+ const { row, col } = this.axesOf_(arrayMeta);
448
+ this.bandSpatialAxes_[i] = { row, col };
449
+ this.bandExtraSelection_[i] = this.resolveExtraSelection_(arrayMeta, this.dimensions_);
450
+ if (this.extraDimensions_.length === 0) {
451
+ this.extraDimensions_ = this.extraDimsOf_(arrayMeta);
452
+ }
453
+ }
454
+ const extent = /** @type {import("ol/extent.js").Extent} */ (this.tileGrid.getExtent());
455
+ const projection = /** @type {import("ol/proj/Projection.js").default} */ (this.projection);
456
+ setTimeout(() => {
457
+ var _a;
458
+ (_a = this.viewResolver) === null || _a === void 0 ? void 0 : _a.call(this, {
459
+ showFullExtent: true,
460
+ projection: projection,
461
+ resolutions: this.tileGrid.getResolutions(),
462
+ center: toUserCoordinate(getCenter(extent), projection),
463
+ extent: toUserExtent(extent, projection),
464
+ zoom: 1,
465
+ });
466
+ });
467
+ }
468
+ /**
469
+ * @param {number} z The z tile index.
470
+ * @param {number} x The x tile index.
471
+ * @param {number} y The y tile index.
472
+ * @param {import('ol/source/DataTile.js').LoaderOptions} options The loader options.
473
+ * @return {Promise<import("ol/DataTile.js").Data>} The composed tile data.
474
+ * @private
475
+ */
476
+ async loadTile_(z, x, y, options) {
477
+ var _a;
478
+ const resolutions = this.tileGrid.getResolutions();
479
+ const tileResolution = this.tileGrid.getResolution(z);
480
+ const tileExtent = this.tileGrid.getTileCoordExtent([z, x, y]);
481
+ // First pass: resolve band metadata (no async)
482
+ /** @type {Array<{path: string, groupIndex: number, minRow: number, maxRow: number, minCol: number, maxCol: number, bandResolution: number, rowResolution: number, flip: boolean}>} */
483
+ const bandInfos = [];
484
+ for (let i = 0, ii = this.bands_.length; i < ii; ++i) {
485
+ const band = this.bands_[i];
486
+ const groupIndex = this.bandGroupIndex_[i];
487
+ let bandMatrixId;
488
+ let bandResolution;
489
+ let bandZ = 0;
490
+ if (!this.bandsByLevel_) {
491
+ // TODO: remove this if we stop supporting legacy attributes
492
+ bandMatrixId = this.tileGrid.getMatrixId(z);
493
+ bandResolution = tileResolution;
494
+ bandZ = z;
495
+ }
496
+ else {
497
+ for (let candidateZ = 0; candidateZ < resolutions.length; candidateZ += 1) {
498
+ const candidateResolution = resolutions[candidateZ];
499
+ if (bandMatrixId && candidateResolution < tileResolution) {
500
+ break;
501
+ }
502
+ const candidateMatrixId = this.tileGrid.getMatrixId(candidateZ);
503
+ if (this.bandsByLevel_[candidateMatrixId].includes(band)) {
504
+ bandMatrixId = candidateMatrixId;
505
+ bandResolution = this.tileGrid.getResolution(candidateZ);
506
+ bandZ = candidateZ;
507
+ }
508
+ }
509
+ }
510
+ if (!bandMatrixId || bandResolution === undefined) {
511
+ throw new Error(`Could not find available resolution for band ${band}`);
512
+ }
513
+ const resolvedBandResolution = bandResolution;
514
+ const isSingleScale = this.bandSingleScaleResolution_[i] !== undefined;
515
+ // For single-scale bands, use the band's own pixel resolution (derived
516
+ // from array shape or spatial metadata) rather than the tile grid level
517
+ // resolution, which may give wrong pixel coordinates.
518
+ if (isSingleScale) {
519
+ bandResolution = this.bandSingleScaleResolution_[i];
520
+ }
521
+ const effectiveResolution = /** @type {number} */ (bandResolution !== null && bandResolution !== void 0 ? bandResolution : resolvedBandResolution);
522
+ // Pixels are not necessarily square (e.g. a square array covering the
523
+ // full EPSG:4326 world), and rows may be stored south-up.
524
+ const rowInfo = (_a = this.levelRowInfo_) === null || _a === void 0 ? void 0 : _a[bandMatrixId];
525
+ const rowResolution = !isSingleScale && rowInfo ? rowInfo.rowResolution : effectiveResolution;
526
+ const flip = !!(rowInfo === null || rowInfo === void 0 ? void 0 : rowInfo.flip) && rowInfo.shapeY !== undefined;
527
+ // The matrix id addresses the level group, unless the level has its
528
+ // own group path (empty for the group root).
529
+ const levelPath = this.levelPaths_
530
+ ? this.levelPaths_[bandMatrixId]
531
+ : bandMatrixId;
532
+ const origin = this.tileGrid.getOrigin(bandZ);
533
+ const minCol = Math.round((tileExtent[0] - origin[0]) / effectiveResolution);
534
+ const maxCol = Math.round((tileExtent[2] - origin[0]) / effectiveResolution);
535
+ let minRow = Math.round((origin[1] - tileExtent[3]) / rowResolution);
536
+ let maxRow = Math.round((origin[1] - tileExtent[1]) / rowResolution);
537
+ if (flip) {
538
+ // South-up: read the vertically mirrored row range and flip the
539
+ // rows after reading.
540
+ const shapeY = /** @type {number} */ (rowInfo.shapeY);
541
+ [minRow, maxRow] = [
542
+ Math.max(0, shapeY - maxRow),
543
+ Math.max(0, shapeY - minRow),
544
+ ];
545
+ }
546
+ bandInfos.push({
547
+ path: isSingleScale || !levelPath ? band : `${levelPath}/${band}`,
548
+ groupIndex,
549
+ minRow,
550
+ maxRow,
551
+ minCol,
552
+ maxCol,
553
+ bandResolution: effectiveResolution,
554
+ rowResolution,
555
+ flip,
556
+ });
557
+ }
558
+ // Open all band arrays in parallel (not sequentially)
559
+ const arrays = await Promise.all(bandInfos.map((info) => this.openArray_(info.groupIndex, info.path)));
560
+ // Fire all get() calls synchronously so getRange() calls from all bands
561
+ // land in the same macrotask tick and can be batched together.
562
+ const bandResolutions = bandInfos.map((info) => info.bandResolution);
563
+ const bandRowResolutions = bandInfos.map((info) => info.rowResolution);
564
+ let bandChunks = await Promise.all(arrays.map((array, i) => {
565
+ const info = bandInfos[i];
566
+ const rowSlice = slice(info.minRow, info.maxRow);
567
+ const colSlice = slice(info.minCol, info.maxCol);
568
+ const extra = this.bandExtraSelection_[i];
569
+ if (!extra) {
570
+ return get(array, [rowSlice, colSlice]);
571
+ }
572
+ // Drop the row/column slices in at the spatial axes; zarrita drops the
573
+ // integer axes, returning a 2-D chunk that composeData consumes unchanged.
574
+ const { row, col } = this.bandSpatialAxes_[i];
575
+ /** @type {Array<number|null|ReturnType<typeof slice>>} */
576
+ const selection = extra.slice();
577
+ selection[row] = rowSlice;
578
+ selection[col] = colSlice;
579
+ return get(array, selection);
580
+ }));
581
+ bandChunks = bandChunks.map((chunk, i) => bandInfos[i].flip ? flipChunkRows(chunk) : chunk);
582
+ const [tileColCount, tileRowCount] = toSize(this.tileGrid.getTileSize(z));
583
+ return composeData(bandChunks, bandResolutions, tileColCount, tileRowCount, tileResolution, this.resampleMethod_, this.fillValue_, bandRowResolutions);
584
+ }
585
+ /**
586
+ * For multi-group mode: determine which group owns each band and supplement
587
+ * bandsByLevel with bands from additional groups.
588
+ * @private
589
+ */
590
+ resolveBandOwnership_() {
591
+ var _a;
592
+ const bandsByLevel = /** @type {Object<string, Array<string>>} */ (this.bandsByLevel_);
593
+ const groupPaths = /** @type {Array<string>} */ (this.groupPaths_);
594
+ const subMetadatas = groupPaths.map((gp) => getSubMetadata(
595
+ /** @type {Object<string, *>} */ (this.consolidatedMetadata_), gp));
596
+ for (let i = 0, ii = this.bands_.length; i < ii; ++i) {
597
+ const band = this.bands_[i];
598
+ const g = this.bandGroupIndex_[i];
599
+ if (g === 0) {
600
+ continue; // primary group bands are already in bandsByLevel_
601
+ }
602
+ let foundAtAnyLevel = false;
603
+ for (const matrixId of Object.keys(bandsByLevel)) {
604
+ const bandMeta = /** @type {Object<string, *>|undefined} */ (subMetadatas[g][`${matrixId}/${band}`]);
605
+ if (bandMeta) {
606
+ foundAtAnyLevel = true;
607
+ if (!bandsByLevel[matrixId].includes(band)) {
608
+ bandsByLevel[matrixId].push(band);
609
+ }
610
+ if (this.fillValue_ === undefined) {
611
+ this.fillValue_ = Number(bandMeta['fill_value']);
612
+ }
613
+ }
614
+ }
615
+ if (!foundAtAnyLevel) {
616
+ // Try single-scale: band lives directly at the group root (no matrixId prefix).
617
+ const bandMeta = /** @type {Object<string, *>|undefined} */ (subMetadatas[g][band]);
618
+ if (bandMeta) {
619
+ for (const matrixId of Object.keys(bandsByLevel)) {
620
+ if (!bandsByLevel[matrixId].includes(band)) {
621
+ bandsByLevel[matrixId].push(band);
622
+ }
623
+ }
624
+ if (this.fillValue_ === undefined) {
625
+ this.fillValue_ = Number(bandMeta['fill_value']);
626
+ }
627
+ // Derive the band's pixel resolution from its array shape so loadTile_
628
+ // uses correct coordinates regardless of the tile grid zoom level.
629
+ const shape = /** @type {Array<number>|undefined} */ (bandMeta['shape']);
630
+ const xSize = shape && shape[this.axesOf_(bandMeta).col];
631
+ if (xSize !== undefined && xSize > 0) {
632
+ const tileExtent = /** @type {import("ol/extent.js").Extent} */ (this.tileGrid.getExtent());
633
+ this.bandSingleScaleResolution_[i] =
634
+ (tileExtent[2] - tileExtent[0]) / xSize;
635
+ }
636
+ foundAtAnyLevel = true;
637
+ }
638
+ }
639
+ if (!foundAtAnyLevel) {
640
+ warn(`Band "${band}" from group "${(_a = this.groupPaths_) === null || _a === void 0 ? void 0 : _a[g]}" is not available at any ` +
641
+ `resolution level compatible with the tile grid.`);
642
+ }
643
+ }
644
+ }
645
+ /**
646
+ * Open a Zarr array (path relative to its group) through the shared cache, so
647
+ * concurrent opens of the same array are deduplicated.
648
+ * @param {number} groupIndex The band's group index.
649
+ * @param {string} path The array path relative to the group.
650
+ * @return {Promise<import('zarrita').Array<import('zarrita').DataType, any>>} The array.
651
+ * @private
652
+ */
653
+ openArray_(groupIndex, path) {
654
+ const cacheKey = `${groupIndex}:${path}`;
655
+ let array = this.arrayCache_.get(cacheKey);
656
+ if (!array) {
657
+ array =
658
+ /** @type {Promise<import('zarrita').Array<import('zarrita').DataType, any>>} */ (this.openFn_(this.groups_[groupIndex].resolve(path), {
659
+ kind: 'array',
660
+ })).catch((/** @type {Error} */ err) => {
661
+ this.arrayCache_.delete(cacheKey);
662
+ throw err;
663
+ });
664
+ this.arrayCache_.set(cacheKey, array);
665
+ }
666
+ return array;
667
+ }
668
+ /**
669
+ * Consolidated metadata for a group, with keys relative to that group.
670
+ * @param {number} groupIndex The group index.
671
+ * @return {Object<string, *>} The group's consolidated metadata.
672
+ * @private
673
+ */
674
+ groupMetadata_(groupIndex) {
675
+ return this.groupPaths_
676
+ ? getSubMetadata(
677
+ /** @type {Object<string, *>} */ (this.consolidatedMetadata_), this.groupPaths_[groupIndex])
678
+ : /** @type {Object<string, *>} */ (this.consolidatedMetadata_);
679
+ }
680
+ /**
681
+ * Look up a band's Zarr v3 array metadata from consolidated metadata, trying
682
+ * the multi-scale key (`<matrixId>/<band>`) first and falling back to a
683
+ * single-scale key (`<band>`).
684
+ * @param {string} band The band name.
685
+ * @param {number} groupIndex The index of the band's group.
686
+ * @return {Object<string, *>|undefined} The array metadata, or undefined when unavailable.
687
+ * @private
688
+ */
689
+ getBandArrayMeta_(band, groupIndex) {
690
+ if (!this.consolidatedMetadata_) {
691
+ return undefined;
692
+ }
693
+ const meta = this.groupMetadata_(groupIndex);
694
+ if (this.bandsByLevel_) {
695
+ for (const matrixId of Object.keys(this.bandsByLevel_)) {
696
+ if (this.bandsByLevel_[matrixId].includes(band) &&
697
+ meta[`${matrixId}/${band}`]) {
698
+ return /** @type {Object<string, *>} */ (meta[`${matrixId}/${band}`]);
699
+ }
700
+ }
701
+ }
702
+ return /** @type {Object<string, *>|undefined} */ (meta[band]);
703
+ }
704
+ /**
705
+ * Locate the 1-D coordinate array for a non-spatial dimension, by name among
706
+ * the group's 1-D arrays.
707
+ * @param {string} name The dimension name.
708
+ * @return {{path: string, groupIndex: number, meta: Object<string, *>}|null} The path
709
+ * (relative to the group), group index, and array metadata; or `null`.
710
+ * @private
711
+ */
712
+ coordinateArray_(name) {
713
+ if (!this.consolidatedMetadata_) {
714
+ return null;
715
+ }
716
+ const groupIndex = this.bandGroupIndex_[0];
717
+ const meta = this.groupMetadata_(groupIndex);
718
+ const suffix = `/${name}`;
719
+ for (const path of Object.keys(meta)) {
720
+ if (path === name || path.endsWith(suffix)) {
721
+ const arrayMeta = /** @type {Object<string, *>} */ (meta[path]);
722
+ if (Array.isArray(arrayMeta === null || arrayMeta === void 0 ? void 0 : arrayMeta.shape) && arrayMeta.shape.length === 1) {
723
+ return { path, groupIndex, meta: arrayMeta };
724
+ }
725
+ }
726
+ }
727
+ return null;
728
+ }
729
+ /**
730
+ * Get the non-spatial dimensions of the bands (e.g. `time`) that can be fixed
731
+ * through the `dimensions` option, keyed by dimension name. Each entry has its
732
+ * `size` and the `attributes` of its coordinate array (e.g. `units`, for
733
+ * interpreting the values from {@link getValue}), or `attributes: null` when
734
+ * there is no coordinate array. Resolves with an empty object for 2-D bands,
735
+ * once the source is `ready`; rejects if the source fails to load.
736
+ * @return {Promise<Object<string, {size: number, attributes: Object|null}>>}
737
+ * The selectable dimensions.
738
+ */
739
+ async getDimensions() {
740
+ var _a;
741
+ await this.ready();
742
+ /** @type {Object<string, {size: number, attributes: Object|null}>} */
743
+ const dimensions = {};
744
+ for (const dimension of this.extraDimensions_) {
745
+ const coord = this.coordinateArray_(dimension.name);
746
+ dimensions[dimension.name] = {
747
+ size: dimension.size,
748
+ attributes: coord ? ((_a = coord.meta.attributes) !== null && _a !== void 0 ? _a : null) : null,
749
+ };
750
+ }
751
+ return dimensions;
752
+ }
753
+ /**
754
+ * Read the coordinate value at an index along a non-spatial dimension (e.g.
755
+ * the timestamp for a `time` index), for labeling the current selection. The
756
+ * value is returned raw (as stored, e.g. a `bigint` for a 64-bit integer
757
+ * axis); use the `attributes` from {@link getDimensions} to interpret it.
758
+ * Returns `null` for a dimension without a coordinate array. Available once
759
+ * the source is `ready`.
760
+ * @param {string} name The dimension name (see {@link getDimensions}).
761
+ * @param {number} index The index along the dimension.
762
+ * @return {Promise<number|bigint|null>} The coordinate value, or null.
763
+ */
764
+ async getValue(name, index) {
765
+ await this.ready();
766
+ const coord = this.coordinateArray_(name);
767
+ if (!coord) {
768
+ return null;
769
+ }
770
+ const size = coord.meta.shape[0];
771
+ if (index < 0 || index >= size) {
772
+ throw new Error(`GeoZarr: index ${index} out of range for dimension "${name}" (size ${size}).`);
773
+ }
774
+ const array = await this.openArray_(coord.groupIndex, coord.path);
775
+ const chunk = await get(array, [slice(index, index + 1)]);
776
+ return /** @type {number|bigint} */ (
777
+ /** @type {ArrayLike<number|bigint>} */ (chunk.data)[0]);
778
+ }
779
+ /**
780
+ * Change the fixed index of one or more non-spatial dimensions (e.g. move to
781
+ * another `time` slice) without rebuilding the source. Values are merged into
782
+ * the current selection, so a partial update like `{time: 3}` leaves the other
783
+ * dimensions untouched. Takes effect immediately when the source is `ready`,
784
+ * otherwise once it becomes ready. Only integer indices are accepted here;
785
+ * coordinate labels are resolved once, when the source configures.
786
+ * @param {Object<string, number>} dimensions Index per dimension name
787
+ * to change; see the `dimensions` constructor option.
788
+ */
789
+ updateDimensions(dimensions) {
790
+ const merged = Object.assign(Object.assign({}, this.dimensions_), dimensions);
791
+ if (this.getState() !== 'ready') {
792
+ // configure_ reads dimensions_ when it resolves; nothing to do yet.
793
+ this.dimensions_ = merged;
794
+ return;
795
+ }
796
+ // Resolve every band before assigning, so an invalid name or index
797
+ // throws without leaving a half-updated selection.
798
+ const selection = this.bands_.map((band, i) => {
799
+ const arrayMeta = this.getBandArrayMeta_(band, this.bandGroupIndex_[i]);
800
+ if (!this.variable_) {
801
+ return this.resolveExtraSelection_(arrayMeta, merged);
802
+ }
803
+ // In `variable` mode, merge into the current selection so the axes
804
+ // fixed through the `dimensions` option are kept.
805
+ const dims = this.extraDimsOf_(arrayMeta);
806
+ const current = this.bandExtraSelection_[i];
807
+ const updated = current ? current.slice() : undefined;
808
+ for (const [name, index] of Object.entries(dimensions)) {
809
+ const dim = dims.find((d) => d.name === name);
810
+ if (!dim || !updated) {
811
+ throw new Error(`GeoZarr: unknown dimension "${name}"; ` +
812
+ `available: [${dims.map((d) => d.name).join(', ')}].`);
813
+ }
814
+ if (dim.axis === this.multiAxis_) {
815
+ throw new Error(`GeoZarr: dimension "${name}" selects the rendered bands; ` +
816
+ `set it when constructing the source instead.`);
817
+ }
818
+ if (typeof index !== 'number' ||
819
+ !Number.isInteger(index) ||
820
+ index < 0 ||
821
+ index >= dim.size) {
822
+ throw new Error(`GeoZarr: invalid index ${index} for dimension "${name}" ` +
823
+ `(size ${dim.size}).`);
824
+ }
825
+ updated[dim.axis] = index;
826
+ }
827
+ return updated;
828
+ });
829
+ this.dimensions_ = merged;
830
+ this.bandExtraSelection_ = selection;
831
+ // Bump the tile key to reload tiles. Deriving it from the selection (rather
832
+ // than a counter) keeps prior selections' tiles cached, so revisiting hits.
833
+ this.setKey(getUid(this) + ':' + JSON.stringify(selection));
834
+ }
835
+ /**
836
+ * Locate the spatial (y, x) axes of an array (see {@link getSpatialAxes}) and
837
+ * its remaining non-spatial axes.
838
+ * @param {Object<string, *>|undefined} arrayMeta Zarr v3 array metadata.
839
+ * @return {{row: number, col: number, extra: Array<number>}} The row (y) and
840
+ * column (x) axis positions and the remaining extra axes, in array order.
841
+ * @private
842
+ */
843
+ axesOf_(arrayMeta) {
844
+ const { row, col } = getSpatialAxes(this.spatialDimensionNames_, arrayMeta);
845
+ const rank = ((arrayMeta && arrayMeta['shape']) || []).length;
846
+ const extra = [];
847
+ for (let axis = 0; axis < rank; ++axis) {
848
+ if (axis !== row && axis !== col) {
849
+ extra.push(axis);
850
+ }
851
+ }
852
+ return { row, col, extra };
853
+ }
854
+ /**
855
+ * Describe the non-spatial dimensions of an array. Each is named by its
856
+ * `dimension_names` entry, or by its axis position when there are none.
857
+ * @param {Object<string, *>|undefined} arrayMeta Zarr v3 array metadata.
858
+ * @return {Array<{name: string, size: number, axis: number}>} The extra dimensions, outermost first.
859
+ * @private
860
+ */
861
+ extraDimsOf_(arrayMeta) {
862
+ if (!arrayMeta) {
863
+ return [];
864
+ }
865
+ const shape = arrayMeta['shape'];
866
+ if (!Array.isArray(shape) || shape.length <= 2) {
867
+ return [];
868
+ }
869
+ const dimensionNames = arrayMeta['dimension_names'];
870
+ const hasNames = Array.isArray(dimensionNames);
871
+ const dims = [];
872
+ for (const axis of this.axesOf_(arrayMeta).extra) {
873
+ const raw = hasNames ? dimensionNames[axis] : undefined;
874
+ const name = raw === null || raw === undefined ? String(axis) : String(raw);
875
+ dims.push({ name, size: shape[axis], axis });
876
+ }
877
+ return dims;
878
+ }
879
+ /**
880
+ * Resolve the fixed index for each non-spatial dimension of a band array from
881
+ * the `dimensions` option. Returns `undefined` for 2-D arrays, otherwise an
882
+ * array aligned to the array rank with a fixed integer at each extra axis and
883
+ * `null` at the two spatial axes (e.g. `[2, null, null]` for a `[time, y, x]`
884
+ * array with `{time: 2}`).
885
+ * @param {Object<string, *>|undefined} arrayMeta Zarr v3 array metadata.
886
+ * @param {Object<string, number|string|Array<number|string>>} dimensions The
887
+ * dimension indices to resolve against.
888
+ * @return {Array<number|null>|undefined} The extra-axis selection template.
889
+ * @private
890
+ */
891
+ resolveExtraSelection_(arrayMeta, dimensions) {
892
+ if (!arrayMeta) {
893
+ return undefined;
894
+ }
895
+ const dims = this.extraDimsOf_(arrayMeta);
896
+ if (dims.length === 0) {
897
+ return undefined;
898
+ }
899
+ const names = dims.map((d) => d.name);
900
+ const dimKeys = Object.keys(dimensions);
901
+ // A single unnamed dimension is lenient: any single key binds to it.
902
+ const singleUnnamed = dims.length === 1 && !Array.isArray(arrayMeta['dimension_names']);
903
+ // Fail loud on a key matching no dimension, rather than silently rendering
904
+ // the wrong slice.
905
+ for (const key of dimKeys) {
906
+ if (!names.includes(key) && !singleUnnamed) {
907
+ throw new Error(`GeoZarr: unknown dimension "${key}" in the \`dimensions\` option; ` +
908
+ `available: [${names.join(', ')}].`);
909
+ }
910
+ }
911
+ const selection = new Array(arrayMeta['shape'].length).fill(null);
912
+ for (const dim of dims) {
913
+ const name = dim.name;
914
+ let index;
915
+ if (name in dimensions) {
916
+ index = dimensions[name];
917
+ }
918
+ else if (singleUnnamed && dimKeys.length === 1) {
919
+ index = dimensions[dimKeys[0]];
920
+ }
921
+ else {
922
+ index = 0; // unspecified extra dimension defaults to the first slice
923
+ }
924
+ if (typeof index === 'string') {
925
+ // Labels are resolved once, when the source configures.
926
+ throw new Error(`GeoZarr: coordinate labels cannot be passed to updateDimensions; ` +
927
+ `pass an integer index for dimension "${name}".`);
928
+ }
929
+ if (!Number.isInteger(index) ||
930
+ /** @type {number} */ (index) < 0 ||
931
+ /** @type {number} */ (index) >= dim.size) {
932
+ throw new Error(`GeoZarr: invalid index ${index} for dimension "${name}" ` +
933
+ `(size ${dim.size}).`);
934
+ }
935
+ selection[dim.axis] = /** @type {number} */ (index);
936
+ }
937
+ return selection;
938
+ }
939
+ /**
940
+ * Build the tile grid and the per-level band layout. Every store is read as a
941
+ * stack of levels holding n-dimensional arrays: in `variable` mode all bands
942
+ * are slices of one array, otherwise each band has an array of its own.
943
+ * @param {Object<string, *>} attributes The dataset attributes.
944
+ * @param {FetchStore} store The store, for metadata requests not covered
945
+ * by consolidated metadata.
946
+ * @return {Promise<boolean>} Whether the tile grid has explicit tile sizes.
947
+ * @private
948
+ */
949
+ async configureLevels_(attributes, store) {
950
+ var _a;
951
+ const variable = this.variable_;
952
+ // The arrays to look for at each level, and how to name them in messages.
953
+ const arrayNames = variable ? [variable] : this.bands_;
954
+ const what = variable
955
+ ? `variable "${variable}"`
956
+ : `bands [${arrayNames.join(', ')}]`;
957
+ // For multi-group mode, use sub-metadata for the first group so that
958
+ // consolidated metadata keys match the expected relative paths.
959
+ const consolidatedMetadata = this.consolidatedMetadata_
960
+ ? this.groupMetadata_(0)
961
+ : null;
962
+ // Collect the multiscale level paths; without multiscale metadata the
963
+ // group itself is the only level.
964
+ const multiscales = attributes['multiscales'];
965
+ /** @type {Array<{path: string, transform: Array<number>|undefined, shape: Array<number>|undefined}>} */
966
+ const rawLevels = [];
967
+ /** @type {string|null} */
968
+ let crsHint = null;
969
+ if (Array.isArray(multiscales) &&
970
+ multiscales.length > 0 &&
971
+ Array.isArray(multiscales[0].datasets)) {
972
+ for (const dataset of multiscales[0].datasets) {
973
+ const path = dataset.path === '.' || !dataset.path
974
+ ? ''
975
+ : String(dataset.path).replace(/\/+$/, '');
976
+ rawLevels.push({
977
+ path,
978
+ transform: dataset['spatial:transform'],
979
+ shape: dataset['spatial:shape'],
980
+ });
981
+ if (!crsHint && typeof dataset['crs'] === 'string') {
982
+ crsHint = dataset['crs'];
983
+ }
984
+ }
985
+ }
986
+ else if (multiscales && Array.isArray(multiscales.layout)) {
987
+ for (const entry of multiscales.layout) {
988
+ // The declared `spatial:transform` is deliberately ignored here: its
989
+ // resolution is rounded in real stores (e.g. 720 where the extent and
990
+ // shape give 722.37), which leaves a sliver of a tile beyond the last
991
+ // full one. All layout levels share the group origin, so deriving the
992
+ // resolution from the extent and shape is both exact and sufficient.
993
+ rawLevels.push({
994
+ path: String(entry.asset),
995
+ transform: undefined,
996
+ shape: entry['spatial:shape'],
997
+ });
998
+ }
999
+ }
1000
+ else {
1001
+ rawLevels.push({
1002
+ path: '',
1003
+ transform: attributes['spatial:transform'],
1004
+ shape: attributes['spatial:shape'],
1005
+ });
1006
+ }
1007
+ // Keep only the levels that hold at least one of the wanted arrays. Without
1008
+ // consolidated metadata their presence cannot be checked up front.
1009
+ /** @type {Array<{path: string, bands: Array<string>, meta: any, transform: Array<number>|undefined, shape: Array<number>|undefined}>} */
1010
+ const levels = [];
1011
+ for (const raw of rawLevels) {
1012
+ const bands = consolidatedMetadata
1013
+ ? arrayNames.filter((name) => consolidatedMetadata[arrayPathOf(raw.path, name)])
1014
+ : arrayNames.slice();
1015
+ if (bands.length === 0) {
1016
+ warn(`No ${what} found at level "${raw.path || '.'}"`);
1017
+ continue;
1018
+ }
1019
+ levels.push(Object.assign(Object.assign({}, raw), { bands, meta: consolidatedMetadata
1020
+ ? consolidatedMetadata[arrayPathOf(raw.path, bands[0])]
1021
+ : undefined }));
1022
+ }
1023
+ if (levels.length === 0) {
1024
+ throw new Error(`No level found holding ${what}`);
1025
+ }
1026
+ // The dimension layout is the same for all levels. It is only needed when
1027
+ // the levels do not declare their shape, or to slice a `variable`.
1028
+ let meta = (_a = levels.find((level) => level.meta)) === null || _a === void 0 ? void 0 : _a.meta;
1029
+ if (!meta && (variable || !levels.every((l) => Array.isArray(l.shape)))) {
1030
+ meta = await getArrayMeta(store, arrayPathOf(levels[0].path, levels[0].bands[0]), this.openFn_ === open.v2);
1031
+ levels[0].meta = meta;
1032
+ }
1033
+ const hasMeta = !!meta && Array.isArray(meta.shape);
1034
+ if (variable && !hasMeta) {
1035
+ throw new Error(`Could not read metadata for variable "${variable}"`);
1036
+ }
1037
+ const ndim = hasMeta ? meta.shape.length : 2;
1038
+ let dimensionNames = hasMeta ? meta['dimension_names'] : undefined;
1039
+ if (!dimensionNames && ndim === 2) {
1040
+ dimensionNames = ['y', 'x'];
1041
+ }
1042
+ if (variable && !dimensionNames) {
1043
+ throw new Error(`Cannot determine dimension names for variable "${variable}"`);
1044
+ }
1045
+ const { row, col } = hasMeta
1046
+ ? getSpatialAxes(attributes['spatial:dimensions'], meta)
1047
+ : { row: 0, col: 1 };
1048
+ // Determine the extent and y axis orientation: declared metadata is
1049
+ // authoritative, otherwise both are inferred from the coordinate arrays.
1050
+ let extent = attributes['spatial:bbox'] || attributes['bounds'];
1051
+ let flipY = false;
1052
+ let orientationKnown = false;
1053
+ const transform0 = levels[0].transform;
1054
+ if (extent &&
1055
+ Array.isArray(transform0) &&
1056
+ transform0.length >= 6 &&
1057
+ transform0[4] !== 0) {
1058
+ flipY = transform0[4] > 0;
1059
+ orientationKnown = true;
1060
+ }
1061
+ else if (extent && levels.every((l) => Array.isArray(l.shape))) {
1062
+ // Levels that declare their `spatial:shape` alongside a declared extent
1063
+ // follow the spatial: convention, which stores rasters north-up. Reading
1064
+ // coordinate arrays would only confirm that, at the cost of two requests.
1065
+ orientationKnown = true;
1066
+ }
1067
+ else {
1068
+ // All levels share the extent; the smallest level is cheapest to read
1069
+ let coordLevel = levels[0];
1070
+ for (const level of levels) {
1071
+ if (level.meta &&
1072
+ coordLevel.meta &&
1073
+ level.meta.shape[col] < coordLevel.meta.shape[col]) {
1074
+ coordLevel = level;
1075
+ }
1076
+ }
1077
+ try {
1078
+ const [x0, x1, xCount] = await this.readCoordinateEndpoints_(coordLevel.path, dimensionNames[col]);
1079
+ const [y0, y1, yCount] = await this.readCoordinateEndpoints_(coordLevel.path, dimensionNames[row]);
1080
+ flipY = y1 > y0;
1081
+ orientationKnown = true;
1082
+ if (x1 < x0) {
1083
+ warn('Descending x coordinates are not supported.');
1084
+ }
1085
+ if (!extent) {
1086
+ // Coordinates are pixel centers; pad by half a pixel
1087
+ const xResolution = Math.abs(x1 - x0) / (xCount - 1);
1088
+ const yResolution = Math.abs(y1 - y0) / (yCount - 1);
1089
+ extent = [
1090
+ Math.min(x0, x1) - xResolution / 2,
1091
+ Math.min(y0, y1) - yResolution / 2,
1092
+ Math.max(x0, x1) + xResolution / 2,
1093
+ Math.max(y0, y1) + yResolution / 2,
1094
+ ];
1095
+ // Normalize [0, 360]-style longitudes to [-180, 180]
1096
+ if (extent[0] >= 0 && extent[2] > 180 && extent[2] <= 360.001) {
1097
+ extent = [extent[0] - 360, extent[1], extent[2] - 360, extent[3]];
1098
+ }
1099
+ }
1100
+ }
1101
+ catch (err) {
1102
+ if (!extent && !this.fallbackExtent_) {
1103
+ throw new Error(`Could not determine the extent of variable "${variable}" ` +
1104
+ `from metadata or coordinate arrays: ${ /** @type {Error} */(err).message}`);
1105
+ }
1106
+ }
1107
+ }
1108
+ if (!extent) {
1109
+ extent = this.fallbackExtent_;
1110
+ }
1111
+ if (!orientationKnown && this.fallbackFlipY_ !== undefined) {
1112
+ flipY = this.fallbackFlipY_;
1113
+ }
1114
+ const extentWidth = extent[2] - extent[0];
1115
+ const extentHeight = extent[3] - extent[1];
1116
+ /** @type {Array<{path: string, bands: Array<string>, meta: any, shape: Array<number>|undefined, resolution: number, rowResolution: number, origin: import("ol/coordinate.js").Coordinate}>} */
1117
+ const configured = [];
1118
+ for (const level of levels) {
1119
+ let resolution;
1120
+ // Pixels are not necessarily square (e.g. a square array covering the
1121
+ // full EPSG:4326 world), so the row resolution is tracked separately.
1122
+ let rowResolution;
1123
+ let origin;
1124
+ if (Array.isArray(level.transform) &&
1125
+ level.transform.length >= 6 &&
1126
+ level.transform[4] < 0) {
1127
+ resolution = level.transform[0];
1128
+ rowResolution = -level.transform[4];
1129
+ origin = [level.transform[2], level.transform[5]];
1130
+ }
1131
+ else if (Array.isArray(level.shape)) {
1132
+ resolution = extentWidth / level.shape[1];
1133
+ rowResolution = extentHeight / level.shape[0];
1134
+ origin = [extent[0], extent[3]];
1135
+ }
1136
+ else if (level.meta && Array.isArray(level.meta.shape)) {
1137
+ resolution = extentWidth / level.meta.shape[col];
1138
+ rowResolution = extentHeight / level.meta.shape[row];
1139
+ origin = [extent[0], extent[3]];
1140
+ }
1141
+ else {
1142
+ warn(`No resolution information for level "${level.path || '.'}"`);
1143
+ continue;
1144
+ }
1145
+ configured.push({
1146
+ path: level.path,
1147
+ bands: level.bands,
1148
+ meta: level.meta,
1149
+ shape: level.shape,
1150
+ resolution,
1151
+ rowResolution,
1152
+ origin,
1153
+ });
1154
+ }
1155
+ if (configured.length === 0) {
1156
+ throw new Error(`No usable level found for ${what}`);
1157
+ }
1158
+ configured.sort((a, b) => b.resolution - a.resolution);
1159
+ if (!this.projection) {
1160
+ this.projection = this.inferProjection_(attributes, crsHint, extent);
1161
+ }
1162
+ if (variable) {
1163
+ // Resolve the selection to per-axis indices; labels are resolved against
1164
+ // the finest level's coordinate arrays
1165
+ const finestPath = configured[configured.length - 1].path;
1166
+ /** @type {Array<{axis: number, indices: Array<number>}>} */
1167
+ const slots = [];
1168
+ this.multiAxis_ = -1;
1169
+ this.extraDimensions_ = [];
1170
+ for (let axis = 0; axis < ndim; ++axis) {
1171
+ if (axis === row || axis === col) {
1172
+ continue;
1173
+ }
1174
+ const dimName = dimensionNames[axis];
1175
+ this.extraDimensions_.push({ name: dimName, size: meta.shape[axis] });
1176
+ let value = this.dimensions_[dimName];
1177
+ if (value === undefined) {
1178
+ warn(`No value given for dimension "${dimName}", using index 0.`);
1179
+ value = 0;
1180
+ }
1181
+ /** @type {Array<number|string>} */
1182
+ let values;
1183
+ if (Array.isArray(value)) {
1184
+ values = value;
1185
+ this.multiAxis_ = axis;
1186
+ }
1187
+ else {
1188
+ values = [value];
1189
+ }
1190
+ const indices = [];
1191
+ for (const v of values) {
1192
+ if (typeof v === 'number') {
1193
+ indices.push(v);
1194
+ }
1195
+ else {
1196
+ indices.push(await this.resolveCoordinateLabel_(dimName, v, arrayPathOf(finestPath, dimName)));
1197
+ }
1198
+ }
1199
+ for (const index of indices) {
1200
+ if (!Number.isInteger(index) ||
1201
+ index < 0 ||
1202
+ index >= meta.shape[axis]) {
1203
+ throw new Error(`GeoZarr: invalid index ${index} for dimension "${dimName}" ` +
1204
+ `(size ${meta.shape[axis]}).`);
1205
+ }
1206
+ }
1207
+ slots.push({ axis, indices });
1208
+ }
1209
+ // Materialize the datacube as the per-band state that loadTile_
1210
+ // consumes: each rendered band is the variable array with fixed indices
1211
+ // at the non-spatial axes.
1212
+ const multiSlot = slots.find((slot) => slot.axis === this.multiAxis_);
1213
+ const count = multiSlot ? multiSlot.indices.length : 1;
1214
+ this.bandCount = count;
1215
+ this.bands_ = new Array(count).fill(variable);
1216
+ this.bandGroupIndex_ = new Array(count).fill(0);
1217
+ this.bandSingleScaleResolution_ = new Array(count).fill(undefined);
1218
+ this.bandSpatialAxes_ = new Array(count).fill({ row, col });
1219
+ this.bandExtraSelection_ = new Array(count)
1220
+ .fill(undefined)
1221
+ .map((_, bandIndex) => {
1222
+ if (slots.length === 0) {
1223
+ return undefined;
1224
+ }
1225
+ const selection = new Array(ndim).fill(null);
1226
+ for (const slot of slots) {
1227
+ selection[slot.axis] =
1228
+ slot.axis === this.multiAxis_
1229
+ ? slot.indices[bandIndex]
1230
+ : slot.indices[0];
1231
+ }
1232
+ return selection;
1233
+ });
1234
+ }
1235
+ if (meta) {
1236
+ this.fillValue_ = parseFillValue(meta['fill_value']);
1237
+ }
1238
+ // Align tiles to shards or chunks: every tile decodes all chunks it
1239
+ // touches, so tiles much smaller than a chunk would decode the same
1240
+ // chunk over and over.
1241
+ const tileSizes = configured.map((level) => {
1242
+ var _a, _b;
1243
+ if (!level.meta) {
1244
+ return undefined;
1245
+ }
1246
+ const shardInfo = getShardInfo(level.meta, row, col);
1247
+ if (shardInfo) {
1248
+ return /** @type {import("ol/size.js").Size} */ ([
1249
+ getTileSizeForShard(shardInfo.shardShape[1], shardInfo.innerChunkShape[1]),
1250
+ getTileSizeForShard(shardInfo.shardShape[0], shardInfo.innerChunkShape[0]),
1251
+ ]);
1252
+ }
1253
+ // Unsharded chunks only need alignment when they are larger than a
1254
+ // default tile, which would decode the same chunk for every tile it spans.
1255
+ const chunkShape = (_b = (_a = level.meta['chunk_grid']) === null || _a === void 0 ? void 0 : _a.configuration) === null || _b === void 0 ? void 0 : _b.chunk_shape;
1256
+ if (Array.isArray(chunkShape) &&
1257
+ Math.max(chunkShape[row], chunkShape[col]) > DEFAULT_TILE_SIZE) {
1258
+ return /** @type {import("ol/size.js").Size} */ ([
1259
+ getTileSizeForChunk(chunkShape[col], level.meta.shape[col]),
1260
+ getTileSizeForChunk(chunkShape[row], level.meta.shape[row]),
1261
+ ]);
1262
+ }
1263
+ return undefined;
1264
+ });
1265
+ const hasTileSizes = tileSizes.some((s) => s !== undefined);
1266
+ this.bandsByLevel_ = {};
1267
+ this.levelPaths_ = {};
1268
+ this.levelRowInfo_ = {};
1269
+ const matrixIds = configured.map((level, i) => level.path || String(i));
1270
+ for (let i = 0; i < configured.length; ++i) {
1271
+ const level = configured[i];
1272
+ this.bandsByLevel_[matrixIds[i]] = level.bands;
1273
+ this.levelPaths_[matrixIds[i]] = level.path;
1274
+ let shapeY;
1275
+ if (level.meta && Array.isArray(level.meta.shape)) {
1276
+ shapeY = level.meta.shape[row];
1277
+ }
1278
+ else if (Array.isArray(level.shape)) {
1279
+ shapeY = level.shape[0];
1280
+ }
1281
+ this.levelRowInfo_[matrixIds[i]] = {
1282
+ rowResolution: level.rowResolution,
1283
+ shapeY,
1284
+ flip: flipY,
1285
+ };
1286
+ }
1287
+ this.tileGrid = new WMTSTileGrid(Object.assign({ extent: extent, origins: configured.map((level) => level.origin), resolutions: configured.map((level) => level.resolution), matrixIds: matrixIds }, (hasTileSizes
1288
+ ? { tileSizes: tileSizes.map((s) => s || [256, 256]) }
1289
+ : {})));
1290
+ return hasTileSizes;
1291
+ }
1292
+ /**
1293
+ * Determine the projection from the store metadata: the proj: convention,
1294
+ * a CRS code from the multiscale metadata or xarray-style attributes, a
1295
+ * `proj4` definition, or (for degree-like extents) EPSG:4326.
1296
+ * @param {Object<string, *>} attributes The dataset attributes.
1297
+ * @param {string|null} crsHint A CRS code from the multiscale metadata.
1298
+ * @param {import("ol/extent.js").Extent} extent The extent.
1299
+ * @return {import("ol/proj/Projection.js").default} The projection.
1300
+ * @private
1301
+ */
1302
+ inferProjection_(attributes, crsHint, extent) {
1303
+ try {
1304
+ const projection = getProjectionFromAttributes(
1305
+ /** @type {DatasetAttributes} */ (attributes));
1306
+ if (projection) {
1307
+ return projection;
1308
+ }
1309
+ }
1310
+ catch (_a) {
1311
+ // not declared through the proj: convention
1312
+ }
1313
+ const code = crsHint ||
1314
+ (typeof attributes['spatial_ref'] === 'string' &&
1315
+ attributes['spatial_ref'].includes(':')
1316
+ ? attributes['spatial_ref']
1317
+ : null);
1318
+ if (code) {
1319
+ const projection = getProjection(code);
1320
+ if (projection) {
1321
+ return projection;
1322
+ }
1323
+ }
1324
+ const definition = attributes['proj4'] || attributes['proj4_params'];
1325
+ if (typeof definition === 'string') {
1326
+ try {
1327
+ const projection = fromProjectionDefinition(definition);
1328
+ if (projection) {
1329
+ return projection;
1330
+ }
1331
+ }
1332
+ catch (err) {
1333
+ warn(`Could not register the proj4 definition: ${ /** @type {Error} */(err).message}`);
1334
+ }
1335
+ }
1336
+ if (extent &&
1337
+ extent[0] >= -360 &&
1338
+ extent[2] <= 360.001 &&
1339
+ extent[1] >= -90 &&
1340
+ extent[3] <= 90) {
1341
+ // Extent magnitude suggests degrees
1342
+ return /** @type {import('ol/proj/Projection.js').default} */ (getProjection('EPSG:4326'));
1343
+ }
1344
+ throw new Error('Could not determine the projection');
1345
+ }
1346
+ /**
1347
+ * Read the first and last value of a 1-dimensional coordinate array.
1348
+ * @param {string} levelPath The level group path ('' for the root).
1349
+ * @param {string} dimName The dimension (and coordinate array) name.
1350
+ * @return {Promise<Array<number>>} The first value, last value, and length.
1351
+ * @private
1352
+ */
1353
+ async readCoordinateEndpoints_(levelPath, dimName) {
1354
+ const path = levelPath ? `${levelPath}/${dimName}` : dimName;
1355
+ const array = await this.openArray_(0, path);
1356
+ const length = array.shape[0];
1357
+ const first = await get(array, [slice(0, 1)]);
1358
+ const last = await get(array, [slice(length - 1, length)]);
1359
+ return [
1360
+ Number(/** @type {ArrayLike<number>} */ (first.data)[0]),
1361
+ Number(/** @type {ArrayLike<number>} */ (last.data)[0]),
1362
+ length,
1363
+ ];
1364
+ }
1365
+ /**
1366
+ * Resolve a coordinate label to its index by reading the dimension's
1367
+ * coordinate array.
1368
+ * @param {string} dimName The dimension name.
1369
+ * @param {string} label The label to resolve.
1370
+ * @param {string} path The coordinate array path, relative to the group.
1371
+ * @return {Promise<number>} The index of the label.
1372
+ * @private
1373
+ */
1374
+ async resolveCoordinateLabel_(dimName, label, path) {
1375
+ try {
1376
+ const array = await this.openArray_(0, path);
1377
+ const chunk = await get(array, [null]);
1378
+ const values = Array.from(chunk.data, (v) => String(v));
1379
+ const index = values.indexOf(label);
1380
+ if (index < 0) {
1381
+ throw new Error(`Label not found. Available: ${values.slice(0, 10).join(', ')}`);
1382
+ }
1383
+ return index;
1384
+ }
1385
+ catch (err) {
1386
+ throw new Error(`Could not resolve label "${label}" for dimension "${dimName}": ` +
1387
+ `${ /** @type {Error} */(err).message} Pass a numeric index instead.`);
1388
+ }
1389
+ }
1390
+ }
1391
+ /**
1392
+ * Extract a sub-view of consolidated metadata for a specific group path.
1393
+ * Keys in the returned object are relative to the group path.
1394
+ * @param {Object<string, *>} rootMetadata The root consolidated metadata.
1395
+ * @param {string} groupPath The group path (e.g. 'measurements/reflectance').
1396
+ * @return {Object<string, *>} Sub-metadata with paths relative to the group.
1397
+ */
1398
+ function getSubMetadata(rootMetadata, groupPath) {
1399
+ const prefix = groupPath + '/';
1400
+ /** @type {Object<string, *>} */
1401
+ const sub = {};
1402
+ for (const key of Object.keys(rootMetadata)) {
1403
+ if (key.startsWith(prefix)) {
1404
+ sub[key.substring(prefix.length)] = rootMetadata[key];
1405
+ }
1406
+ }
1407
+ return sub;
1408
+ }
1409
+ /**
1410
+ * Create a store wrapper that serves Zarr v3 metadata from consolidated
1411
+ * metadata, avoiding per-child HTTP requests.
1412
+ * @param {import('zarrita').FetchStore} store The underlying store.
1413
+ * @param {Uint8Array} groupBytes The already-fetched group zarr.json bytes.
1414
+ * @param {Object<string, *>} consolidatedMetadata The parsed consolidated_metadata.metadata entries.
1415
+ * @return {import('zarrita').Readable} A store-compatible object.
1416
+ */
1417
+ function createCachedStore(store, groupBytes, consolidatedMetadata) {
1418
+ var _a;
1419
+ const cache = new Map();
1420
+ cache.set('/zarr.json', groupBytes);
1421
+ const encoder = new TextEncoder();
1422
+ for (const [key, value] of Object.entries(consolidatedMetadata)) {
1423
+ cache.set(
1424
+ /** @type {`/${string}`} */ (`/${key}/zarr.json`), encoder.encode(JSON.stringify(value)));
1425
+ }
1426
+ return withChunkCache({
1427
+ async get(
1428
+ /** @type {string} */ key,
1429
+ /** @type {import('zarrita').GetOptions|undefined} */ opts) {
1430
+ if (cache.has(key)) {
1431
+ return cache.get(key);
1432
+ }
1433
+ return store.get(/** @type {`/${string}`} */ (key), opts);
1434
+ },
1435
+ getRange: (_a = store.getRange) === null || _a === void 0 ? void 0 : _a.bind(store),
1436
+ });
1437
+ }
1438
+ /***
1439
+ * @typedef {{
1440
+ * multiscales: Multiscales,
1441
+ * zarr_conventions: Array<{uuid: string}>,
1442
+ * 'spatial:bbox': import("ol/extent.js").Extent,
1443
+ * 'spatial:shape': Array<number>,
1444
+ * 'spatial:dimensions'?: Array<string>,
1445
+ * 'proj:wkt2'?: string,
1446
+ * 'proj:projjson'?: Object,
1447
+ * 'proj:code'?: string | null,
1448
+ * }} DatasetAttributes
1449
+ */
1450
+ /**
1451
+ * @typedef {Object} Multiscales
1452
+ * @property {Array<Object<string, *>>} layout The layout.
1453
+ */
1454
+ /**
1455
+ * @typedef {Object} LegacyDatasetAttributes
1456
+ * @property {LegacyMultiscales} multiscales The multiscales attribute.
1457
+ */
1458
+ /**
1459
+ * @typedef {Object} LegacyMultiscales
1460
+ * @property {any} tile_matrix_limits The tile matrix limits.
1461
+ * @property {any} tile_matrix_set The tile matrix set.
1462
+ */
1463
+ /**
1464
+ * @typedef {Object} TileGridInfo
1465
+ * @property {WMTSTileGrid} tileGrid The tile grid.
1466
+ * @property {import("ol/proj/Projection.js").default} projection The projection.
1467
+ */
1468
+ /**
1469
+ * Maximum tile size for rendering.
1470
+ * @type {number}
1471
+ */
1472
+ const MAX_TILE_SIZE = 512;
1473
+ /**
1474
+ * Minimum tile size when sharding is used.
1475
+ * @type {number}
1476
+ */
1477
+ const MIN_TILE_SIZE = 64;
1478
+ /**
1479
+ * @typedef {Object} ShardInfo
1480
+ * @property {Array<number>} shardShape The shard (outer chunk) shape [rows, cols].
1481
+ * @property {Array<number>} innerChunkShape The inner chunk shape [rows, cols].
1482
+ */
1483
+ /**
1484
+ * The path of a band array within its group.
1485
+ * @param {string} levelPath The level group path (empty for the group root).
1486
+ * @param {string} name The array name.
1487
+ * @return {string} The path, relative to the group.
1488
+ */
1489
+ function arrayPathOf(levelPath, name) {
1490
+ return levelPath ? `${levelPath}/${name}` : name;
1491
+ }
1492
+ /**
1493
+ * Locate the row (y) and column (x) axis positions of an array by matching the
1494
+ * group's `spatial:dimensions` names (`[y, x]`) against the array's
1495
+ * `dimension_names`, falling back to the two trailing axes when either is absent.
1496
+ * @param {Array<string>|undefined} spatialDimensionNames The `spatial:dimensions` value (`[y, x]` names).
1497
+ * @param {Object<string, *>|undefined} arrayMeta Zarr v3 array metadata.
1498
+ * @return {{row: number, col: number}} The row (y) and column (x) axis positions.
1499
+ */
1500
+ function getSpatialAxes(spatialDimensionNames, arrayMeta) {
1501
+ const rank = ((arrayMeta && arrayMeta['shape']) || []).length;
1502
+ const names = arrayMeta && arrayMeta['dimension_names'];
1503
+ if (Array.isArray(spatialDimensionNames) &&
1504
+ spatialDimensionNames.length === 2 &&
1505
+ Array.isArray(names)) {
1506
+ const row = names.indexOf(spatialDimensionNames[0]);
1507
+ const col = names.indexOf(spatialDimensionNames[1]);
1508
+ if (row !== -1 && col !== -1) {
1509
+ return { row, col };
1510
+ }
1511
+ }
1512
+ return { row: rank - 2, col: rank - 1 };
1513
+ }
1514
+ /**
1515
+ * FIXME Remove this when GeoZarr datasets provide correct TileMatrixSet info or similar.
1516
+ *
1517
+ * Get the shard and inner chunk shapes from the Zarr v3 array metadata.
1518
+ * Only returns info when a `sharding_indexed` codec is present, meaning
1519
+ * `chunk_grid.configuration.chunk_shape` represents the shard (outer chunk) size.
1520
+ * @param {Object<string, *>} arrayMeta The Zarr v3 array metadata from consolidated metadata.
1521
+ * @param {number} row The row (y) axis position.
1522
+ * @param {number} col The column (x) axis position.
1523
+ * @return {ShardInfo|undefined} The shard info, or undefined.
1524
+ */
1525
+ function getShardInfo(arrayMeta, row, col) {
1526
+ const chunkGrid = arrayMeta['chunk_grid'];
1527
+ if (!chunkGrid || chunkGrid['name'] !== 'regular') {
1528
+ return undefined;
1529
+ }
1530
+ const codecs = arrayMeta['codecs'];
1531
+ if (!Array.isArray(codecs)) {
1532
+ return undefined;
1533
+ }
1534
+ const shardingCodec = codecs.find((c) => c['name'] === 'sharding_indexed');
1535
+ if (!shardingCodec) {
1536
+ return undefined;
1537
+ }
1538
+ // Keep only the two spatial axes; the chunk/shard shapes are N-D for N-D arrays.
1539
+ const shardShape = chunkGrid['configuration']['chunk_shape'];
1540
+ const innerChunkShape = shardingCodec['configuration']['chunk_shape'];
1541
+ return {
1542
+ shardShape: [shardShape[row], shardShape[col]],
1543
+ innerChunkShape: [innerChunkShape[row], innerChunkShape[col]],
1544
+ };
1545
+ }
1546
+ /**
1547
+ * FIXME Remove this when GeoZarr datasets provide correct TileMatrixSet info or similar.
1548
+ *
1549
+ * Compute a tile size that is a multiple of the inner chunk size, evenly divides
1550
+ * the shard size, is at most MAX_TILE_SIZE, and is at least MIN_TILE_SIZE.
1551
+ * Aligning with inner chunk boundaries avoids fetching the same inner chunk
1552
+ * data for adjacent tiles.
1553
+ * @param {number} shardSize The shard size in pixels along one dimension.
1554
+ * @param {number} innerChunkSize The inner chunk size in pixels along one dimension.
1555
+ * @return {number} The tile size.
1556
+ */
1557
+ function getTileSizeForShard(shardSize, innerChunkSize) {
1558
+ if (innerChunkSize > MAX_TILE_SIZE) {
1559
+ // Use the largest divisor of the inner chunk size that fits, so that a
1560
+ // whole number of tiles covers each inner chunk.
1561
+ for (let size = MAX_TILE_SIZE; size >= MIN_TILE_SIZE; --size) {
1562
+ if (innerChunkSize % size === 0) {
1563
+ return size;
1564
+ }
1565
+ }
1566
+ return MAX_TILE_SIZE;
1567
+ }
1568
+ // Find the largest multiple of innerChunkSize that divides shardSize
1569
+ // and is within [MIN_TILE_SIZE, MAX_TILE_SIZE].
1570
+ const maxChunks = Math.floor(MAX_TILE_SIZE / innerChunkSize);
1571
+ for (let n = maxChunks; n >= 1; --n) {
1572
+ const candidate = n * innerChunkSize;
1573
+ if (candidate >= MIN_TILE_SIZE && shardSize % candidate === 0) {
1574
+ return candidate;
1575
+ }
1576
+ }
1577
+ // No ideal size found. Use shard size itself when it fits, otherwise
1578
+ // use the largest chunk-aligned size that fits within MAX_TILE_SIZE.
1579
+ if (shardSize <= MAX_TILE_SIZE && shardSize >= MIN_TILE_SIZE) {
1580
+ return shardSize;
1581
+ }
1582
+ if (shardSize < MIN_TILE_SIZE) {
1583
+ return MIN_TILE_SIZE;
1584
+ }
1585
+ return Math.max(maxChunks * innerChunkSize, MIN_TILE_SIZE);
1586
+ }
1587
+ /**
1588
+ * @param {LegacyDatasetAttributes} attributes The dataset attributes.
1589
+ * @return {TileGridInfo} The tile grid info.
1590
+ */
1591
+ function getTileGridInfoFromLegacyAttributes(attributes) {
1592
+ var _a, _b;
1593
+ const multiscales = attributes.multiscales;
1594
+ const tileMatrixSet = multiscales.tile_matrix_set;
1595
+ const tileMatrixLimitsObject = multiscales.tile_matrix_limits;
1596
+ const numMatrices = tileMatrixSet.tileMatrices.length;
1597
+ const tileMatrixLimits = new Array(numMatrices);
1598
+ let overrideTileSize = false;
1599
+ for (let i = 0; i < numMatrices; i += 1) {
1600
+ const tileMatrix = tileMatrixSet.tileMatrices[i];
1601
+ const tilematrixId = tileMatrix.id;
1602
+ if (tileMatrix.tileWidth > 512 || tileMatrix.tileHeight > 512) {
1603
+ // Avoid tile sizes that are too large for rendering
1604
+ overrideTileSize = true;
1605
+ }
1606
+ tileMatrixLimits[i] = tileMatrixLimitsObject[tilematrixId];
1607
+ }
1608
+ const info = parseTileMatrixSet({}, tileMatrixSet, undefined, tileMatrixLimits);
1609
+ let tileGrid = info.grid;
1610
+ // Tile size sanity
1611
+ if (overrideTileSize) {
1612
+ tileGrid = new WMTSTileGrid({
1613
+ tileSize: 512,
1614
+ extent: (_a = tileGrid.getExtent()) !== null && _a !== void 0 ? _a : undefined,
1615
+ origins: (_b = tileGrid.getOrigins()) !== null && _b !== void 0 ? _b : undefined,
1616
+ resolutions: tileGrid.getResolutions(),
1617
+ matrixIds: tileGrid.getMatrixIds(),
1618
+ });
1619
+ }
1620
+ return {
1621
+ tileGrid,
1622
+ projection: /** @type {import("ol/proj/Projection.js").default} */ (info.projection),
1623
+ };
1624
+ }
1625
+ /**
1626
+ * @param {Array<import("zarrita").Chunk<import("zarrita").DataType>>} chunks The input chunks.
1627
+ * @param {Array<number>} chunkResolutions The resolutions for each band.
1628
+ * @param {number} tileColCount The number of columns in the output data.
1629
+ * @param {number} tileRowCount The number of rows in the output data.
1630
+ * @param {number} tileResolution The tile resolution.
1631
+ * @param {ResampleMethod} resampleMethod The resampling method.
1632
+ * @param {number|undefined} fillValue The fill value.
1633
+ * @param {Array<number>} [chunkRowResolutions] The row resolutions for each
1634
+ * band, for data with non-square pixels. Defaults to `chunkResolutions`.
1635
+ * @return {Float32Array} The tile data.
1636
+ */
1637
+ function composeData(chunks, chunkResolutions, tileColCount, tileRowCount, tileResolution, resampleMethod, fillValue, chunkRowResolutions = chunkResolutions) {
1638
+ const chunkCount = chunks.length;
1639
+ const addAlpha = fillValue !== null && fillValue !== undefined;
1640
+ const isNoDataValue = isNaN(/** @type {number} */ (fillValue))
1641
+ ? (/** @type {number} */ v) => isNaN(v)
1642
+ : (/** @type {number} */ v) => v === fillValue;
1643
+ const bandCount = chunkCount + (addAlpha ? 1 : 0);
1644
+ const tileData = new Float32Array(tileColCount * tileRowCount * bandCount);
1645
+ for (let tileRow = 0; tileRow < tileRowCount; tileRow++) {
1646
+ for (let tileCol = 0; tileCol < tileColCount; tileCol++) {
1647
+ let hasData = false;
1648
+ for (let chunkIndex = 0; chunkIndex < chunkCount; ++chunkIndex) {
1649
+ const chunk = chunks[chunkIndex];
1650
+ const chunkRowCount = chunk.shape[0];
1651
+ const chunkColCount = chunk.shape[1];
1652
+ const chunkData = /** @type {ArrayLike<number>} */ (chunk.data);
1653
+ const scaleFactor = tileResolution / chunkResolutions[chunkIndex];
1654
+ const rowScaleFactor = tileResolution / chunkRowResolutions[chunkIndex];
1655
+ let value = 0;
1656
+ let inBounds = false;
1657
+ if (scaleFactor === 1 && rowScaleFactor === 1) {
1658
+ if (tileRow < chunkRowCount && tileCol < chunkColCount) {
1659
+ inBounds = true;
1660
+ value = chunkData[tileRow * chunkColCount + tileCol];
1661
+ }
1662
+ }
1663
+ else {
1664
+ const chunkRow = tileRow * rowScaleFactor;
1665
+ const chunkCol = tileCol * scaleFactor;
1666
+ switch (resampleMethod) {
1667
+ case 'nearest': {
1668
+ const valueRow = Math.round(chunkRow);
1669
+ const valueCol = Math.round(chunkCol);
1670
+ if (valueRow < chunkRowCount && valueCol < chunkColCount) {
1671
+ inBounds = true;
1672
+ value = chunkData[valueRow * chunkColCount + valueCol];
1673
+ }
1674
+ break;
1675
+ }
1676
+ case 'linear': {
1677
+ const row0 = Math.floor(chunkRow);
1678
+ const col0 = Math.floor(chunkCol);
1679
+ if (row0 < chunkRowCount && col0 < chunkColCount) {
1680
+ inBounds = true;
1681
+ const row1 = Math.min(row0 + 1, chunkRowCount - 1);
1682
+ const col1 = Math.min(col0 + 1, chunkColCount - 1);
1683
+ const v00 = chunkData[row0 * chunkColCount + col0];
1684
+ const v01 = chunkData[row0 * chunkColCount + col1];
1685
+ const v10 = chunkData[row1 * chunkColCount + col0];
1686
+ const v11 = chunkData[row1 * chunkColCount + col1];
1687
+ const dx = chunkCol - col0;
1688
+ const dy = chunkRow - row0;
1689
+ value =
1690
+ (1 - dy) * ((1 - dx) * v00 + dx * v01) +
1691
+ dy * ((1 - dx) * v10 + dx * v11);
1692
+ }
1693
+ break;
1694
+ }
1695
+ default: {
1696
+ throw new Error(`Unsupported resample method: ${resampleMethod}`);
1697
+ }
1698
+ }
1699
+ }
1700
+ if (inBounds && !isNoDataValue(value)) {
1701
+ hasData = true;
1702
+ }
1703
+ if (isNaN(value)) {
1704
+ value = 0;
1705
+ }
1706
+ tileData[bandCount * (tileRow * tileColCount + tileCol) + chunkIndex] =
1707
+ value;
1708
+ }
1709
+ if (addAlpha) {
1710
+ tileData[bandCount * (tileRow * tileColCount + tileCol) + chunkCount] =
1711
+ hasData ? 1 : 0;
1712
+ }
1713
+ }
1714
+ }
1715
+ return tileData;
1716
+ }
1717
+ /**
1718
+ * @param {DatasetAttributes} attributes Attriutes.
1719
+ * @return {import("ol/proj/Projection.js").default} The projection.
1720
+ */
1721
+ function getProjectionFromAttributes(attributes) {
1722
+ const projCode = attributes['proj:code'];
1723
+ if (projCode) {
1724
+ return /** @type {import("ol/proj/Projection.js").default} */ (getProjection(projCode));
1725
+ }
1726
+ const projDef = attributes['proj:projjson'] || attributes['proj:wkt2'];
1727
+ return /** @type {import("ol/proj/Projection.js").default} */ (fromProjectionDefinition(projDef));
1728
+ }
1729
+ /**
1730
+ * Maximum number of requests to keep in the per-source chunk cache.
1731
+ * @type {number}
1732
+ */
1733
+ const MAX_CACHED_CHUNKS = 32;
1734
+ /**
1735
+ * Wrap a store with a small LRU cache for chunk requests. Adjacent tiles
1736
+ * often read from the same chunk (in the extreme case, a store with a single
1737
+ * chunk per array); without this cache each tile would fetch the chunk
1738
+ * again, as zarrita does not cache requests.
1739
+ * @param {import('zarrita').FetchStore|import('zarrita').Readable} store The store to wrap.
1740
+ * @return {import('zarrita').Readable} A store-compatible object.
1741
+ */
1742
+ function withChunkCache(store) {
1743
+ var _a;
1744
+ /** @type {Map<string, Promise<Uint8Array|undefined>>} */
1745
+ const chunkCache = new Map();
1746
+ return /** @type {import('zarrita').Readable} */ ({
1747
+ async get(
1748
+ /** @type {string} */ key,
1749
+ /** @type {import('zarrita').GetOptions|undefined} */ opts) {
1750
+ if (chunkCache.has(key)) {
1751
+ // Re-insert to mark the entry as most recently used
1752
+ const cached = /** @type {Promise<Uint8Array|undefined>} */ (chunkCache.get(key));
1753
+ chunkCache.delete(key);
1754
+ chunkCache.set(key, cached);
1755
+ return cached;
1756
+ }
1757
+ const promise = /** @type {Promise<Uint8Array|undefined>} */ (store.get(/** @type {`/${string}`} */ (key), opts));
1758
+ chunkCache.set(key, promise);
1759
+ promise.catch(() => chunkCache.delete(key));
1760
+ if (chunkCache.size > MAX_CACHED_CHUNKS) {
1761
+ chunkCache.delete(
1762
+ /** @type {string} */ (chunkCache.keys().next().value));
1763
+ }
1764
+ return promise;
1765
+ },
1766
+ getRange: /** @type {*} */ (_a = (store).getRange) === null || _a === void 0 ? void 0 : _a.bind(store),
1767
+ });
1768
+ }
1769
+ /**
1770
+ * Like {@link createCachedStore}, for Zarr v2 consolidated metadata: the
1771
+ * .zmetadata entries are served under their raw keys (e.g. `path/.zarray`).
1772
+ * @param {import('zarrita').FetchStore} store The underlying store.
1773
+ * @param {Object<string, *>} v2Metadata The .zmetadata `metadata` entries.
1774
+ * @return {import('zarrita').Readable} A store-compatible object.
1775
+ */
1776
+ function createCachedStoreV2(store, v2Metadata) {
1777
+ var _a;
1778
+ const cache = new Map();
1779
+ const encoder = new TextEncoder();
1780
+ for (const [key, value] of Object.entries(v2Metadata)) {
1781
+ cache.set(`/${key}`, encoder.encode(JSON.stringify(value)));
1782
+ }
1783
+ return withChunkCache({
1784
+ async get(
1785
+ /** @type {string} */ key,
1786
+ /** @type {import('zarrita').GetOptions|undefined} */ opts) {
1787
+ if (cache.has(key)) {
1788
+ return cache.get(key);
1789
+ }
1790
+ return store.get(/** @type {`/${string}`} */ (key), opts);
1791
+ },
1792
+ getRange: (_a = store.getRange) === null || _a === void 0 ? void 0 : _a.bind(store),
1793
+ });
1794
+ }
1795
+ /**
1796
+ * Maximum tile size when tiles are aligned to (unsharded) chunks. Larger
1797
+ * than MAX_TILE_SIZE because decoding one big chunk into a single tile is
1798
+ * cheaper than decoding it once per covering tile.
1799
+ * @type {number}
1800
+ */
1801
+ const MAX_CHUNK_TILE_SIZE = 2048;
1802
+ /**
1803
+ * Compute a tile size for an unsharded array along one dimension: a multiple
1804
+ * of the chunk size when chunks are small, or (a cap of) the chunk size
1805
+ * itself when chunks are large.
1806
+ * @param {number} chunkSize The chunk size in pixels along one dimension.
1807
+ * @param {number} arraySize The array size in pixels along the same dimension.
1808
+ * @return {number} The tile size.
1809
+ */
1810
+ function getTileSizeForChunk(chunkSize, arraySize) {
1811
+ let size;
1812
+ if (chunkSize >= MAX_TILE_SIZE) {
1813
+ size = Math.min(chunkSize, MAX_CHUNK_TILE_SIZE);
1814
+ }
1815
+ else {
1816
+ size = Math.floor(MAX_TILE_SIZE / chunkSize) * chunkSize;
1817
+ }
1818
+ return Math.max(MIN_TILE_SIZE, Math.min(size, arraySize));
1819
+ }
1820
+ /**
1821
+ * Parse a Zarr fill value (which may be encoded as a string like "NaN").
1822
+ * @param {number|string|null|undefined} value The raw fill value.
1823
+ * @return {number|undefined} The fill value as a number, or undefined.
1824
+ */
1825
+ function parseFillValue(value) {
1826
+ if (typeof value === 'number') {
1827
+ return value;
1828
+ }
1829
+ if (typeof value === 'string') {
1830
+ return Number(value);
1831
+ }
1832
+ return undefined;
1833
+ }
1834
+ /**
1835
+ * Normalize Zarr v2 consolidated metadata (.zmetadata entries like
1836
+ * `path/.zarray` and `path/.zattrs`) into the Zarr v3 shape that this
1837
+ * source reads.
1838
+ * @param {Object<string, *>} v2Metadata The .zmetadata `metadata` entries.
1839
+ * @return {Object<string, *>} Array metadata by path.
1840
+ */
1841
+ function normalizeV2Metadata(v2Metadata) {
1842
+ /** @type {Object<string, *>} */
1843
+ const normalized = {};
1844
+ for (const [key, value] of Object.entries(v2Metadata)) {
1845
+ if (!key.endsWith('/.zarray')) {
1846
+ continue;
1847
+ }
1848
+ const path = key.slice(0, -'/.zarray'.length);
1849
+ const attributes = v2Metadata[`${path}/.zattrs`] || {};
1850
+ normalized[path] = {
1851
+ shape: value['shape'],
1852
+ fill_value: value['fill_value'],
1853
+ dimension_names: attributes['_ARRAY_DIMENSIONS'],
1854
+ chunk_grid: {
1855
+ name: 'regular',
1856
+ configuration: { chunk_shape: value['chunks'] },
1857
+ },
1858
+ attributes,
1859
+ };
1860
+ }
1861
+ return normalized;
1862
+ }
1863
+ /**
1864
+ * Probe the store for an optional metadata document. Absence yields
1865
+ * `undefined`; any other failure is thrown, so that a server error is not
1866
+ * mistaken for a missing document.
1867
+ * @param {FetchStore} store The store.
1868
+ * @param {string} key The key to probe.
1869
+ * @return {Promise<Uint8Array|undefined>} The document, if present.
1870
+ */
1871
+ function probe(store, key) {
1872
+ return store.get(/** @type {`/${string}`} */ (key));
1873
+ }
1874
+ /**
1875
+ * Fetch for the Zarr store, reporting a 403 as a missing key. S3 answers 403
1876
+ * for a key that is not there whenever the caller lacks `s3:ListBucket`, the
1877
+ * usual setup for publicly readable objects.
1878
+ * @param {Request} request The request.
1879
+ * @return {Promise<Response>} The response.
1880
+ */
1881
+ function storeFetch(request) {
1882
+ return fetch(request).then((response) => response.status === 403 ? new Response(null, { status: 404 }) : response);
1883
+ }
1884
+ /**
1885
+ * Read a single array's metadata directly from the store, for stores
1886
+ * without consolidated metadata (Zarr v3 zarr.json or v2 .zarray/.zattrs).
1887
+ * @param {FetchStore} store The store.
1888
+ * @param {string} path The array path.
1889
+ * @param {boolean} v2 Whether the store is Zarr v2.
1890
+ * @return {Promise<Object|undefined>} The array metadata (v3 shape).
1891
+ */
1892
+ async function getArrayMeta(store, path, v2) {
1893
+ const decoder = new TextDecoder();
1894
+ if (!v2) {
1895
+ const bytes = await probe(store, `/${path}/zarr.json`);
1896
+ return bytes ? JSON.parse(decoder.decode(bytes)) : undefined;
1897
+ }
1898
+ const bytes = await probe(store, `/${path}/.zarray`);
1899
+ if (!bytes) {
1900
+ return undefined;
1901
+ }
1902
+ const zarray = JSON.parse(decoder.decode(bytes));
1903
+ const attrBytes = await probe(store, `/${path}/.zattrs`);
1904
+ const attributes = attrBytes ? JSON.parse(decoder.decode(attrBytes)) : {};
1905
+ return {
1906
+ shape: zarray['shape'],
1907
+ fill_value: zarray['fill_value'],
1908
+ dimension_names: attributes['_ARRAY_DIMENSIONS'],
1909
+ chunk_grid: {
1910
+ name: 'regular',
1911
+ configuration: { chunk_shape: zarray['chunks'] },
1912
+ },
1913
+ attributes,
1914
+ };
1915
+ }
1916
+ /**
1917
+ * Flip the row order of a 2-dimensional chunk (for south-up data).
1918
+ * @param {{data: any, shape: Array<number>, stride: Array<number>}} chunk The chunk.
1919
+ * @return {{data: any, shape: Array<number>, stride: Array<number>}} The flipped chunk.
1920
+ */
1921
+ function flipChunkRows(chunk) {
1922
+ const [rowCount, colCount] = chunk.shape;
1923
+ const source = chunk.data;
1924
+ const data = new source.constructor(source.length);
1925
+ for (let row = 0; row < rowCount; ++row) {
1926
+ data.set(source.subarray((rowCount - 1 - row) * colCount, (rowCount - row) * colCount), row * colCount);
1927
+ }
1928
+ return { data, shape: chunk.shape, stride: [colCount, 1] };
1929
+ }
1930
+ //# sourceMappingURL=GeoZarr.js.map