maplibre-gl-raster 0.9.0 → 0.10.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.
@@ -18206,6 +18206,7 @@ function toLayerInfo(layer) {
18206
18206
  bandCount: layer.bandCount,
18207
18207
  bandNames: layer.bandNames ? new Map(layer.bandNames) : null,
18208
18208
  beforeId: layer.beforeId,
18209
+ attribution: layer.attribution,
18209
18210
  bounds: layer.bounds ? { ...layer.bounds } : null,
18210
18211
  loading: layer.loading,
18211
18212
  error: layer.error,
@@ -18232,6 +18233,26 @@ function deriveLayerName(source) {
18232
18233
  /** Default engine when none is configured: the deck.gl GPU pipeline. */
18233
18234
  var DEFAULT_ENGINE = "maplibre-gl-raster";
18234
18235
  /**
18236
+ * Clamps a bounds' latitudes to the valid WGS84 range. GeoTIFF bounds are
18237
+ * derived as `origin + n * pixelSize`, so a global raster whose pixel size was
18238
+ * stored rounded (e.g. GEBCO's 1/240° stored as 0.004166666666667) can
18239
+ * overshoot the poles by a floating-point epsilon — and MapLibre's LngLat
18240
+ * rejects any latitude outside [-90, 90], crashing fitBounds. Longitudes are
18241
+ * left alone: MapLibre accepts any longitude, and clamping would corrupt
18242
+ * antimeridian-crossing rasters.
18243
+ */
18244
+ function clampBoundsLatitude(bounds) {
18245
+ const clamp = (lat) => Math.min(90, Math.max(-90, lat));
18246
+ const south = clamp(bounds.south);
18247
+ const north = clamp(bounds.north);
18248
+ if (south === bounds.south && north === bounds.north) return bounds;
18249
+ return {
18250
+ ...bounds,
18251
+ south,
18252
+ north
18253
+ };
18254
+ }
18255
+ /**
18235
18256
  * The band indexes a layer actually samples, deduped and sorted. Mirrors the
18236
18257
  * `requested` logic in render-pipeline's buildRenderTile: RGB samples the first
18237
18258
  * three entries (R, G, B); single-band / colormap / palette the first one. The
@@ -18358,6 +18379,9 @@ var LayerManager = class {
18358
18379
  _colormapTexture = null;
18359
18380
  _handlers = new globalThis.Map();
18360
18381
  _crsFailed = /* @__PURE__ */ new Set();
18382
+ _attributions = new globalThis.Map();
18383
+ _attribStyleReady = false;
18384
+ _onAttribStyleLoad = null;
18361
18385
  _destroyed = false;
18362
18386
  /**
18363
18387
  * Creates a LayerManager bound to a map.
@@ -18444,6 +18468,7 @@ var LayerManager = class {
18444
18468
  palette: null,
18445
18469
  paletteTexture: null,
18446
18470
  beforeId: options?.beforeId?.trim() || null,
18471
+ attribution: options?.attribution?.trim() || null,
18447
18472
  bounds: null,
18448
18473
  zoomTo: options?.zoomTo ?? true,
18449
18474
  loading: true,
@@ -18649,6 +18674,11 @@ var LayerManager = class {
18649
18674
  }
18650
18675
  this._layers = [];
18651
18676
  this._selectedId = null;
18677
+ for (const id of [...this._attributions.keys()]) this._removeAttribution(id);
18678
+ if (this._onAttribStyleLoad) {
18679
+ this._map.off("load", this._onAttribStyleLoad);
18680
+ this._onAttribStyleLoad = null;
18681
+ }
18652
18682
  if (this._overlay) {
18653
18683
  this._deps.removeOverlay(this._map, this._overlay);
18654
18684
  this._overlay = null;
@@ -18700,10 +18730,10 @@ var LayerManager = class {
18700
18730
  const layer = this.getLayer(id);
18701
18731
  if (!layer) return;
18702
18732
  const boundsArrived = !layer.bounds;
18703
- layer.bounds = bounds;
18733
+ layer.bounds = clampBoundsLatitude(bounds);
18704
18734
  if (zoomTo && layer.zoomTo) {
18705
18735
  layer.zoomTo = false;
18706
- this._fitBounds(bounds);
18736
+ this._fitBounds(layer.bounds);
18707
18737
  }
18708
18738
  if (boundsArrived) this._emit({
18709
18739
  type: "rasterchange",
@@ -18793,10 +18823,66 @@ var LayerManager = class {
18793
18823
  duration: 800
18794
18824
  });
18795
18825
  }
18826
+ /** Reconciles per-layer attributions with the map's attribution control.
18827
+ *
18828
+ * Neither engine gives every raster a MapLibre source the control could read
18829
+ * an attribution from (the deck.gl path renders through an overlay with no
18830
+ * source at all), so each attributed, visible layer gets a helper: an empty
18831
+ * GeoJSON source carrying the attribution string plus a no-op circle layer
18832
+ * referencing it. The layer marks the source as used, which is what makes
18833
+ * the stock AttributionControl display (and de-duplicate) the string; the
18834
+ * empty feature collection means nothing is ever drawn or fetched. */
18835
+ _syncAttributions() {
18836
+ if (!this._attribStyleReady) if (this._map.isStyleLoaded()) this._attribStyleReady = true;
18837
+ else {
18838
+ if (!this._onAttribStyleLoad) {
18839
+ this._onAttribStyleLoad = () => {
18840
+ this._onAttribStyleLoad = null;
18841
+ this._attribStyleReady = true;
18842
+ this._syncAttributions();
18843
+ };
18844
+ this._map.once("load", this._onAttribStyleLoad);
18845
+ }
18846
+ return;
18847
+ }
18848
+ const desired = /* @__PURE__ */ new Map();
18849
+ for (const l of this._layers) if (l.attribution && l.geotiff && l.state.visible && !l.error && !this._crsFailed.has(l.id)) desired.set(l.id, l.attribution);
18850
+ for (const [id, applied] of this._attributions) if (desired.get(id) !== applied) this._removeAttribution(id);
18851
+ for (const [id, attribution] of desired) {
18852
+ if (this._attributions.has(id)) continue;
18853
+ const helperId = `mlr-attribution-${id}`;
18854
+ try {
18855
+ this._map.addSource(helperId, {
18856
+ type: "geojson",
18857
+ data: {
18858
+ type: "FeatureCollection",
18859
+ features: []
18860
+ },
18861
+ attribution
18862
+ });
18863
+ this._map.addLayer({
18864
+ id: helperId,
18865
+ type: "circle",
18866
+ source: helperId
18867
+ });
18868
+ this._attributions.set(id, attribution);
18869
+ } catch {}
18870
+ }
18871
+ }
18872
+ /** Removes a layer's attribution helper source/layer, if present. */
18873
+ _removeAttribution(id) {
18874
+ if (!this._attributions.delete(id)) return;
18875
+ const helperId = `mlr-attribution-${id}`;
18876
+ try {
18877
+ if (this._map.getLayer(helperId)) this._map.removeLayer(helperId);
18878
+ if (this._map.getSource(helperId)) this._map.removeSource(helperId);
18879
+ } catch {}
18880
+ }
18796
18881
  /** Re-derives the deck.gl layer array from current layer states and pushes
18797
18882
  * it to the overlay. Layer ids are stable so deck.gl preserves each
18798
18883
  * layer's tile cache across rebuilds. */
18799
18884
  _rebuild() {
18885
+ this._syncAttributions();
18800
18886
  if (this._engine === "cog-tiler-wasm") {
18801
18887
  this._overlay?.setProps({ layers: [] });
18802
18888
  this._ensureCogEngine().sync(this._cogRenderableLayers());
@@ -18823,7 +18909,7 @@ var LayerManager = class {
18823
18909
  }),
18824
18910
  onGeoTIFFLoad: (_tiff, options) => {
18825
18911
  const boundsArrived = !layer.bounds;
18826
- layer.bounds = options.geographicBounds;
18912
+ layer.bounds = clampBoundsLatitude(options.geographicBounds);
18827
18913
  if (layer.zoomTo) {
18828
18914
  layer.zoomTo = false;
18829
18915
  this._fitBounds(layer.bounds);
@@ -19152,7 +19238,8 @@ var AddDataSection = class {
19152
19238
  const addFiles = (files) => {
19153
19239
  if (!files) return;
19154
19240
  const beforeId = currentBeforeId();
19155
- for (const file of Array.from(files)) if (isTiff(file)) options.onAddFile(file, beforeId);
19241
+ const attribution = currentAttribution();
19242
+ for (const file of Array.from(files)) if (isTiff(file)) options.onAddFile(file, beforeId, attribution);
19156
19243
  };
19157
19244
  const input = el("input", {
19158
19245
  className: "mlr-input",
@@ -19179,10 +19266,18 @@ var AddDataSection = class {
19179
19266
  title: "Id of an existing map layer to insert the raster beneath (e.g. a label layer). Leave empty to draw on top."
19180
19267
  });
19181
19268
  const currentBeforeId = () => beforeIdInput.value.trim() || void 0;
19269
+ const attributionInput = el("input", {
19270
+ className: "mlr-input",
19271
+ type: "text",
19272
+ placeholder: "Attribution (optional)",
19273
+ ariaLabel: "attribution",
19274
+ title: "Data credit shown in the map's attribution control while the layer is visible (plain text or an HTML link)."
19275
+ });
19276
+ const currentAttribution = () => attributionInput.value.trim() || void 0;
19182
19277
  const submitUrl = () => {
19183
19278
  const url = input.value.trim();
19184
19279
  if (!url) return;
19185
- options.onAddUrl(url, currentBeforeId());
19280
+ options.onAddUrl(url, currentBeforeId(), currentAttribution());
19186
19281
  };
19187
19282
  loadBtn.addEventListener("click", submitUrl);
19188
19283
  input.addEventListener("keydown", (e) => {
@@ -19234,6 +19329,7 @@ var AddDataSection = class {
19234
19329
  setMenuOpen(false);
19235
19330
  trigger.focus();
19236
19331
  input.value = sample.url;
19332
+ if (sample.attribution) attributionInput.value = sample.attribution;
19237
19333
  loadBtn.disabled = input.value.trim().length === 0;
19238
19334
  submitUrl();
19239
19335
  });
@@ -19295,7 +19391,7 @@ var AddDataSection = class {
19295
19391
  this.el = el("div", { className: "mlr-section mlr-add-data" }, el("div", {
19296
19392
  className: "mlr-section-title",
19297
19393
  text: "Add data"
19298
- }), urlRow, dropZone, beforeIdInput, ...sampleRow ? [sampleRow] : []);
19394
+ }), urlRow, dropZone, beforeIdInput, attributionInput, ...sampleRow ? [sampleRow] : []);
19299
19395
  }
19300
19396
  };
19301
19397
  //#endregion
@@ -20461,11 +20557,17 @@ var PanelUI = class {
20461
20557
  initialUrl: options?.defaultUrl,
20462
20558
  sampleData: options?.sampleData,
20463
20559
  sampleDataLabel: options?.sampleDataLabel,
20464
- onAddUrl: (url, beforeId) => {
20465
- this._manager.addRaster(url, { beforeId }).catch(() => {});
20560
+ onAddUrl: (url, beforeId, attribution) => {
20561
+ this._manager.addRaster(url, {
20562
+ beforeId,
20563
+ attribution
20564
+ }).catch(() => {});
20466
20565
  },
20467
- onAddFile: (file, beforeId) => {
20468
- this._manager.addRaster(file, { beforeId }).catch(() => {});
20566
+ onAddFile: (file, beforeId, attribution) => {
20567
+ this._manager.addRaster(file, {
20568
+ beforeId,
20569
+ attribution
20570
+ }).catch(() => {});
20469
20571
  }
20470
20572
  });
20471
20573
  this._layerList = new LayerList({
@@ -21430,4 +21532,4 @@ var RasterControl = class {
21430
21532
  //#endregion
21431
21533
  export { statsForBand as A, COLORMAP_NAMES as C, colormaps_default as D, colormapDisplayName as E, createResilientEpsgResolver as F, computeAutoStats as M, percentileFromHistogram as N, DEFAULT_INDEX_RANGE as O, readBandNames as P, COLORMAP_DISPLAY_NAMES as S, COLORMAP_ROW_COUNT as T, debounce as _, NORMALIZED_DIFFERENCE_INDICES as a, throttle as b, readPixelValues as c, PALETTE_COLORMAP as d, isKnownColormap as f, classNames as g, clamp as h, CUSTOM_NORMALIZED_DIFFERENCE as i, MAX_SAMPLE_TILES as j, autoRangeFor as k, DEFAULT_ENGINE as l, sampleColormapStops as m, Colorbar as n, guessBandForRole as o, loadColormapSprite as p, CUSTOM_INDEX_ID as r, indexById as s, RasterControl as t, LayerManager as u, formatNumericValue as v, COLORMAP_OPTIONS as w, loadGeoTIFF as x, generateId as y };
21432
21534
 
21433
- //# sourceMappingURL=RasterControl-BfTDWAnW.js.map
21535
+ //# sourceMappingURL=RasterControl-D9JDQUVb.js.map