maplibre-gl-raster 0.13.0 → 0.14.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.
@@ -19065,6 +19065,149 @@ function classNames(classes) {
19065
19065
  return Object.entries(classes).filter(([, value]) => value).map(([key]) => key).join(" ");
19066
19066
  }
19067
19067
  //#endregion
19068
+ //#region src/lib/state/RasterLayer.ts
19069
+ /** Default visualization state for a freshly added layer. Mode/bands are
19070
+ * re-picked automatically once the band count is known (unless the caller
19071
+ * supplied them explicitly); single-band rasters default to the image's
19072
+ * embedded color table when present ('palette') or grayscale otherwise. */
19073
+ var DEFAULT_LAYER_STATE = {
19074
+ mode: "rgb",
19075
+ bands: [
19076
+ 1,
19077
+ 2,
19078
+ 3
19079
+ ],
19080
+ rescale: null,
19081
+ colormap: "gray",
19082
+ reversed: false,
19083
+ nodata: "auto",
19084
+ opacity: 1,
19085
+ gamma: 1,
19086
+ stretch: "linear",
19087
+ visible: true,
19088
+ minZoom: 0,
19089
+ maxZoom: 24
19090
+ };
19091
+ /**
19092
+ * Creates a complete layer state from optional overrides.
19093
+ *
19094
+ * @param overrides - Partial state merged over the defaults
19095
+ * @returns A fully populated RasterLayerState
19096
+ */
19097
+ function createLayerState(overrides) {
19098
+ return {
19099
+ ...DEFAULT_LAYER_STATE,
19100
+ ...overrides
19101
+ };
19102
+ }
19103
+ /**
19104
+ * Resolves a layer's min/max zoom bounds, filling in the full [0, 24] range for
19105
+ * any bound left unset. Shared by the render engines so the deck.gl overlay and
19106
+ * the native MapLibre raster layers apply an identical zoom range.
19107
+ *
19108
+ * @param state - The layer state (only its zoom bounds are read)
19109
+ * @returns The concrete `{ minZoom, maxZoom }` bounds
19110
+ */
19111
+ function resolveZoomRange(state) {
19112
+ return {
19113
+ minZoom: state.minZoom ?? 0,
19114
+ maxZoom: state.maxZoom ?? 24
19115
+ };
19116
+ }
19117
+ /**
19118
+ * Derives the public snapshot for a layer.
19119
+ *
19120
+ * @param layer - Internal layer record
19121
+ * @returns A read-only info object safe to hand to consumers
19122
+ */
19123
+ function toLayerInfo(layer) {
19124
+ return {
19125
+ id: layer.id,
19126
+ name: layer.name,
19127
+ source: layer.source,
19128
+ memberUrls: layer.members ? layer.members.map((m) => m.url) : null,
19129
+ bandCount: layer.bandCount,
19130
+ bandNames: layer.bandNames ? new Map(layer.bandNames) : null,
19131
+ beforeId: layer.beforeId,
19132
+ attribution: layer.attribution,
19133
+ bounds: layer.bounds ? { ...layer.bounds } : null,
19134
+ loading: layer.loading,
19135
+ error: layer.error,
19136
+ state: { ...layer.state }
19137
+ };
19138
+ }
19139
+ /** True when `lngLat` falls inside `bounds` (edges included). */
19140
+ function containsPoint(bounds, lngLat) {
19141
+ const [lng, lat] = lngLat;
19142
+ return lng >= bounds.west && lng <= bounds.east && lat >= bounds.south && lat <= bounds.north;
19143
+ }
19144
+ /**
19145
+ * The layer's images that could carry a sample at `lngLat`, topmost first.
19146
+ *
19147
+ * A plain layer has exactly one image. A mosaic VRT layer has one per member,
19148
+ * and only the members whose extent covers the point are worth reading — a
19149
+ * member whose bounds have not been reported yet stays a candidate rather than
19150
+ * being skipped, since bounds only arrive once a member renders.
19151
+ *
19152
+ * Members are returned in reverse document order because that is the order they
19153
+ * are drawn in: `LayerManager` builds one deck.gl layer per member in member
19154
+ * order, so later members paint over earlier ones — matching GDAL, where a
19155
+ * VRT's later sources overwrite earlier ones. Sources placed at their natural
19156
+ * position can still overlap (adjacent scenes commonly do), so where they do,
19157
+ * the last one is what the user sees and therefore what a reader should report.
19158
+ *
19159
+ * @param layer - The layer to read from
19160
+ * @param lngLat - The location, [lng, lat] in WGS84
19161
+ * @returns Candidate images, topmost first; empty when the layer has not loaded
19162
+ * or the point falls outside every member
19163
+ */
19164
+ function imagesAt(layer, lngLat) {
19165
+ if (!layer.members) return layer.geotiff ? [layer.geotiff] : [];
19166
+ return layer.members.filter((m) => !m.bounds || containsPoint(m.bounds, lngLat)).reverse().map((m) => m.geotiff);
19167
+ }
19168
+ /**
19169
+ * The mosaic assets whose extent covers `lngLat`, in manifest order.
19170
+ *
19171
+ * A mosaic manifest layer (MosaicJSON or STAC) has no opened image of its own —
19172
+ * the deck.gl {@link import('@developmentseed/deck.gl-geotiff').MosaicLayer}
19173
+ * opens each asset lazily while it is in view — so a reader works from the
19174
+ * manifest's bboxes and opens what it needs. This is the mosaic counterpart to
19175
+ * {@link imagesAt}, returning URLs because the COGs may not be open yet.
19176
+ *
19177
+ * Unlike a VRT's members these are *not* reversed. A VRT has a defined paint
19178
+ * order (later sources overwrite earlier ones), but a mosaic's assets are drawn
19179
+ * in whatever order its spatial index returns them, so no candidate is reliably
19180
+ * "topmost". Manifest order is used instead: it matches MosaicJSON's convention
19181
+ * that the first asset listed for a location is the preferred one. Where assets
19182
+ * genuinely overlap the drawn pixel is ambiguous, and the first covering asset
19183
+ * is as defensible a report as any.
19184
+ *
19185
+ * @param layer - The layer to read from
19186
+ * @param lngLat - The location, [lng, lat] in WGS84
19187
+ * @returns Covering asset URLs in manifest order; empty when the layer is not a
19188
+ * mosaic manifest, has not parsed yet, or the point falls outside every asset
19189
+ */
19190
+ function assetsAt(layer, lngLat) {
19191
+ if (!layer.mosaicAssets) return [];
19192
+ const [lng, lat] = lngLat;
19193
+ return layer.mosaicAssets.filter((a) => lng >= a.bbox[0] && lng <= a.bbox[2] && lat >= a.bbox[1] && lat <= a.bbox[3]).map((a) => a.url);
19194
+ }
19195
+ /**
19196
+ * Derives a display name from a URL or file name: the last path segment
19197
+ * without query string.
19198
+ *
19199
+ * @param source - URL string or file name
19200
+ * @returns A short human-readable layer name
19201
+ */
19202
+ function deriveLayerName(source) {
19203
+ try {
19204
+ const segment = (source.includes("://") ? new URL(source).pathname : source).split("/").filter(Boolean).pop();
19205
+ return segment ? decodeURIComponent(segment) : source;
19206
+ } catch {
19207
+ return source;
19208
+ }
19209
+ }
19210
+ //#endregion
19068
19211
  //#region src/lib/state/CogTilerEngine.ts
