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/layer/STAC.js CHANGED
@@ -10,7 +10,6 @@ import TileLayer from 'ol/layer/Tile.js';
10
10
  import VectorLayer from 'ol/layer/Vector.js';
11
11
  import VectorTileLayer from 'ol/layer/VectorTile.js';
12
12
  import WebGLTileLayer from 'ol/layer/WebGLTile.js';
13
- import { transformExtent } from 'ol/proj.js';
14
13
  import GeoTIFF from 'ol/source/GeoTIFF.js';
15
14
  import StaticImage from 'ol/source/ImageStatic.js';
16
15
  import TileJSON from 'ol/source/TileJSON.js';
@@ -23,12 +22,14 @@ import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
23
22
  import * as pmtiles from 'pmtiles';
24
23
  import create, { Asset } from 'stac-js';
25
24
  import { fixGeoJson, toGeoJSON, unionBoundingBox } from 'stac-js/src/geo.js';
26
- import { geojsonMediaType, geotiffMediaTypes, wozMediaTypes, } from 'stac-js/src/mediatypes.js';
25
+ import { geojsonMediaType, geotiffMediaTypes, wozMediaTypes, zarrMediaTypes, } from 'stac-js/src/mediatypes.js';
27
26
  import { isObject } from 'stac-js/src/utils.js';
28
27
  import ErrorEvent from '../events/ErrorEvent.js';
28
+ import { createImageLoadFunction, createTileLoadFunction } from '../http.js';
29
29
  import { getProjection } from '../proj.js';
30
30
  import SourceType from '../source/type.js';
31
- import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsStyle, getClassificationStyle, getGeoTiffSourceInfoFromAsset, getGeoZarrSourceOptionsFromAsset, getSpecificWebMapUrl, isScalar, } from '../util.js';
31
+ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, exceedsDisplayLimit, getBoundsStyle, getDisplayPixels, getGeoTiffSourceInfoFromAsset, getGeoTiffStyleFromAsset, getGeoZarrSourceOptionsFromAsset, getGeoZarrStyleFromAsset, getSpecificWebMapUrl, isScalar, toContinuousBBox, toOlExtent, } from '../util.js';
32
+ import LayerType from './type.js';
32
33
  /**
33
34
  * @typedef {import("ol/extent.js").Extent} Extent
34
35
  */
@@ -56,6 +57,18 @@ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsS
56
57
  /**
57
58
  * @typedef {import('../source/type.js').SourceOptions} SourceOptions
58
59
  */
