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/layer/STAC.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @module ol/layer/STAC
3
3
  */
4
+ import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
4
5
  import { isEmpty } from 'ol/extent.js';
5
6
  import GeoJSON from 'ol/format/GeoJSON.js';
6
7
  import WMTSCapabilities from 'ol/format/WMTSCapabilities.js';
@@ -18,16 +19,17 @@ import VectorSource from 'ol/source/Vector.js';
18
19
  import VectorTileSource from 'ol/source/VectorTile.js';
19
20
  import WMTS, { optionsFromCapabilities } from 'ol/source/WMTS.js';
20
21
  import XYZ from 'ol/source/XYZ.js';
21
- import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
22
22
  import * as pmtiles from 'pmtiles';
23
23
  import create, { Asset } from 'stac-js';
24
24
  import { fixGeoJson, toGeoJSON, unionBoundingBox } from 'stac-js/src/geo.js';
25
- import { geojsonMediaType, geotiffMediaTypes, wozMediaTypes, } from 'stac-js/src/mediatypes.js';
25
+ import { geojsonMediaType, geotiffMediaTypes, wozMediaTypes, zarrMediaTypes, } from 'stac-js/src/mediatypes.js';
26
26
  import { isObject } from 'stac-js/src/utils.js';
27
27
  import ErrorEvent from '../events/ErrorEvent.js';
28
+ import { createImageLoadFunction, createTileLoadFunction } from '../http.js';
28
29
  import { getProjection } from '../proj.js';
29
30
  import SourceType from '../source/type.js';
30
- import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsStyle, getClassificationStyle, getGeoTiffSourceInfoFromAsset, getGeoZarrSourceOptionsFromAsset, getSpecificWebMapUrl, isScalar, toContinuousBBox, toOlExtent, } from '../util.js';
31
+ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, exceedsDisplayLimit, getBoundsStyle, getDisplayPixels, getGeoTiffSourceInfoFromAsset, getGeoTiffStyleFromAsset, getGeoZarrSourceOptionsFromAsset, getGeoZarrStyleFromAsset, getSpecificWebMapUrl, getWebMapLinks, isScalar, toContinuousBBox, toOlExtent, } from '../util.js';
32
+ import LayerType from './type.js';
31
33
  /**
32
34
  * @typedef {import("ol/extent.js").Extent} Extent
33
35
  */
@@ -55,6 +57,18 @@ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsS
55
57
  /**
56
58
  * @typedef {import('../source/type.js').SourceOptions} SourceOptions
57
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
+ */
58
72
  /**
59
73
  * @typedef {Object} Options
60
74
  * @property {string} [url] The STAC URL. Any of `url` and `data` must be provided.
@@ -75,8 +89,15 @@ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsS
75
89
  * Optional function that can be used to configure the underlying sources. The function can do any additional work
76
90
  * and return the completed options or a promise for the same. The function will be called with the current source options
77
91
  * and the STAC Asset or Link.
78
- * This can be useful for adding auth information such as an API token, either via query parameter or HTTP headers.
79
- * 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.
80
101
  * @property {boolean} [displayFootprint=true] Allows to hide the footprints (bounding box/geometry) of the STAC object
81
102
  * by default.
82
103
  * @property {boolean} [displayGeoTiffByDefault=false] Allow to choose non-cloud-optimized GeoTiffs as default image to show,
@@ -85,8 +106,16 @@ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsS
85
106
  * i.e. assets with any of the roles `thumbnail`, `overview`, or a link with relation type `preview`.
86
107
  * The previews are usually not covering the full extents and as such may be placed incorrectly on the map.
87
108
  * For performance reasons, it is recommended to enable this option if you pass in STAC API Items instead of `displayOverview`.
88
- * @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,
89
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.
90
119
  * @property {string|boolean|Array<Link|string>} [displayWebMapLink=false] Allow to display a layer
91
120
  * based on the information provided through the web map links extension.
92
121
  * If an array of links or link ids (property `id` in a Link Object) is provided, all corresponding layers will be shown.
@@ -94,13 +123,19 @@ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsS
94
123
  * it lets this library choose a web map link to show, but only if no other data is shown.
95
124
  * To disable the functionality set this to `false`.
96
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.
97
130
  * @property {Style} [boundsStyle] The style for the overall bounds / footprint.
98
131
  * @property {Style} [collectionStyle] The style for individual children in a list of STAC Items or Collections.
99
132
  * @property {null|string} [crossOrigin] For thumbnails: The `crossOrigin` attribute for loaded images / tiles.
100
133
  * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.
101
- * @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),
102
135
  * which will be used instead of the client-side GeoTIFF rendering (except if `useTileLayerAsFallback` is `true`).
103
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.
104
139
  * @property {boolean} [useTileLayerAsFallback=false] Uses the given URL template only when the client-side GeoTIFF rendering fails.
105
140
  * @property {number} [opacity=1] Opacity (0, 1).
106
141
  * @property {boolean} [visible=true] Visibility.
@@ -124,6 +159,29 @@ import { LABEL_EXTENSION, defaultBoundsStyle, defaultCollectionStyle, getBoundsS
124
159
  * @property {function(string,string):(*)} [httpRequestFn=null] Sets a custom function to make HTTP requests with.
125
160
  * The first parameter is the URL to request and the output is a promise that resolves with the response body.
126
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, boolean):(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), the URL, and whether the URL is a
176
+ * tile URL template. It returns the new URL or `null` to keep the URL unchanged.
177
+ * The rewrite is applied to the initial source URL before `getSourceOptions` is called;
178
+ * templates discovered in fetched documents (TileJSON manifests, WMTS capabilities) are
179
+ * rewritten afterwards and are not passed to `getSourceOptions`.
180
+ * For tiled sources the tile URL template is rewritten, not the individual tile URLs.
181
+ * URL templates originate from XYZ web map links, TileJSON manifests, WMTS links
182
+ * (`uriTemplate`) and capabilities, and `buildTileUrlTemplate`. The returned URL must keep
183
+ * the template placeholders such as `{z}` unchanged, i.e. they must not be percent-encoded
184
+ * (e.g. through URL normalization).
127
185
  */
