ol-stac 1.6.0 → 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.
package/source/GeoZarr.js CHANGED
@@ -1,21 +1,17 @@
1
1
  /**
2
2
  * @module ol/source/GeoZarr
3
3
  */
4
- import { FetchStore, get, open, slice, withRangeCoalescing } from 'zarrita';
4
+ import { FetchStore, NotFoundError, get, open, slice, withRangeCoalescing, } from 'zarrita';
5
5
  import { warn } from 'ol/console.js';
6
6
  import { getCenter } from 'ol/extent.js';
7
7
  import { get as getProjection, toUserCoordinate, toUserExtent } from 'ol/proj.js';
8
8
  import { fromProjectionDefinition } from '../shim/proj4.js';
9
9
  import { toSize } from 'ol/size.js';
10
10
  import WMTSTileGrid from 'ol/tilegrid/WMTS.js';
11
+ import { DEFAULT_TILE_SIZE } from 'ol/tilegrid/common.js';
11
12
  import { getUid } from 'ol/util.js';
12
13
  import DataTileSource from 'ol/source/DataTile.js';
13
14
  import { parseTileMatrixSet } from '../shim/ogcTileUtil.js';
14
- const REQUIRED_ZARR_CONVENTIONS = [
15
- 'd35379db-88df-4056-af3a-620245f8e347',
16
- 'f17cb550-5864-4468-aeb7-f3180cfb622f',
17
- '689b58e2-cf7b-45e0-9fff-9cfc0883d6b4', // spatial:
18
- ];
19
15
  /**
20
16
  * @typedef {'nearest'|'linear'} ResampleMethod
21
17
  */