60
+ /**
61
+ * @typedef {import('./type.js').LayerOptions} LayerOptions
62
+ */
63
+ /**
64
+ * @typedef {import('../http.js').GetHeadersFn} GetHeadersFn
65
+ */
66
+ /**
67
+ * @typedef {import('../http.js').OnErrorFn} OnErrorFn
68
+ */
69
+ /**
70
+ * @typedef {function((import("ol/Image.js").default|import("ol/Tile.js").default), string): void} LoadFunction
71
+ */
59
72
  /**
60
73
  * @typedef {Object} Options
61
74
  * @property {string} [url] The STAC URL. Any of `url` and `data` must be provided.
@@ -76,8 +89,15 @@ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsS
76
89
  * Optional function that can be used to configure the underlying sources. The function can do any additional work
77
90
  * and return the completed options or a promise for the same. The function will be called with the current source options
78
91
  * and the STAC Asset or Link.
79
- * This can be useful for adding auth information such as an API token, either via query parameter or HTTP headers.
80
- * Please be aware that sending HTTP headers may not be supported by all sources.
92
+ * This can be useful for advanced per-source customization such as signed URLs.
93
+ * To add credentials via query parameters, use the `getRequestUrl` option instead;
94
+ * to send credentials via HTTP headers, use the `getRequestHeaders` option instead.
95
+ * @property {function(LayerType, LayerOptions, (Asset|Link)):(LayerOptions|Promise<LayerOptions>)} [getLayerOptions]
96
+ * Optional function that can be used to configure the individual layers that are created for the assets and links.
97
+ * The function can do any additional (asynchronous) work and return the completed options or a promise for the same.
98
+ * The function will be called with the layer type, the current layer options and the STAC Asset or Link.
99
+ * This can be useful to customize the layers, e.g. to apply a style to a GeoTIFF or GeoZarr layer that is
100
+ * loaded from the STAC metadata.
81
101
  * @property {boolean} [displayFootprint=true] Allows to hide the footprints (bounding box/geometry) of the STAC object
82
102
  * by default.
83
103
  * @property {boolean} [displayGeoTiffByDefault=false] Allow to choose non-cloud-optimized GeoTiffs as default image to show,
@@ -86,8 +106,16 @@ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsS
86
106
  * i.e. assets with any of the roles `thumbnail`, `overview`, or a link with relation type `preview`.
87
107
  * The previews are usually not covering the full extents and as such may be placed incorrectly on the map.
88
108
  * For performance reasons, it is recommended to enable this option if you pass in STAC API Items instead of `displayOverview`.
89
- * @property {boolean} [displayOverview=true] Allow to display COGs/WOZs and, if `displayGeoTiffByDefault` is enabled, GeoTiffs,
109
+ * @property {boolean} [displayOverview=true] Allow to display COGs, Zarr and, if `displayGeoTiffByDefault` is enabled, GeoTiffs,
90
110
  * usually an asset with role `overview` or `visual`.
111
+ * Zarr assets other than Web-Optimized Zarr are only displayed if the STAC metadata declares what to render
112
+ * (see the datacube extension) and the store is within the `maxDisplayPixels` limit.
113
+ * @property {number} [maxDisplayPixels=16777216] The maximum number of pixels the coarsest resolution level
114
+ * of a GeoTIFF or Zarr asset may have to be displayed client-side, as displaying the full extent of an asset
115
+ * loads every tile of that level. Files without (sufficient) overviews can easily exceed this limit.
116
+ * Larger assets are not chosen for the default visualization, and selecting one explicitly through `assets`
117
+ * reports an error through the `error` event (or renders through the tile server if `buildTileUrlTemplate`
118
+ * and `useTileLayerAsFallback` are set). Set to `Infinity` to display assets of any size.
91
119
  * @property {string|boolean|Array<Link|string>} [displayWebMapLink=false] Allow to display a layer
92
120
  * based on the information provided through the web map links extension.
93
121
  * If an array of links or link ids (property `id` in a Link Object) is provided, all corresponding layers will be shown.
@@ -95,13 +123,19 @@ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsS
95
123
  * it lets this library choose a web map link to show, but only if no other data is shown.
96
124
  * To disable the functionality set this to `false`.
97
125
  * @property {import("ol/layer/WebGLTile.js").Style|null} [style=null] The style for GeoTIFF and GeoZarr layers (WebGLTileLayer style).
126
+ * @property {Array<import("ol/color.js").Color|string>|null} [defaultColormap=null] The colors of the colormap
127
+ * that is used for continuous single-band data when neither the STAC metadata nor the `style` option define
128
+ * a coloring. The colors are evenly distributed over the value range of the data (e.g. from the STAC
129
+ * `statistics`). If not set, the data is stretched to grayscale.
98
130
  * @property {Style} [boundsStyle] The style for the overall bounds / footprint.
99
131
  * @property {Style} [collectionStyle] The style for individual children in a list of STAC Items or Collections.
100
132
  * @property {null|string} [crossOrigin] For thumbnails: The `crossOrigin` attribute for loaded images / tiles.
101
133
  * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.
102
- * @property {function((Asset|Link)):Promise<string>|string|null} [buildTileUrlTemplate=null] A function that generates a URL template for a tile server (XYZ),
134
+ * @property {function((Asset|Link)):Promise<string|null>|string|null} [buildTileUrlTemplate=null] A function that generates a URL template for a tile server (XYZ),
103
135
  * which will be used instead of the client-side GeoTIFF rendering (except if `useTileLayerAsFallback` is `true`).
104
136
  * The function provided can return a promise (i.e. be async) or a string.
137
+ * The function can return `null` to not pass the given asset or link to the tile server,
138
+ * e.g. to filter by media type or protocol. In this case client-side rendering is used instead.
105
139
  * @property {boolean} [useTileLayerAsFallback=false] Uses the given URL template only when the client-side GeoTIFF rendering fails.
106
140
  * @property {number} [opacity=1] Opacity (0, 1).
107
141
  * @property {boolean} [visible=true] Visibility.
@@ -125,6 +159,22 @@ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsS
125
159
  * @property {function(string,string):(*)} [httpRequestFn=null] Sets a custom function to make HTTP requests with.
126
160
  * The first parameter is the URL to request and the output is a promise that resolves with the response body.
127
161
  * The second parameter is the return type, either `json` (default) or `text`.
162
+ * The STAC Asset or Link the request is made for is passed as third parameter, if available.
163
+ * @property {Object<string, string>|function((Asset|Link|STACObject|null), string):(Object<string, string>|null)} [getRequestHeaders=null]
164
+ * The HTTP headers (e.g. for authentication) to send with the requests made by this layer,
165
+ * either as a plain object or as a function that returns the headers (or `null` for none) and
166
+ * is called with the STAC Asset or Link that is shown (if available) and the URL that is requested.
167
+ * Use a function to restrict the headers to specific hosts, as tile server URLs and asset URLs
168
+ * may point to hosts that should not receive the credentials.
169
+ * The headers are attached to requests made by the default `httpRequestFn`, to GeoTIFF, GeoZarr
170
+ * and PMTiles requests, and via image/tile load functions (through the Fetch API and object URLs)
171
+ * to preview images and XYZ, TileJSON, WMS and WMTS tiles.
172
+ * @property {function((Asset|Link|STACObject|null), string):(string|null)} [getRequestUrl=null]
173
+ * Rewrites a URL before a request is made or a source is created, e.g. to append query
174
+ * parameters for authentication (signed URLs, API keys). The function is called with the
175
+ * STAC Asset or Link that is shown (if available) and the URL, and returns the new URL or
176
+ * `null` to keep the URL unchanged. The rewrite is applied before `getSourceOptions` is called.
177
+ * For tiled sources the tile URL template is rewritten, not the individual tile URLs.
128
178
  */
129
179
  /**
130
180
  * @classdesc
@@ -160,6 +210,21 @@ class STACLayer extends LayerGroup {
160
210
  * @private
161
211
  */
162
212
  this.getSourceOptions_ = options.getSourceOptions;
213
+ /**
214
+ * @type {function(LayerType, LayerOptions, (Asset|Link)):(LayerOptions|Promise<LayerOptions>)}
215
+ * @private
216
+ */
217
+ this.getLayerOptions_ = options.getLayerOptions;
218
+ /**
219
+ * @type {Object<string, string>|function((Asset|Link|STACObject|null), string):(Object<string, string>|null)|null}
220
+ * @private
221
+ */
222
+ this.getRequestHeaders_ = options.getRequestHeaders || null;
223
+ /**
224
+ * @type {function((Asset|Link|STACObject|null), string):(string|null)|null}
225
+ * @private
226
+ */
227
+ this.getRequestUrl_ = options.getRequestUrl || null;
163
228
  /**
164
229
  * @type {Array<STAC>|null}
165
230
  * @private
@@ -210,7 +275,12 @@ class STACLayer extends LayerGroup {
210
275
  */