128
186
  /**
129
187
  * @classdesc
@@ -159,6 +217,21 @@ class STACLayer extends LayerGroup {
159
217
  * @private
160
218
  */
161
219
  this.getSourceOptions_ = options.getSourceOptions;
220
+ /**
221
+ * @type {function(LayerType, LayerOptions, (Asset|Link)):(LayerOptions|Promise<LayerOptions>)}
222
+ * @private
223
+ */
224
+ this.getLayerOptions_ = options.getLayerOptions;
225
+ /**
226
+ * @type {Object<string, string>|function((Asset|Link|STACObject|null), string):(Object<string, string>|null)|null}
227
+ * @private
228
+ */
229
+ this.getRequestHeaders_ = options.getRequestHeaders || null;
230
+ /**
231
+ * @type {function((Asset|Link|STACObject|null), string, boolean):(string|null)|null}
232
+ * @private
233
+ */
234
+ this.getRequestUrl_ = options.getRequestUrl || null;
162
235
  /**
163
236
  * @type {Array<STAC>|null}
164
237
  * @private
@@ -209,7 +282,12 @@ class STACLayer extends LayerGroup {
209
282
  */
210
283
  this.displayWebMapLink_ = options.displayWebMapLink || false;
211
284
  /**
212
- * @type {function((Asset|Link)):Promise<string>|string|null}
285
+ * @type {number|undefined}
286
+ * @private
287
+ */
288
+ this.maxDisplayPixels_ = options.maxDisplayPixels;
289
+ /**
290
+ * @type {function((Asset|Link)):Promise<string|null>|string|null}
213
291
  * @private
214
292
  */
215
293
  this.buildTileUrlTemplate_ = options.buildTileUrlTemplate || null;
