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