211
276
  this.displayWebMapLink_ = options.displayWebMapLink || false;
212
277
  /**
213
- * @type {function((Asset|Link)):Promise<string>|string|null}
278
+ * @type {number|undefined}
279
+ * @private
280
+ */
281
+ this.maxDisplayPixels_ = options.maxDisplayPixels;
282
+ /**
283
+ * @type {function((Asset|Link)):Promise<string|null>|string|null}
214
284
  * @private
215
285
  */
216
286
  this.buildTileUrlTemplate_ = options.buildTileUrlTemplate || null;
@@ -224,6 +294,11 @@ class STACLayer extends LayerGroup {
224
294
  * @private
225
295
  */
226
296
  this.style_ = options.style || null;
297
+ /**
298
+ * @type {Array<import("ol/color.js").Color|string>|null}
299
+ * @private
300
+ */
301
+ this.defaultColormap_ = options.defaultColormap || null;
227
302
  /**
228
303
  * @type {Style}
229
304
  * @private
@@ -269,19 +344,70 @@ class STACLayer extends LayerGroup {
269
344
  if (!options.url) {
270
345
  throw new Error('Either url or data must be provided');
271
346
  }
272
- this.fetch_(options.url)
347
+ this.fetch_(this.getRequestUrlFor_(options.url))
273
348
  .then((data) => this.configure_(data, options.url, options.children, options.assets, options.bands))
274
349
  .catch((error) => this.handleError_(error));
275
350
  }
351
+ /**
352
+ * Rewrites the given URL based on the `getRequestUrl` option.
353
+ *
354
+ * @param {string} url The URL that is requested.
355
+ * @param {Asset|Link|STACObject|null} [ref] The STAC Asset or Link that is shown, if available.
356
+ * @return {string} The rewritten URL, or the given URL if it is not rewritten.
357
+ */
358
+ getRequestUrlFor_(url, ref = null) {
359
+ if (typeof this.getRequestUrl_ === 'function' && typeof url === 'string') {
360
+ const newUrl = this.getRequestUrl_(ref, url);
361
+ if (typeof newUrl === 'string' && newUrl.length > 0) {
362
+ return newUrl;
363
+ }
364
+ }
365
+ return url;
366
+ }
367
+ /**
368
+ * Returns the HTTP headers to send for the given URL, based on the
369
+ * `getRequestHeaders` option.
370
+ *
371
+ * @param {string} url The URL that is requested.
372
+ * @param {Asset|Link|STACObject|null} [ref] The STAC Asset or Link that is shown, if available.
373
+ * @return {Object<string, string>|null} The headers, or `null` if there are none.
374
+ */
375
+ getRequestHeadersFor_(url, ref = null) {
376
+ let headers = this.getRequestHeaders_;
377
+ if (typeof headers === 'function') {
378
+ headers = headers(ref, url);
379
+ }
380
+ if (isObject(headers) && Object.keys(headers).length > 0) {
381
+ return /** @type {Object<string, string>} */ (headers);
382
+ }
383
+ return null;
384
+ }
385
+ /**
386
+ * Creates a load function for images or tiles that attaches the headers
387
+ * from the `getRequestHeaders` option and reports errors through the
388
+ * layer's error event, or `undefined` if no headers are configured.
389
+ *
390
+ * @param {function(GetHeadersFn, OnErrorFn=):LoadFunction} factory `createImageLoadFunction` or `createTileLoadFunction`.
391
+ * @param {Asset|Link|STACObject|null} ref The STAC Asset or Link that is shown, if available.
392
+ * @return {LoadFunction|undefined} The load function.
393
+ */
394
+ createLoadFunction_(factory, ref) {
395
+ if (!this.getRequestHeaders_) {
396
+ return undefined;
397
+ }
398
+ return factory((url) => this.getRequestHeadersFor_(url, ref), (error) => this.handleError_(error));
399
+ }
276
400
  /**
277
401
  * Default function make HTTP requests with.
278
402
  *
279
403
  * @param {string} url The URL to request and the output is a promise that resolves with the response body.
280
404
  * @param {string} responseType The return type, either `json` (default) or `text`.
405
+ * @param {Asset|Link|STACObject|null} [ref] The STAC Asset or Link the request is made for, if available.
281
406
  * @return {Promise<*>} The (parsed) response body.
282
407
  */
283
- async fetch_(url, responseType = 'json') {
284
- const response = await fetch(url);
408
+ async fetch_(url, responseType = 'json', ref = null) {
409
+ const headers = this.getRequestHeadersFor_(url, ref);
410
+ const response = await fetch(url, headers ? { headers } : undefined);
285
411
  if (!response.ok) {
286
412
  throw new Error(`Unexpected response from ${url}: ${response.status}`);
287
413
  }
@@ -315,7 +441,7 @@ class STACLayer extends LayerGroup {
315
441
  return false;
316
442
  }
317
443
  const bbox = this.getData().getBoundingBox();
318
- if (!bbox || isEmpty(bbox)) {
444
+ if (!bbox || isEmpty(toContinuousBBox(bbox))) {
319
445
  return true;
320
446
  }
321
447
  return !this.boundsLayer_ || !this.displayFootprint_;
@@ -479,18 +605,21 @@ class STACLayer extends LayerGroup {
479
605
  * @type {import("ol/source/ImageStatic.js").Options}
480
606
  */
481
607
  let options = {
482
- url: image.getAbsoluteUrl(),
608
+ url: this.getRequestUrlFor_(image.getAbsoluteUrl(), image),
483
609
  projection,
484
- imageExtent: transformExtent(bbox, 'EPSG:4326', projection),
610
+ imageExtent: toOlExtent(bbox, projection),
485
611
  crossOrigin: this.crossOrigin_,
486
612
  };
613
+ const imageLoadFunction = this.createLoadFunction_(createImageLoadFunction, image);
614
+ if (imageLoadFunction) {
615
+ options.imageLoadFunction = imageLoadFunction;
616
+ }
487
617
  if (this.getSourceOptions_) {
488
618
  // @ts-ignore
489
619
  options = await this.getSourceOptions_(SourceType.ImageStatic, options, image);
490
620
  }
491
- const layer = new ImageLayer({
492
- source: new StaticImage(options),
493
- });
621
+ const layerOptions = await this.updateLayerOptions_(LayerType.Image, { source: new StaticImage(options) }, image);
622
+ const layer = new ImageLayer(layerOptions);
494
623
  this.addLayer_(layer, image);
495
624
  return layer;
496
625
  }
@@ -504,16 +633,21 @@ class STACLayer extends LayerGroup {
504
633
  */
505
634
  async addLayerForLink(link) {
506
635
  // Replace any occurances of {s} if possible, otherwise return
507
- const url = getSpecificWebMapUrl(link);
636
+ let url = getSpecificWebMapUrl(link);
508
637
  if (!url) {
509
638
  return;
510
639
  }
640
+ url = this.getRequestUrlFor_(url, link);
511
641
  const options = {
512
642
  attributions: link.getMetadata('attribution') ||
513
643
  this.getData().getMetadata('attribution'),
514
644
  crossOrigin: this.crossOrigin_,
515
645
  url,
516
646
  };
647
+ const tileLoadFunction = this.createLoadFunction_(createTileLoadFunction, link);
648
+ if (tileLoadFunction && link.rel !== 'pmtiles') {
649
+ options.tileLoadFunction = tileLoadFunction;
650
+ }
517
651
  const updateOptions = async (type, options) => {
518
652
  if (this.getSourceOptions_) {
519
653
  options = await this.getSourceOptions_(type, options, link);
@@ -522,28 +656,82 @@ class STACLayer extends LayerGroup {
522
656
  };
523
657
  const sources = [];
524
658
  switch (link.rel) {
525
- case 'pmtiles':
526
- const p = new pmtiles.PMTiles(options.url);
527
- const headers = await p.getHeader();
528
- let source;
529
- switch (headers.tileType) {
659
+ case 'pmtiles': {
660
+ const snapshot = JSON.stringify(options);
661
+ /** @type {*} */
662
+ let pmOptions = await updateOptions(SourceType.PMTiles, options);
663
+ // Whether getSourceOptions reacted to SourceType.PMTiles,
664
+ // see the backward compatibility handling below
665
+ const handled = JSON.stringify(pmOptions) !== snapshot;
666
+ const headers = this.getRequestHeadersFor_(pmOptions.url, link);
667
+ const withHeaders = (url) => headers && typeof url === 'string'
668
+ ? new pmtiles.FetchSource(url, new Headers(headers))
669
+ : url;
670
+ let pmtilesHeader;
671
+ try {
672
+ const p = new pmtiles.PMTiles(withHeaders(pmOptions.url));
673
+ pmtilesHeader = await p.getHeader();
674
+ }
675
+ catch (error) {
676
+ this.handleError_(error);
677
+ return;
678
+ }
679
+ let type;
680
+ switch (pmtilesHeader.tileType) {
530
681
  case pmtiles.TileType.Mvt:
531
- source = new PMTilesVectorSource(await updateOptions(SourceType.PMTilesVector, options));
682
+ type = SourceType.PMTilesVector;
532
683
  break;
533
684
  case pmtiles.TileType.Avif:
534
685
  case pmtiles.TileType.Jpeg:
535
686
  case pmtiles.TileType.Png:
536
687
  case pmtiles.TileType.Webp:
537
- source = new PMTilesRasterSource(await updateOptions(SourceType.PMTilesRaster, options));
688
+ type = SourceType.PMTilesRaster;
538
689
  break;
539
690
  default:
540
691
  return; // Unsupported
541
692
  }
693
+ if (!handled) {
694
+ // Backward compatibility for getSourceOptions callbacks that don't
695
+ // handle SourceType.PMTiles yet: call them with the deprecated
696
+ // type-specific source type once the tile type is known.
697
+ // As before v1.6.0, these rewrites don't apply to the tile type
698
+ // sniff above. TODO: Remove in 2.0.0
699
+ pmOptions = await updateOptions(type, pmOptions);
700
+ }
701
+ pmOptions.url = withHeaders(pmOptions.url);
702
+ const source = type === SourceType.PMTilesVector
703
+ ? new PMTilesVectorSource(pmOptions)
704
+ : new PMTilesRasterSource(pmOptions);
542
705
  sources.push(source);
543
706
  break;
544
- case 'tilejson':
545
- sources.push(new TileJSON(await updateOptions(SourceType.TileJSON, options)));
707
+ }
708
+ case 'tilejson': {
709
+ /** @type {*} */
710
+ const tjOptions = await updateOptions(SourceType.TileJSON, options);
711
+ if (tjOptions.jsonp || tjOptions.tileJSON) {
712
+ // Let the source load the manifest itself (e.g. via JSONP)
713
+ sources.push(new TileJSON(tjOptions));
714
+ break;
715
+ }
716
+ // Load the manifest through the request function so that
717
+ // credentials are attached
718
+ let tileJSON;
719
+ try {
720
+ tileJSON = await this.fetch_(tjOptions.url, 'json', link);
721
+ }
722
+ catch (error) {
723
+ this.handleError_(error);
724
+ return;
725
+ }
726
+ if (isObject(tileJSON) && Array.isArray(tileJSON.tiles)) {
727
+ // The tile templates from the manifest must be rewritten as well
728
+ tileJSON.tiles = tileJSON.tiles.map((template) => this.getRequestUrlFor_(template, link));
729
+ }
730
+ delete tjOptions.url;
731
+ tjOptions.tileJSON = tileJSON;
732
+ sources.push(new TileJSON(tjOptions));
546
733
  break;
734
+ }
547
735
  case 'wms':
548
736
  if (!Array.isArray(link['wms:layers'])) {
549
737
  break;
@@ -570,7 +758,7 @@ class STACLayer extends LayerGroup {
570
758
  }
571
759
  break;
572
760
  case 'wmts':
573
- const wmtsCapabilities = await this.getWmtsCapabilities_(url, link['wmts:encoding']);
761
+ const wmtsCapabilities = await this.getWmtsCapabilities_(url, link, link['wmts:encoding']);
574
762
  if (!wmtsCapabilities) {
575
763
  return;
576
764
  }
@@ -594,6 +782,10 @@ class STACLayer extends LayerGroup {
594
782
  if (opts === null) {
595
783
  continue;
596
784
  }
785
+ if (wmtsOptions.tileLoadFunction) {
786
+ // Not passed through by optionsFromCapabilities
787
+ opts.tileLoadFunction = wmtsOptions.tileLoadFunction;
788
+ }
597
789
  if (typeof link.uriTemplate === 'string') {
598
790
  let uriTemplate = link.uriTemplate;
599
791
  const vars = isObject(link.variables) ? link.variables : {};
@@ -617,7 +809,7 @@ class STACLayer extends LayerGroup {
617
809
  }
618
810
  }
619
811
  delete opts.urls;
620
- opts.url = uriTemplate;
812
+ opts.url = this.getRequestUrlFor_(uriTemplate, link);
621
813
  }
622
814
  sources.push(new WMTS(opts));
623
815
  }
@@ -628,34 +820,42 @@ class STACLayer extends LayerGroup {
628
820
  default:
629
821
  return;
630
822
  }
631
- return sources.map((source) => {
823
+ return await Promise.all(sources.map(async (source) => {
632
824
  let layer;
633
825
  if (source instanceof VectorTileSource) {
634
- layer = new VectorTileLayer({
635
- source,
636
- declutter: true,
637
- });
826
+ const layerOptions = await this.updateLayerOptions_(LayerType.VectorTile, { source, declutter: true }, link);
827
+ layer = new VectorTileLayer(layerOptions);
638
828
  }
639
829
  else if (source instanceof PMTilesRasterSource) {
640
- layer = new WebGLTileLayer({ source });
830
+ const layerOptions = await this.updateLayerOptions_(LayerType.WebGLTile, { source }, link);
831
+ layer = new WebGLTileLayer(layerOptions);
641
832
  }
642
833
  else {
643
- layer = new TileLayer({ source });
834
+ const layerOptions = await this.updateLayerOptions_(LayerType.Tile, { source }, link);
835
+ layer = new TileLayer(layerOptions);
644
836
  }
645
837
  this.addLayer_(layer, link);
646
838
  return layer;
647
- });
839
+ }));
648
840
  }
649
841
  /**
650
842
  * @param {Asset} [asset] A STAC Asset
843
+ * @param {boolean} [autoDisplay] Whether the asset was chosen automatically
844
+ * (not explicitly requested): skip it silently instead of reporting an
845
+ * error when it can't be displayed within the configured limits.
651
846
  * @return {Promise<Layer|undefined>} Resolves with a Layer or undefined when complete.
652
847
  * @private
653
848
  */
654
- async addGeoTiff_(asset) {
849
+ async addGeoTiff_(asset, autoDisplay = false) {
655
850
  if (this.buildTileUrlTemplate_ && !this.useTileLayerAsFallback_) {
656
- return await this.addTileLayerForImagery_(asset);
851
+ const layer = await this.addTileLayerForImagery_(asset);
852
+ // If no tile server URL was provided for the asset, continue with client-side rendering
853
+ if (layer) {
854
+ return layer;
855
+ }
657
856
  }
658
857
  const sourceInfo = getGeoTiffSourceInfoFromAsset(asset, this.bands_);
858
+ sourceInfo.url = this.getRequestUrlFor_(sourceInfo.url, asset);
659
859
  /**
660
860
  * @type {import("ol/source/GeoTIFF.js").Options}
661
861
  */
@@ -668,10 +868,14 @@ class STACLayer extends LayerGroup {
668
868
  if (projection) {
669
869
  options.projection = projection;
670
870
  }
671
- const classificationStyle = getClassificationStyle(asset, sourceInfo.bands);
672
- if (classificationStyle) {
871
+ const metadataStyle = getGeoTiffStyleFromAsset(asset, sourceInfo, this.defaultColormap_);
872
+ if (metadataStyle) {
673
873
  options.normalize = false;
674
874
  }
875
+ const headers = this.getRequestHeadersFor_(sourceInfo.url, asset);
876
+ if (headers) {
877
+ options.sourceOptions = { headers };
878
+ }
675
879
  if (this.getSourceOptions_) {
676
880
  // @ts-ignore
677
881
  options = await this.getSourceOptions_(SourceType.GeoTIFF, options, asset);
@@ -691,34 +895,51 @@ class STACLayer extends LayerGroup {
691
895
  });
692
896
  try {
693
897
  await status;
694
- const layerOptions = { source };
898
+ if (this.checkDisplayLimit_(source, asset, autoDisplay)) {
899
+ return;
900
+ }
901
+ /**
902
+ * @type {import("ol/layer/WebGLTile.js").Options}
903
+ */
904
+ let layerOptions = { source };
695
905
  if (this.style_) {
696
906
  layerOptions.style = this.style_;
697
907
  }
698
- else if (classificationStyle) {
699
- layerOptions.style = classificationStyle;
908
+ else if (metadataStyle) {
909
+ layerOptions.style = metadataStyle;
700
910
  }
911
+ layerOptions = await this.updateLayerOptions_(LayerType.WebGLTile, layerOptions, asset);
701
912
  const layer = new WebGLTileLayer(layerOptions);
702
913
  this.addLayer_(layer, asset);
703
914
  return layer;
704
915
  }
705
916
  catch (error) {
706
917
  if (this.useTileLayerAsFallback_) {
707
- return await this.addTileLayerForImagery_(asset);
918
+ const layer = await this.addTileLayerForImagery_(asset);
919
+ if (layer) {
920
+ return layer;
921
+ }
708
922
  }
709
923
  this.handleError_(error);
710
924
  }
711
925
  }
712
926
  /**
713
927
  * @param {Asset|Link} [data] A STAC Asset or Link
714
- * @return {Promise<TileLayer>} Resolves with a TileLayer when complete.
928
+ * @return {Promise<TileLayer|undefined>} Resolves with a TileLayer, or undefined if no tile server URL was provided.
715
929
  * @private
716
930
  */
717
931
  async addTileLayerForImagery_(data) {
932
+ if (typeof this.buildTileUrlTemplate_ !== 'function') {
933
+ return;
934
+ }
718
935
  let url = this.buildTileUrlTemplate_(data);
719
936
  if (url instanceof Promise) {
720
937
  url = await url;
721
938
  }
939
+ if (!url) {
940
+ return;
941
+ }
942
+ url = this.getRequestUrlFor_(url, data);
722
943
  /**
723
944
  * @type {import("ol/source/XYZ.js").Options}
724
945
  */
@@ -726,15 +947,33 @@ class STACLayer extends LayerGroup {
726
947
  crossOrigin: this.crossOrigin_,
727
948
  url,
728
949
  };
950
+ const tileLoadFunction = this.createLoadFunction_(createTileLoadFunction, data);
951
+ if (tileLoadFunction) {
952
+ options.tileLoadFunction = tileLoadFunction;
953
+ }
729
954
  if (this.getSourceOptions_) {
730
955
  options = await this.getSourceOptions_(SourceType.XYZ, options, data);
731
956
  }
732
- const layer = new TileLayer({
733
- source: new XYZ(options),
734
- });
957
+ const layerOptions = await this.updateLayerOptions_(LayerType.Tile, { source: new XYZ(options) }, data);
958
+ const layer = new TileLayer(layerOptions);
735
959
  this.addLayer_(layer, data);
736
960
  return layer;
737
961
  }
962
+ /**
963
+ * Passes the layer options through the `getLayerOptions` function, if given.
964
+ *
965
+ * @param {LayerType} type The type of the layer that is going to be created.
966
+ * @param {LayerOptions} options The layer options.
967
+ * @param {Asset|Link} reference The STAC Asset or Link the layer is created for.
968
+ * @return {Promise<*>} The updated layer options.
969
+ * @private
970
+ */
971
+ async updateLayerOptions_(type, options, reference) {
972
+ if (this.getLayerOptions_) {
973
+ options = await this.getLayerOptions_(type, options, reference);
974
+ }
975
+ return options;
976
+ }
738
977
  /**
739
978
  * @param {Layer|LayerGroup} [layer] A Layer to add to the LayerGroup
740
979
  * @param {STACObject} [data] The STAC object, can be any class exposed by stac-js
@@ -767,7 +1006,7 @@ class STACLayer extends LayerGroup {
767
1006
  geojson = data.toGeoJSON(fixAntimeridian);
768
1007
  }
769
1008
  if (geojson) {
770
- const layer = this.createGeoJsonLayer_(geojson, getBoundsStyle(this.boundsStyle_, this), this.displayFootprint_);
1009
+ const layer = new VectorLayer(this.getGeoJsonLayerOptions_(geojson, getBoundsStyle(this.boundsStyle_, this), this.displayFootprint_));
771
1010
  layer.set('bounds', true);
772
1011
  layer.on('change', () => this.setMap_(layer.getMapInternal()));
773
1012
  this.addLayer_(layer, data, 1);
@@ -782,8 +1021,9 @@ class STACLayer extends LayerGroup {
782
1021
  */
783
1022
  async addGeoJson_(asset) {
784
1023
  try {
785
- const geojson = await this.fetch_(asset.getAbsoluteUrl());
786
- const layer = this.createGeoJsonLayer_(geojson);
1024
+ const geojson = await this.fetch_(this.getRequestUrlFor_(asset.getAbsoluteUrl(), asset), 'json', asset);
1025
+ const layerOptions = await this.updateLayerOptions_(LayerType.Vector, this.getGeoJsonLayerOptions_(geojson), asset);
1026
+ const layer = new VectorLayer(layerOptions);
787
1027
  this.addLayer_(layer, asset);
788
1028
  return layer;
789
1029
  }
@@ -792,15 +1032,15 @@ class STACLayer extends LayerGroup {
792
1032
  }
793
1033
  }
794
1034
  /**
795
- * Creates a GeoJSON vector layer from the given GeoJSON object.
1035
+ * Creates the options for a GeoJSON vector layer from the given GeoJSON object.
796
1036
  *
797
1037
  * @param {GeoJSON} [geojson] The GeoJSON object.
798
1038
  * @param {Style} [style] The style for the layer.
799
1039
  * @param {boolean} [visible] Whether the layer is visible.
800
- * @return {VectorLayer} The new vector layer.
1040
+ * @return {import("ol/layer/Vector.js").Options} The vector layer options.
801
1041
  * @private
802
1042
  */
803
- createGeoJsonLayer_(geojson, style = null, visible = true) {
1043
+ getGeoJsonLayerOptions_(geojson, style = null, visible = true) {
804
1044
  const format = new GeoJSON();
805
1045
  const source = new VectorSource({
806
1046
  format,
@@ -815,7 +1055,7 @@ class STACLayer extends LayerGroup {
815
1055
  if (!style) {
816
1056
  style = defaultCollectionStyle;
817
1057
  }
818
- return new VectorLayer({ source, style, visible });
1058
+ return { source, style, visible };
819
1059
  }
820
1060
  /**
821
1061
  * Adds GeoJSON labels and GeoTIFF source imagery to the map based on the label extension.
@@ -850,7 +1090,7 @@ class STACLayer extends LayerGroup {
850
1090
  if (labelAsset && sourceLinks.length > 0) {
851
1091
  const promises = sourceLinks.map(async (link) => {
852
1092
  try {
853
- const response = await this.fetch_(link.getAbsoluteUrl());
1093
+ const response = await this.fetch_(this.getRequestUrlFor_(link.getAbsoluteUrl(), link), 'json', link);
854
1094
  const stac = create(response);
855
1095
  return stac;
856
1096
  }
@@ -870,17 +1110,78 @@ class STACLayer extends LayerGroup {
870
1110
  this.handleError_(error);
871
1111
  }
872
1112
  }
873
- async addGeoZarr_(asset) {
1113
+ /**
1114
+ * Checks the `maxDisplayPixels` limit for the given source.
1115
+ * Returns `true` when the layer must not be added: automatically chosen
1116
+ * assets are limited silently, for explicitly requested assets an error
1117
+ * is thrown so that callers can fall back or report it.
1118
+ * @param {import('ol/source/Tile.js').default} source The configured (ready) source.
1119
+ * @param {Asset} asset The asset the source was created for.
1120
+ * @param {boolean} autoDisplay Whether the asset was chosen automatically.
1121
+ * @return {boolean} `true` if the asset must not be displayed.
1122
+ * @private
1123
+ */
1124
+ checkDisplayLimit_(source, asset, autoDisplay) {
1125
+ if (!exceedsDisplayLimit(source, this.maxDisplayPixels_)) {
1126
+ return false;
1127
+ }
1128
+ if (!autoDisplay) {
1129
+ const megapixels = Math.ceil(getDisplayPixels(source) / 1048576);
1130
+ const error = new Error(`Asset ${asset.getKey()} is too large to display safely` +
1131
+ ` (~${megapixels} megapixels at the coarsest resolution);` +
1132
+ ` set the maxDisplayPixels option to display it anyway`);
1133
+ // Allows applications to detect the error without matching the message
1134
+ error.name = 'DisplayLimitError';
1135
+ throw error;
1136
+ }
1137
+ return true;
1138
+ }
1139
+ /**
1140
+ * Adds a layer for a GeoZarr asset.
1141
+ * @param {Asset} asset The Zarr asset to show.
1142
+ * @param {boolean} [autoDisplay] Whether the asset was chosen automatically
1143
+ * (not explicitly requested): skip it silently instead of reporting an
1144
+ * error when it doesn't declare what to render or can't be displayed
1145
+ * within the configured limits.
1146
+ * @return {Promise<Layer|undefined>} The layer, if one was added.
1147
+ * @private
1148
+ */
1149
+ async addGeoZarr_(asset, autoDisplay = false) {
874
1150
  if (this.buildTileUrlTemplate_ && !this.useTileLayerAsFallback_) {
875
- return await this.addTileLayerForImagery_(asset);
1151
+ const layer = await this.addTileLayerForImagery_(asset);
1152
+ // If no tile server URL was provided for the asset, continue with client-side rendering
1153
+ if (layer) {
1154
+ return layer;
1155
+ }
876
1156
  }
877
1157
  let options = getGeoZarrSourceOptionsFromAsset(asset, this.bands_);
1158
+ // Web-Optimized Zarr describes its bands in the store metadata,
1159
+ // everything else must declare what to render in the STAC metadata
1160
+ // (or provide it through getSourceOptions for explicitly selected assets)
1161
+ const declaresRendering = () => asset.isType(wozMediaTypes) ||
1162
+ Boolean(options.variable) ||
1163
+ Boolean(options.bands && options.bands.length > 0);
1164
+ if (autoDisplay && !declaresRendering()) {
1165
+ return;
1166
+ }
1167
+ options.url = this.getRequestUrlFor_(options.url, asset);
1168
+ const projection = await getProjection(asset);
1169
+ if (projection) {
1170
+ options.projection = projection;
1171
+ }
1172
+ const headers = this.getRequestHeadersFor_(options.url, asset);
1173
+ if (headers) {
1174
+ options.storeOptions = { headers };
1175
+ }
878
1176
  if (this.getSourceOptions_) {
879
1177
  // @ts-ignore
880
1178
  options = await this.getSourceOptions_(SourceType.GeoZarr, options, asset);
881
1179
  }
882
1180
  try {
883
- const GeoZarr = (await import('ol/source/GeoZarr.js')).default;
1181
+ if (!declaresRendering()) {
1182
+ throw new Error(`Asset ${asset.getKey()} declares neither bands nor a datacube variable to render`);
1183
+ }
1184
+ const GeoZarr = (await import('../source/GeoZarr.js')).default;
884
1185
  const source = new GeoZarr(options);
885
1186
  await new Promise((resolve, reject) => {
886
1187
  source.on('change', () => {
@@ -892,17 +1193,33 @@ class STACLayer extends LayerGroup {
892
1193
  }
893
1194
  });
894
1195
  });
895
- const layerOptions = { source };
1196
+ if (this.checkDisplayLimit_(source, asset, autoDisplay)) {
1197
+ return;
1198
+ }
1199
+ /**
1200
+ * @type {import("ol/layer/WebGLTile.js").Options}
1201
+ */
1202
+ let layerOptions = { source };
896
1203
  if (this.style_) {
897
1204
  layerOptions.style = this.style_;
898
1205
  }
1206
+ else {
1207
+ const style = getGeoZarrStyleFromAsset(asset, options, this.defaultColormap_);
1208
+ if (style) {
1209
+ layerOptions.style = style;
1210
+ }
1211
+ }
1212
+ layerOptions = await this.updateLayerOptions_(LayerType.WebGLTile, layerOptions, asset);
899
1213
  const layer = new WebGLTileLayer(layerOptions);
900
1214
  this.addLayer_(layer, asset);
901
1215
  return layer;
902
1216
  }
903
1217
  catch (error) {
904
1218
  if (this.useTileLayerAsFallback_) {
905
- return await this.addTileLayerForImagery_(asset);
1219
+ const layer = await this.addTileLayerForImagery_(asset);
1220
+ if (layer) {
1221
+ return layer;
1222
+ }
906
1223
  }
907
1224
  this.handleError_(error);
908
1225
  }
@@ -947,7 +1264,7 @@ class STACLayer extends LayerGroup {
947
1264
  if (ref.isType(geotiffMediaTypes)) {
948
1265
  return await this.addGeoTiff_(ref);
949
1266
  }
950
- if (ref.isType(wozMediaTypes)) {
1267
+ if (ref.isType(zarrMediaTypes)) {
951
1268
  return await this.addGeoZarr_(ref);
952
1269
  }
953
1270
  if (ref.canBrowserDisplayImage()) {
@@ -979,14 +1296,13 @@ class STACLayer extends LayerGroup {
979
1296
  // Find a GeoTiff asset that we can visualize
980
1297
  const geotiff = data.getDefaultGeoFile('geotiff', true, !this.displayGeoTiffByDefault_);
981
1298
  if (geotiff) {
982
- layer = await this.addGeoTiff_(geotiff);
1299
+ layer = await this.addGeoTiff_(geotiff, true);
983
1300
  }
984
1301
  }
985
1302
  if (this.displayOverview_ && !layer) {
986
- // Find a Web-Optimized GeoZarr asset that we can visualize
987
- const geozarr = data.getDefaultGeoFile('geozarr', true, true);
1303
+ const geozarr = data.getDefaultGeoFile('geozarr', true, false);
988
1304
  if (geozarr) {
989
- layer = await this.addGeoZarr_(geozarr);
1305
+ layer = await this.addGeoZarr_(geozarr, true);
990
1306
  }
991
1307
  }
992
1308
  // Show web map links if available
@@ -1072,6 +1388,26 @@ class STACLayer extends LayerGroup {
1072
1388
  }
1073
1389
  }
1074
1390
  }
1391
+ /**
1392
+ * Set the colors of the colormap that is used for continuous single-band
1393
+ * data when neither the STAC metadata nor the `style` option define a
1394
+ * coloring. The colors are evenly distributed over the value range of the
1395
+ * data (e.g. from the STAC `statistics`). Set to `null` to stretch the
1396
+ * data to grayscale instead.
1397
+ * @param {Array<import("ol/color.js").Color|string>|null} colormap The colors of the colormap.
1398
+ * @return {Promise} Resolves once the layers are updated.
1399
+ * @api
1400
+ */
1401
+ async setDefaultColormap(colormap) {
1402
+ if (colormap === this.defaultColormap_) {
1403
+ return;
1404
+ }
1405
+ this.defaultColormap_ = colormap || null;
1406
+ // The layers are recreated, as switching between a styled and an
1407
+ // unstyled visualization also changes how the sources are configured
1408
+ // (e.g. the normalization of GeoTIFF sources)
1409
+ await this.updateLayers();
1410
+ }
1075
1411
  /**
1076
1412
  * Update the assets to be rendered.
1077
1413
  * @param {Array<string|Asset>|null} assets The assets to show.
@@ -1207,7 +1543,7 @@ class STACLayer extends LayerGroup {
1207
1543
  bbox = unionBoundingBox(bboxes);
1208
1544
  }
1209
1545
  if (bbox) {
1210
- return transformExtent(bbox, 'EPSG:4326', view.getProjection());
1546
+ return toOlExtent(bbox, view.getProjection());
1211
1547
  }
1212
1548
  }
1213
1549
  getLayerState() {
@@ -1245,18 +1581,19 @@ class STACLayer extends LayerGroup {
1245
1581
  /**
1246
1582
  * Gets the WMTS capabilities from the given web-map-links URL.
1247
1583
  * @param {string} url Base URL for the WMTS
1584
+ * @param {Link} link The web map link the request is made for.
1248
1585
  * @param {string} [encoding] The request encoding, either `kvp` (default) or `rest`.
1249
1586
  * @return {Promise<Object|null>} Resolves with the WMTS Capabilities object
1250
1587
  * @private
1251
1588
  */
1252
- async getWmtsCapabilities_(url, encoding = 'kvp') {
1589
+ async getWmtsCapabilities_(url, link, encoding = 'kvp') {
1253
1590
  try {
1254
1591
  const urlObj = new URL(url);
1255
1592
  if (encoding !== 'rest') {
1256
1593
  urlObj.searchParams.set('service', 'wmts');
1257
1594
  urlObj.searchParams.set('request', 'GetCapabilities');
1258
1595
  }
1259
- const response = await this.fetch_(urlObj.toString(), 'text');
1596
+ const response = await this.fetch_(urlObj.toString(), 'text', link);
1260
1597
  return new WMTSCapabilities().read(response);
1261
1598
  }
1262
1599
  catch (_) {