19069
19212
  /**
19070
19213
  * The colormaps assumed renderable before the wasm module has loaded.
@@ -19407,7 +19550,11 @@ var CogTilerEngine = class {
19407
19550
  assets: layer.assets ?? null,
19408
19551
  render
19409
19552
  });
19410
- if (this._map.getLayer(lyrId)) this._map.setPaintProperty(lyrId, "raster-opacity", layer.state.opacity);
19553
+ if (this._map.getLayer(lyrId)) {
19554
+ this._map.setPaintProperty(lyrId, "raster-opacity", layer.state.opacity);
19555
+ const { minZoom, maxZoom } = resolveZoomRange(layer.state);
19556
+ this._map.setLayerZoomRange(lyrId, minZoom, maxZoom);
19557
+ }
19411
19558
  }
19412
19559
  _addMapLayer(layer, renderKey) {
19413
19560
  const srcId = this._srcId(layer.id);
@@ -19712,10 +19859,13 @@ var TiTilerEngine = class {
19712
19859
  ...tilejson.bounds ? { bounds: tilejson.bounds } : {},
19713
19860
  ...typeof tilejson.maxzoom === "number" ? { maxzoom: tilejson.maxzoom } : {}
19714
19861
  });
19862
+ const { minZoom, maxZoom } = resolveZoomRange(layer.state);
19715
19863
  this._map.addLayer({
19716
19864
  id: lyrId,
19717
19865
  type: "raster",
19718
19866
  source: srcId,
19867
+ minzoom: minZoom,
19868
+ maxzoom: maxZoom,
19719
19869
  paint: { "raster-opacity": layer.state.opacity }
19720
19870
  }, this._beforeMapId(layer));
19721
19871
  } catch (err) {
@@ -19747,7 +19897,11 @@ var TiTilerEngine = class {
19747
19897
  }
19748
19898
  _setOpacity(layer) {
19749
19899
  const lyrId = this._lyrId(layer.id);
19750
- if (this._map.getLayer(lyrId)) this._map.setPaintProperty(lyrId, "raster-opacity", layer.state.opacity);
19900
+ if (this._map.getLayer(lyrId)) {
19901
+ this._map.setPaintProperty(lyrId, "raster-opacity", layer.state.opacity);
19902
+ const { minZoom, maxZoom } = resolveZoomRange(layer.state);
19903
+ this._map.setLayerZoomRange(lyrId, minZoom, maxZoom);
19904
+ }
19751
19905
  }
19752
19906
  /** Enforces the draw order (first = bottom), honoring each layer's style
19753
19907
  * beforeId when present. */
