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
package/util.js
CHANGED
|
@@ -183,6 +183,73 @@ export async function getStacObjectsForEvent(event, exclude = null, selectedFeat
|
|
|
183
183
|
});
|
|
184
184
|
return [...objects];
|
|
185
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
|
+
}
|
|
186
253
|
/**
|
|
187
254
|
* Get the source info for the GeoTiff from the asset.
|
|
188
255
|
* @param {import('stac-js').Asset} asset The asset to read the information from.
|
|
@@ -205,22 +272,7 @@ export function getGeoTiffSourceInfoFromAsset(asset, selectedBands) {
|
|
|
205
272
|
const nodataValues = new Array(bandCount).fill(undefined);
|
|
206
273
|
let index = 0;
|
|
207
274
|
for (const source of sources) {
|
|
208
|
-
const
|
|
209
|
-
let { minimum, maximum } = stats;
|
|
210
|
-
const { mean, stddev } = stats;
|
|
211
|
-
// Use mean ± 2σ for a better visualization stretch (~95% of values)
|
|
212
|
-
if (typeof mean === 'number' && typeof stddev === 'number' && stddev > 0) {
|
|
213
|
-
const stretchMin = mean - 2 * stddev;
|
|
214
|
-
const stretchMax = mean + 2 * stddev;
|
|
215
|
-
minimum =
|
|
216
|
-
typeof minimum === 'number'
|
|
217
|
-
? Math.max(minimum, stretchMin)
|
|
218
|
-
: stretchMin;
|
|
219
|
-
maximum =
|
|
220
|
-
typeof maximum === 'number'
|
|
221
|
-
? Math.min(maximum, stretchMax)
|
|
222
|
-
: stretchMax;
|
|
223
|
-
}
|
|
275
|
+
const { minimum, maximum } = getStatisticsStretch(source.getStatistics());
|
|
224
276
|
if (typeof minimum === 'number') {
|
|
225
277
|
minValues[index] = minimum;
|
|
226
278
|
}
|
|
@@ -236,16 +288,27 @@ export function getGeoTiffSourceInfoFromAsset(asset, selectedBands) {
|
|
|
236
288
|
}
|
|
237
289
|
index++;
|
|
238
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 : [];
|
|
239
296
|
const defined = (v) => v !== undefined;
|
|
240
|
-
if (
|
|
241
|
-
sourceInfo.min =
|
|
242
|
-
|
|
243
|
-
: 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];
|
|
244
300
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
}
|
|
249
312
|
}
|
|
250
313
|
if (nodataValues.some(defined)) {
|
|
251
314
|
if (perBand) {
|
|
@@ -258,6 +321,13 @@ export function getGeoTiffSourceInfoFromAsset(asset, selectedBands) {
|
|
|
258
321
|
}
|
|
259
322
|
}
|
|
260
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
|
+
}
|
|
261
331
|
if (selectedBands.length > 0) {
|
|
262
332
|
sourceInfo.bands = selectedBands
|
|
263
333
|
.map((band) => {
|
|
@@ -275,13 +345,26 @@ export function getGeoTiffSourceInfoFromAsset(asset, selectedBands) {
|
|
|
275
345
|
.filter((band) => band !== null);
|
|
276
346
|
}
|
|
277
347
|
else {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
+
}
|
|
285
368
|
}
|
|
286
369
|
}
|
|
287
370
|
return sourceInfo;
|
|
@@ -305,6 +388,13 @@ export function getBoundsStyle(originalStyle, layerGroup) {
|
|
|
305
388
|
/**
|
|
306
389
|
* Parse the GeoZarr source options from an asset.
|
|
307
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
|
+
* selection 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
|
+
*
|
|
308
398
|
* @param {Asset} asset The asset to read the information from.
|
|
309
399
|
* @param {Array<number|string>} selectedBands The bands to show. One-based index of the band, or the name of the band.
|
|
310
400
|
* @return {Object} The GeoZarr source options
|
|
@@ -314,6 +404,38 @@ export function getGeoZarrSourceOptionsFromAsset(asset, selectedBands) {
|
|
|
314
404
|
const options = {
|
|
315
405
|
url: asset.getAbsoluteUrl(),
|
|
316
406
|
};
|
|
407
|
+
const cube = getDatacubeRenderingInfo(asset);
|
|
408
|
+
if (cube) {
|
|
409
|
+
options.variable = cube.variable;
|
|
410
|
+
options.dimensions = {};
|
|
411
|
+
if (cube.bandDimension) {
|
|
412
|
+
const indices = getDatacubeBandIndices(cube.bandDimension.values, selectedBands, asset);
|
|
413
|
+
if (indices.length > 0) {
|
|
414
|
+
options.dimensions[cube.bandDimension.name] = indices;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
for (const dimension of cube.extraDimensions) {
|
|
418
|
+
options.dimensions[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
|
+
}
|
|
317
439
|
if (selectedBands.length > 0) {
|
|
318
440
|
options.bands = selectedBands
|
|
319
441
|
.map((band) => {
|
|
@@ -325,24 +447,581 @@ export function getGeoZarrSourceOptionsFromAsset(asset, selectedBands) {
|
|
|
325
447
|
if (isObject(bandObj) && typeof bandObj.name === 'string') {
|
|
326
448
|
return bandObj.name;
|
|
327
449
|
}
|
|
328
|
-
// eslint-disable-next-line no-console
|
|
329
|
-
console.error(`Band with index ${band} not found in asset ${asset.getKey()}`);
|
|
330
450
|
return null;
|
|
331
451
|
})
|
|
332
452
|
.filter(Boolean);
|
|
333
453
|
}
|
|
334
454
|
else {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
}
|
|
342
472
|
}
|
|
343
473
|
}
|
|
474
|
+
if (!Array.isArray(options.bands)) {
|
|
475
|
+
options.bands = [];
|
|
476
|
+
}
|
|
344
477
|
return options;
|
|
345
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.dimensions)) {
|
|
736
|
+
for (const key in sourceOptions.dimensions) {
|
|
737
|
+
if (Array.isArray(sourceOptions.dimensions[key])) {
|
|
738
|
+
bandCount = sourceOptions.dimensions[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
|
+
}
|
|
346
1025
|
/**
|
|
347
1026
|
* Get a URL from a web-map-link that is specific enough, i.e.
|
|
348
1027
|
* replaces any occurances of {s} if possible, otherwise returns null.
|
|
@@ -363,6 +1042,50 @@ export function getSpecificWebMapUrl(link) {
|
|
|
363
1042
|
}
|
|
364
1043
|
return url;
|
|
365
1044
|
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Returns all potential web map links for the given STAC entity,
|
|
1047
|
+
* based on the given value for `displayWebMapLink`.
|
|
1048
|
+
* See the STACLayer option of the same name for details.
|
|
1049
|
+
* @param {import('stac-js').STACObject} data The STAC entity to get the links from.
|
|
1050
|
+
* @param {string|boolean|Array<import('./layer/STAC.js').Link|string>} [displayWebMapLink] The value of the `displayWebMapLink` option of the STACLayer.
|
|
1051
|
+
* @return {Array<import('./layer/STAC.js').Link>} An array of links.
|
|
1052
|
+
* @api
|
|
1053
|
+
*/
|
|
1054
|
+
export function getWebMapLinks(data, displayWebMapLink = true) {
|
|
1055
|
+
if (displayWebMapLink === false) {
|
|
1056
|
+
return [];
|
|
1057
|
+
}
|
|
1058
|
+
if (!data || data.isAsset) {
|
|
1059
|
+
return [];
|
|
1060
|
+
}
|
|
1061
|
+
let types = ['xyz', 'tilejson', 'pmtiles', 'wmts', 'wms']; // This also defines the priority
|
|
1062
|
+
if (typeof displayWebMapLink === 'string') {
|
|
1063
|
+
types = [displayWebMapLink];
|
|
1064
|
+
}
|
|
1065
|
+
let mapLinks = data.getLinksWithRels(types);
|
|
1066
|
+
if (Array.isArray(displayWebMapLink)) {
|
|
1067
|
+
mapLinks = displayWebMapLink
|
|
1068
|
+
.map((link) => {
|
|
1069
|
+
if (typeof link === 'string') {
|
|
1070
|
+
const match = mapLinks.find((candidate) => candidate.id === link);
|
|
1071
|
+
if (match) {
|
|
1072
|
+
return match;
|
|
1073
|
+
}
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
return link;
|
|
1077
|
+
})
|
|
1078
|
+
.filter((link) => !!link);
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
mapLinks.sort((a, b) => {
|
|
1082
|
+
const prioA = types.indexOf(a.rel);
|
|
1083
|
+
const prioB = types.indexOf(b.rel);
|
|
1084
|
+
return prioA - prioB;
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
return mapLinks;
|
|
1088
|
+
}
|
|
366
1089
|
/**
|
|
367
1090
|
* Checks whether the given value is a scalar (string, number, boolean).
|
|
368
1091
|
* @param {*} value The value to check
|
|
@@ -404,6 +1127,10 @@ export function getClassificationClasses(asset, bands) {
|
|
|
404
1127
|
classes = bandObj['classification:classes'];
|
|
405
1128
|
}
|
|
406
1129
|
}
|
|
1130
|
+
else if ((!bands || bands.length === 0) && assetBands.length === 1) {
|
|
1131
|
+
// A single-band asset without an explicit selection
|
|
1132
|
+
classes = assetBands[0]['classification:classes'];
|
|
1133
|
+
}
|
|
407
1134
|
// Fall back to asset-level classification or single band
|
|
408
1135
|
if (!Array.isArray(classes)) {
|
|
409
1136
|
classes = asset.getMetadata('classification:classes');
|
|
@@ -421,10 +1148,12 @@ export function getClassificationClasses(asset, bands) {
|
|
|
421
1148
|
*
|
|
422
1149
|
* @param {import('stac-js').Asset} asset The STAC asset
|
|
423
1150
|
* @param {Array<number>} [bands] The selected bands (one-based)
|
|
1151
|
+
* @param {number} [styleBand] The one-based band to style, if it differs
|
|
1152
|
+
* from the selected band (e.g. when the source only loads the selected band)
|
|
424
1153
|
* @return {Object|null} A WebGL tile layer style object, or null
|
|
425
1154
|
* @api
|
|
426
1155
|
*/
|
|
427
|
-
export function getClassificationStyle(asset, bands) {
|
|
1156
|
+
export function getClassificationStyle(asset, bands, styleBand = null) {
|
|
428
1157
|
const classes = getClassificationClasses(asset, bands);
|
|
429
1158
|
if (!classes) {
|
|
430
1159
|
return null;
|
|
@@ -437,9 +1166,12 @@ export function getClassificationStyle(asset, bands) {
|
|
|
437
1166
|
return null;
|
|
438
1167
|
}
|
|
439
1168
|
// Build the match expression: ['match', ['band', n], value, color, ..., fallback]
|
|
440
|
-
let band =
|
|
441
|
-
if (
|
|
442
|
-
band =
|
|
1169
|
+
let band = styleBand;
|
|
1170
|
+
if (!band) {
|
|
1171
|
+
band = 1;
|
|
1172
|
+
if (bands && bands.length === 1) {
|
|
1173
|
+
band = bands[0];
|
|
1174
|
+
}
|
|
443
1175
|
}
|
|
444
1176
|
const matchExpr = ['match', ['band', band]];
|
|
445
1177
|
for (const cls of classesWithColor) {
|
|
@@ -451,4 +1183,80 @@ export function getClassificationStyle(asset, bands) {
|
|
|
451
1183
|
matchExpr.push(['color', 0, 0, 0, 0]);
|
|
452
1184
|
return { color: matchExpr };
|
|
453
1185
|
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Creates a WebGLTileLayer style for a GeoTIFF layer from the STAC metadata.
|
|
1188
|
+
*
|
|
1189
|
+
* A usable render from the render extension (see {@link isUsableRender})
|
|
1190
|
+
* defines the complete visualization: for single-band data its `colormap`
|
|
1191
|
+
* provides the colors (see {@link createColormapStyle} for the supported
|
|
1192
|
+
* forms), applied over the range from its `rescale`.
|
|
1193
|
+
*
|
|
1194
|
+
* Without a usable render, the classification extension
|
|
1195
|
+
* (`classification:classes`) provides the coloring for single-band
|
|
1196
|
+
* categorical data (not applied to floating point data, which is assumed
|
|
1197
|
+
* to be continuous), and continuous single-band data is colored with the
|
|
1198
|
+
* given default colormap (over the value range from the statistics).
|
|
1199
|
+
*
|
|
1200
|
+
* When no style is returned, the value ranges from
|
|
1201
|
+
* {@link getGeoTiffSourceInfoFromAsset} stretch the data (to grayscale for
|
|
1202
|
+
* a single band) instead. The returned styles operate on the raw data
|
|
1203
|
+
* values, i.e. the GeoTIFF source must be configured with
|
|
1204
|
+
* `normalize: false`.
|
|
1205
|
+
*
|
|
1206
|
+
* @param {import('stac-js').Asset} asset The STAC asset
|
|
1207
|
+
* @param {import('ol/source/GeoTIFF.js').SourceInfo} sourceInfo The source info
|
|
1208
|
+
* (for the selected bands).
|
|
1209
|
+
* @param {Array<import('ol/color.js').Color|string>|null} [defaultColormap] The colors
|
|
1210
|
+
* of the colormap for continuous single-band data, evenly distributed over the value range.
|
|
1211
|
+
* @return {Object|null} A WebGL tile layer style object, or null
|
|
1212
|
+
* @api
|
|
1213
|
+
*/
|
|
1214
|
+
export function getGeoTiffStyleFromAsset(asset, sourceInfo, defaultColormap = null) {
|
|
1215
|
+
const selected = Array.isArray(sourceInfo.bands) ? sourceInfo.bands : [];
|
|
1216
|
+
const bandCount = selected.length > 0
|
|
1217
|
+
? selected.length
|
|
1218
|
+
: Math.max(asset.getBands().length, 1);
|
|
1219
|
+
const render = getRenderForAsset(asset);
|
|
1220
|
+
if (isUsableRender(render)) {
|
|
1221
|
+
if (bandCount === 1 && render['colormap']) {
|
|
1222
|
+
const rescale = Array.isArray(render.rescale) ? render.rescale : [];
|
|
1223
|
+
const range = Array.isArray(rescale[0]) && rescale[0].length >= 2 ? rescale[0] : null;
|
|
1224
|
+
return createColormapStyle(render['colormap'], range);
|
|
1225
|
+
}
|
|
1226
|
+
// The value range from the render's rescale is applied through the
|
|
1227
|
+
// source info instead
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
if (bandCount !== 1) {
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
// Default visualization: the classification extension for categorical
|
|
1234
|
+
// data (floating point data is assumed to be continuous)
|
|
1235
|
+
const bands = asset.getBands();
|
|
1236
|
+
let bandSource = asset;
|
|
1237
|
+
if (selected.length === 1 && bands.length > 0) {
|
|
1238
|
+
bandSource = bands[selected[0] - 1] || asset;
|
|
1239
|
+
}
|
|
1240
|
+
else if (selected.length === 0 && bands.length === 1) {
|
|
1241
|
+
bandSource = bands[0];
|
|
1242
|
+
}
|
|
1243
|
+
if (!isFloatingPoint(bandSource)) {
|
|
1244
|
+
// The source only loads the selected bands, so a single selected band
|
|
1245
|
+
// is rendered as band 1
|
|
1246
|
+
const classification = getClassificationStyle(asset, selected, selected.length === 1 ? 1 : null);
|
|
1247
|
+
if (classification) {
|
|
1248
|
+
return classification;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
// Continuous data is colored with the default colormap, if given
|
|
1252
|
+
if (Array.isArray(defaultColormap) && defaultColormap.length >= 2) {
|
|
1253
|
+
const range = getStatisticsRange(bandSource) ||
|
|
1254
|
+
(bandSource !== asset ? getStatisticsRange(asset) : null);
|
|
1255
|
+
if (range) {
|
|
1256
|
+
return createGradientStyle(defaultColormap, 1, range);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
// Otherwise it is stretched to grayscale through the source info
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
454
1262
|
//# sourceMappingURL=util.js.map
|