ol-stac 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/http.d.ts +35 -0
- package/http.d.ts.map +1 -0
- package/http.js +101 -0
- package/http.js.map +1 -0
- package/layer/STAC.d.ts +224 -17
- package/layer/STAC.d.ts.map +1 -1
- package/layer/STAC.js +407 -70
- package/layer/STAC.js.map +1 -1
- package/layer/type.d.ts +88 -0
- package/layer/type.d.ts.map +1 -0
- package/layer/type.js +83 -0
- package/layer/type.js.map +1 -0
- package/package.json +3 -2
- package/proj.d.ts.map +1 -1
- package/proj.js +51 -5
- package/proj.js.map +1 -1
- package/shim/ogcTileUtil.d.ts +11 -0
- package/shim/ogcTileUtil.d.ts.map +1 -0
- package/shim/ogcTileUtil.js +20 -0
- package/shim/ogcTileUtil.js.map +1 -0
- package/shim/proj4.d.ts +9 -0
- package/shim/proj4.d.ts.map +1 -0
- package/shim/proj4.js +28 -0
- package/shim/proj4.js.map +1 -0
- package/source/GeoZarr.d.ts +601 -0
- package/source/GeoZarr.d.ts.map +1 -0
- package/source/GeoZarr.js +1990 -0
- package/source/GeoZarr.js.map +1 -0
- package/source/type.d.ts +20 -3
- package/source/type.d.ts.map +1 -1
- package/source/type.js +19 -2
- package/source/type.js.map +1 -1
- package/util.d.ts +164 -1
- package/util.d.ts.map +1 -1
- package/util.js +856 -44
- package/util.js.map +1 -1
package/util.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @module ol/util
|
|
3
3
|
*/
|
|
4
4
|
import VectorLayer from 'ol/layer/Vector.js';
|
|
5
|
+
import { transformExtent } from 'ol/proj.js';
|
|
5
6
|
import Circle from 'ol/style/Circle.js';
|
|
6
7
|
import Fill from 'ol/style/Fill.js';
|
|
7
8
|
import Stroke from 'ol/style/Stroke.js';
|
|
@@ -33,6 +34,53 @@ import { isObject } from 'stac-js/src/utils.js';
|
|
|
33
34
|
* @type {string}
|
|
34
35
|
*/
|
|
35
36
|
export const LABEL_EXTENSION = 'https://stac-extensions.github.io/label/v1.*/schema.json';
|
|
37
|
+
/**
|
|
38
|
+
* Makes a bounding box continuous for use as an (OpenLayers) extent.
|
|
39
|
+
*
|
|
40
|
+
* Bounding boxes that cross the antimeridian have a western longitude that is
|
|
41
|
+
* larger than the eastern longitude (as defined by RFC 7946, section 5.2).
|
|
42
|
+
* For those, the eastern longitude is shifted by +360 so that the extent is
|
|
43
|
+
* continuous across the antimeridian (i.e. `minX <= maxX`).
|
|
44
|
+
*
|
|
45
|
+
* Accepts both 2D (four values) and 3D (six values) bounding boxes and always
|
|
46
|
+
* returns a 2D extent (four values).
|
|
47
|
+
*
|
|
48
|
+
* @param {Array<number>} bbox The bounding box in lon/lat degrees.
|
|
49
|
+
* @return {Array<number>} The continuous 2D bounding box.
|
|
50
|
+
* @api
|
|
51
|
+
*/
|
|
52
|
+
export function toContinuousBBox(bbox) {
|
|
53
|
+
// STAC bounding boxes may contain a third dimension, i.e. six values
|
|
54
|
+
// (west, south, minZ, east, north, maxZ). Extract the horizontal 2D extent.
|
|
55
|
+
const hasZ = bbox.length >= 6;
|
|
56
|
+
const west = bbox[0];
|
|
57
|
+
const south = bbox[1];
|
|
58
|
+
const east = bbox[hasZ ? 3 : 2];
|
|
59
|
+
const north = bbox[hasZ ? 4 : 3];
|
|
60
|
+
if (west > east) {
|
|
61
|
+
return [west, south, east + 360, north];
|
|
62
|
+
}
|
|
63
|
+
return [west, south, east, north];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Converts a lon/lat (EPSG:4326) bounding box into a continuous OpenLayers
|
|
67
|
+
* extent in the given projection.
|
|
68
|
+
*
|
|
69
|
+
* Handles antimeridian-crossing bounding boxes (west > east), see
|
|
70
|
+
* {@link toContinuousBBox}.
|
|
71
|
+
*
|
|
72
|
+
* When fitting an antimeridian-crossing extent, configure the OpenLayers
|
|
73
|
+
* `View` with `multiWorld: true`; otherwise the default world constraint may
|
|
74
|
+
* clamp the fitted view and clip the wrapped portion.
|
|
75
|
+
*
|
|
76
|
+
* @param {Array<number>} bbox The bounding box in lon/lat degrees (EPSG:4326).
|
|
77
|
+
* @param {import("ol/proj.js").ProjectionLike} projection The target projection.
|
|
78
|
+
* @return {Array<number>} The extent in the target projection.
|
|
79
|
+
* @api
|
|
80
|
+
*/
|
|
81
|
+
export function toOlExtent(bbox, projection) {
|
|
82
|
+
return transformExtent(toContinuousBBox(bbox), 'EPSG:4326', projection);
|
|
83
|
+
}
|
|
36
84
|
const transparentFill = new Fill({ color: 'rgba(0,0,0,0)' });
|
|
37
85
|
/**
|
|
38
86
|
* Check whether the installed OL version is at least the given version.
|
|
@@ -135,6 +183,73 @@ export async function getStacObjectsForEvent(event, exclude = null, selectedFeat
|
|
|
135
183
|
});
|
|
136
184
|
return [...objects];
|
|
137
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Determines the value range for a visualization stretch from STAC
|
|
188
|
+
* statistics: mean ± 2σ (~95% of the values), clamped to the declared
|
|
189
|
+
* minimum/maximum.
|
|
190
|
+
* @param {Object} stats The statistics (e.g. from stac-js `getStatistics()`).
|
|
191
|
+
* @return {{minimum: (number|undefined), maximum: (number|undefined)}} The stretch range.
|
|
192
|
+
*/
|
|
193
|
+
function getStatisticsStretch(stats) {
|
|
194
|
+
let minimum;
|
|
195
|
+
let maximum;
|
|
196
|
+
if (isObject(stats)) {
|
|
197
|
+
({ minimum, maximum } = stats);
|
|
198
|
+
const { mean, stddev } = stats;
|
|
199
|
+
if (typeof mean === 'number' && typeof stddev === 'number' && stddev > 0) {
|
|
200
|
+
const stretchMin = mean - 2 * stddev;
|
|
201
|
+
const stretchMax = mean + 2 * stddev;
|
|
202
|
+
minimum =
|
|
203
|
+
typeof minimum === 'number'
|
|
204
|
+
? Math.max(minimum, stretchMin)
|
|
205
|
+
: stretchMin;
|
|
206
|
+
maximum =
|
|
207
|
+
typeof maximum === 'number'
|
|
208
|
+
? Math.min(maximum, stretchMax)
|
|
209
|
+
: stretchMax;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { minimum, maximum };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Determines the value range for a visualization stretch from the STAC
|
|
216
|
+
* statistics of a band or asset, if complete.
|
|
217
|
+
* @param {Asset|Band} source The band or asset to read the statistics from.
|
|
218
|
+
* @return {Array<number>|null} The [min, max] range, or `null`.
|
|
219
|
+
*/
|
|
220
|
+
function getStatisticsRange(source) {
|
|
221
|
+
const { minimum, maximum } = getStatisticsStretch(source.getStatistics());
|
|
222
|
+
if (typeof minimum === 'number' && typeof maximum === 'number') {
|
|
223
|
+
return [minimum, maximum];
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Parses a nodata value from render extension metadata, which may be a
|
|
229
|
+
* number or a string (`nan`, `inf` and `-inf`, as titiler accepts them).
|
|
230
|
+
* @param {*} value The nodata value.
|
|
231
|
+
* @return {number|undefined} The nodata value, or `undefined`.
|
|
232
|
+
*/
|
|
233
|
+
function parseNoDataValue(value) {
|
|
234
|
+
if (typeof value === 'number') {
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
238
|
+
switch (value.trim().toLowerCase()) {
|
|
239
|
+
case 'nan':
|
|
240
|
+
return NaN;
|
|
241
|
+
case 'inf':
|
|
242
|
+
return Infinity;
|
|
243
|
+
case '-inf':
|
|
244
|
+
return -Infinity;
|
|
245
|
+
default: {
|
|
246
|
+
const parsed = Number(value);
|
|
247
|
+
return isNaN(parsed) ? undefined : parsed;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
138
253
|
/**
|
|
139
254
|
* Get the source info for the GeoTiff from the asset.
|
|
140
255
|
* @param {import('stac-js').Asset} asset The asset to read the information from.
|
|
@@ -157,22 +272,7 @@ export function getGeoTiffSourceInfoFromAsset(asset, selectedBands) {
|
|
|
157
272
|
const nodataValues = new Array(bandCount).fill(undefined);
|
|
158
273
|
let index = 0;
|
|
159
274
|
for (const source of sources) {
|
|
160
|
-
const
|
|
161
|
-
let { minimum, maximum } = stats;
|
|
162
|
-
const { mean, stddev } = stats;
|
|
163
|
-
// Use mean ± 2σ for a better visualization stretch (~95% of values)
|
|
164
|
-
if (typeof mean === 'number' && typeof stddev === 'number' && stddev > 0) {
|
|
165
|
-
const stretchMin = mean - 2 * stddev;
|
|
166
|
-
const stretchMax = mean + 2 * stddev;
|
|
167
|
-
minimum =
|
|
168
|
-
typeof minimum === 'number'
|
|
169
|
-
? Math.max(minimum, stretchMin)
|
|
170
|
-
: stretchMin;
|
|
171
|
-
maximum =
|
|
172
|
-
typeof maximum === 'number'
|
|
173
|
-
? Math.min(maximum, stretchMax)
|
|
174
|
-
: stretchMax;
|
|
175
|
-
}
|
|
275
|
+
const { minimum, maximum } = getStatisticsStretch(source.getStatistics());
|
|
176
276
|
if (typeof minimum === 'number') {
|
|
177
277
|
minValues[index] = minimum;
|
|
178
278
|
}
|
|
@@ -188,16 +288,27 @@ export function getGeoTiffSourceInfoFromAsset(asset, selectedBands) {
|
|
|
188
288
|
}
|
|
189
289
|
index++;
|
|
190
290
|
}
|
|
291
|
+
const render = getRenderForAsset(asset);
|
|
292
|
+
const usableRender = isUsableRender(render);
|
|
293
|
+
// A usable render defines the complete visualization, so its value range
|
|
294
|
+
// takes precedence over the statistics of the default visualization
|
|
295
|
+
const rescale = usableRender && Array.isArray(render.rescale) ? render.rescale : [];
|
|
191
296
|
const defined = (v) => v !== undefined;
|
|
192
|
-
if (
|
|
193
|
-
sourceInfo.min =
|
|
194
|
-
|
|
195
|
-
: Math.min(...minValues.filter(defined));
|
|
297
|
+
if (Array.isArray(rescale[0]) && rescale[0].length >= 2) {
|
|
298
|
+
sourceInfo.min = rescale[0][0];
|
|
299
|
+
sourceInfo.max = rescale[0][1];
|
|
196
300
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
301
|
+
else {
|
|
302
|
+
if (minValues.some(defined)) {
|
|
303
|
+
sourceInfo.min = perBand
|
|
304
|
+
? minValues
|
|
305
|
+
: Math.min(...minValues.filter(defined));
|
|
306
|
+
}
|
|
307
|
+
if (maxValues.some(defined)) {
|
|
308
|
+
sourceInfo.max = perBand
|
|
309
|
+
? maxValues
|
|
310
|
+
: Math.max(...maxValues.filter(defined));
|
|
311
|
+
}
|
|
201
312
|
}
|
|
202
313
|
if (nodataValues.some(defined)) {
|
|
203
314
|
if (perBand) {
|
|
@@ -210,6 +321,13 @@ export function getGeoTiffSourceInfoFromAsset(asset, selectedBands) {
|
|
|
210
321
|
}
|
|
211
322
|
}
|
|
212
323
|
}
|
|
324
|
+
else if (usableRender) {
|
|
325
|
+
// As a last resort, use the nodata value from the render extension
|
|
326
|
+
const nodata = parseNoDataValue(render.nodata);
|
|
327
|
+
if (nodata !== undefined) {
|
|
328
|
+
sourceInfo.nodata = nodata;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
213
331
|
if (selectedBands.length > 0) {
|
|
214
332
|
sourceInfo.bands = selectedBands
|
|
215
333
|
.map((band) => {
|
|
@@ -227,13 +345,26 @@ export function getGeoTiffSourceInfoFromAsset(asset, selectedBands) {
|
|
|
227
345
|
.filter((band) => band !== null);
|
|
228
346
|
}
|
|
229
347
|
else {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
348
|
+
// A usable render defines the complete visualization, including the
|
|
349
|
+
// bands to show (paired with its value ranges)
|
|
350
|
+
if (usableRender && Array.isArray(render.bands)) {
|
|
351
|
+
const indices = render.bands
|
|
352
|
+
.map((name) => asset.findBand(name))
|
|
353
|
+
.filter(isObject)
|
|
354
|
+
.map((band) => band.getIndex() + 1);
|
|
355
|
+
if (indices.length > 0) {
|
|
356
|
+
sourceInfo.bands = indices;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!sourceInfo.bands) {
|
|
360
|
+
const visualBands = asset.findVisualBands();
|
|
361
|
+
if (visualBands) {
|
|
362
|
+
sourceInfo.bands = [
|
|
363
|
+
visualBands.red.getIndex() + 1,
|
|
364
|
+
visualBands.green.getIndex() + 1,
|
|
365
|
+
visualBands.blue.getIndex() + 1,
|
|
366
|
+
];
|
|
367
|
+
}
|
|
237
368
|
}
|
|
238
369
|
}
|
|
239
370
|
return sourceInfo;
|
|
@@ -257,6 +388,13 @@ export function getBoundsStyle(originalStyle, layerGroup) {
|
|
|
257
388
|
/**
|
|
258
389
|
* Parse the GeoZarr source options from an asset.
|
|
259
390
|
*
|
|
391
|
+
* If the asset (or its containing Item/Collection) describes the store
|
|
392
|
+
* through the datacube extension (`cube:variables` and `cube:dimensions`),
|
|
393
|
+
* the store is treated as an n-dimensional datacube: the data variable and a
|
|
394
|
+
* selector for its non-spatial dimensions are derived from the metadata.
|
|
395
|
+
* Otherwise, each band is expected to be a separate array in the store,
|
|
396
|
+
* addressed by the band names from the STAC `bands` field.
|
|
397
|
+
*
|
|
260
398
|
* @param {Asset} asset The asset to read the information from.
|
|
261
399
|
* @param {Array<number|string>} selectedBands The bands to show. One-based index of the band, or the name of the band.
|
|
262
400
|
* @return {Object} The GeoZarr source options
|
|
@@ -266,6 +404,38 @@ export function getGeoZarrSourceOptionsFromAsset(asset, selectedBands) {
|
|
|
266
404
|
const options = {
|
|
267
405
|
url: asset.getAbsoluteUrl(),
|
|
268
406
|
};
|
|
407
|
+
const cube = getDatacubeRenderingInfo(asset);
|
|
408
|
+
if (cube) {
|
|
409
|
+
options.variable = cube.variable;
|
|
410
|
+
options.selector = {};
|
|
411
|
+
if (cube.bandDimension) {
|
|
412
|
+
const indices = getDatacubeBandIndices(cube.bandDimension.values, selectedBands, asset);
|
|
413
|
+
if (indices.length > 0) {
|
|
414
|
+
options.selector[cube.bandDimension.name] = indices;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
for (const dimension of cube.extraDimensions) {
|
|
418
|
+
options.selector[dimension.name] = dimension.defaultIndex;
|
|
419
|
+
}
|
|
420
|
+
if (cube.extent) {
|
|
421
|
+
options.extent = cube.extent;
|
|
422
|
+
}
|
|
423
|
+
const projBBox = asset.getMetadata('proj:bbox');
|
|
424
|
+
if (Array.isArray(projBBox) && projBBox.length >= 4) {
|
|
425
|
+
// proj:bbox may be 3D (xmin, ymin, zmin, xmax, ymax, zmax)
|
|
426
|
+
options.extent =
|
|
427
|
+
projBBox.length >= 6
|
|
428
|
+
? [projBBox[0], projBBox[1], projBBox[3], projBBox[4]]
|
|
429
|
+
: projBBox.slice(0, 4);
|
|
430
|
+
}
|
|
431
|
+
const projTransform = asset.getMetadata('proj:transform');
|
|
432
|
+
if (Array.isArray(projTransform) &&
|
|
433
|
+
projTransform.length >= 6 &&
|
|
434
|
+
projTransform[4] > 0) {
|
|
435
|
+
options.flipY = true;
|
|
436
|
+
}
|
|
437
|
+
return options;
|
|
438
|
+
}
|
|
269
439
|
if (selectedBands.length > 0) {
|
|
270
440
|
options.bands = selectedBands
|
|
271
441
|
.map((band) => {
|
|
@@ -277,24 +447,581 @@ export function getGeoZarrSourceOptionsFromAsset(asset, selectedBands) {
|
|
|
277
447
|
if (isObject(bandObj) && typeof bandObj.name === 'string') {
|
|
278
448
|
return bandObj.name;
|
|
279
449
|
}
|
|
280
|
-
// eslint-disable-next-line no-console
|
|
281
|
-
console.error(`Band with index ${band} not found in asset ${asset.getKey()}`);
|
|
282
450
|
return null;
|
|
283
451
|
})
|
|
284
452
|
.filter(Boolean);
|
|
285
453
|
}
|
|
286
454
|
else {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
455
|
+
// A usable render defines the complete visualization, including the
|
|
456
|
+
// bands to show (paired with its value ranges)
|
|
457
|
+
const render = getRenderForAsset(asset);
|
|
458
|
+
if (isUsableRender(render) &&
|
|
459
|
+
Array.isArray(render.bands) &&
|
|
460
|
+
render.bands.length > 0) {
|
|
461
|
+
options.bands = render.bands;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
const bands = asset.findVisualBands();
|
|
465
|
+
if (bands) {
|
|
466
|
+
options.bands = [
|
|
467
|
+
bands.red.name,
|
|
468
|
+
bands.green.name,
|
|
469
|
+
bands.blue.name,
|
|
470
|
+
].filter(Boolean);
|
|
471
|
+
}
|
|
294
472
|
}
|
|
295
473
|
}
|
|
474
|
+
if (!Array.isArray(options.bands)) {
|
|
475
|
+
options.bands = [];
|
|
476
|
+
}
|
|
296
477
|
return options;
|
|
297
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* The default maximum number of pixels of the coarsest resolution level for
|
|
481
|
+
* an asset to be shown. Showing the full extent of an asset loads every
|
|
482
|
+
* tile of the coarsest level, so files without (sufficient) overviews
|
|
483
|
+
* would trigger excessive tile loads.
|
|
484
|
+
* @type {number}
|
|
485
|
+
*/
|
|
486
|
+
export const MAX_DEFAULT_DISPLAY_PIXELS = 16 * 1024 * 1024;
|
|
487
|
+
/**
|
|
488
|
+
* Returns the number of pixels of the coarsest resolution level of a
|
|
489
|
+
* configured tile source (e.g. GeoTIFF or GeoZarr). This is the amount of
|
|
490
|
+
* data that displaying the full extent of the asset requires to load.
|
|
491
|
+
* @param {import('ol/source/Tile.js').default} source The configured (ready) source.
|
|
492
|
+
* @return {number} The number of pixels, or `Infinity` if unknown.
|
|
493
|
+
* @api
|
|
494
|
+
*/
|
|
495
|
+
export function getDisplayPixels(source) {
|
|
496
|
+
const tileGrid = source.getTileGrid();
|
|
497
|
+
if (!tileGrid) {
|
|
498
|
+
return Infinity;
|
|
499
|
+
}
|
|
500
|
+
const extent = tileGrid.getExtent();
|
|
501
|
+
const resolution = tileGrid.getResolutions()[0];
|
|
502
|
+
if (!extent || !(resolution > 0)) {
|
|
503
|
+
return Infinity;
|
|
504
|
+
}
|
|
505
|
+
let rowResolution = resolution;
|
|
506
|
+
// GeoZarr pixels are not necessarily square (e.g. a square array covering
|
|
507
|
+
// the full EPSG:4326 world); use the row resolution the source tracks for
|
|
508
|
+
// the coarsest level, if available
|
|
509
|
+
const zarrSource = /** @type {*} */ (source);
|
|
510
|
+
const wmtsTileGrid = /** @type {*} */ (tileGrid);
|
|
511
|
+
if (zarrSource.levelRowInfo_ &&
|
|
512
|
+
typeof wmtsTileGrid.getMatrixIds === 'function') {
|
|
513
|
+
const rowInfo = zarrSource.levelRowInfo_[wmtsTileGrid.getMatrixIds()[0]];
|
|
514
|
+
if (rowInfo && rowInfo.rowResolution > 0) {
|
|
515
|
+
rowResolution = rowInfo.rowResolution;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return (((extent[2] - extent[0]) / resolution) *
|
|
519
|
+
((extent[3] - extent[1]) / rowResolution));
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Checks whether displaying the full extent of a configured tile source at
|
|
523
|
+
* the coarsest available resolution requires excessive tile loads.
|
|
524
|
+
* @param {import('ol/source/Tile.js').default} source The configured (ready) source.
|
|
525
|
+
* @param {number} [maxPixels] The pixel limit for the coarsest resolution level.
|
|
526
|
+
* @return {boolean} `true` if the source exceeds the limit.
|
|
527
|
+
* @api
|
|
528
|
+
*/
|
|
529
|
+
export function exceedsDisplayLimit(source, maxPixels = MAX_DEFAULT_DISPLAY_PIXELS) {
|
|
530
|
+
return getDisplayPixels(source) > maxPixels;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Returns the render (from the render extension's `renders` field) that
|
|
534
|
+
* applies to the given asset: the first render that lists the asset's key
|
|
535
|
+
* in its `assets` field, or the first render without an `assets` field.
|
|
536
|
+
*
|
|
537
|
+
* @param {Asset} asset The asset to find the render for.
|
|
538
|
+
* @return {Object|null} The render object, or `null`.
|
|
539
|
+
* @api
|
|
540
|
+
*/
|
|
541
|
+
export function getRenderForAsset(asset) {
|
|
542
|
+
const renders = asset.getMetadata('renders');
|
|
543
|
+
if (!isObject(renders)) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const key = asset.getKey();
|
|
547
|
+
let fallback = null;
|
|
548
|
+
for (const name in renders) {
|
|
549
|
+
const render = renders[name];
|
|
550
|
+
if (!isObject(render)) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (Array.isArray(render.assets)) {
|
|
554
|
+
if (render.assets.includes(key)) {
|
|
555
|
+
return render;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else if (!fallback) {
|
|
559
|
+
fallback = render;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return fallback;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Checks whether a render (from the render extension) can be implemented:
|
|
566
|
+
* a render defines a complete visualization, so it is only used when it
|
|
567
|
+
* defines at least one aspect that is supported (`bands`, `rescale`,
|
|
568
|
+
* `colormap`, `nodata`) and none that can't be implemented reliably
|
|
569
|
+
* (band math expressions, color formulas, and named colormaps, which are
|
|
570
|
+
* not interoperable). Otherwise the default visualization derived from the
|
|
571
|
+
* general STAC metadata applies.
|
|
572
|
+
* @param {Object|null} render The render object.
|
|
573
|
+
* @return {boolean} `true` if the render can be used.
|
|
574
|
+
*/
|
|
575
|
+
function isUsableRender(render) {
|
|
576
|
+
if (!isObject(render)) {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
if (typeof render.expression === 'string' ||
|
|
580
|
+
typeof render['color_formula'] === 'string' ||
|
|
581
|
+
typeof render['colormap_name'] === 'string') {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
return Boolean((Array.isArray(render.bands) && render.bands.length > 0) ||
|
|
585
|
+
(Array.isArray(render.rescale) && render.rescale.length > 0) ||
|
|
586
|
+
isObject(render['colormap']) ||
|
|
587
|
+
Array.isArray(render['colormap']) ||
|
|
588
|
+
render.nodata !== undefined);
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Checks whether a band or asset declares a floating point data type.
|
|
592
|
+
* Floating point data can be assumed to be continuous (not categorical).
|
|
593
|
+
* @param {Asset|Band} source The band or asset to read the data type from.
|
|
594
|
+
* @return {boolean} `true` if the data type is declared as floating point.
|
|
595
|
+
*/
|
|
596
|
+
function isFloatingPoint(source) {
|
|
597
|
+
const type = source.getMetadata('data_type');
|
|
598
|
+
return typeof type === 'string' && type.includes('float');
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Creates a linear stretch expression that maps the given value range of a
|
|
602
|
+
* band to 0-255.
|
|
603
|
+
* @param {number} band The one-based rendered band.
|
|
604
|
+
* @param {Array<number>} range The [min, max] value range.
|
|
605
|
+
* @return {Array<*>} The expression.
|
|
606
|
+
*/
|
|
607
|
+
function stretch(band, range) {
|
|
608
|
+
return [
|
|
609
|
+
'interpolate',
|
|
610
|
+
['linear'],
|
|
611
|
+
['band', band],
|
|
612
|
+
range[0],
|
|
613
|
+
0,
|
|
614
|
+
range[1],
|
|
615
|
+
255,
|
|
616
|
+
];
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Creates a WebGLTileLayer style that colors the given value range of a
|
|
620
|
+
* band with the given colors, evenly distributed over the range.
|
|
621
|
+
* @param {Array<import('ol/color.js').Color|string>} colors The colors.
|
|
622
|
+
* @param {number} band The one-based rendered band.
|
|
623
|
+
* @param {Array<number>} range The [min, max] value range.
|
|
624
|
+
* @return {Object} A WebGLTileLayer style.
|
|
625
|
+
*/
|
|
626
|
+
function createGradientStyle(colors, band, range) {
|
|
627
|
+
/** @type {Array<*>} */
|
|
628
|
+
const color = ['interpolate', ['linear'], ['band', band]];
|
|
629
|
+
const last = colors.length - 1;
|
|
630
|
+
colors.forEach((stop, i) => {
|
|
631
|
+
color.push(range[0] + (i / last) * (range[1] - range[0]), stop);
|
|
632
|
+
});
|
|
633
|
+
return { color };
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Creates a WebGLTileLayer color expression from a render extension
|
|
637
|
+
* `colormap`, following the rio-tiler semantics that the render extension
|
|
638
|
+
* references (`colormap_name` is not supported, as the available names are
|
|
639
|
+
* not standardized):
|
|
640
|
+
* - An array of intervals `[[[min, max], color], ...]` colors the raw data
|
|
641
|
+
* values with `min <= value < max`; later intervals take precedence.
|
|
642
|
+
* - An object with exactly the integer keys 0-255 is a lookup table for the
|
|
643
|
+
* data after rescaling it to 0-255 (truncated, as rio-tiler casts to
|
|
644
|
+
* uint8).
|
|
645
|
+
* - Any other object colors the (rescaled) data values that exactly match a
|
|
646
|
+
* key.
|
|
647
|
+
* Unmatched values are transparent. Colors are `[r, g, b]` or
|
|
648
|
+
* `[r, g, b, a]` arrays (alpha in 0-255, as in rio-tiler).
|
|
649
|
+
* @param {Object|Array} colormap The colormap.
|
|
650
|
+
* @param {Array<number>|null} range The range to rescale the data to 0-255, if any.
|
|
651
|
+
* @return {Object|null} A WebGLTileLayer style, or `null`.
|
|
652
|
+
*/
|
|
653
|
+
function createColormapStyle(colormap, range) {
|
|
654
|
+
if (Array.isArray(colormap)) {
|
|
655
|
+
// rio-tiler applies intervals sequentially so that later intervals
|
|
656
|
+
// overwrite earlier ones; a case expression picks the first match
|
|
657
|
+
const cases = [];
|
|
658
|
+
for (const entry of colormap.slice().reverse()) {
|
|
659
|
+
if (!Array.isArray(entry) || !Array.isArray(entry[0])) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
cases.push([
|
|
663
|
+
'all',
|
|
664
|
+
['>=', ['band', 1], entry[0][0]],
|
|
665
|
+
['<', ['band', 1], entry[0][1]],
|
|
666
|
+
]);
|
|
667
|
+
cases.push(toColor(entry[1]));
|
|
668
|
+
}
|
|
669
|
+
if (cases.length > 0) {
|
|
670
|
+
return { color: ['case', ...cases, [0, 0, 0, 0]] };
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
if (!isObject(colormap)) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
const keys = Object.keys(colormap).filter((key) => !isNaN(Number(key)));
|
|
678
|
+
if (keys.length === 0) {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
// Rescale the data to 0-255 and truncate, as rio-tiler does before
|
|
682
|
+
// applying a colormap
|
|
683
|
+
/** @type {Array<*>} */
|
|
684
|
+
let value = ['band', 1];
|
|
685
|
+
if (Array.isArray(range) && range[0] < range[1]) {
|
|
686
|
+
value = ['floor', ['clamp', stretch(1, range), 0, 255]];
|
|
687
|
+
}
|
|
688
|
+
const isLut = keys.length === 256 &&
|
|
689
|
+
keys.every((key) => {
|
|
690
|
+
const index = Number(key);
|
|
691
|
+
return Number.isInteger(index) && index >= 0 && index < 256;
|
|
692
|
+
});
|
|
693
|
+
if (isLut) {
|
|
694
|
+
const palette = new Array(256);
|
|
695
|
+
for (const key of keys) {
|
|
696
|
+
palette[Number(key)] = toColor(colormap[key]);
|
|
697
|
+
}
|
|
698
|
+
return { color: ['palette', value, palette] };
|
|
699
|
+
}
|
|
700
|
+
const cases = [];
|
|
701
|
+
for (const key of keys) {
|
|
702
|
+
cases.push(['==', value, Number(key)], toColor(colormap[key]));
|
|
703
|
+
}
|
|
704
|
+
return { color: ['case', ...cases, [0, 0, 0, 0]] };
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Creates a WebGLTileLayer style for a GeoZarr layer from the STAC metadata.
|
|
708
|
+
*
|
|
709
|
+
* A usable render from the render extension (see {@link isUsableRender})
|
|
710
|
+
* defines the complete visualization: the value range comes from its
|
|
711
|
+
* `rescale` and single-band colors from its `colormap` (see
|
|
712
|
+
* {@link createColormapStyle} for the supported forms).
|
|
713
|
+
*
|
|
714
|
+
* Without a usable render, a default visualization is derived from the
|
|
715
|
+
* general STAC metadata: the value range to stretch comes from the
|
|
716
|
+
* `statistics` of the rendered bands (or the asset), the classification
|
|
717
|
+
* extension (`classification:classes`) provides the coloring for
|
|
718
|
+
* single-band categorical data (not applied to floating point data, which
|
|
719
|
+
* is assumed to be continuous), and continuous data is colored with the
|
|
720
|
+
* given default colormap or stretched to grayscale (consistent with
|
|
721
|
+
* single-band COGs).
|
|
722
|
+
*
|
|
723
|
+
* @param {Asset} asset The asset to read the metadata from.
|
|
724
|
+
* @param {Object} sourceOptions The GeoZarr source options (to determine the rendered bands).
|
|
725
|
+
* @param {Array<import('ol/color.js').Color|string>|null} [defaultColormap] The colors
|
|
726
|
+
* of the colormap for continuous single-band data, evenly distributed over the value range.
|
|
727
|
+
* @return {Object|null} A WebGLTileLayer style, or `null` if the metadata provides none.
|
|
728
|
+
* @api
|
|
729
|
+
*/
|
|
730
|
+
export function getGeoZarrStyleFromAsset(asset, sourceOptions, defaultColormap = null) {
|
|
731
|
+
let bandCount = 1;
|
|
732
|
+
/** @type {Array<string>} */
|
|
733
|
+
let bandNames = [];
|
|
734
|
+
if (sourceOptions.variable) {
|
|
735
|
+
if (isObject(sourceOptions.selector)) {
|
|
736
|
+
for (const key in sourceOptions.selector) {
|
|
737
|
+
if (Array.isArray(sourceOptions.selector[key])) {
|
|
738
|
+
bandCount = sourceOptions.selector[key].length;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
else if (Array.isArray(sourceOptions.bands)) {
|
|
744
|
+
bandCount = sourceOptions.bands.length || 1;
|
|
745
|
+
bandNames = sourceOptions.bands.filter((band) => typeof band === 'string');
|
|
746
|
+
}
|
|
747
|
+
// The band (or the asset) that describes the rendered band
|
|
748
|
+
const bandSource = (index) => {
|
|
749
|
+
if (bandNames.length > index) {
|
|
750
|
+
const band = asset.findBand(bandNames[index]);
|
|
751
|
+
if (band) {
|
|
752
|
+
return band;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return asset;
|
|
756
|
+
};
|
|
757
|
+
const render = getRenderForAsset(asset);
|
|
758
|
+
const rescale = isUsableRender(render) && Array.isArray(render.rescale)
|
|
759
|
+
? render.rescale
|
|
760
|
+
: [];
|
|
761
|
+
// A usable render that defines any styling aspect defines the complete
|
|
762
|
+
// visualization; a render that only selects bands (or sets nodata) leaves
|
|
763
|
+
// the styling to the default visualization
|
|
764
|
+
const renderStyles = rescale.length > 0 || (isUsableRender(render) && render['colormap']);
|
|
765
|
+
// The value range to stretch per rendered band: only the render's rescale
|
|
766
|
+
// when the render styles the data, otherwise the STAC statistics of the
|
|
767
|
+
// rendered band (or the asset)
|
|
768
|
+
const rangeFor = (index) => {
|
|
769
|
+
if (renderStyles) {
|
|
770
|
+
if (Array.isArray(rescale[index]) && rescale[index].length >= 2) {
|
|
771
|
+
return rescale[index];
|
|
772
|
+
}
|
|
773
|
+
if (Array.isArray(rescale[0]) && rescale[0].length >= 2) {
|
|
774
|
+
return rescale[0];
|
|
775
|
+
}
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
return getStatisticsRange(bandSource(index)) || getStatisticsRange(asset);
|
|
779
|
+
};
|
|
780
|
+
if (bandCount >= 3) {
|
|
781
|
+
const ranges = [rangeFor(0), rangeFor(1), rangeFor(2)];
|
|
782
|
+
if (!ranges[0]) {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
color: [
|
|
787
|
+
'color',
|
|
788
|
+
stretch(1, ranges[0]),
|
|
789
|
+
stretch(2, ranges[1] || ranges[0]),
|
|
790
|
+
stretch(3, ranges[2] || ranges[0]),
|
|
791
|
+
],
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
// Single band
|
|
795
|
+
if (renderStyles) {
|
|
796
|
+
if (render['colormap']) {
|
|
797
|
+
const style = createColormapStyle(render['colormap'], rangeFor(0));
|
|
798
|
+
if (style) {
|
|
799
|
+
return style;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// A rescale without a colormap is a grayscale stretch (as in rio-tiler)
|
|
803
|
+
const range = rangeFor(0);
|
|
804
|
+
if (range) {
|
|
805
|
+
const gray = stretch(1, range);
|
|
806
|
+
return { color: ['color', gray, gray, gray] };
|
|
807
|
+
}
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
// Default visualization: the classification extension for categorical
|
|
811
|
+
// data (floating point data is assumed to be continuous)
|
|
812
|
+
if (!isFloatingPoint(bandSource(0))) {
|
|
813
|
+
let lookupBands;
|
|
814
|
+
if (bandNames.length === 1) {
|
|
815
|
+
const band = asset.findBand(bandNames[0]);
|
|
816
|
+
if (band) {
|
|
817
|
+
lookupBands = [band.getIndex() + 1];
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const classification = getClassificationStyle(asset, lookupBands, 1);
|
|
821
|
+
if (classification) {
|
|
822
|
+
return classification;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// Continuous data is colored with the default colormap or stretched to
|
|
826
|
+
// grayscale (consistent with single-band COGs)
|
|
827
|
+
const range = rangeFor(0);
|
|
828
|
+
if (range) {
|
|
829
|
+
if (Array.isArray(defaultColormap) && defaultColormap.length >= 2) {
|
|
830
|
+
return createGradientStyle(defaultColormap, 1, range);
|
|
831
|
+
}
|
|
832
|
+
const gray = stretch(1, range);
|
|
833
|
+
return { color: ['color', gray, gray, gray] };
|
|
834
|
+
}
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Normalizes a color from render extension metadata (alpha in 0-255, as in
|
|
839
|
+
* rio-tiler) to an OpenLayers color (alpha in 0-1).
|
|
840
|
+
* @param {Array<number>|string} color The color to normalize.
|
|
841
|
+
* @return {Array<number>|string} The OpenLayers color.
|
|
842
|
+
*/
|
|
843
|
+
function toColor(color) {
|
|
844
|
+
if (Array.isArray(color) && color.length === 4) {
|
|
845
|
+
return [color[0], color[1], color[2], color[3] / 255];
|
|
846
|
+
}
|
|
847
|
+
return color;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Information for rendering a datacube asset.
|
|
851
|
+
*
|
|
852
|
+
* @typedef {Object} DatacubeRenderingInfo
|
|
853
|
+
* @property {string} variable The name of the data variable to render.
|
|
854
|
+
* @property {{name: string, values: Array<string>}|null} bandDimension The
|
|
855
|
+
* bands dimension of the variable with its ordered values, if any.
|
|
856
|
+
* @property {Array<{name: string, defaultIndex: number}>} extraDimensions All
|
|
857
|
+
* other non-spatial dimensions of the variable with the index to show by
|
|
858
|
+
* default (the most recent value for temporal dimensions, otherwise 0).
|
|
859
|
+
* @property {Array<number>|null} extent The extent of the spatial dimensions
|
|
860
|
+
* (in their reference system), if declared.
|
|
861
|
+
*/
|
|
862
|
+
/**
|
|
863
|
+
* Reads the datacube extension metadata (`cube:variables` and
|
|
864
|
+
* `cube:dimensions`, inherited from the containing Item/Collection if not
|
|
865
|
+
* present on the asset) and determines the data variable to render and how
|
|
866
|
+
* its non-spatial dimensions should be sliced.
|
|
867
|
+
*
|
|
868
|
+
* @param {Asset} asset The asset to read the information from.
|
|
869
|
+
* @return {DatacubeRenderingInfo|null} The rendering info, or `null` if the
|
|
870
|
+
* asset is not described as a datacube.
|
|
871
|
+
*/
|
|
872
|
+
function getDatacubeRenderingInfo(asset) {
|
|
873
|
+
const dimensions = asset.getMetadata('cube:dimensions');
|
|
874
|
+
const variables = asset.getMetadata('cube:variables');
|
|
875
|
+
if (!isObject(dimensions) || !isObject(variables)) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
const spatialDims = [];
|
|
879
|
+
const spatialExtents = { x: null, y: null };
|
|
880
|
+
let bandDimension = null;
|
|
881
|
+
for (const name in dimensions) {
|
|
882
|
+
const dimension = dimensions[name];
|
|
883
|
+
if (!isObject(dimension)) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (dimension.type === 'spatial') {
|
|
887
|
+
spatialDims.push(name);
|
|
888
|
+
if ((dimension.axis === 'x' || dimension.axis === 'y') &&
|
|
889
|
+
Array.isArray(dimension.extent) &&
|
|
890
|
+
dimension.extent.length === 2) {
|
|
891
|
+
spatialExtents[dimension.axis] = dimension.extent;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
else if (dimension.type === 'bands') {
|
|
895
|
+
bandDimension = {
|
|
896
|
+
name,
|
|
897
|
+
values: Array.isArray(dimension.values) ? dimension.values : [],
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (spatialDims.length < 2) {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
// Find the data variable that covers the spatial dimensions. When the
|
|
905
|
+
// asset declares STAC `bands`, each band is expected to be its own array
|
|
906
|
+
// in the store (e.g. EOPF), which the `bands` mode handles instead —
|
|
907
|
+
// unless a variable packs the bands dimension into a single array.
|
|
908
|
+
const hasStacBands = asset.getBands().length > 0;
|
|
909
|
+
let variable = null;
|
|
910
|
+
let variableDims = null;
|
|
911
|
+
for (const name in variables) {
|
|
912
|
+
const v = variables[name];
|
|
913
|
+
if (!isObject(v) || !Array.isArray(v.dimensions)) {
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (typeof v.type === 'string' && v.type !== 'data') {
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (!spatialDims.every((dim) => v.dimensions.includes(dim))) {
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if (hasStacBands &&
|
|
923
|
+
!(bandDimension && v.dimensions.includes(bandDimension.name))) {
|
|
924
|
+
// With STAC bands declared, each band is expected to be its own array
|
|
925
|
+
// (addressed through the `bands` mode), unless the variable packs the
|
|
926
|
+
// declared bands dimension
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
// Prefer a variable that includes the bands dimension
|
|
930
|
+
if (bandDimension && v.dimensions.includes(bandDimension.name)) {
|
|
931
|
+
variable = name;
|
|
932
|
+
variableDims = v.dimensions;
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
if (!variable) {
|
|
936
|
+
variable = name;
|
|
937
|
+
variableDims = v.dimensions;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (!variable) {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
if (bandDimension && !variableDims.includes(bandDimension.name)) {
|
|
944
|
+
bandDimension = null;
|
|
945
|
+
}
|
|
946
|
+
const extraDimensions = [];
|
|
947
|
+
for (const name of variableDims) {
|
|
948
|
+
if (spatialDims.includes(name) ||
|
|
949
|
+
(bandDimension && name === bandDimension.name)) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const dimension = dimensions[name];
|
|
953
|
+
let defaultIndex = 0;
|
|
954
|
+
if (isObject(dimension) &&
|
|
955
|
+
dimension.type === 'temporal' &&
|
|
956
|
+
Array.isArray(dimension.values) &&
|
|
957
|
+
dimension.values.length > 0) {
|
|
958
|
+
// Show the most recent time step by default. Timestamps are compared
|
|
959
|
+
// as instants, as string comparison breaks with mixed UTC offsets.
|
|
960
|
+
defaultIndex = dimension.values.reduce((latest, value, index, values) => Date.parse(value) > Date.parse(values[latest]) ? index : latest, 0);
|
|
961
|
+
}
|
|
962
|
+
extraDimensions.push({ name, defaultIndex });
|
|
963
|
+
}
|
|
964
|
+
let extent = null;
|
|
965
|
+
if (spatialExtents.x && spatialExtents.y) {
|
|
966
|
+
extent = [
|
|
967
|
+
spatialExtents.x[0],
|
|
968
|
+
spatialExtents.y[0],
|
|
969
|
+
spatialExtents.x[1],
|
|
970
|
+
spatialExtents.y[1],
|
|
971
|
+
];
|
|
972
|
+
}
|
|
973
|
+
return { variable, bandDimension, extraDimensions, extent };
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Determines the (0-based) indices into the bands dimension of a datacube
|
|
977
|
+
* to render.
|
|
978
|
+
*
|
|
979
|
+
* @param {Array<string>} values The ordered values of the bands dimension.
|
|
980
|
+
* @param {Array<number|string>} selectedBands The bands to show. One-based index of the band, or the name of the band.
|
|
981
|
+
* @param {Asset} asset The asset, for finding the RGB bands.
|
|
982
|
+
* @return {Array<number>} The band indices.
|
|
983
|
+
*/
|
|
984
|
+
function getDatacubeBandIndices(values, selectedBands, asset) {
|
|
985
|
+
const isValid = (index) => index >= 0 && (values.length === 0 || index < values.length);
|
|
986
|
+
if (selectedBands.length > 0) {
|
|
987
|
+
return selectedBands
|
|
988
|
+
.map((band) => {
|
|
989
|
+
const index = typeof band === 'number' ? band - 1 : values.indexOf(band);
|
|
990
|
+
if (!isValid(index)) {
|
|
991
|
+
// eslint-disable-next-line no-console
|
|
992
|
+
console.error(`Band ${band} not found in asset ${asset.getKey()}`);
|
|
993
|
+
return -1;
|
|
994
|
+
}
|
|
995
|
+
return index;
|
|
996
|
+
})
|
|
997
|
+
.filter((index) => index >= 0);
|
|
998
|
+
}
|
|
999
|
+
// A usable render defines the complete visualization, including the bands
|
|
1000
|
+
// to show (paired with its value ranges)
|
|
1001
|
+
const render = getRenderForAsset(asset);
|
|
1002
|
+
if (isUsableRender(render) &&
|
|
1003
|
+
Array.isArray(render.bands) &&
|
|
1004
|
+
render.bands.length > 0) {
|
|
1005
|
+
const indices = render.bands
|
|
1006
|
+
.map((name) => values.indexOf(name))
|
|
1007
|
+
.filter((index) => index >= 0);
|
|
1008
|
+
if (indices.length > 0) {
|
|
1009
|
+
return indices;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
// Otherwise, prefer the RGB bands if they can be identified
|
|
1013
|
+
const visual = asset.findVisualBands();
|
|
1014
|
+
if (visual) {
|
|
1015
|
+
const indices = [visual.red.name, visual.green.name, visual.blue.name]
|
|
1016
|
+
.map((name) => values.indexOf(name))
|
|
1017
|
+
.filter((index) => index >= 0);
|
|
1018
|
+
if (indices.length === 3) {
|
|
1019
|
+
return indices;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
// Default to the first (up to) three bands, shown as RGB
|
|
1023
|
+
return values.slice(0, 3).map((_, index) => index);
|
|
1024
|
+
}
|
|
298
1025
|
/**
|
|
299
1026
|
* Get a URL from a web-map-link that is specific enough, i.e.
|
|
300
1027
|
* replaces any occurances of {s} if possible, otherwise returns null.
|
|
@@ -356,6 +1083,10 @@ export function getClassificationClasses(asset, bands) {
|
|
|
356
1083
|
classes = bandObj['classification:classes'];
|
|
357
1084
|
}
|
|
358
1085
|
}
|
|
1086
|
+
else if ((!bands || bands.length === 0) && assetBands.length === 1) {
|
|
1087
|
+
// A single-band asset without an explicit selection
|
|
1088
|
+
classes = assetBands[0]['classification:classes'];
|
|
1089
|
+
}
|
|
359
1090
|
// Fall back to asset-level classification or single band
|
|
360
1091
|
if (!Array.isArray(classes)) {
|
|
361
1092
|
classes = asset.getMetadata('classification:classes');
|
|
@@ -373,10 +1104,12 @@ export function getClassificationClasses(asset, bands) {
|
|
|
373
1104
|
*
|
|
374
1105
|
* @param {import('stac-js').Asset} asset The STAC asset
|
|
375
1106
|
* @param {Array<number>} [bands] The selected bands (one-based)
|
|
1107
|
+
* @param {number} [styleBand] The one-based band to style, if it differs
|
|
1108
|
+
* from the selected band (e.g. when the source only loads the selected band)
|
|
376
1109
|
* @return {Object|null} A WebGL tile layer style object, or null
|
|
377
1110
|
* @api
|
|
378
1111
|
*/
|
|
379
|
-
export function getClassificationStyle(asset, bands) {
|
|
1112
|
+
export function getClassificationStyle(asset, bands, styleBand = null) {
|
|
380
1113
|
const classes = getClassificationClasses(asset, bands);
|
|
381
1114
|
if (!classes) {
|
|
382
1115
|
return null;
|
|
@@ -389,9 +1122,12 @@ export function getClassificationStyle(asset, bands) {
|
|
|
389
1122
|
return null;
|
|
390
1123
|
}
|
|
391
1124
|
// Build the match expression: ['match', ['band', n], value, color, ..., fallback]
|
|
392
|
-
let band =
|
|
393
|
-
if (
|
|
394
|
-
band =
|
|
1125
|
+
let band = styleBand;
|
|
1126
|
+
if (!band) {
|
|
1127
|
+
band = 1;
|
|
1128
|
+
if (bands && bands.length === 1) {
|
|
1129
|
+
band = bands[0];
|
|
1130
|
+
}
|
|
395
1131
|
}
|
|
396
1132
|
const matchExpr = ['match', ['band', band]];
|
|
397
1133
|
for (const cls of classesWithColor) {
|
|
@@ -403,4 +1139,80 @@ export function getClassificationStyle(asset, bands) {
|
|
|
403
1139
|
matchExpr.push(['color', 0, 0, 0, 0]);
|
|
404
1140
|
return { color: matchExpr };
|
|
405
1141
|
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Creates a WebGLTileLayer style for a GeoTIFF layer from the STAC metadata.
|
|
1144
|
+
*
|
|
1145
|
+
* A usable render from the render extension (see {@link isUsableRender})
|
|
1146
|
+
* defines the complete visualization: for single-band data its `colormap`
|
|
1147
|
+
* provides the colors (see {@link createColormapStyle} for the supported
|
|
1148
|
+
* forms), applied over the range from its `rescale`.
|
|
1149
|
+
*
|
|
1150
|
+
* Without a usable render, the classification extension
|
|
1151
|
+
* (`classification:classes`) provides the coloring for single-band
|
|
1152
|
+
* categorical data (not applied to floating point data, which is assumed
|
|
1153
|
+
* to be continuous), and continuous single-band data is colored with the
|
|
1154
|
+
* given default colormap (over the value range from the statistics).
|
|
1155
|
+
*
|
|
1156
|
+
* When no style is returned, the value ranges from
|
|
1157
|
+
* {@link getGeoTiffSourceInfoFromAsset} stretch the data (to grayscale for
|
|
1158
|
+
* a single band) instead. The returned styles operate on the raw data
|
|
1159
|
+
* values, i.e. the GeoTIFF source must be configured with
|
|
1160
|
+
* `normalize: false`.
|
|
1161
|
+
*
|
|
1162
|
+
* @param {import('stac-js').Asset} asset The STAC asset
|
|
1163
|
+
* @param {import('ol/source/GeoTIFF.js').SourceInfo} sourceInfo The source info
|
|
1164
|
+
* (for the selected bands).
|
|
1165
|
+
* @param {Array<import('ol/color.js').Color|string>|null} [defaultColormap] The colors
|
|
1166
|
+
* of the colormap for continuous single-band data, evenly distributed over the value range.
|
|
1167
|
+
* @return {Object|null} A WebGL tile layer style object, or null
|
|
1168
|
+
* @api
|
|
1169
|
+
*/
|
|
1170
|
+
export function getGeoTiffStyleFromAsset(asset, sourceInfo, defaultColormap = null) {
|
|
1171
|
+
const selected = Array.isArray(sourceInfo.bands) ? sourceInfo.bands : [];
|
|
1172
|
+
const bandCount = selected.length > 0
|
|
1173
|
+
? selected.length
|
|
1174
|
+
: Math.max(asset.getBands().length, 1);
|
|
1175
|
+
const render = getRenderForAsset(asset);
|
|
1176
|
+
if (isUsableRender(render)) {
|
|
1177
|
+
if (bandCount === 1 && render['colormap']) {
|
|
1178
|
+
const rescale = Array.isArray(render.rescale) ? render.rescale : [];
|
|
1179
|
+
const range = Array.isArray(rescale[0]) && rescale[0].length >= 2 ? rescale[0] : null;
|
|
1180
|
+
return createColormapStyle(render['colormap'], range);
|
|
1181
|
+
}
|
|
1182
|
+
// The value range from the render's rescale is applied through the
|
|
1183
|
+
// source info instead
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
if (bandCount !== 1) {
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
// Default visualization: the classification extension for categorical
|
|
1190
|
+
// data (floating point data is assumed to be continuous)
|
|
1191
|
+
const bands = asset.getBands();
|
|
1192
|
+
let bandSource = asset;
|
|
1193
|
+
if (selected.length === 1 && bands.length > 0) {
|
|
1194
|
+
bandSource = bands[selected[0] - 1] || asset;
|
|
1195
|
+
}
|
|
1196
|
+
else if (selected.length === 0 && bands.length === 1) {
|
|
1197
|
+
bandSource = bands[0];
|
|
1198
|
+
}
|
|
1199
|
+
if (!isFloatingPoint(bandSource)) {
|
|
1200
|
+
// The source only loads the selected bands, so a single selected band
|
|
1201
|
+
// is rendered as band 1
|
|
1202
|
+
const classification = getClassificationStyle(asset, selected, selected.length === 1 ? 1 : null);
|
|
1203
|
+
if (classification) {
|
|
1204
|
+
return classification;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
// Continuous data is colored with the default colormap, if given
|
|
1208
|
+
if (Array.isArray(defaultColormap) && defaultColormap.length >= 2) {
|
|
1209
|
+
const range = getStatisticsRange(bandSource) ||
|
|
1210
|
+
(bandSource !== asset ? getStatisticsRange(asset) : null);
|
|
1211
|
+
if (range) {
|
|
1212
|
+
return createGradientStyle(defaultColormap, 1, range);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
// Otherwise it is stretched to grayscale through the source info
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
406
1218
|
//# sourceMappingURL=util.js.map
|