@@ -223,6 +301,11 @@ class STACLayer extends LayerGroup {
223
301
  * @private
224
302
  */
225
303
  this.style_ = options.style || null;
304
+ /**
305
+ * @type {Array<import("ol/color.js").Color|string>|null}
306
+ * @private
307
+ */
308
+ this.defaultColormap_ = options.defaultColormap || null;
226
309
  /**
227
310
  * @type {Style}
228
311
  * @private
@@ -268,19 +351,71 @@ class STACLayer extends LayerGroup {
268
351
  if (!options.url) {
269
352
  throw new Error('Either url or data must be provided');
270
353
  }
271
- this.fetch_(options.url)
354
+ this.fetch_(this.getRequestUrlFor_(options.url))
272
355
  .then((data) => this.configure_(data, options.url, options.children, options.assets, options.bands))
273
356
  .catch((error) => this.handleError_(error));
274
357
  }
358
+ /**
359
+ * Rewrites the given URL based on the `getRequestUrl` option.
360
+ *
361
+ * @param {string} url The URL that is requested.
362
+ * @param {Asset|Link|STACObject|null} [ref] The STAC Asset or Link that is shown, if available.
363
+ * @param {boolean} [isTemplate] Whether the URL is a tile URL template with placeholders such as `{z}`.
364
+ * @return {string} The rewritten URL, or the given URL if it is not rewritten.
365
+ */
366
+ getRequestUrlFor_(url, ref = null, isTemplate = false) {
367
+ if (typeof this.getRequestUrl_ === 'function' && typeof url === 'string') {
368
+ const newUrl = this.getRequestUrl_(ref, url, isTemplate);
369
+ if (typeof newUrl === 'string' && newUrl.length > 0) {
370
+ return newUrl;
371
+ }
372
+ }
373
+ return url;
374
+ }
375
+ /**
376
+ * Returns the HTTP headers to send for the given URL, based on the
377
+ * `getRequestHeaders` option.
378
+ *
379
+ * @param {string} url The URL that is requested.
380
+ * @param {Asset|Link|STACObject|null} [ref] The STAC Asset or Link that is shown, if available.
381
+ * @return {Object<string, string>|null} The headers, or `null` if there are none.
382
+ */
383
+ getRequestHeadersFor_(url, ref = null) {
384
+ let headers = this.getRequestHeaders_;
385
+ if (typeof headers === 'function') {
386
+ headers = headers(ref, url);
387
+ }
388
+ if (isObject(headers) && Object.keys(headers).length > 0) {
389
+ return /** @type {Object<string, string>} */ (headers);
390
+ }
391
+ return null;
392
+ }
393
+ /**
394
+ * Creates a load function for images or tiles that attaches the headers
395
+ * from the `getRequestHeaders` option and reports errors through the
396
+ * layer's error event, or `undefined` if no headers are configured.
397
+ *
398
+ * @param {function(GetHeadersFn, OnErrorFn=):LoadFunction} factory `createImageLoadFunction` or `createTileLoadFunction`.
399
+ * @param {Asset|Link|STACObject|null} ref The STAC Asset or Link that is shown, if available.
400
+ * @return {LoadFunction|undefined} The load function.
401
+ */
402
+ createLoadFunction_(factory, ref) {
403
+ if (!this.getRequestHeaders_) {
404
+ return undefined;
405
+ }
406
+ return factory((url) => this.getRequestHeadersFor_(url, ref), (error) => this.handleError_(error));
407
+ }
275
408
  /**
276
409
  * Default function make HTTP requests with.
277
410
  *
278
411
  * @param {string} url The URL to request and the output is a promise that resolves with the response body.
279
412
  * @param {string} responseType The return type, either `json` (default) or `text`.
413
+ * @param {Asset|Link|STACObject|null} [ref] The STAC Asset or Link the request is made for, if available.
280
414
  * @return {Promise<*>} The (parsed) response body.
281
415
  */
282
- async fetch_(url, responseType = 'json') {
283
- const response = await fetch(url);
416
+ async fetch_(url, responseType = 'json', ref = null) {
417
+ const headers = this.getRequestHeadersFor_(url, ref);
418
+ const response = await fetch(url, headers ? { headers } : undefined);
284
419
  if (!response.ok) {
285
420
  throw new Error(`Unexpected response from ${url}: ${response.status}`);
286
421
  }
@@ -359,7 +494,14 @@ class STACLayer extends LayerGroup {
359
494
  }
360
495
  this.set('stac', stac);
361
496
  this.bands_ = bands;
362
- this.boundsLayer_ = this.addFootprint_();
497
+ try {
498
+ this.boundsLayer_ = this.addFootprint_();
499
+ }
500
+ catch (error) {
501
+ // Don't fail the whole layer if the footprint can't be shown,
502
+ // the map can still zoom to the bounding box of the STAC entity
503
+ this.handleError_(error);
504
+ }
363
505
  const updateBoundsStyle = () => {
364
506
  if (this.boundsLayer_) {
365
507
  this.boundsLayer_.setStyle(getBoundsStyle(this.boundsStyle_, this));
@@ -478,18 +620,21 @@ class STACLayer extends LayerGroup {
478
620
  * @type {import("ol/source/ImageStatic.js").Options}
479
621
  */
480
622
  let options = {
481
- url: image.getAbsoluteUrl(),
623
+ url: this.getRequestUrlFor_(image.getAbsoluteUrl(), image),
482
624
  projection,
483
625
  imageExtent: toOlExtent(bbox, projection),
484
626
  crossOrigin: this.crossOrigin_,
485
627
  };
628
+ const imageLoadFunction = this.createLoadFunction_(createImageLoadFunction, image);
629
+ if (imageLoadFunction) {
630
+ options.imageLoadFunction = imageLoadFunction;
631
+ }
486
632
  if (this.getSourceOptions_) {
487
633
  // @ts-ignore
488
634
  options = await this.getSourceOptions_(SourceType.ImageStatic, options, image);
489
635
  }
490
- const layer = new ImageLayer({
491
- source: new StaticImage(options),
492
- });
636
+ const layerOptions = await this.updateLayerOptions_(LayerType.Image, { source: new StaticImage(options) }, image);
637
+ const layer = new ImageLayer(layerOptions);
493
638
  this.addLayer_(layer, image);
494
639
  return layer;
495
640
  }
@@ -503,16 +648,21 @@ class STACLayer extends LayerGroup {
503
648
  */
504
649
  async addLayerForLink(link) {
505
650
  // Replace any occurances of {s} if possible, otherwise return
506
- const url = getSpecificWebMapUrl(link);
651
+ let url = getSpecificWebMapUrl(link);
507
652
  if (!url) {
508
653
  return;
509
654
  }
655
+ url = this.getRequestUrlFor_(url, link, link.rel === 'xyz');
510
656
  const options = {
511
657
  attributions: link.getMetadata('attribution') ||
512
658
  this.getData().getMetadata('attribution'),
513
659
  crossOrigin: this.crossOrigin_,
514
660
  url,
515
661
  };
662
+ const tileLoadFunction = this.createLoadFunction_(createTileLoadFunction, link);
663
+ if (tileLoadFunction && link.rel !== 'pmtiles') {
664
+ options.tileLoadFunction = tileLoadFunction;
665
+ }
516
666
  const updateOptions = async (type, options) => {
517
667
  if (this.getSourceOptions_) {
518
668
  options = await this.getSourceOptions_(type, options, link);
@@ -521,28 +671,82 @@ class STACLayer extends LayerGroup {
521
671
  };
522
672
  const sources = [];
523
673
  switch (link.rel) {
524
- case 'pmtiles':
525
- const p = new pmtiles.PMTiles(options.url);
526
- const headers = await p.getHeader();
527
- let source;
528
- switch (headers.tileType) {
674
+ case 'pmtiles': {
675
+ const snapshot = JSON.stringify(options);
676
+ /** @type {*} */
677
+ let pmOptions = await updateOptions(SourceType.PMTiles, options);
678
+ // Whether getSourceOptions reacted to SourceType.PMTiles,
679
+ // see the backward compatibility handling below
680
+ const handled = JSON.stringify(pmOptions) !== snapshot;
681
+ const headers = this.getRequestHeadersFor_(pmOptions.url, link);
682
+ const withHeaders = (url) => headers && typeof url === 'string'
683
+ ? new pmtiles.FetchSource(url, new Headers(headers))
684
+ : url;
685
+ let pmtilesHeader;
686
+ try {
687
+ const p = new pmtiles.PMTiles(withHeaders(pmOptions.url));
688
+ pmtilesHeader = await p.getHeader();
689
+ }
690
+ catch (error) {
691
+ this.handleError_(error);
692
+ return;
693
+ }
694
+ let type;
695
+ switch (pmtilesHeader.tileType) {
529
696
  case pmtiles.TileType.Mvt:
530
- source = new PMTilesVectorSource(await updateOptions(SourceType.PMTilesVector, options));
697
+ type = SourceType.PMTilesVector;
531
698
  break;
532
699
  case pmtiles.TileType.Avif:
533
700
  case pmtiles.TileType.Jpeg:
534
701
  case pmtiles.TileType.Png:
535
702
  case pmtiles.TileType.Webp:
536
- source = new PMTilesRasterSource(await updateOptions(SourceType.PMTilesRaster, options));
703
+ type = SourceType.PMTilesRaster;
537
704
  break;
538
705
  default:
539
706
  return; // Unsupported
540
707
  }
708
+ if (!handled) {
709
+ // Backward compatibility for getSourceOptions callbacks that don't
710
+ // handle SourceType.PMTiles yet: call them with the deprecated
711
+ // type-specific source type once the tile type is known.
712
+ // As before v1.6.0, these rewrites don't apply to the tile type
713
+ // sniff above. TODO: Remove in 2.0.0
714
+ pmOptions = await updateOptions(type, pmOptions);
715
+ }
716
+ pmOptions.url = withHeaders(pmOptions.url);
717
+ const source = type === SourceType.PMTilesVector
718
+ ? new PMTilesVectorSource(pmOptions)
719
+ : new PMTilesRasterSource(pmOptions);
541
720
  sources.push(source);
542
721
  break;
543
- case 'tilejson':
544
- sources.push(new TileJSON(await updateOptions(SourceType.TileJSON, options)));
722
+ }
723
+ case 'tilejson': {
724
+ /** @type {*} */
725
+ const tjOptions = await updateOptions(SourceType.TileJSON, options);
726
+ if (tjOptions.jsonp || tjOptions.tileJSON) {
727
+ // Let the source load the manifest itself (e.g. via JSONP)
728
+ sources.push(new TileJSON(tjOptions));
729
+ break;
730
+ }
731
+ // Load the manifest through the request function so that
732
+ // credentials are attached
733
+ let tileJSON;
734
+ try {
735
+ tileJSON = await this.fetch_(tjOptions.url, 'json', link);
736
+ }
737
+ catch (error) {
738
+ this.handleError_(error);
739
+ return;
740
+ }
741
+ if (isObject(tileJSON) && Array.isArray(tileJSON.tiles)) {
742
+ // The tile templates from the manifest must be rewritten as well
743
+ tileJSON.tiles = tileJSON.tiles.map((template) => this.getRequestUrlFor_(template, link, true));
744
+ }
745
+ delete tjOptions.url;
746
+ tjOptions.tileJSON = tileJSON;
747
+ sources.push(new TileJSON(tjOptions));
545
748
  break;
749
+ }
546
750
  case 'wms':
547
751
  if (!Array.isArray(link['wms:layers'])) {
548
752
  break;
@@ -569,7 +773,7 @@ class STACLayer extends LayerGroup {
569
773
  }
570
774
  break;
571
775
  case 'wmts':
572
- const wmtsCapabilities = await this.getWmtsCapabilities_(url, link['wmts:encoding']);
776
+ const wmtsCapabilities = await this.getWmtsCapabilities_(url, link, link['wmts:encoding']);
573
777
  if (!wmtsCapabilities) {
574
778
  return;
575
779
  }
@@ -580,7 +784,7 @@ class STACLayer extends LayerGroup {
580
784
  else if (typeof link['wmts:layer'] === 'string') {
581
785
  layers = [link['wmts:layer']];
582
786
  }
583
- for (const layer of layers) {
787
+ layers: for (const layer of layers) {
584
788
  let wmtsOptions = Object.assign({}, options, {
585
789
  layer,
586
790
  requestEncoding: link['wmts:encoding'] === 'rest' ? 'REST' : 'KVP',
@@ -593,6 +797,10 @@ class STACLayer extends LayerGroup {
593
797
  if (opts === null) {
594
798
  continue;
595
799
  }
800
+ if (wmtsOptions.tileLoadFunction) {
801
+ // Not passed through by optionsFromCapabilities
802
+ opts.tileLoadFunction = wmtsOptions.tileLoadFunction;
803
+ }
596
804
  if (typeof link.uriTemplate === 'string') {
597
805
  let uriTemplate = link.uriTemplate;
598
806
  const vars = isObject(link.variables) ? link.variables : {};
@@ -612,11 +820,15 @@ class STACLayer extends LayerGroup {
612
820
  uriTemplate = uriTemplate.replaceAll(`{${key}}`, String(value));
613
821
  }
614
822
  else {
615
- continue; // We don't know which value to use, so we can't visualize the layer
823
+ // We don't know which value to use, so we can't visualize the layer
824
+ continue layers;
616
825
  }
617
826
  }
618
827
  delete opts.urls;
619
- opts.url = uriTemplate;
828
+ opts.url = this.getRequestUrlFor_(uriTemplate, link, true);
829
+ }
830
+ else if (Array.isArray(opts.urls)) {
831
+ opts.urls = opts.urls.map((u) => this.getRequestUrlFor_(u, link, opts.requestEncoding === 'REST'));
620
832
  }
621
833
  sources.push(new WMTS(opts));
622
834
  }
@@ -627,34 +839,42 @@ class STACLayer extends LayerGroup {
627
839
  default:
628
840
  return;
629
841
  }
630
- return sources.map((source) => {
842
+ return await Promise.all(sources.map(async (source) => {
631
843
  let layer;
632
844
  if (source instanceof VectorTileSource) {
633
- layer = new VectorTileLayer({
634
- source,
635
- declutter: true,
636
- });
845
+ const layerOptions = await this.updateLayerOptions_(LayerType.VectorTile, { source, declutter: true }, link);
846
+ layer = new VectorTileLayer(layerOptions);
637
847
  }
638
848
  else if (source instanceof PMTilesRasterSource) {
639
- layer = new WebGLTileLayer({ source });
849
+ const layerOptions = await this.updateLayerOptions_(LayerType.WebGLTile, { source }, link);
850
+ layer = new WebGLTileLayer(layerOptions);
640
851
  }
641
852
  else {
642
- layer = new TileLayer({ source });
853
+ const layerOptions = await this.updateLayerOptions_(LayerType.Tile, { source }, link);
854
+ layer = new TileLayer(layerOptions);
643
855
  }
644
856
  this.addLayer_(layer, link);
645
857
  return layer;
646
- });
858
+ }));
647
859
  }
648
860
  /**
649
861
  * @param {Asset} [asset] A STAC Asset
862
+ * @param {boolean} [autoDisplay] Whether the asset was chosen automatically
863
+ * (not explicitly requested): skip it silently instead of reporting an
864
+ * error when it can't be displayed within the configured limits.
650
865
  * @return {Promise<Layer|undefined>} Resolves with a Layer or undefined when complete.
651
866
  * @private
652
867
  */
653
- async addGeoTiff_(asset) {
868
+ async addGeoTiff_(asset, autoDisplay = false) {
654
869
  if (this.buildTileUrlTemplate_ && !this.useTileLayerAsFallback_) {
655
- return await this.addTileLayerForImagery_(asset);
870
+ const layer = await this.addTileLayerForImagery_(asset);
871
+ // If no tile server URL was provided for the asset, continue with client-side rendering
872
+ if (layer) {
873
+ return layer;
874
+ }
656
875
  }
657
876
  const sourceInfo = getGeoTiffSourceInfoFromAsset(asset, this.bands_);
877
+ sourceInfo.url = this.getRequestUrlFor_(sourceInfo.url, asset);
658
878
  /**
659
879
  * @type {import("ol/source/GeoTIFF.js").Options}
660
880
  */
@@ -667,10 +887,14 @@ class STACLayer extends LayerGroup {
667
887
  if (projection) {
668
888
  options.projection = projection;
669
889
  }
670
- const classificationStyle = getClassificationStyle(asset, sourceInfo.bands);
671
- if (classificationStyle) {
890
+ const metadataStyle = getGeoTiffStyleFromAsset(asset, sourceInfo, this.defaultColormap_);
891
+ if (metadataStyle) {
672
892
  options.normalize = false;
673
893
  }
894
+ const headers = this.getRequestHeadersFor_(sourceInfo.url, asset);
895
+ if (headers) {
896
+ options.sourceOptions = { headers };
897
+ }
674
898
  if (this.getSourceOptions_) {
675
899
  // @ts-ignore
676
900
  options = await this.getSourceOptions_(SourceType.GeoTIFF, options, asset);
@@ -690,34 +914,51 @@ class STACLayer extends LayerGroup {
690
914
  });
691
915
  try {
692
916
  await status;
693
- const layerOptions = { source };
917
+ if (this.checkDisplayLimit_(source, asset, autoDisplay)) {
918
+ return;
919
+ }
920
+ /**
921
+ * @type {import("ol/layer/WebGLTile.js").Options}
922
+ */
923
+ let layerOptions = { source };
694
924
  if (this.style_) {
695
925
  layerOptions.style = this.style_;
696
926
  }
697
- else if (classificationStyle) {
698
- layerOptions.style = classificationStyle;
927
+ else if (metadataStyle) {
928
+ layerOptions.style = metadataStyle;
699
929
  }
930
+ layerOptions = await this.updateLayerOptions_(LayerType.WebGLTile, layerOptions, asset);
700
931
  const layer = new WebGLTileLayer(layerOptions);
701
932
  this.addLayer_(layer, asset);
702
933
  return layer;
703
934
  }
704
935
  catch (error) {
705
936
  if (this.useTileLayerAsFallback_) {
706
- return await this.addTileLayerForImagery_(asset);
937
+ const layer = await this.addTileLayerForImagery_(asset);
938
+ if (layer) {
939
+ return layer;
940
+ }
707
941
  }
708
942
  this.handleError_(error);
709
943
  }
710
944
  }
711
945
  /**
712
946
  * @param {Asset|Link} [data] A STAC Asset or Link
713
- * @return {Promise<TileLayer>} Resolves with a TileLayer when complete.
947
+ * @return {Promise<TileLayer|undefined>} Resolves with a TileLayer, or undefined if no tile server URL was provided.
714
948
  * @private
715
949
  */
716
950
  async addTileLayerForImagery_(data) {
951
+ if (typeof this.buildTileUrlTemplate_ !== 'function') {
952
+ return;
953
+ }
717
954
  let url = this.buildTileUrlTemplate_(data);
718
955
  if (url instanceof Promise) {
719
956
  url = await url;
720
957
  }
958
+ if (!url) {
959
+ return;
960
+ }
961
+ url = this.getRequestUrlFor_(url, data, true);
721
962
  /**
722
963
  * @type {import("ol/source/XYZ.js").Options}
723
964
  */
@@ -725,15 +966,33 @@ class STACLayer extends LayerGroup {
725
966
  crossOrigin: this.crossOrigin_,
726
967
  url,
727
968
  };
969
+ const tileLoadFunction = this.createLoadFunction_(createTileLoadFunction, data);
970
+ if (tileLoadFunction) {
971
+ options.tileLoadFunction = tileLoadFunction;
972
+ }
728
973
  if (this.getSourceOptions_) {
729
974
  options = await this.getSourceOptions_(SourceType.XYZ, options, data);
730
975
  }
731
- const layer = new TileLayer({
732
- source: new XYZ(options),
733
- });
976
+ const layerOptions = await this.updateLayerOptions_(LayerType.Tile, { source: new XYZ(options) }, data);
977
+ const layer = new TileLayer(layerOptions);
734
978
  this.addLayer_(layer, data);
735
979
  return layer;
736
980
  }
981
+ /**
982
+ * Passes the layer options through the `getLayerOptions` function, if given.
983
+ *
984
+ * @param {LayerType} type The type of the layer that is going to be created.
985
+ * @param {LayerOptions} options The layer options.
986
+ * @param {Asset|Link} reference The STAC Asset or Link the layer is created for.
987
+ * @return {Promise<*>} The updated layer options.
988
+ * @private
989
+ */
990
+ async updateLayerOptions_(type, options, reference) {
991
+ if (this.getLayerOptions_) {
992
+ options = await this.getLayerOptions_(type, options, reference);
993
+ }
994
+ return options;
995
+ }
737
996
  /**
738
997
  * @param {Layer|LayerGroup} [layer] A Layer to add to the LayerGroup
739
998
  * @param {STACObject} [data] The STAC object, can be any class exposed by stac-js
@@ -766,7 +1025,7 @@ class STACLayer extends LayerGroup {
766
1025
  geojson = data.toGeoJSON(fixAntimeridian);
767
1026
  }
768
1027
  if (geojson) {
769
- const layer = this.createGeoJsonLayer_(geojson, getBoundsStyle(this.boundsStyle_, this), this.displayFootprint_);
1028
+ const layer = new VectorLayer(this.getGeoJsonLayerOptions_(geojson, getBoundsStyle(this.boundsStyle_, this), this.displayFootprint_));
770
1029
  layer.set('bounds', true);
771
1030
  layer.on('change', () => this.setMap_(layer.getMapInternal()));
772
1031
  this.addLayer_(layer, data, 1);
@@ -781,8 +1040,9 @@ class STACLayer extends LayerGroup {
781
1040
  */
782
1041
  async addGeoJson_(asset) {
783
1042
  try {
784
- const geojson = await this.fetch_(asset.getAbsoluteUrl());
785
- const layer = this.createGeoJsonLayer_(geojson);
1043
+ const geojson = await this.fetch_(this.getRequestUrlFor_(asset.getAbsoluteUrl(), asset), 'json', asset);
1044
+ const layerOptions = await this.updateLayerOptions_(LayerType.Vector, this.getGeoJsonLayerOptions_(geojson), asset);
1045
+ const layer = new VectorLayer(layerOptions);
786
1046
  this.addLayer_(layer, asset);
787
1047
  return layer;
788
1048
  }
@@ -791,15 +1051,15 @@ class STACLayer extends LayerGroup {
791
1051
  }
792
1052
  }
793
1053
  /**
794
- * Creates a GeoJSON vector layer from the given GeoJSON object.
1054
+ * Creates the options for a GeoJSON vector layer from the given GeoJSON object.
795
1055
  *
796
1056
  * @param {GeoJSON} [geojson] The GeoJSON object.
797
1057
  * @param {Style} [style] The style for the layer.
798
1058
  * @param {boolean} [visible] Whether the layer is visible.
799
- * @return {VectorLayer} The new vector layer.
1059
+ * @return {import("ol/layer/Vector.js").Options} The vector layer options.
800
1060
  * @private
801
1061
  */
802
- createGeoJsonLayer_(geojson, style = null, visible = true) {
1062
+ getGeoJsonLayerOptions_(geojson, style = null, visible = true) {
803
1063
  const format = new GeoJSON();
804
1064
  const source = new VectorSource({
805
1065
  format,
@@ -814,7 +1074,7 @@ class STACLayer extends LayerGroup {
814
1074
  if (!style) {
815
1075
  style = defaultCollectionStyle;
816
1076
  }
817
- return new VectorLayer({ source, style, visible });
1077
+ return { source, style, visible };
818
1078
  }
819
1079
  /**
820
1080
  * Adds GeoJSON labels and GeoTIFF source imagery to the map based on the label extension.
@@ -849,7 +1109,7 @@ class STACLayer extends LayerGroup {
849
1109
  if (labelAsset && sourceLinks.length > 0) {
850
1110
  const promises = sourceLinks.map(async (link) => {
851
1111
  try {
852
- const response = await this.fetch_(link.getAbsoluteUrl());
1112
+ const response = await this.fetch_(this.getRequestUrlFor_(link.getAbsoluteUrl(), link), 'json', link);
853
1113
  const stac = create(response);
854
1114
  return stac;
855
1115
  }
@@ -869,17 +1129,78 @@ class STACLayer extends LayerGroup {
869
1129
  this.handleError_(error);
870
1130
  }
871
1131
  }
872
- async addGeoZarr_(asset) {
1132
+ /**
1133
+ * Checks the `maxDisplayPixels` limit for the given source.
1134
+ * Returns `true` when the layer must not be added: automatically chosen
1135
+ * assets are limited silently, for explicitly requested assets an error
1136
+ * is thrown so that callers can fall back or report it.
1137
+ * @param {import('ol/source/Tile.js').default} source The configured (ready) source.
1138
+ * @param {Asset} asset The asset the source was created for.
1139
+ * @param {boolean} autoDisplay Whether the asset was chosen automatically.
1140
+ * @return {boolean} `true` if the asset must not be displayed.
1141
+ * @private
1142
+ */
1143
+ checkDisplayLimit_(source, asset, autoDisplay) {
1144
+ if (!exceedsDisplayLimit(source, this.maxDisplayPixels_)) {
1145
+ return false;
1146
+ }
1147
+ if (!autoDisplay) {
1148
+ const megapixels = Math.ceil(getDisplayPixels(source) / 1048576);
1149
+ const error = new Error(`Asset ${asset.getKey()} is too large to display safely` +
1150
+ ` (~${megapixels} megapixels at the coarsest resolution);` +
1151
+ ` set the maxDisplayPixels option to display it anyway`);
1152
+ // Allows applications to detect the error without matching the message
1153
+ error.name = 'DisplayLimitError';
1154
+ throw error;
1155
+ }
1156
+ return true;
1157
+ }
1158
+ /**
1159
+ * Adds a layer for a GeoZarr asset.
1160
+ * @param {Asset} asset The Zarr asset to show.
1161
+ * @param {boolean} [autoDisplay] Whether the asset was chosen automatically
1162
+ * (not explicitly requested): skip it silently instead of reporting an
1163
+ * error when it doesn't declare what to render or can't be displayed
1164
+ * within the configured limits.
1165
+ * @return {Promise<Layer|undefined>} The layer, if one was added.
1166
+ * @private
1167
+ */
1168
+ async addGeoZarr_(asset, autoDisplay = false) {
873
1169
  if (this.buildTileUrlTemplate_ && !this.useTileLayerAsFallback_) {
874
- return await this.addTileLayerForImagery_(asset);
1170
+ const layer = await this.addTileLayerForImagery_(asset);
1171
+ // If no tile server URL was provided for the asset, continue with client-side rendering
1172
+ if (layer) {
1173
+ return layer;
1174
+ }
875
1175
  }
876
1176
  let options = getGeoZarrSourceOptionsFromAsset(asset, this.bands_);
1177
+ // Web-Optimized Zarr describes its bands in the store metadata,
1178
+ // everything else must declare what to render in the STAC metadata
1179
+ // (or provide it through getSourceOptions for explicitly selected assets)
1180
+ const declaresRendering = () => asset.isType(wozMediaTypes) ||
1181
+ Boolean(options.variable) ||
1182
+ Boolean(options.bands && options.bands.length > 0);
1183
+ if (autoDisplay && !declaresRendering()) {
1184
+ return;
1185
+ }
1186
+ options.url = this.getRequestUrlFor_(options.url, asset);
1187
+ const projection = await getProjection(asset);
1188
+ if (projection) {
1189
+ options.projection = projection;
1190
+ }
1191
+ const headers = this.getRequestHeadersFor_(options.url, asset);
1192
+ if (headers) {
1193
+ options.storeOptions = { headers };
1194
+ }
877
1195
  if (this.getSourceOptions_) {
878
1196
  // @ts-ignore
879
1197
  options = await this.getSourceOptions_(SourceType.GeoZarr, options, asset);
880
1198
  }
881
1199
  try {
882
- const GeoZarr = (await import('ol/source/GeoZarr.js')).default;
1200
+ if (!declaresRendering()) {
1201
+ throw new Error(`Asset ${asset.getKey()} declares neither bands nor a datacube variable to render`);
1202
+ }
1203
+ const GeoZarr = (await import('../source/GeoZarr.js')).default;
883
1204
  const source = new GeoZarr(options);
884
1205
  await new Promise((resolve, reject) => {
885
1206
  source.on('change', () => {
@@ -891,17 +1212,33 @@ class STACLayer extends LayerGroup {
891
1212
  }
892
1213
  });
893
1214
  });
894
- const layerOptions = { source };
1215
+ if (this.checkDisplayLimit_(source, asset, autoDisplay)) {
1216
+ return;
1217
+ }
1218
+ /**
1219
+ * @type {import("ol/layer/WebGLTile.js").Options}
1220
+ */
1221
+ let layerOptions = { source };
895
1222
  if (this.style_) {
896
1223
  layerOptions.style = this.style_;
897
1224
  }
1225
+ else {
1226
+ const style = getGeoZarrStyleFromAsset(asset, options, this.defaultColormap_);
1227
+ if (style) {
1228
+ layerOptions.style = style;
1229
+ }
1230
+ }
1231
+ layerOptions = await this.updateLayerOptions_(LayerType.WebGLTile, layerOptions, asset);
898
1232
  const layer = new WebGLTileLayer(layerOptions);
899
1233
  this.addLayer_(layer, asset);
900
1234
  return layer;
901
1235
  }
902
1236
  catch (error) {
903
1237
  if (this.useTileLayerAsFallback_) {
904
- return await this.addTileLayerForImagery_(asset);
1238
+ const layer = await this.addTileLayerForImagery_(asset);
1239
+ if (layer) {
1240
+ return layer;
1241
+ }
905
1242
  }
906
1243
  this.handleError_(error);
907
1244
  }
@@ -946,7 +1283,7 @@ class STACLayer extends LayerGroup {
946
1283
  if (ref.isType(geotiffMediaTypes)) {
947
1284
  return await this.addGeoTiff_(ref);
948
1285
  }
949
- if (ref.isType(wozMediaTypes)) {
1286
+ if (ref.isType(zarrMediaTypes)) {
950
1287
  return await this.addGeoZarr_(ref);
951
1288
  }
952
1289
  if (ref.canBrowserDisplayImage()) {
@@ -978,14 +1315,13 @@ class STACLayer extends LayerGroup {
978
1315
  // Find a GeoTiff asset that we can visualize
979
1316
  const geotiff = data.getDefaultGeoFile('geotiff', true, !this.displayGeoTiffByDefault_);
980
1317
  if (geotiff) {
981
- layer = await this.addGeoTiff_(geotiff);
1318
+ layer = await this.addGeoTiff_(geotiff, true);
982
1319
  }
983
1320
  }
984
1321
  if (this.displayOverview_ && !layer) {
985
- // Find a Web-Optimized GeoZarr asset that we can visualize
986
- const geozarr = data.getDefaultGeoFile('geozarr', true, true);
1322
+ const geozarr = data.getDefaultGeoFile('geozarr', true, false);
987
1323
  if (geozarr) {
988
- layer = await this.addGeoZarr_(geozarr);
1324
+ layer = await this.addGeoZarr_(geozarr, true);
989
1325
  }
990
1326
  }
991
1327
  // Show web map links if available
@@ -1023,40 +1359,7 @@ class STACLayer extends LayerGroup {
1023
1359
  * @api
1024
1360
  */
1025
1361
  getWebMapLinks() {
1026
- if (this.displayWebMapLink_ === false) {
1027
- return [];
1028
- }
1029
- const data = this.getData();
1030
- if (!data || data.isAsset) {
1031
- return [];
1032
- }
1033
- let types = ['xyz', 'tilejson', 'pmtiles', 'wmts', 'wms']; // This also defines the priority
1034
- if (typeof this.displayWebMapLink_ === 'string') {
1035
- types = [this.displayWebMapLink_];
1036
- }
1037
- let mapLinks = data.getLinksWithRels(types);
1038
- if (Array.isArray(this.displayWebMapLink_)) {
1039
- mapLinks = this.displayWebMapLink_
1040
- .map((link) => {
1041
- if (typeof link === 'string') {
1042
- const match = mapLinks.find((candidate) => candidate.id === link);
1043
- if (match) {
1044
- return match;
1045
- }
1046
- return null;
1047
- }
1048
- return link;
1049
- })
1050
- .filter((link) => !!link);
1051
- }
1052
- else {
1053
- mapLinks.sort((a, b) => {
1054
- const prioA = types.indexOf(a.rel);
1055
- const prioB = types.indexOf(b.rel);
1056
- return prioA - prioB;
1057
- });
1058
- }
1059
- return mapLinks;
1362
+ return getWebMapLinks(this.getData(), this.displayWebMapLink_);
1060
1363
  }
1061
1364
  /**
1062
1365
  * Set the style for GeoTIFF and GeoZarr layers (WebGLTileLayer style).
@@ -1071,6 +1374,26 @@ class STACLayer extends LayerGroup {
1071
1374
  }
1072
1375
  }
1073
1376
  }
1377
+ /**
1378
+ * Set the colors of the colormap that is used for continuous single-band
1379
+ * data when neither the STAC metadata nor the `style` option define a
1380
+ * coloring. The colors are evenly distributed over the value range of the
1381
+ * data (e.g. from the STAC `statistics`). Set to `null` to stretch the
1382
+ * data to grayscale instead.
1383
+ * @param {Array<import("ol/color.js").Color|string>|null} colormap The colors of the colormap.
1384
+ * @return {Promise} Resolves once the layers are updated.
1385
+ * @api
1386
+ */
1387
+ async setDefaultColormap(colormap) {
1388
+ if (colormap === this.defaultColormap_) {
1389
+ return;
1390
+ }
1391
+ this.defaultColormap_ = colormap || null;
1392
+ // The layers are recreated, as switching between a styled and an
1393
+ // unstyled visualization also changes how the sources are configured
1394
+ // (e.g. the normalization of GeoTIFF sources)
1395
+ await this.updateLayers();
1396
+ }
1074
1397
  /**
1075
1398
  * Update the assets to be rendered.
1076
1399
  * @param {Array<string|Asset>|null} assets The assets to show.
@@ -1244,18 +1567,19 @@ class STACLayer extends LayerGroup {
1244
1567
  /**
1245
1568
  * Gets the WMTS capabilities from the given web-map-links URL.
1246
1569
  * @param {string} url Base URL for the WMTS
1570
+ * @param {Link} link The web map link the request is made for.
1247
1571
  * @param {string} [encoding] The request encoding, either `kvp` (default) or `rest`.
1248
1572
  * @return {Promise<Object|null>} Resolves with the WMTS Capabilities object
1249
1573
  * @private
1250
1574
  */
1251
- async getWmtsCapabilities_(url, encoding = 'kvp') {
1575
+ async getWmtsCapabilities_(url, link, encoding = 'kvp') {
1252
1576
  try {
1253
1577
  const urlObj = new URL(url);
1254
1578
  if (encoding !== 'rest') {
1255
1579
  urlObj.searchParams.set('service', 'wmts');
1256
1580
  urlObj.searchParams.set('request', 'GetCapabilities');
1257
1581
  }
1258
- const response = await this.fetch_(urlObj.toString(), 'text');
1582
+ const response = await this.fetch_(urlObj.toString(), 'text', link);
1259
1583
  return new WMTSCapabilities().read(response);
1260
1584
  }
1261
1585
  catch (_) {