@@ -20079,133 +20233,6 @@ async function loadMosaic(url, signal) {
20079
20233
  return parseMosaic(await resp.json());
20080
20234
  }
20081
20235
  //#endregion
20082
- //#region src/lib/state/RasterLayer.ts
20083
- /** Default visualization state for a freshly added layer. Mode/bands are
20084
- * re-picked automatically once the band count is known (unless the caller
20085
- * supplied them explicitly); single-band rasters default to the image's
20086
- * embedded color table when present ('palette') or grayscale otherwise. */
20087
- var DEFAULT_LAYER_STATE = {
20088
- mode: "rgb",
20089
- bands: [
20090
- 1,
20091
- 2,
20092
- 3
20093
- ],
20094
- rescale: null,
20095
- colormap: "gray",
20096
- reversed: false,
20097
- nodata: "auto",
20098
- opacity: 1,
20099
- gamma: 1,
20100
- stretch: "linear",
20101
- visible: true
20102
- };
20103
- /**
20104
- * Creates a complete layer state from optional overrides.
20105
- *
20106
- * @param overrides - Partial state merged over the defaults
20107
- * @returns A fully populated RasterLayerState
20108
- */
20109
- function createLayerState(overrides) {
20110
- return {
20111
- ...DEFAULT_LAYER_STATE,
20112
- ...overrides
20113
- };
20114
- }
20115
- /**
20116
- * Derives the public snapshot for a layer.
20117
- *
20118
- * @param layer - Internal layer record
20119
- * @returns A read-only info object safe to hand to consumers
20120
- */
20121
- function toLayerInfo(layer) {
20122
- return {
20123
- id: layer.id,
20124
- name: layer.name,
20125
- source: layer.source,
20126
- memberUrls: layer.members ? layer.members.map((m) => m.url) : null,
20127
- bandCount: layer.bandCount,
20128
- bandNames: layer.bandNames ? new Map(layer.bandNames) : null,
20129
- beforeId: layer.beforeId,
20130
- attribution: layer.attribution,
20131
- bounds: layer.bounds ? { ...layer.bounds } : null,
20132
- loading: layer.loading,
20133
- error: layer.error,
20134
- state: { ...layer.state }
20135
- };
20136
- }
20137
- /** True when `lngLat` falls inside `bounds` (edges included). */
20138
- function containsPoint(bounds, lngLat) {
20139
- const [lng, lat] = lngLat;
20140
- return lng >= bounds.west && lng <= bounds.east && lat >= bounds.south && lat <= bounds.north;
20141
- }
20142
- /**
20143
- * The layer's images that could carry a sample at `lngLat`, topmost first.
20144
- *
20145
- * A plain layer has exactly one image. A mosaic VRT layer has one per member,
20146
- * and only the members whose extent covers the point are worth reading — a
20147
- * member whose bounds have not been reported yet stays a candidate rather than
20148
- * being skipped, since bounds only arrive once a member renders.
20149
- *
20150
- * Members are returned in reverse document order because that is the order they
20151
- * are drawn in: `LayerManager` builds one deck.gl layer per member in member
20152
- * order, so later members paint over earlier ones — matching GDAL, where a
20153
- * VRT's later sources overwrite earlier ones. Sources placed at their natural
20154
- * position can still overlap (adjacent scenes commonly do), so where they do,
20155
- * the last one is what the user sees and therefore what a reader should report.
20156
- *
20157
- * @param layer - The layer to read from
20158
- * @param lngLat - The location, [lng, lat] in WGS84
20159
- * @returns Candidate images, topmost first; empty when the layer has not loaded
20160
- * or the point falls outside every member
20161
- */
20162
- function imagesAt(layer, lngLat) {
20163
- if (!layer.members) return layer.geotiff ? [layer.geotiff] : [];
20164
- return layer.members.filter((m) => !m.bounds || containsPoint(m.bounds, lngLat)).reverse().map((m) => m.geotiff);
20165
- }
20166
- /**
20167
- * The mosaic assets whose extent covers `lngLat`, in manifest order.
20168
- *
20169
- * A mosaic manifest layer (MosaicJSON or STAC) has no opened image of its own —
20170
- * the deck.gl {@link import('@developmentseed/deck.gl-geotiff').MosaicLayer}
20171
- * opens each asset lazily while it is in view — so a reader works from the
20172
- * manifest's bboxes and opens what it needs. This is the mosaic counterpart to
20173
- * {@link imagesAt}, returning URLs because the COGs may not be open yet.
20174
- *
20175
- * Unlike a VRT's members these are *not* reversed. A VRT has a defined paint
20176
- * order (later sources overwrite earlier ones), but a mosaic's assets are drawn
20177
- * in whatever order its spatial index returns them, so no candidate is reliably
20178
- * "topmost". Manifest order is used instead: it matches MosaicJSON's convention
20179
- * that the first asset listed for a location is the preferred one. Where assets
20180
- * genuinely overlap the drawn pixel is ambiguous, and the first covering asset
20181
- * is as defensible a report as any.
20182
- *
20183
- * @param layer - The layer to read from
20184
- * @param lngLat - The location, [lng, lat] in WGS84
20185
- * @returns Covering asset URLs in manifest order; empty when the layer is not a
20186
- * mosaic manifest, has not parsed yet, or the point falls outside every asset
20187
- */
20188
- function assetsAt(layer, lngLat) {
20189
- if (!layer.mosaicAssets) return [];
20190
- const [lng, lat] = lngLat;
20191
- return layer.mosaicAssets.filter((a) => lng >= a.bbox[0] && lng <= a.bbox[2] && lat >= a.bbox[1] && lat <= a.bbox[3]).map((a) => a.url);
20192
- }
20193
- /**
20194
- * Derives a display name from a URL or file name: the last path segment
20195
- * without query string.
20196
- *
20197
- * @param source - URL string or file name
20198
- * @returns A short human-readable layer name
20199
- */
20200
- function deriveLayerName(source) {
20201
- try {
20202
- const segment = (source.includes("://") ? new URL(source).pathname : source).split("/").filter(Boolean).pop();
20203
- return segment ? decodeURIComponent(segment) : source;
20204
- } catch {
20205
- return source;
20206
- }
20207
- }
20208
- //#endregion
20209
20236
  //#region src/lib/state/LayerManager.ts
