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