ol-stac 1.5.1 → 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
@@ -22,12 +22,14 @@ 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, 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,22 @@ 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):(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.
127
178
  */
128
179
  /**
129
180
  * @classdesc
@@ -159,6 +210,21 @@ class STACLayer extends LayerGroup {
159
210
  * @private
160
211
  */
161
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;
162
228
  /**
163
229
  * @type {Array<STAC>|null}
164
230
  * @private
@@ -209,7 +275,12 @@ class STACLayer extends LayerGroup {
209
275
  */
210
276
  this.displayWebMapLink_ = options.displayWebMapLink || false;
211
277
  /**
212
- * @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}
213
284
  * @private
214
285
  */
215
286
  this.buildTileUrlTemplate_ = options.buildTileUrlTemplate || null;
@@ -223,6 +294,11 @@ class STACLayer extends LayerGroup {
223
294
  * @private
224
295
  */
225
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;
226
302
  /**
227
303
  * @type {Style}
228
304
  * @private
@@ -268,19 +344,70 @@ class STACLayer extends LayerGroup {
268
344
  if (!options.url) {
269
345
  throw new Error('Either url or data must be provided');
270
346
  }
271
- this.fetch_(options.url)
347
+ this.fetch_(this.getRequestUrlFor_(options.url))
272
348
  .then((data) => this.configure_(data, options.url, options.children, options.assets, options.bands))
273
349
  .catch((error) => this.handleError_(error));
274
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
+ }
275
400
  /**
276
401
  * Default function make HTTP requests with.
277
402
  *
278
403
  * @param {string} url The URL to request and the output is a promise that resolves with the response body.
279
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.
280
406
  * @return {Promise<*>} The (parsed) response body.
281
407
  */
282
- async fetch_(url, responseType = 'json') {
283
- 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);
284
411
  if (!response.ok) {
285
412
  throw new Error(`Unexpected response from ${url}: ${response.status}`);
286
413
  }
@@ -478,18 +605,21 @@ class STACLayer extends LayerGroup {
478
605
  * @type {import("ol/source/ImageStatic.js").Options}
479
606
  */
480
607
  let options = {
481
- url: image.getAbsoluteUrl(),
608
+ url: this.getRequestUrlFor_(image.getAbsoluteUrl(), image),
482
609
  projection,
483
610
  imageExtent: toOlExtent(bbox, projection),
484
611
  crossOrigin: this.crossOrigin_,
485
612
  };
613
+ const imageLoadFunction = this.createLoadFunction_(createImageLoadFunction, image);
614
+ if (imageLoadFunction) {
615
+ options.imageLoadFunction = imageLoadFunction;
616
+ }
486
617
  if (this.getSourceOptions_) {
487
618
  // @ts-ignore
488
619
  options = await this.getSourceOptions_(SourceType.ImageStatic, options, image);
489
620
  }
490
- const layer = new ImageLayer({
491
- source: new StaticImage(options),
492
- });
621
+ const layerOptions = await this.updateLayerOptions_(LayerType.Image, { source: new StaticImage(options) }, image);
622
+ const layer = new ImageLayer(layerOptions);
493
623
  this.addLayer_(layer, image);
494
624
  return layer;
495
625
  }
@@ -503,16 +633,21 @@ class STACLayer extends LayerGroup {
503
633
  */
504
634
  async addLayerForLink(link) {
505
635
  // Replace any occurances of {s} if possible, otherwise return
506
- const url = getSpecificWebMapUrl(link);
636
+ let url = getSpecificWebMapUrl(link);
507
637
  if (!url) {
508
638
  return;
509
639
  }
640
+ url = this.getRequestUrlFor_(url, link);
510
641
  const options = {
511
642
  attributions: link.getMetadata('attribution') ||
512
643
  this.getData().getMetadata('attribution'),
513
644
  crossOrigin: this.crossOrigin_,
514
645
  url,
515
646
  };
647
+ const tileLoadFunction = this.createLoadFunction_(createTileLoadFunction, link);
648
+ if (tileLoadFunction && link.rel !== 'pmtiles') {
649
+ options.tileLoadFunction = tileLoadFunction;
650
+ }
516
651
  const updateOptions = async (type, options) => {
517
652
  if (this.getSourceOptions_) {
518
653
  options = await this.getSourceOptions_(type, options, link);
@@ -521,28 +656,82 @@ class STACLayer extends LayerGroup {
521
656
  };
522
657
  const sources = [];
523
658
  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) {
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) {
529
681
  case pmtiles.TileType.Mvt:
530
- source = new PMTilesVectorSource(await updateOptions(SourceType.PMTilesVector, options));
682
+ type = SourceType.PMTilesVector;
531
683
  break;
532
684
  case pmtiles.TileType.Avif:
533
685
  case pmtiles.TileType.Jpeg:
534
686
  case pmtiles.TileType.Png:
535
687
  case pmtiles.TileType.Webp:
536
- source = new PMTilesRasterSource(await updateOptions(SourceType.PMTilesRaster, options));
688
+ type = SourceType.PMTilesRaster;
537
689
  break;
538
690
  default:
539
691
  return; // Unsupported
540
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);
541
705
  sources.push(source);
542
706
  break;
543
- case 'tilejson':
544
- 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));
545
733
  break;
734
+ }
546
735
  case 'wms':
547
736
  if (!Array.isArray(link['wms:layers'])) {
548
737
  break;
@@ -569,7 +758,7 @@ class STACLayer extends LayerGroup {
569
758
  }
570
759
  break;
571
760
  case 'wmts':
572
- const wmtsCapabilities = await this.getWmtsCapabilities_(url, link['wmts:encoding']);
761
+ const wmtsCapabilities = await this.getWmtsCapabilities_(url, link, link['wmts:encoding']);
573
762
  if (!wmtsCapabilities) {
574
763
  return;
575
764
  }
@@ -593,6 +782,10 @@ class STACLayer extends LayerGroup {
593
782
  if (opts === null) {
594
783
  continue;
595
784
  }
785
+ if (wmtsOptions.tileLoadFunction) {
786
+ // Not passed through by optionsFromCapabilities
787
+ opts.tileLoadFunction = wmtsOptions.tileLoadFunction;
788
+ }
596
789
  if (typeof link.uriTemplate === 'string') {
597
790
  let uriTemplate = link.uriTemplate;
598
791
  const vars = isObject(link.variables) ? link.variables : {};
@@ -616,7 +809,7 @@ class STACLayer extends LayerGroup {
616
809
  }
617
810
  }
618
811
  delete opts.urls;
619
- opts.url = uriTemplate;
812
+ opts.url = this.getRequestUrlFor_(uriTemplate, link);
620
813
  }
621
814
  sources.push(new WMTS(opts));
622
815
  }
@@ -627,34 +820,42 @@ class STACLayer extends LayerGroup {
627
820
  default:
628
821
  return;
629
822
  }
630
- return sources.map((source) => {
823
+ return await Promise.all(sources.map(async (source) => {
631
824
  let layer;
632
825
  if (source instanceof VectorTileSource) {
633
- layer = new VectorTileLayer({
634
- source,
635
- declutter: true,
636
- });
826
+ const layerOptions = await this.updateLayerOptions_(LayerType.VectorTile, { source, declutter: true }, link);
827
+ layer = new VectorTileLayer(layerOptions);
637
828
  }
638
829
  else if (source instanceof PMTilesRasterSource) {
639
- layer = new WebGLTileLayer({ source });
830
+ const layerOptions = await this.updateLayerOptions_(LayerType.WebGLTile, { source }, link);
831
+ layer = new WebGLTileLayer(layerOptions);
640
832
  }
641
833
  else {
642
- layer = new TileLayer({ source });
834
+ const layerOptions = await this.updateLayerOptions_(LayerType.Tile, { source }, link);
835
+ layer = new TileLayer(layerOptions);
643
836
  }
644
837
  this.addLayer_(layer, link);
645
838
  return layer;
646
- });
839
+ }));
647
840
  }
648
841
  /**
649
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.
650
846
  * @return {Promise<Layer|undefined>} Resolves with a Layer or undefined when complete.
651
847
  * @private
652
848
  */
653
- async addGeoTiff_(asset) {
849
+ async addGeoTiff_(asset, autoDisplay = false) {
654
850
  if (this.buildTileUrlTemplate_ && !this.useTileLayerAsFallback_) {
655
- 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
+ }
656
856
  }
657
857
  const sourceInfo = getGeoTiffSourceInfoFromAsset(asset, this.bands_);
858
+ sourceInfo.url = this.getRequestUrlFor_(sourceInfo.url, asset);
658
859
  /**
659
860
  * @type {import("ol/source/GeoTIFF.js").Options}
660
861
  */
@@ -667,10 +868,14 @@ class STACLayer extends LayerGroup {
667
868
  if (projection) {
668
869
  options.projection = projection;
669
870
  }
670
- const classificationStyle = getClassificationStyle(asset, sourceInfo.bands);
671
- if (classificationStyle) {
871
+ const metadataStyle = getGeoTiffStyleFromAsset(asset, sourceInfo, this.defaultColormap_);
872
+ if (metadataStyle) {
672
873
  options.normalize = false;
673
874
  }
875
+ const headers = this.getRequestHeadersFor_(sourceInfo.url, asset);
876
+ if (headers) {
877
+ options.sourceOptions = { headers };
878
+ }
674
879
  if (this.getSourceOptions_) {
675
880
  // @ts-ignore
676
881
  options = await this.getSourceOptions_(SourceType.GeoTIFF, options, asset);
@@ -690,34 +895,51 @@ class STACLayer extends LayerGroup {
690
895
  });
691
896
  try {
692
897
  await status;
693
- 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 };
694
905
  if (this.style_) {
695
906
  layerOptions.style = this.style_;
696
907
  }
697
- else if (classificationStyle) {
698
- layerOptions.style = classificationStyle;
908
+ else if (metadataStyle) {
909
+ layerOptions.style = metadataStyle;
699
910
  }
911
+ layerOptions = await this.updateLayerOptions_(LayerType.WebGLTile, layerOptions, asset);
700
912
  const layer = new WebGLTileLayer(layerOptions);
701
913
  this.addLayer_(layer, asset);
702
914
  return layer;
703
915
  }
704
916
  catch (error) {
705
917
  if (this.useTileLayerAsFallback_) {
706
- return await this.addTileLayerForImagery_(asset);
918
+ const layer = await this.addTileLayerForImagery_(asset);
919
+ if (layer) {
920
+ return layer;
921
+ }
707
922
  }
708
923
  this.handleError_(error);
709
924
  }
710
925
  }
711
926
  /**
712
927
  * @param {Asset|Link} [data] A STAC Asset or Link
713
- * @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.
714
929
  * @private
715
930
  */
716
931
  async addTileLayerForImagery_(data) {
932
+ if (typeof this.buildTileUrlTemplate_ !== 'function') {
933
+ return;
934
+ }
717
935
  let url = this.buildTileUrlTemplate_(data);
718
936
  if (url instanceof Promise) {
719
937
  url = await url;
720
938
  }
939
+ if (!url) {
940
+ return;
941
+ }
942
+ url = this.getRequestUrlFor_(url, data);
721
943
  /**
722
944
  * @type {import("ol/source/XYZ.js").Options}
723
945
  */
@@ -725,15 +947,33 @@ class STACLayer extends LayerGroup {
725
947
  crossOrigin: this.crossOrigin_,
726
948
  url,
727
949
  };
950
+ const tileLoadFunction = this.createLoadFunction_(createTileLoadFunction, data);
951
+ if (tileLoadFunction) {
952
+ options.tileLoadFunction = tileLoadFunction;
953
+ }
728
954
  if (this.getSourceOptions_) {
729
955
  options = await this.getSourceOptions_(SourceType.XYZ, options, data);
730
956
  }
731
- const layer = new TileLayer({
732
- source: new XYZ(options),
733
- });
957
+ const layerOptions = await this.updateLayerOptions_(LayerType.Tile, { source: new XYZ(options) }, data);
958
+ const layer = new TileLayer(layerOptions);
734
959
  this.addLayer_(layer, data);
735
960
  return layer;
736
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
+ }
737
977
  /**
738
978
  * @param {Layer|LayerGroup} [layer] A Layer to add to the LayerGroup
739
979
  * @param {STACObject} [data] The STAC object, can be any class exposed by stac-js
@@ -766,7 +1006,7 @@ class STACLayer extends LayerGroup {
766
1006
  geojson = data.toGeoJSON(fixAntimeridian);
767
1007
  }
768
1008
  if (geojson) {
769
- 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_));
770
1010
  layer.set('bounds', true);
771
1011
  layer.on('change', () => this.setMap_(layer.getMapInternal()));
772
1012
  this.addLayer_(layer, data, 1);
@@ -781,8 +1021,9 @@ class STACLayer extends LayerGroup {
781
1021
  */
782
1022
  async addGeoJson_(asset) {
783
1023
  try {
784
- const geojson = await this.fetch_(asset.getAbsoluteUrl());
785
- 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);
786
1027
  this.addLayer_(layer, asset);
787
1028
  return layer;
788
1029
  }
@@ -791,15 +1032,15 @@ class STACLayer extends LayerGroup {
791
1032
  }
792
1033
  }
793
1034
  /**
794
- * Creates a GeoJSON vector layer from the given GeoJSON object.
1035
+ * Creates the options for a GeoJSON vector layer from the given GeoJSON object.
795
1036
  *
796
1037
  * @param {GeoJSON} [geojson] The GeoJSON object.
797
1038
  * @param {Style} [style] The style for the layer.
798
1039
  * @param {boolean} [visible] Whether the layer is visible.
799
- * @return {VectorLayer} The new vector layer.
1040
+ * @return {import("ol/layer/Vector.js").Options} The vector layer options.
800
1041
  * @private
801
1042
  */
802
- createGeoJsonLayer_(geojson, style = null, visible = true) {
1043
+ getGeoJsonLayerOptions_(geojson, style = null, visible = true) {
803
1044
  const format = new GeoJSON();
804
1045
  const source = new VectorSource({
805
1046
  format,
@@ -814,7 +1055,7 @@ class STACLayer extends LayerGroup {
814
1055
  if (!style) {
815
1056
  style = defaultCollectionStyle;
816
1057
  }
817
- return new VectorLayer({ source, style, visible });
1058
+ return { source, style, visible };
818
1059
  }
819
1060
  /**
820
1061
  * Adds GeoJSON labels and GeoTIFF source imagery to the map based on the label extension.
@@ -849,7 +1090,7 @@ class STACLayer extends LayerGroup {
849
1090
  if (labelAsset && sourceLinks.length > 0) {
850
1091
  const promises = sourceLinks.map(async (link) => {
851
1092
  try {
852
- const response = await this.fetch_(link.getAbsoluteUrl());
1093
+ const response = await this.fetch_(this.getRequestUrlFor_(link.getAbsoluteUrl(), link), 'json', link);
853
1094
  const stac = create(response);
854
1095
  return stac;
855
1096
  }
@@ -869,17 +1110,78 @@ class STACLayer extends LayerGroup {
869
1110
  this.handleError_(error);
870
1111
  }
871
1112
  }
872
- 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) {
873
1150
  if (this.buildTileUrlTemplate_ && !this.useTileLayerAsFallback_) {
874
- 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
+ }
875
1156
  }
876
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
+ }
877
1176
  if (this.getSourceOptions_) {
878
1177
  // @ts-ignore
879
1178
  options = await this.getSourceOptions_(SourceType.GeoZarr, options, asset);
880
1179
  }
881
1180
  try {
882
- 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;
883
1185
  const source = new GeoZarr(options);
884
1186
  await new Promise((resolve, reject) => {
885
1187
  source.on('change', () => {
@@ -891,17 +1193,33 @@ class STACLayer extends LayerGroup {
891
1193
  }
892
1194
  });
893
1195
  });
894
- 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 };
895
1203
  if (this.style_) {
896
1204
  layerOptions.style = this.style_;
897
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);
898
1213
  const layer = new WebGLTileLayer(layerOptions);
899
1214
  this.addLayer_(layer, asset);
900
1215
  return layer;
901
1216
  }
902
1217
  catch (error) {
903
1218
  if (this.useTileLayerAsFallback_) {
904
- return await this.addTileLayerForImagery_(asset);
1219
+ const layer = await this.addTileLayerForImagery_(asset);
1220
+ if (layer) {
1221
+ return layer;
1222
+ }
905
1223
  }
906
1224
  this.handleError_(error);
907
1225
  }
@@ -946,7 +1264,7 @@ class STACLayer extends LayerGroup {
946
1264
  if (ref.isType(geotiffMediaTypes)) {
947
1265
  return await this.addGeoTiff_(ref);
948
1266
  }
949
- if (ref.isType(wozMediaTypes)) {
1267
+ if (ref.isType(zarrMediaTypes)) {
950
1268
  return await this.addGeoZarr_(ref);
951
1269
  }
952
1270
  if (ref.canBrowserDisplayImage()) {
@@ -978,14 +1296,13 @@ class STACLayer extends LayerGroup {
978
1296
  // Find a GeoTiff asset that we can visualize
979
1297
  const geotiff = data.getDefaultGeoFile('geotiff', true, !this.displayGeoTiffByDefault_);
980
1298
  if (geotiff) {
981
- layer = await this.addGeoTiff_(geotiff);
1299
+ layer = await this.addGeoTiff_(geotiff, true);
982
1300
  }
983
1301
  }
984
1302
  if (this.displayOverview_ && !layer) {
985
- // Find a Web-Optimized GeoZarr asset that we can visualize
986
- const geozarr = data.getDefaultGeoFile('geozarr', true, true);
1303
+ const geozarr = data.getDefaultGeoFile('geozarr', true, false);
987
1304
  if (geozarr) {
988
- layer = await this.addGeoZarr_(geozarr);
1305
+ layer = await this.addGeoZarr_(geozarr, true);
989
1306
  }
990
1307
  }
991
1308
  // Show web map links if available
@@ -1071,6 +1388,26 @@ class STACLayer extends LayerGroup {
1071
1388
  }
1072
1389
  }
1073
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
+ }
1074
1411
  /**
1075
1412
  * Update the assets to be rendered.
1076
1413
  * @param {Array<string|Asset>|null} assets The assets to show.
@@ -1244,18 +1581,19 @@ class STACLayer extends LayerGroup {
1244
1581
  /**
1245
1582
  * Gets the WMTS capabilities from the given web-map-links URL.
1246
1583
  * @param {string} url Base URL for the WMTS
1584
+ * @param {Link} link The web map link the request is made for.
1247
1585
  * @param {string} [encoding] The request encoding, either `kvp` (default) or `rest`.
1248
1586
  * @return {Promise<Object|null>} Resolves with the WMTS Capabilities object
1249
1587
  * @private
1250
1588
  */
1251
- async getWmtsCapabilities_(url, encoding = 'kvp') {
1589
+ async getWmtsCapabilities_(url, link, encoding = 'kvp') {
1252
1590
  try {
1253
1591
  const urlObj = new URL(url);
1254
1592
  if (encoding !== 'rest') {
1255
1593
  urlObj.searchParams.set('service', 'wmts');
1256
1594
  urlObj.searchParams.set('request', 'GetCapabilities');
1257
1595
  }
1258
- const response = await this.fetch_(urlObj.toString(), 'text');
1596
+ const response = await this.fetch_(urlObj.toString(), 'text', link);
1259
1597
  return new WMTSCapabilities().read(response);
1260
1598
  }
1261
1599
  catch (_) {