20210
20237
  /** Default engine when none is configured: the deck.gl GPU pipeline. */
20211
20238
  var DEFAULT_ENGINE = "maplibre-gl-raster";
@@ -20429,6 +20456,8 @@ var LayerManager = class {
20429
20456
  _attribStyleReady = false;
20430
20457
  _onAttribStyleLoad = null;
20431
20458
  _destroyed = false;
20459
+ _onZoom = null;
20460
+ _zoomVisibleSig = "";
20432
20461
  /**
20433
20462
  * Creates a LayerManager bound to a map.
20434
20463
  *
@@ -20445,6 +20474,8 @@ var LayerManager = class {
20445
20474
  ...DEFAULT_DEPS$1,
20446
20475
  ...deps
20447
20476
  };
20477
+ this._onZoom = () => this._syncZoomVisibility();
20478
+ this._map.on("zoom", this._onZoom);
20448
20479
  }
20449
20480
  /** The id of the layer currently selected for editing, or null. */
20450
20481
  get selectedId() {
@@ -20934,6 +20965,10 @@ var LayerManager = class {
20934
20965
  this._map.off("load", this._onAttribStyleLoad);
20935
20966
  this._onAttribStyleLoad = null;
20936
20967
  }
20968
+ if (this._onZoom) {
20969
+ this._map.off("zoom", this._onZoom);
20970
+ this._onZoom = null;
20971
+ }
20937
20972
  if (this._overlay) {
20938
20973
  this._deps.removeOverlay(this._map, this._overlay);
20939
20974
  this._overlay = null;
@@ -21241,10 +21276,37 @@ var LayerManager = class {
21241
21276
  return;
21242
21277
  }
21243
21278
  if (!this._overlay) return;
21244
- const layers = this._layers.filter((l) => (l.geotiff || l.mosaicAssets) && l.state.visible && !this._crsFailed.has(l.id)).flatMap((l) => this._buildCogLayers(l));
21279
+ const renderable = this._layers.filter((l) => (l.geotiff || l.mosaicAssets) && l.state.visible && !this._crsFailed.has(l.id) && this._withinZoomRange(l.state));
21280
+ this._zoomVisibleSig = renderable.map((l) => l.id).join("|");
21281
+ const layers = renderable.flatMap((l) => this._buildCogLayers(l));
21245
21282
  this._overlay.setProps({ layers });
21246
21283
  }
21247
21284
  /**
21285
+ * Whether a layer draws at the map's current zoom, following MapLibre's
21286
+ * per-layer zoom semantics: visible while `minZoom <= zoom < maxZoom`, hidden
21287
+ * outside that. An unset bound falls back to the full [0, 24] range, so a
21288
+ * layer with no constraint always passes.
21289
+ */
21290
+ _withinZoomRange(state) {
21291
+ const zoom = this._map.getZoom();
21292
+ const min = state.minZoom ?? 0;
21293
+ const max = state.maxZoom ?? 24;
21294
+ return zoom >= min && zoom < max;
21295
+ }
21296
+ /**
21297
+ * Re-renders the deck.gl overlay when the map zoom crosses a layer's min/max
21298
+ * boundary. Runs on every 'zoom' event, so it stays cheap: it rebuilds only
21299
+ * when the set of in-range layers actually changes, leaving pans and in-range
21300
+ * zooms untouched (a needless rebuild would remount the COGLayers and refetch
21301
+ * their tiles). The native engines carry their zoom range on the MapLibre
21302
+ * layer itself, so this is a no-op for them.
21303
+ */
21304
+ _syncZoomVisibility() {
21305
+ if (this._engine !== "maplibre-gl-raster" || !this._overlay) return;
21306
+ if (this._layers.filter((l) => (l.geotiff || l.mosaicAssets) && l.state.visible && !this._crsFailed.has(l.id) && this._withinZoomRange(l.state)).map((l) => l.id).join("|") === this._zoomVisibleSig) return;
21307
+ this._rebuild();
21308
+ }
21309
+ /**
21248
21310
  * The deck.gl layers that draw one managed layer: exactly one for a plain
21249
21311
  * raster, and one per member for a mosaic VRT.
21250
21312
  *
@@ -24137,4 +24199,4 @@ var RasterControl = class {
24137
24199
  //#endregion
24138
24200
  export { DEFAULT_TITILER_ENDPOINT as A, COLORMAP_ROW_COUNT as B, throttle as C, loadVrt as D, isVrtUrl as E, loadColormapSprite as F, statsForBand as G, colormaps_default as H, sampleColormapStops as I, mergeAutoStats as J, MAX_SAMPLE_TILES as K, COLORMAP_DISPLAY_NAMES as L, isMosaicJsonUrl as M, PALETTE_COLORMAP as N, parseVrt as O, isKnownColormap as P, createResilientEpsgResolver as Q, COLORMAP_NAMES as R, generateId as S, isVrtFile as T, DEFAULT_INDEX_RANGE as U, colormapDisplayName as V, autoRangeFor as W, percentileFromHistogram as X, mergeBandStats as Y, readBandNames as Z, parseMosaic as _, NORMALIZED_DIFFERENCE_INDICES as a, debounce as b, readPixelValues as c, MAX_MOSAIC_ASSETS as d, MosaicUnsupportedError as f, mosaicMinZoom as g, mosaicInitialView as h, CUSTOM_NORMALIZED_DIFFERENCE as i, TITILER_TMS as j, loadGeoTIFF as k, DEFAULT_ENGINE as l, loadMosaic as m, Colorbar as n, guessBandForRole as o, assetUrlToHttps as p, computeAutoStats as q, CUSTOM_INDEX_ID as r, indexById as s, RasterControl as t, LayerManager as u, clamp as v, VrtUnsupportedError as w, formatNumericValue as x, classNames as y, COLORMAP_OPTIONS as z };
24139
24201
 
24140
- //# sourceMappingURL=RasterControl-BDDduhO9.js.map
24202
+ //# sourceMappingURL=RasterControl-hvApJoZQ.js.map