@@ -55,23 +51,21 @@ const REQUIRED_ZARR_CONVENTIONS = [
55
51
  * To disable the opacity transition, pass `transition: 0`.
56
52
  * @property {boolean} [wrapX=false] Render tiles beyond the tile grid extent.
57
53
  * @property {ResampleMethod} [resample='linear'] Resampling method if bands are not available for all multi-scale levels.
58
- * @property {Object<string, number|string>} [dimensions] Fixed index for each non-spatial
59
- * dimension of the band arrays, keyed by dimension name (e.g. `{time: 0}` for the first time step
60
- * of a `[time, y, x]` cube); unspecified dimensions default to `0`. Names come from each array's
61
- * `dimension_names`, or are the axis position as a string when it has none. Only integer indices
62
- * are supported. Use the names from {@link getDimensions}, and change the selection on the fly with
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
63
  * {@link module:ol/source/GeoZarr~GeoZarr#updateDimensions}.
64
64
  * @property {string} [variable] The name of an n-dimensional data array (variable) to
65
65
  * render, for stores where all bands are packed into a single array (e.g. a
66
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
- * @property {Object<string, number|string|Array<number|string>>} [selector] For
69
- * `variable` mode: how to slice each non-spatial dimension, keyed by dimension name
70
- * (from the array's `dimension_names`). Values are 0-based indices (number), coordinate
71
- * labels (string), or an array of these. At most one dimension may map to an array; its
72
- * entries are rendered as separate bands (in the given order). Unlisted non-spatial
73
- * dimensions default to index 0. Labels are resolved against the dimension's coordinate
74
- * array; if that array cannot be read, pass indices instead.
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`.
75
69
  * @property {import("ol/extent.js").Extent} [extent] Fallback extent of the data, in
76
70
  * coordinates of the source projection. Only used when the store neither declares its
77
71
  * extent (`spatial:bbox` or `bounds` attributes) nor has coordinate arrays to infer it.
@@ -85,19 +79,25 @@ const REQUIRED_ZARR_CONVENTIONS = [
85
79
  * - [Geospatial projection convention](https://github.com/zarr-conventions/geo-proj)
86
80
  * - [Spatial convention](https://github.com/zarr-conventions/spatial)
87
81
  *
88
- * When all three conventions are present, multiple resolution levels are supported.
89
- * When only proj: and spatial: are present, a single-resolution tile grid is derived
90
- * from `spatial:bbox`, `proj:code`, and `spatial:shape`.
91
- * The legacy `tile_matrix_set` attribute is also supported, as is the GeoZarr-style
92
- * `multiscales` array attribute (`[{tile_matrix_set, datasets: [{path, ...}]}]`).
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
93
  *
94
94
  * Two data layouts are supported:
95
95
  * - One array per band (`bands` option), addressed by name at `<matrixId>/<bandName>`
96
- * (multi-scale) or at the group root (single-scale). Supports Zarr v3.
97
- * - A single n-dimensional data array shared by all bands (`variable` + `selector`
98
- * options), e.g. a `(time, band, y, x)` datacube. Supports Zarr v2 and v3; the
99
- * extent, resolution, projection and y-axis orientation are read from the store
100
- * metadata or inferred from the coordinate arrays.
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
101
  */
102
102
  export default class GeoZarr extends DataTileSource {
103
103
  /**
@@ -123,8 +123,9 @@ export default class GeoZarr extends DataTileSource {
123
123
  */
124
124
  this.storeOptions_ = options.storeOptions;
125
125
  /**
126
- * Fixed index per non-spatial dimension name, from the `dimensions` option.
127
- * @type {Object<string, number|string>}
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>>}
128
129
  * @private
129
130
  */
130
131
  this.dimensions_ = options.dimensions || {};
@@ -133,11 +134,6 @@ export default class GeoZarr extends DataTileSource {
133
134
  * @private
134
135
  */
135
136
  this.variable_ = options.variable;
136
- /**
137
- * @type {Object<string, number|string|Array<number|string>>}
138
- * @private
139
- */
140
- this.selector_ = options.selector || {};
141
137
  /**
142
138
  * @type {import("ol/extent.js").Extent|undefined}
143
139
  * @private
@@ -149,12 +145,13 @@ export default class GeoZarr extends DataTileSource {
149
145
  */
150
146
  this.fallbackFlipY_ = options.flipY;
151
147
  /**
152
- * The zarrita open function, pinned to v2 when the store cannot be
153
- * probed for v3 metadata (e.g. servers answering 403 for missing keys).
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.
154
151
  * @type {Function}
155
152
  * @private
156
153
  */
157
- this.openFn_ = open;
154
+ this.openFn_ = open.v3;
158
155
  /**
159
156
  * Group path prefix per tile matrix id, for stores whose levels are not
160
157
  * addressed by the matrix id itself (empty string for the group root).
@@ -171,7 +168,7 @@ export default class GeoZarr extends DataTileSource {
171
168
  */
172
169
  this.levelRowInfo_ = null;
173
170
  /**
174
- * The axis selected as multiple bands through the `selector` option,
171
+ * The axis selected as multiple bands through the `dimensions` option,
175
172
  * or -1.
176
173
  * @type {number}
177
174
  * @private
@@ -285,19 +282,24 @@ export default class GeoZarr extends DataTileSource {
285
282
  * Number of bands.
286
283
  * @type {number}
287
284
  */
288
- this.bandCount = this.bands_.length;
289
- if (this.variable_) {
290
- let bandCount = 1;
291
- for (const key in this.selector_) {
292
- const value = this.selector_[key];
293
- if (Array.isArray(value)) {
294
- if (bandCount > 1) {
295
- throw new Error('Only one selector dimension may select multiple values');
296
- }
297
- bandCount = value.length;
298
- }
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;
299
293
  }
300
- this.bandCount = bandCount;
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;
301
303
  }
302
304
  /**
303
305
  * @type {import("ol/tilegrid/WMTS.js").default}
@@ -310,29 +312,34 @@ export default class GeoZarr extends DataTileSource {
310
312
  this.setState('ready');
311
313
  })
312
314
  .catch((err) => {
313
- this.error_ = 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;
314
321
  this.setState('error');
315
322
  });
316
323
  }
317
324
  async configure_() {
318
- var _a;
325
+ var _a, _b, _c;
319
326
  const overrides = /** @type {RequestInit} */ (this.storeOptions_) || {};
320
- const store = /** @type {FetchStore} */ (withRangeCoalescing(new FetchStore(this.url_, { overrides })));
321
- // Fetch group zarr.json once for both opening the group and extracting
322
- // consolidated metadata. Without this, open() and the manual metadata
323
- // read would each make a separate HTTP request for the same file.
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.
324
330
  const groupBytes = await probe(store, '/zarr.json');
331
+ this.openFn_ = groupBytes ? open.v3 : open.v2;
325
332
  if (groupBytes) {
326
333
  try {
327
334
  this.consolidatedMetadata_ = JSON.parse(new TextDecoder().decode(groupBytes)).consolidated_metadata.metadata;
328
335
  }
329
- catch (_b) {
336
+ catch (_d) {
330
337
  // no consolidated metadata
331
338
  }
332
339
  }
333
340
  /** @type {Object<string, *>|null} */
334
341
  let v2Metadata = null;
335
- if (!this.consolidatedMetadata_) {
342
+ if (!groupBytes) {
336
343
  // Zarr v2: consolidated metadata lives in .zmetadata
337
344
  const v2Bytes = await probe(store, '/.zmetadata');
338
345
  if (v2Bytes) {
@@ -341,7 +348,7 @@ export default class GeoZarr extends DataTileSource {
341
348
  this.consolidatedMetadata_ = normalizeV2Metadata(
342
349
  /** @type {Object<string, *>} */ (v2Metadata));
343
350
  }
344
- catch (_c) {
351
+ catch (_e) {
345
352
  // no consolidated metadata
346
353
  }
347
354
  }
@@ -357,14 +364,14 @@ export default class GeoZarr extends DataTileSource {
357
364
  const groupPromises = [];
358
365
  if (this.groupPaths_) {
359
366
  // Multi-group mode: open root, then each sub-group
360
- const rootGroup = await open(cachedStore, { kind: 'group' });
367
+ const rootGroup = await this.openFn_(cachedStore, { kind: 'group' });
361
368
  for (const groupPath of this.groupPaths_) {
362
- groupPromises.push(open(rootGroup.resolve(groupPath), { kind: 'group' }));
369
+ groupPromises.push(this.openFn_(rootGroup.resolve(groupPath), { kind: 'group' }));
363
370
  }
364
371
  }
365
372
  else {
366
373
  // Single group mode
367
- groupPromises.push(this.openGroup_(cachedStore));
374
+ groupPromises.push(this.openFn_(cachedStore, { kind: 'group' }));
368
375
  }
369
376
  this.groups_.push(...(await Promise.all(groupPromises)));
370
377
  const attributes =
@@ -377,85 +384,38 @@ export default class GeoZarr extends DataTileSource {
377
384
  if (Array.isArray(spatialDimensions) && spatialDimensions.length === 2) {
378
385
  this.spatialDimensionNames_ = spatialDimensions;
379
386
  }
380
- // For multi-group mode, use sub-metadata for the first group so that
381
- // consolidated metadata keys match the expected relative paths.
382
- const consolidatedMetadata = this.groupPaths_ && this.consolidatedMetadata_
383
- ? getSubMetadata(this.consolidatedMetadata_, this.groupPaths_[0])
384
- : this.consolidatedMetadata_;
385
- if (this.variable_) {
386
- await this.configureDatacube_(attributes, store);
387
- }
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;
388
395
  let hasTileSizes = false;
389
- if (!this.tileGrid &&
390
- 'zarr_conventions' in attributes &&
391
- Array.isArray(attributes.zarr_conventions) &&
392
- REQUIRED_ZARR_CONVENTIONS.every((uuid) => attributes.zarr_conventions.find((c) => c.uuid === uuid)) &&
393
- 'layout' in attributes.multiscales) {
394
- const { tileGrid, projection, bandsByLevel, fillValue, tileSizes } = getTileGridInfoFromAttributes(
395
- /** @type {DatasetAttributes} */ (attributes), consolidatedMetadata, this.bands_);
396
- this.bandsByLevel_ = bandsByLevel;
397
- this.tileGrid = tileGrid;
398
- this.projection = projection;
399
- this.fillValue_ = fillValue;
400
- hasTileSizes = !!tileSizes;
401
- }
402
- if (!hasTileSizes &&
403
- attributes.multiscales &&
404
- 'tile_matrix_set' in attributes.multiscales) {
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) {
405
404
  // If available, use tile_matrix_set (legacy attributes) to get a tile grid, because it
406
405
  // should provide a better mapping of tiles to zarr chunks.
407
406
  const { tileGrid, projection } = getTileGridInfoFromLegacyAttributes(
408
407
  /** @type {LegacyDatasetAttributes} */ (attributes));
409
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;
410
414
  if (!this.projection) {
411
- // If there were no required zarr conventions, we don't have a projection yet
415
+ // Only the tile matrix set declares one for a legacy-only store
412
416
  this.projection = projection;
413
417
  }
414
418
  }
415
- if (!this.tileGrid && 'spatial:bbox' in attributes) {
416
- // Standalone single-scale group: build tile grid directly from
417
- // spatial:bbox and spatial:shape (`[height, width]`), or the x axis size
418
- // from the array metadata.
419
- let xSize = (_a = attributes['spatial:shape']) === null || _a === void 0 ? void 0 : _a[1];
420
- if (xSize === undefined && consolidatedMetadata) {
421
- for (const band of this.bands_) {
422
- const bandMeta = consolidatedMetadata[band];
423
- if (bandMeta === null || bandMeta === void 0 ? void 0 : bandMeta.shape) {
424
- xSize = bandMeta.shape[this.axesOf_(bandMeta).col];
425
- break;
426
- }
427
- }
428
- }
429
- if (xSize !== undefined) {
430
- const extent = attributes['spatial:bbox'];
431
- const resolution = (extent[2] - extent[0]) / xSize;
432
- if (!this.projection) {
433
- this.projection = getProjectionFromAttributes(attributes);
434
- }
435
- if (consolidatedMetadata) {
436
- this.bandsByLevel_ = { level0: [] };
437
- for (const band of this.bands_) {
438
- if (consolidatedMetadata[band]) {
439
- this.bandsByLevel_['level0'].push(band);
440
- if (this.fillValue_ === undefined) {
441
- this.fillValue_ = Number(consolidatedMetadata[band]['fill_value']);
442
- }
443
- }
444
- }
445
- }
446
- this.tileGrid = new WMTSTileGrid({
447
- extent: extent,
448
- origins: [[extent[0], extent[3]]],
449
- resolutions: [resolution],
450
- matrixIds: ['level0'],
451
- });
452
- for (let i = 0; i < this.bands_.length; ++i) {
453
- if (this.bandGroupIndex_[i] === 0) {
454
- this.bandSingleScaleResolution_[i] = resolution;
455
- }
456
- }
457
- }
458
- }
459
419
  // For multi-group: determine which group owns each band and supplement
460
420
  // bandsByLevel with bands from additional groups.
461
421
  if (this.groupPaths_ && this.consolidatedMetadata_ && this.bandsByLevel_) {
@@ -469,9 +429,19 @@ export default class GeoZarr extends DataTileSource {
469
429
  if (!this.tileGrid) {
470
430
  throw new Error('Could not determine tile grid');
471
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
+ }
472
442
  // Resolve, per band, the spatial axes and the fixed indices for any
473
443
  // non-spatial dimensions, and record the selectable dimensions.
474
- // (In `variable` mode, configureDatacube_ resolves these instead.)
444
+ // (In `variable` mode, configureLevels_ resolves these instead.)
475
445
  for (let i = 0, ii = this.variable_ ? 0 : this.bands_.length; i < ii; ++i) {
476
446
  const arrayMeta = this.getBandArrayMeta_(this.bands_[i], this.bandGroupIndex_[i]);
477
447
  const { row, col } = this.axesOf_(arrayMeta);
@@ -811,8 +781,9 @@ export default class GeoZarr extends DataTileSource {
811
781
  * another `time` slice) without rebuilding the source. Values are merged into
812
782
  * the current selection, so a partial update like `{time: 3}` leaves the other
813
783
  * dimensions untouched. Takes effect immediately when the source is `ready`,
814
- * otherwise once it becomes ready.
815
- * @param {Object<string, number|string>} dimensions Index per dimension name
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
816
787
  * to change; see the `dimensions` constructor option.
817
788
  */
818
789
  updateDimensions(dimensions) {
@@ -830,7 +801,7 @@ export default class GeoZarr extends DataTileSource {
830
801
  return this.resolveExtraSelection_(arrayMeta, merged);
831
802
  }
832
803
  // In `variable` mode, merge into the current selection so the axes
833
- // fixed through the `selector` option are kept.
804
+ // fixed through the `dimensions` option are kept.
834
805
  const dims = this.extraDimsOf_(arrayMeta);
835
806
  const current = this.bandExtraSelection_[i];
836
807
  const updated = current ? current.slice() : undefined;
@@ -842,7 +813,7 @@ export default class GeoZarr extends DataTileSource {
842
813
  }
843
814
  if (dim.axis === this.multiAxis_) {
844
815
  throw new Error(`GeoZarr: dimension "${name}" selects the rendered bands; ` +
845
- `set it through the \`selector\` option instead.`);
816
+ `set it when constructing the source instead.`);
846
817
  }
847
818
  if (typeof index !== 'number' ||
848
819
  !Number.isInteger(index) ||
@@ -912,8 +883,8 @@ export default class GeoZarr extends DataTileSource {
912
883
  * `null` at the two spatial axes (e.g. `[2, null, null]` for a `[time, y, x]`
913
884
  * array with `{time: 2}`).
914
885
  * @param {Object<string, *>|undefined} arrayMeta Zarr v3 array metadata.
915
- * @param {Object<string, number|string>} dimensions The dimension indices
916
- * to resolve against.
886
+ * @param {Object<string, number|string|Array<number|string>>} dimensions The
887
+ * dimension indices to resolve against.
917
888
  * @return {Array<number|null>|undefined} The extra-axis selection template.
918
889
  * @private
919
890
  */
@@ -951,48 +922,43 @@ export default class GeoZarr extends DataTileSource {
951
922
  index = 0; // unspecified extra dimension defaults to the first slice
952
923
  }
953
924
  if (typeof index === 'string') {
954
- // Datetime-label selection is not implemented yet; only integer indices.
955
- throw new Error(`GeoZarr: datetime-label selection for dimension "${name}" is not yet ` +
956
- `implemented; pass an integer index in the \`dimensions\` option.`);
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}".`);
957
928
  }
958
- if (!Number.isInteger(index) || index < 0 || index >= dim.size) {
929
+ if (!Number.isInteger(index) ||
930
+ /** @type {number} */ (index) < 0 ||
931
+ /** @type {number} */ (index) >= dim.size) {
959
932
  throw new Error(`GeoZarr: invalid index ${index} for dimension "${name}" ` +
960
933
  `(size ${dim.size}).`);
961
934
  }
962
- selection[dim.axis] = index;
935
+ selection[dim.axis] = /** @type {number} */ (index);
963
936
  }
964
937
  return selection;
965
938
  }
966
939
  /**
967
- * Open the group, retrying as Zarr v2 without probing for v3 metadata
968
- * first, for servers that answer 403 for missing keys.
969
- * @param {*} source The store or location to open.
970
- * @return {Promise<import('zarrita').Group<any>>} The opened group.
971
- * @private
972
- */
973
- async openGroup_(source) {
974
- try {
975
- return await this.openFn_(source, { kind: 'group' });
976
- }
977
- catch (err) {
978
- if (this.openFn_ !== open) {
979
- throw err;
980
- }
981
- const group = await open.v2(source, { kind: 'group' });
982
- this.openFn_ = open.v2;
983
- return group;
984
- }
985
- }
986
- /**
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.
987
943
  * @param {Object<string, *>} attributes The dataset attributes.
988
944
  * @param {FetchStore} store The store, for metadata requests not covered
989
945
  * by consolidated metadata.
946
+ * @return {Promise<boolean>} Whether the tile grid has explicit tile sizes.
990
947
  * @private
991
948
  */
992
- async configureDatacube_(attributes, store) {
949
+ async configureLevels_(attributes, store) {
993
950
  var _a;
994
- const variable = /** @type {string} */ (this.variable_);
995
- const consolidatedMetadata = this.consolidatedMetadata_;
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;
996
962
  // Collect the multiscale level paths; without multiscale metadata the
997
963
  // group itself is the only level.
998
964
  const multiscales = attributes['multiscales'];
@@ -1019,6 +985,11 @@ export default class GeoZarr extends DataTileSource {
1019
985
  }
1020
986
  else if (multiscales && Array.isArray(multiscales.layout)) {
1021
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.
1022
993
  rawLevels.push({
1023
994
  path: String(entry.asset),
1024
995
  transform: undefined,
@@ -1033,40 +1004,47 @@ export default class GeoZarr extends DataTileSource {
1033
1004
  shape: attributes['spatial:shape'],
1034
1005
  });
1035
1006
  }
1036
- /** @type {Array<{path: string, arrayPath: string, meta: any, transform: Array<number>|undefined, shape: Array<number>|undefined}>} */
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}>} */
1037
1010
  const levels = [];
1038
1011
  for (const raw of rawLevels) {
1039
- const arrayPath = raw.path ? `${raw.path}/${variable}` : variable;
1040
- const meta = consolidatedMetadata
1041
- ? consolidatedMetadata[arrayPath]
1042
- : undefined;
1043
- if (consolidatedMetadata && !meta) {
1044
- warn(`Variable "${variable}" not found at level "${raw.path || '.'}"`);
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 || '.'}"`);
1045
1017
  continue;
1046
1018
  }
1047
- levels.push(Object.assign(Object.assign({}, raw), { arrayPath, meta }));
1019
+ levels.push(Object.assign(Object.assign({}, raw), { bands, meta: consolidatedMetadata
1020
+ ? consolidatedMetadata[arrayPathOf(raw.path, bands[0])]
1021
+ : undefined }));
1048
1022
  }
1049
1023
  if (levels.length === 0) {
1050
- throw new Error(`Variable "${variable}" not found in any level`);
1024
+ throw new Error(`No level found holding ${what}`);
1051
1025
  }
1052
- // The dimension layout is the same for all levels
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`.
1053
1028
  let meta = (_a = levels.find((level) => level.meta)) === null || _a === void 0 ? void 0 : _a.meta;
1054
- if (!meta) {
1055
- meta = await getArrayMeta(store, levels[0].arrayPath);
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);
1056
1031
  levels[0].meta = meta;
1057
1032
  }
1058
- if (!meta || !Array.isArray(meta.shape)) {
1033
+ const hasMeta = !!meta && Array.isArray(meta.shape);
1034
+ if (variable && !hasMeta) {
1059
1035
  throw new Error(`Could not read metadata for variable "${variable}"`);
1060
1036
  }
1061
- const ndim = meta.shape.length;
1062
- let dimensionNames = meta['dimension_names'];
1037
+ const ndim = hasMeta ? meta.shape.length : 2;
1038
+ let dimensionNames = hasMeta ? meta['dimension_names'] : undefined;
1063
1039
  if (!dimensionNames && ndim === 2) {
1064
1040
  dimensionNames = ['y', 'x'];
1065
1041
  }
1066
- if (!dimensionNames) {
1042
+ if (variable && !dimensionNames) {
1067
1043
  throw new Error(`Cannot determine dimension names for variable "${variable}"`);
1068
1044
  }
1069
- const { row, col } = getSpatialAxes(attributes['spatial:dimensions'], meta);
1045
+ const { row, col } = hasMeta
1046
+ ? getSpatialAxes(attributes['spatial:dimensions'], meta)
1047
+ : { row: 0, col: 1 };
1070
1048
  // Determine the extent and y axis orientation: declared metadata is
1071
1049
  // authoritative, otherwise both are inferred from the coordinate arrays.
1072
1050
  let extent = attributes['spatial:bbox'] || attributes['bounds'];
@@ -1080,6 +1058,12 @@ export default class GeoZarr extends DataTileSource {
1080
1058
  flipY = transform0[4] > 0;
1081
1059
  orientationKnown = true;
1082
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
+ }
1083
1067
  else {
1084
1068
  // All levels share the extent; the smallest level is cheapest to read
1085
1069
  let coordLevel = levels[0];
@@ -1129,7 +1113,7 @@ export default class GeoZarr extends DataTileSource {
1129
1113
  }
1130
1114
  const extentWidth = extent[2] - extent[0];
1131
1115
  const extentHeight = extent[3] - extent[1];
1132
- /** @type {Array<{path: string, arrayPath: string, meta: any, resolution: number, rowResolution: number, origin: import("ol/coordinate.js").Coordinate}>} */
1116
+ /** @type {Array<{path: string, bands: Array<string>, meta: any, shape: Array<number>|undefined, resolution: number, rowResolution: number, origin: import("ol/coordinate.js").Coordinate}>} */
1133
1117
  const configured = [];
1134
1118
  for (const level of levels) {
1135
1119
  let resolution;
@@ -1160,94 +1144,97 @@ export default class GeoZarr extends DataTileSource {
1160
1144
  }
1161
1145
  configured.push({
1162
1146
  path: level.path,
1163
- arrayPath: level.arrayPath,
1147
+ bands: level.bands,
1164
1148
  meta: level.meta,
1149
+ shape: level.shape,
1165
1150
  resolution,
1166
1151
  rowResolution,
1167
1152
  origin,
1168
1153
  });
1169
1154
  }
1170
1155
  if (configured.length === 0) {
1171
- throw new Error(`No usable level found for variable "${variable}"`);
1156
+ throw new Error(`No usable level found for ${what}`);
1172
1157
  }
1173
1158
  configured.sort((a, b) => b.resolution - a.resolution);
1174
1159
  if (!this.projection) {
1175
1160
  this.projection = this.inferProjection_(attributes, crsHint, extent);
1176
1161
  }
1177
- // Resolve the selector to per-axis indices; labels are resolved against
1178
- // the finest level's coordinate arrays
1179
- const finestPath = configured[configured.length - 1].path;
1180
- /** @type {Array<{axis: number, indices: Array<number>}>} */
1181
- const slots = [];
1182
- this.multiAxis_ = -1;
1183
- this.extraDimensions_ = [];
1184
- for (let axis = 0; axis < ndim; ++axis) {
1185
- if (axis === row || axis === col) {
1186
- continue;
1187
- }
1188
- const dimName = dimensionNames[axis];
1189
- this.extraDimensions_.push({ name: dimName, size: meta.shape[axis] });
1190
- // `dimensions` (also merged by updateDimensions) takes precedence
1191
- // over the initial `selector` for scalar values
1192
- let value = dimName in this.dimensions_
1193
- ? this.dimensions_[dimName]
1194
- : this.selector_[dimName];
1195
- if (value === undefined) {
1196
- warn(`No selector value given for dimension "${dimName}", using index 0.`);
1197
- value = 0;
1198
- }
1199
- /** @type {Array<number|string>} */
1200
- let values;
1201
- if (Array.isArray(value)) {
1202
- values = value;
1203
- this.multiAxis_ = axis;
1204
- }
1205
- else {
1206
- values = [value];
1207
- }
1208
- const indices = [];
1209
- for (const v of values) {
1210
- if (typeof v === 'number') {
1211
- indices.push(v);
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;
1212
1186
  }
1213
1187
  else {
1214
- indices.push(await this.resolveCoordinateLabel_(dimName, v, finestPath));
1188
+ values = [value];
1215
1189
  }
1216
- }
1217
- for (const index of indices) {
1218
- if (!Number.isInteger(index) ||
1219
- index < 0 ||
1220
- index >= meta.shape[axis]) {
1221
- throw new Error(`GeoZarr: invalid index ${index} for dimension "${dimName}" ` +
1222
- `(size ${meta.shape[axis]}).`);
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
+ }
1223
1198
  }
1224
- }
1225
- slots.push({ axis, indices });
1226
- }
1227
- // Materialize the datacube as the per-band state that loadTile_
1228
- // consumes: each rendered band is the variable array with fixed indices
1229
- // at the non-spatial axes.
1230
- const count = this.bandCount;
1231
- this.bands_ = new Array(count).fill(variable);
1232
- this.bandGroupIndex_ = new Array(count).fill(0);
1233
- this.bandSingleScaleResolution_ = new Array(count).fill(undefined);
1234
- this.bandSpatialAxes_ = new Array(count).fill({ row, col });
1235
- this.bandExtraSelection_ = new Array(count)
1236
- .fill(undefined)
1237
- .map((_, bandIndex) => {
1238
- if (slots.length === 0) {
1239
- return undefined;
1240
- }
1241
- const selection = new Array(ndim).fill(null);
1242
- for (const slot of slots) {
1243
- selection[slot.axis] =
1244
- slot.axis === this.multiAxis_
1245
- ? slot.indices[bandIndex]
1246
- : slot.indices[0];
1247
- }
1248
- return selection;
1249
- });
1250
- this.fillValue_ = parseFillValue(meta['fill_value']);
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
+ }
1251
1238
  // Align tiles to shards or chunks: every tile decodes all chunks it
1252
1239
  // touches, so tiles much smaller than a chunk would decode the same
1253
1240
  // chunk over and over.
@@ -1263,8 +1250,11 @@ export default class GeoZarr extends DataTileSource {
1263
1250
  getTileSizeForShard(shardInfo.shardShape[0], shardInfo.innerChunkShape[0]),
1264
1251
  ]);
1265
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.
1266
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;
1267
- if (Array.isArray(chunkShape)) {
1256
+ if (Array.isArray(chunkShape) &&
1257
+ Math.max(chunkShape[row], chunkShape[col]) > DEFAULT_TILE_SIZE) {
1268
1258
  return /** @type {import("ol/size.js").Size} */ ([
1269
1259
  getTileSizeForChunk(chunkShape[col], level.meta.shape[col]),
1270
1260
  getTileSizeForChunk(chunkShape[row], level.meta.shape[row]),
@@ -1279,19 +1269,25 @@ export default class GeoZarr extends DataTileSource {
1279
1269
  const matrixIds = configured.map((level, i) => level.path || String(i));
1280
1270
  for (let i = 0; i < configured.length; ++i) {
1281
1271
  const level = configured[i];
1282
- this.bandsByLevel_[matrixIds[i]] = this.bands_;
1272
+ this.bandsByLevel_[matrixIds[i]] = level.bands;
1283
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
+ }
1284
1281
  this.levelRowInfo_[matrixIds[i]] = {
1285
1282
  rowResolution: level.rowResolution,
1286
- shapeY: level.meta && Array.isArray(level.meta.shape)
1287
- ? level.meta.shape[row]
1288
- : undefined,
1283
+ shapeY,
1289
1284
  flip: flipY,
1290
1285
  };
1291
1286
  }
1292
1287
  this.tileGrid = new WMTSTileGrid(Object.assign({ extent: extent, origins: configured.map((level) => level.origin), resolutions: configured.map((level) => level.resolution), matrixIds: matrixIds }, (hasTileSizes
1293
1288
  ? { tileSizes: tileSizes.map((s) => s || [256, 256]) }
1294
1289
  : {})));
1290
+ return hasTileSizes;
1295
1291
  }
1296
1292
  /**
1297
1293
  * Determine the projection from the store metadata: the proj: convention,
@@ -1371,13 +1367,12 @@ export default class GeoZarr extends DataTileSource {
1371
1367
  * coordinate array.
1372
1368
  * @param {string} dimName The dimension name.
1373
1369
  * @param {string} label The label to resolve.
1374
- * @param {string} [levelPath] The level group path ('' for the root).
1370
+ * @param {string} path The coordinate array path, relative to the group.
1375
1371
  * @return {Promise<number>} The index of the label.
1376
1372
  * @private
1377
1373
  */
1378
- async resolveCoordinateLabel_(dimName, label, levelPath = '') {
1374
+ async resolveCoordinateLabel_(dimName, label, path) {
1379
1375
  try {
1380
- const path = levelPath ? `${levelPath}/${dimName}` : dimName;
1381
1376
  const array = await this.openArray_(0, path);
1382
1377
  const chunk = await get(array, [null]);
1383
1378
  const values = Array.from(chunk.data, (v) => String(v));
@@ -1469,9 +1464,6 @@ function createCachedStore(store, groupBytes, consolidatedMetadata) {
1469
1464
  * @typedef {Object} TileGridInfo
1470
1465
  * @property {WMTSTileGrid} tileGrid The tile grid.
1471
1466
  * @property {import("ol/proj/Projection.js").default} projection The projection.
1472
- * @property {Object<string, Array<string>>} [bandsByLevel] Available bands by level.
1473
- * @property {number} [fillValue] The fill value.
1474
- * @property {Array<import("ol/size.js").Size>|undefined} [tileSizes] The tile sizes for each level, if available.
1475
1467
  */
1476
1468
  /**
1477
1469
  * Maximum tile size for rendering.
@@ -1488,6 +1480,15 @@ const MIN_TILE_SIZE = 64;
1488
1480
  * @property {Array<number>} shardShape The shard (outer chunk) shape [rows, cols].
1489
1481
  * @property {Array<number>} innerChunkShape The inner chunk shape [rows, cols].
1490
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
+ }
1491
1492
  /**
1492
1493
  * Locate the row (y) and column (x) axis positions of an array by matching the
1493
1494
  * group's `spatial:dimensions` names (`[y, x]`) against the array's
@@ -1583,78 +1584,6 @@ function getTileSizeForShard(shardSize, innerChunkSize) {
1583
1584
  }
1584
1585
  return Math.max(maxChunks * innerChunkSize, MIN_TILE_SIZE);
1585
1586
  }
1586
- /**
1587
- * @param {DatasetAttributes} attributes The dataset attributes.
1588
- * @param {Object<string, *>|null|undefined} consolidatedMetadata The consolidated metadata.
1589
- * @param {Array<string>} wantedBands The wanted bands.
1590
- * @return {TileGridInfo} The tile grid info.
1591
- */
1592
- function getTileGridInfoFromAttributes(attributes, consolidatedMetadata, wantedBands) {
1593
- const multiscales = attributes.multiscales;
1594
- const extent = attributes['spatial:bbox'];
1595
- const projection = getProjectionFromAttributes(attributes);
1596
- const extentWidth = extent[2] - extent[0];
1597
- const origin = [extent[0], extent[3]];
1598
- /** @type {Array<{matrixId: string, resolution: number, origin: import("ol/coordinate.js").Coordinate, tileSize: import("ol/size.js").Size|undefined}>} */
1599
- const groupInfo = [];
1600
- /** @type {Object<string, Array<string>>|undefined} */
1601
- const bandsByLevel = consolidatedMetadata ? {} : undefined;
1602
- let fillValue;
1603
- for (const groupMetadata of multiscales.layout) {
1604
- const matrixId = /** @type {string} */ (groupMetadata['asset']);
1605
- const spatialShape = /** @type {Array<number>} */ (groupMetadata['spatial:shape']);
1606
- const resolution = extentWidth / spatialShape[1];
1607
- /** @type {import("ol/size.js").Size|undefined} */
1608
- let tileSize;
1609
- if (consolidatedMetadata && bandsByLevel) {
1610
- const availableBands = [];
1611
- for (const band of wantedBands) {
1612
- const bandArray = /** @type {Object<string, *>|undefined} */ (consolidatedMetadata[`${matrixId}/${band}`]);
1613
- if (bandArray) {
1614
- availableBands.push(band);
1615
- if (fillValue === undefined) {
1616
- fillValue = Number(bandArray['fill_value']);
1617
- }
1618
- //FIXME Remove this when GeoZarr datasets provide correct TileMatrixSet info or similar
1619
- if (!tileSize) {
1620
- const { row, col } = getSpatialAxes(attributes['spatial:dimensions'], bandArray);
1621
- const shardInfo = getShardInfo(bandArray, row, col);
1622
- if (shardInfo) {
1623
- tileSize = [
1624
- getTileSizeForShard(shardInfo.shardShape[1], shardInfo.innerChunkShape[1]),
1625
- getTileSizeForShard(shardInfo.shardShape[0], shardInfo.innerChunkShape[0]),
1626
- ];
1627
- }
1628
- }
1629
- }
1630
- }
1631
- bandsByLevel[matrixId] = availableBands;
1632
- }
1633
- groupInfo.push({
1634
- matrixId,
1635
- resolution,
1636
- origin,
1637
- tileSize,
1638
- });
1639
- }
1640
- groupInfo.sort((a, b) => b.resolution - a.resolution);
1641
- const tileSizes = groupInfo.map((g) => g.tileSize);
1642
- const hasTileSizes = tileSizes.some((s) => s !== undefined);
1643
- const tileGrid = new WMTSTileGrid(Object.assign({ extent: extent, origins: groupInfo.map((g) => g.origin), resolutions: groupInfo.map((g) => g.resolution), matrixIds: groupInfo.map((g) => g.matrixId) }, (hasTileSizes
1644
- ? {
1645
- tileSizes: /** @type {Array<import("ol/size.js").Size>} */ (tileSizes.map((s) => s || [256, 256])),
1646
- }
1647
- : {})));
1648
- return {
1649
- tileGrid,
1650
- projection,
1651
- bandsByLevel,
1652
- fillValue,
1653
- tileSizes: hasTileSizes
1654
- ? /** @type {Array<import("ol/size.js").Size>} */ (tileSizes.map((s) => s || [256, 256]))
1655
- : undefined,
1656
- };
1657
- }
1658
1587
  /**
1659
1588
  * @param {LegacyDatasetAttributes} attributes The dataset attributes.
1660
1589
  * @return {TileGridInfo} The tile grid info.
@@ -1932,46 +1861,57 @@ function normalizeV2Metadata(v2Metadata) {
1932
1861
  return normalized;
1933
1862
  }
1934
1863
  /**
1935
- * Probe the store for an optional metadata document. Absence is an expected
1936
- * outcome (version probing), and some servers answer 403 instead of 404 for
1937
- * missing keys, which zarrita treats as an error.
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.
1938
1867
  * @param {FetchStore} store The store.
1939
1868
  * @param {string} key The key to probe.
1940
1869
  * @return {Promise<Uint8Array|undefined>} The document, if present.
1941
1870
  */
1942
1871
  function probe(store, key) {
1943
- return store.get(/** @type {`/${string}`} */ (key)).catch(() => undefined);
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);
1944
1883
  }
1945
1884
  /**
1946
1885
  * Read a single array's metadata directly from the store, for stores
1947
1886
  * without consolidated metadata (Zarr v3 zarr.json or v2 .zarray/.zattrs).
1948
1887
  * @param {FetchStore} store The store.
1949
1888
  * @param {string} path The array path.
1889
+ * @param {boolean} v2 Whether the store is Zarr v2.
1950
1890
  * @return {Promise<Object|undefined>} The array metadata (v3 shape).
1951
1891
  */
1952
- async function getArrayMeta(store, path) {
1892
+ async function getArrayMeta(store, path, v2) {
1953
1893
  const decoder = new TextDecoder();
1954
- let bytes = await probe(store, `/${path}/zarr.json`);
1955
- if (bytes) {
1956
- return JSON.parse(decoder.decode(bytes));
1894
+ if (!v2) {
1895
+ const bytes = await probe(store, `/${path}/zarr.json`);
1896
+ return bytes ? JSON.parse(decoder.decode(bytes)) : undefined;
1957
1897
  }
1958
- bytes = await probe(store, `/${path}/.zarray`);
1959
- if (bytes) {
1960
- const zarray = JSON.parse(decoder.decode(bytes));
1961
- const attrBytes = await probe(store, `/${path}/.zattrs`);
1962
- const attributes = attrBytes ? JSON.parse(decoder.decode(attrBytes)) : {};
1963
- return {
1964
- shape: zarray['shape'],
1965
- fill_value: zarray['fill_value'],
1966
- dimension_names: attributes['_ARRAY_DIMENSIONS'],
1967
- chunk_grid: {
1968
- name: 'regular',
1969
- configuration: { chunk_shape: zarray['chunks'] },
1970
- },
1971
- attributes,
1972
- };
1898
+ const bytes = await probe(store, `/${path}/.zarray`);
1899
+ if (!bytes) {
1900
+ return undefined;
1973
1901
  }
1974
- return undefined;
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
+ };
1975
1915
  }
1976
1916
  /**
1977
1917
  * Flip the row order of a 2-dimensional chunk (for south-up data).