maplibre-gl-raster 0.7.0 → 0.8.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/README.md CHANGED
@@ -190,9 +190,10 @@ Per-layer visualization state, editable via the panel or `setRasterState`:
190
190
 
191
191
  ```typescript
192
192
  interface RasterLayerState {
193
- mode: "rgb" | "single"; // RGB composite or single band + colormap
194
- bands: number[]; // 1-indexed band selection
195
- rescale: [number, number][] | null; // per-channel min/max; null = auto (2-98%)
193
+ mode: "rgb" | "single" | "index"; // RGB composite, single band + colormap, or normalized-difference index
194
+ bands: number[]; // 1-indexed band selection ([A, B] in index mode)
195
+ index?: string; // normalized-difference preset id (index mode), e.g. "ndvi"
196
+ rescale: [number, number][] | null; // per-channel min/max; null = auto (2-98%, or [-1, 1] for index)
196
197
  colormap: string; // colormap name; "palette" = embedded color table
197
198
  reversed: boolean; // sample the named colormap back-to-front
198
199
  nodata: number | "off" | "auto"; // nodata handling
@@ -213,6 +214,8 @@ interface RasterLayerState {
213
214
 
214
215
  When a raster loads, the mode and bands are picked automatically (3+ bands → RGB `[1, 2, 3]`; otherwise single-band), and the rescale range defaults to the 2-98% percentile of sampled statistics. Single-band rasters use the image's embedded color table when it carries one (`colormap: "palette"`) and grayscale otherwise. The first four bands are fetched as GPU textures, so band combinations among them re-render instantly without re-downloading tiles.
215
216
 
217
+ **Index mode** computes a normalized-difference index `(A - B) / (A + B)` of two bands entirely on the GPU (no server or download), then colors the `[-1, 1]` result with a colormap. Presets (`NORMALIZED_DIFFERENCE_INDICES`) pre-fill the band roles and a default ramp for NDVI, NDWI, NDMI, NBR, NDBI, and NDSI, and a "Custom" option lets you pick any two bands; the settings panel seeds the operand bands from the file's band names when present. Index mode requires the default `maplibre-gl-raster` engine (the `cog-tiler-wasm` engine has no band-math endpoint and falls back to a colormapped view of the first operand).
218
+
216
219
  ### RasterControlReact
217
220
 
218
221
  React wrapper component for `RasterControl`.
@@ -1979,6 +1979,26 @@ var FilterNaN = {
1979
1979
  }
1980
1980
  ` }
1981
1981
  };
1982
+ /** Normalized-difference index: replaces the color with `(A - B) / (A + B)`
1983
+ * broadcast across RGB, where `A` is the red channel and `B` the green channel
1984
+ * populated by a preceding `CompositeBands` (r → band A, g → band B). The
1985
+ * result lies in [-1, 1] and is scale-invariant, so it can run on the raw
1986
+ * sampled band values regardless of the texture's sample scale — a downstream
1987
+ * `PerBandLinearRescale` maps the chosen window (default [-1, 1]) into [0, 1]
1988
+ * for the colormap. A zero denominator yields 0 rather than a NaN/Inf. Runs
1989
+ * before rescale / gamma / colormap. */
1990
+ var NormalizedDifference = {
1991
+ name: "normalizedDifference",
1992
+ inject: { "fs:DECKGL_FILTER_COLOR": `
1993
+ {
1994
+ float ndA = color.r;
1995
+ float ndB = color.g;
1996
+ float ndDenom = ndA + ndB;
1997
+ float nd = abs(ndDenom) < 1e-12 ? 0.0 : (ndA - ndB) / ndDenom;
1998
+ color.rgb = vec3(nd);
1999
+ }
2000
+ ` }
2001
+ };
1982
2002
  /** Per-channel `LinearRescale`. Same idea as the shipped `LinearRescale`,
1983
2003
  * but min/max are vec3 so each band gets its own range. */
1984
2004
  var PerBandLinearRescale = {
@@ -2176,6 +2196,29 @@ function pickMapping(data, requested, mode) {
2176
2196
  if (b) mapping.b = b;
2177
2197
  return mapping;
2178
2198
  }
2199
+ /** Push the nodata-discard modules for a tile, in the correct order. Filters
2200
+ * nodata BEFORE any rescale / gamma / colormap so the comparison happens
2201
+ * against the texture's native sample value. NaN nodata uses the custom
2202
+ * isnan() shader; everything else uses FilterNoDataVal with the value
2203
+ * normalized into the GPU's sample space (uint8 255 → 1.0 for r8unorm). Also
2204
+ * applies implicit NaN-as-nodata for float COGs — IEEE-754 NaN is invalid by
2205
+ * definition, and GDAL/QGIS treat it as transparent even when the file
2206
+ * declares no GDAL_NODATA tag (e.g. Sentinel-2 derivatives that mask
2207
+ * outside-swath pixels with NaN). Gated on float textures via sampleScale
2208
+ * (r8unorm uint8 textures can't carry NaN), and skipped when the user
2209
+ * explicitly set nodata to "off" or when FilterNaN is already in the pipeline
2210
+ * (state.nodata was numerically NaN). */
2211
+ function pushNodataFilters(state, data, pipeline) {
2212
+ const nodata = effectiveNodata(state, data.nodata);
2213
+ let explicitNodataModule = null;
2214
+ if (nodata !== null) {
2215
+ explicitNodataModule = nodataModule(nodata, data.sampleScale);
2216
+ if (explicitNodataModule) pipeline.push(explicitNodataModule);
2217
+ }
2218
+ const isFloatTexture = data.sampleScale === 1;
2219
+ const filterNaNAlreadyPushed = explicitNodataModule?.module === FilterNaN;
2220
+ if (isFloatTexture && state.nodata !== "off" && !filterNaNAlreadyPushed) pipeline.push({ module: FilterNaN });
2221
+ }
2179
2222
  /** Shared renderTile builder. Handles both RGB and single-band paths
2180
2223
  * via a discriminated `mode`. The two paths differ only in: (a) which
2181
2224
  * bands feed CompositeBands, (b) whether a Colormap module is appended
@@ -2195,15 +2238,7 @@ function buildRenderTile(state, autoStats, mode) {
2195
2238
  module: CompositeBands,
2196
2239
  props: buildCompositeBandsProps(mapping, data.bands)
2197
2240
  }];
2198
- const nodata = effectiveNodata(state, data.nodata);
2199
- let explicitNodataModule = null;
2200
- if (nodata !== null) {
2201
- explicitNodataModule = nodataModule(nodata, data.sampleScale);
2202
- if (explicitNodataModule) pipeline.push(explicitNodataModule);
2203
- }
2204
- const isFloatTexture = data.sampleScale === 1;
2205
- const filterNaNAlreadyPushed = explicitNodataModule?.module === FilterNaN;
2206
- if (isFloatTexture && state.nodata !== "off" && !filterNaNAlreadyPushed) pipeline.push({ module: FilterNaN });
2241
+ pushNodataFilters(state, data, pipeline);
2207
2242
  if (mode.kind === "palette") {
2208
2243
  const max = 255 / data.sampleScale;
2209
2244
  pipeline.push({
@@ -2284,6 +2319,61 @@ function buildPaletteCompositeRenderTile(state, paletteTexture) {
2284
2319
  colormapTexture: paletteTexture
2285
2320
  });
2286
2321
  }
2322
+ /** Default rescale window for a normalized-difference index: the full [-1, 1]
2323
+ * range the formula can produce. Used when `state.rescale` is unset. */
2324
+ var DEFAULT_INDEX_RANGE = [-1, 1];
2325
+ /** Index renderTile: computes a normalized difference `(A - B) / (A + B)` of
2326
+ * two bands on the GPU, then colormaps the [-1, 1] result. `CompositeBands`
2327
+ * loads band A into the red channel and band B into green; the
2328
+ * `NormalizedDifference` module collapses them to a single value broadcast
2329
+ * across RGB; `PerBandLinearRescale` maps the chosen window (default
2330
+ * {@link DEFAULT_INDEX_RANGE}) into [0, 1] for the colormap. The index is
2331
+ * scale-invariant, so — unlike the single/RGB paths — the rescale window is
2332
+ * NOT divided by `data.sampleScale`. Re-renders without a re-fetch when the
2333
+ * colormap / rescale change (within the cached band set). */
2334
+ function buildIndexCompositeRenderTile(state, colormapTexture) {
2335
+ const colormapIndex = COLORMAP_INDEX[(state.colormap ?? "rdylgn").toLowerCase()] ?? COLORMAP_INDEX.rdylgn;
2336
+ return function renderTile(data) {
2337
+ if (data.bands.size === 0) return { renderPipeline: [] };
2338
+ const bandA = pickBand(data, state.bands?.[0] ?? 1);
2339
+ if (!bandA) return { renderPipeline: [] };
2340
+ const pipeline = [{
2341
+ module: CompositeBands,
2342
+ props: buildCompositeBandsProps({
2343
+ r: bandA,
2344
+ g: pickBand(data, state.bands?.[1] ?? 2) ?? bandA
2345
+ }, data.bands)
2346
+ }];
2347
+ pushNodataFilters(state, data, pipeline);
2348
+ pipeline.push({ module: NormalizedDifference });
2349
+ const [lo, hi] = safeRange(state.rescale?.[0] ?? DEFAULT_INDEX_RANGE);
2350
+ pipeline.push({
2351
+ module: PerBandLinearRescale,
2352
+ props: {
2353
+ rescaleMin: [
2354
+ lo,
2355
+ lo,
2356
+ lo
2357
+ ],
2358
+ rescaleMax: [
2359
+ hi,
2360
+ hi,
2361
+ hi
2362
+ ]
2363
+ }
2364
+ });
2365
+ pushAdjustments(state, pipeline);
2366
+ pipeline.push({
2367
+ module: Colormap,
2368
+ props: {
2369
+ colormapTexture,
2370
+ colormapIndex,
2371
+ reversed: state.reversed ?? false
2372
+ }
2373
+ });
2374
+ return { renderPipeline: pipeline };
2375
+ };
2376
+ }
2287
2377
  //#endregion
2288
2378
  //#region node_modules/@developmentseed/geotiff/dist/colormap.js
2289
2379
  /**
@@ -18010,13 +18100,14 @@ var CogTilerEngine = class {
18010
18100
  * is intentionally excluded (applied as a paint property instead). */
18011
18101
  _renderOptionsFor(layer) {
18012
18102
  const s = layer.state;
18013
- const bidx = s.mode === "single" ? [s.bands[0] ?? 1] : s.bands.slice(0, 3).map((b) => b || 1);
18103
+ const colormapped = s.mode === "single" || s.mode === "index";
18104
+ const bidx = colormapped ? [s.bands[0] ?? 1] : s.bands.slice(0, 3).map((b) => b || 1);
18014
18105
  const opts = {
18015
18106
  bidx,
18016
18107
  stretch: s.stretch,
18017
18108
  gamma: s.gamma
18018
18109
  };
18019
- if (s.mode === "single") {
18110
+ if (colormapped) {
18020
18111
  opts.reversed = s.reversed;
18021
18112
  if (s.colormap && s.colormap !== "palette") opts.colormap = s.colormap;
18022
18113
  }
@@ -18162,7 +18253,7 @@ function fetchBandsFor(layer) {
18162
18253
  1,
18163
18254
  2,
18164
18255
  3
18165
- ]).slice(0, 3) : [bands?.[0] ?? 1];
18256
+ ]).slice(0, 3) : layer.state.mode === "index" ? (bands && bands.length > 0 ? bands : [1, 2]).slice(0, 2) : [bands?.[0] ?? 1];
18166
18257
  const unique = [...new Set(sampled.filter((b) => Number.isInteger(b) && b >= 1))].sort((a, b) => a - b);
18167
18258
  if (unique.length === 0) unique.push(1);
18168
18259
  return unique.slice(0, 4);
@@ -18795,6 +18886,7 @@ var LayerManager = class {
18795
18886
  if (layer.paletteTexture) return buildPaletteCompositeRenderTile(layer.state, layer.paletteTexture);
18796
18887
  } else if (this._colormapTexture) return buildSingleCompositeRenderTile(layer.state, this._colormapTexture, layer.autoStats);
18797
18888
  }
18889
+ if (layer.state.mode === "index" && this._colormapTexture) return buildIndexCompositeRenderTile(layer.state, this._colormapTexture);
18798
18890
  return buildRgbCompositeRenderTile(layer.state, layer.autoStats);
18799
18891
  }
18800
18892
  };
@@ -19337,6 +19429,135 @@ var LayerList = class {
19337
19429
  }
19338
19430
  };
19339
19431
  //#endregion
19432
+ //#region src/lib/raster/indices.ts
19433
+ /** The `id` used for a free-form normalized difference (generic Band A / Band
19434
+ * B, no role hints). */
19435
+ var CUSTOM_INDEX_ID = "custom";
19436
+ /**
19437
+ * Built-in normalized-difference index presets, in menu order. Each maps two
19438
+ * band roles onto the shared `(A - B) / (A + B)` formula.
19439
+ */
19440
+ var NORMALIZED_DIFFERENCE_INDICES = [
19441
+ {
19442
+ id: "ndvi",
19443
+ label: "NDVI",
19444
+ name: "Normalized Difference Vegetation Index — (NIR - Red) / (NIR + Red)",
19445
+ roleA: "NIR",
19446
+ roleB: "Red",
19447
+ colormap: "rdylgn"
19448
+ },
19449
+ {
19450
+ id: "ndwi",
19451
+ label: "NDWI",
19452
+ name: "Normalized Difference Water Index — (Green - NIR) / (Green + NIR)",
19453
+ roleA: "Green",
19454
+ roleB: "NIR",
19455
+ colormap: "blues"
19456
+ },
19457
+ {
19458
+ id: "ndmi",
19459
+ label: "NDMI",
19460
+ name: "Normalized Difference Moisture Index — (NIR - SWIR1) / (NIR + SWIR1)",
19461
+ roleA: "NIR",
19462
+ roleB: "SWIR1",
19463
+ colormap: "brbg"
19464
+ },
19465
+ {
19466
+ id: "nbr",
19467
+ label: "NBR",
19468
+ name: "Normalized Burn Ratio — (NIR - SWIR2) / (NIR + SWIR2)",
19469
+ roleA: "NIR",
19470
+ roleB: "SWIR2",
19471
+ colormap: "rdylgn"
19472
+ },
19473
+ {
19474
+ id: "ndbi",
19475
+ label: "NDBI",
19476
+ name: "Normalized Difference Built-up Index — (SWIR1 - NIR) / (SWIR1 + NIR)",
19477
+ roleA: "SWIR1",
19478
+ roleB: "NIR",
19479
+ colormap: "inferno"
19480
+ },
19481
+ {
19482
+ id: "ndsi",
19483
+ label: "NDSI",
19484
+ name: "Normalized Difference Snow Index — (Green - SWIR1) / (Green + SWIR1)",
19485
+ roleA: "Green",
19486
+ roleB: "SWIR1",
19487
+ colormap: "blues"
19488
+ }
19489
+ ];
19490
+ /** The generic "custom" preset: two unlabeled bands and a neutral diverging
19491
+ * ramp. Kept separate from the named presets so callers can list it last. */
19492
+ var CUSTOM_NORMALIZED_DIFFERENCE = {
19493
+ id: CUSTOM_INDEX_ID,
19494
+ label: "Custom",
19495
+ name: "Custom normalized difference — (Band A - Band B) / (Band A + Band B)",
19496
+ roleA: "Band A",
19497
+ roleB: "Band B",
19498
+ colormap: "rdylgn"
19499
+ };
19500
+ /** Look up a preset by id (including the custom preset), or null. */
19501
+ function indexById(id) {
19502
+ if (!id) return null;
19503
+ if (id === "custom") return CUSTOM_NORMALIZED_DIFFERENCE;
19504
+ return NORMALIZED_DIFFERENCE_INDICES.find((i) => i.id === id) ?? null;
19505
+ }
19506
+ /**
19507
+ * Guess the 1-based band number that plays a spectral role (e.g. "NIR") for a
19508
+ * raster, matching the role against GDAL band names. Common Sentinel-2 /
19509
+ * Landsat aliases are recognized. Returns null when no band name matches.
19510
+ *
19511
+ * @param role - The role label from a preset (e.g. "Red", "NIR", "SWIR1").
19512
+ * @param bandNames - 1-indexed band-number → name map, when the COG carries one.
19513
+ */
19514
+ function guessBandForRole(role, bandNames) {
19515
+ if (!bandNames || bandNames.size === 0) return null;
19516
+ const aliases = ROLE_ALIASES[role.toLowerCase()] ?? [role.toLowerCase()];
19517
+ for (const [band, rawName] of bandNames) {
19518
+ const name = rawName.toLowerCase();
19519
+ if (aliases.some((alias) => name.includes(alias))) return band;
19520
+ }
19521
+ return null;
19522
+ }
19523
+ /** Substrings that identify a spectral role inside a GDAL band name. */
19524
+ var ROLE_ALIASES = {
19525
+ red: [
19526
+ "red",
19527
+ "b04",
19528
+ "b4"
19529
+ ],
19530
+ green: [
19531
+ "green",
19532
+ "b03",
19533
+ "b3"
19534
+ ],
19535
+ blue: [
19536
+ "blue",
19537
+ "b02",
19538
+ "b2"
19539
+ ],
19540
+ nir: [
19541
+ "nir",
19542
+ "near infrared",
19543
+ "b08",
19544
+ "b8",
19545
+ "b8a"
19546
+ ],
19547
+ swir1: [
19548
+ "swir1",
19549
+ "swir 1",
19550
+ "swir_1",
19551
+ "b11"
19552
+ ],
19553
+ swir2: [
19554
+ "swir2",
19555
+ "swir 2",
19556
+ "swir_2",
19557
+ "b12"
19558
+ ]
19559
+ };
19560
+ //#endregion
19340
19561
  //#region src/lib/ui/BandHistogram.ts
19341
19562
  var HANDLE_SIZE = 12;
19342
19563
  /**
@@ -19558,9 +19779,11 @@ var RGB_CHANNELS = [
19558
19779
  }
19559
19780
  ];
19560
19781
  var HELP = {
19561
- mode: "RGB / composite picks one band per output channel for true- or false-color images. Single band sends one band through a colormap.",
19782
+ mode: "RGB / composite picks one band per output channel for true- or false-color images. Single band sends one band through a colormap. Index computes a normalized-difference index of two bands.",
19562
19783
  bandsRgb: "Pick which band feeds each output channel. Native order is usually 1=red, 2=green, 3=blue; reorder to make false-color composites.",
19563
19784
  bandSingle: "Which band's pixel values feed the colormap.",
19785
+ index: "Compute a normalized-difference index (A - B) / (A + B) of two bands, then color the [-1, 1] result. Pick a preset (e.g. NDVI) or Custom.",
19786
+ indexBands: "Assign each operand of the index to a band. Presets name a role (e.g. NIR, Red); the initial guess uses the file's band names when present.",
19564
19787
  rescale: "Maps a window of source values to the colormap input. Drag the histogram handles, type values, or pick a preset.",
19565
19788
  colormap: "Color lookup applied to the rescaled value (after the curve, before nodata).",
19566
19789
  reversed: "Sample the colormap from end to start, equivalent to a reversed variant of the ramp.",
@@ -19758,13 +19981,14 @@ var SettingsSection = class {
19758
19981
  const bandOptions = Array.from({ length: Math.max(1, layer.bandCount ?? 1) }, (_, i) => i + 1);
19759
19982
  const paletteActive = mode === "single" && state.colormap === "palette" && layer.palette !== null;
19760
19983
  this._body.appendChild(this._buildModeField(state, mode, bandOptions));
19761
- this._body.appendChild(this._buildBandsField(layer, state, mode, bandOptions));
19984
+ if (mode === "index") this._body.appendChild(this._buildIndexField(layer, state, bandOptions));
19985
+ else this._body.appendChild(this._buildBandsField(layer, state, mode, bandOptions));
19762
19986
  if (!paletteActive) this._body.appendChild(this._buildRescaleField(layer, state, mode));
19763
- if (mode === "single") {
19987
+ if (mode === "single" || mode === "index") {
19764
19988
  const picker = new ColormapPicker({
19765
19989
  value: state.colormap,
19766
- palette: layer.palette,
19767
- stats: statsForBand(layer.autoStats, state.bands[0] ?? 1),
19990
+ palette: mode === "single" ? layer.palette : null,
19991
+ stats: mode === "single" ? statsForBand(layer.autoStats, state.bands[0] ?? 1) : null,
19768
19992
  onChange: (name) => {
19769
19993
  this._setState({ colormap: name });
19770
19994
  this.render();
@@ -19791,14 +20015,22 @@ var SettingsSection = class {
19791
20015
  2,
19792
20016
  3
19793
20017
  ].map((b) => Math.min(b, maxBand));
19794
- return field("Mode", select([{
19795
- value: "rgb",
19796
- label: "RGB / composite"
19797
- }, {
19798
- value: "single",
19799
- label: "Single band + colormap"
19800
- }], mode, (next) => {
19801
- this._setState({
20018
+ return field("Mode", select([
20019
+ {
20020
+ value: "rgb",
20021
+ label: "RGB / composite"
20022
+ },
20023
+ {
20024
+ value: "single",
20025
+ label: "Single band + colormap"
20026
+ },
20027
+ {
20028
+ value: "index",
20029
+ label: "Index (normalized difference)"
20030
+ }
20031
+ ], mode, (next) => {
20032
+ if (next === "index") this._setState(this._indexPatchFor(indexById(state.index)));
20033
+ else this._setState({
19802
20034
  mode: next,
19803
20035
  bands: next === "single" ? [state.bands[0] ?? 1] : rgbDefault,
19804
20036
  rescale: null
@@ -19806,6 +20038,26 @@ var SettingsSection = class {
19806
20038
  this.render();
19807
20039
  }, "mode"), HELP.mode);
19808
20040
  }
20041
+ /** Builds the state patch for entering / switching an index preset: assigns
20042
+ * each operand a band (guessed from band names, else 1 and 2), applies the
20043
+ * preset's default colormap, and resets rescale to the [-1, 1] auto range. */
20044
+ _indexPatchFor(preset) {
20045
+ const index = preset ?? NORMALIZED_DIFFERENCE_INDICES[0];
20046
+ const layer = this._getLayer();
20047
+ const bandCount = Math.max(1, layer?.bandCount ?? 1);
20048
+ const names = layer?.bandNames ?? null;
20049
+ const clamp = (b) => Math.min(Math.max(1, b), bandCount);
20050
+ const a = clamp(guessBandForRole(index.roleA, names) ?? 1);
20051
+ const b = clamp(guessBandForRole(index.roleB, names) ?? (a === 2 ? 1 : 2));
20052
+ return {
20053
+ mode: "index",
20054
+ index: index.id,
20055
+ bands: [a, b],
20056
+ colormap: index.colormap,
20057
+ reversed: false,
20058
+ rescale: null
20059
+ };
20060
+ }
19809
20061
  _buildBandsField(layer, state, mode, bandOptions) {
19810
20062
  const options = bandOptions.map((n) => ({
19811
20063
  value: String(n),
@@ -19826,14 +20078,46 @@ var SettingsSection = class {
19826
20078
  });
19827
20079
  return field("Bands (R, G, B)", row, HELP.bandsRgb);
19828
20080
  }
20081
+ /** Index mode UI: a preset selector plus a band picker for each operand of
20082
+ * `(A - B) / (A + B)`, labelled with the preset's roles (e.g. NIR / Red). */
20083
+ _buildIndexField(layer, state, bandOptions) {
20084
+ const preset = indexById(state.index) ?? NORMALIZED_DIFFERENCE_INDICES[0];
20085
+ const wrap = el("div");
20086
+ const presetSelect = select([...NORMALIZED_DIFFERENCE_INDICES.map((i) => ({
20087
+ value: i.id,
20088
+ label: i.label
20089
+ })), {
20090
+ value: CUSTOM_NORMALIZED_DIFFERENCE.id,
20091
+ label: CUSTOM_NORMALIZED_DIFFERENCE.label
20092
+ }], preset.id, (next) => {
20093
+ this._setState(this._indexPatchFor(indexById(next)));
20094
+ this.render();
20095
+ }, "index-preset");
20096
+ wrap.appendChild(field("Index", presetSelect, preset.name));
20097
+ const bandChoices = bandOptions.map((n) => ({
20098
+ value: String(n),
20099
+ label: bandLabel(n, layer.bandNames)
20100
+ }));
20101
+ const operandSelect = (slot, ariaLabel) => select(bandChoices, String(state.bands[slot] ?? (slot === 0 ? 1 : 2)), (next) => {
20102
+ const bands = [state.bands[0] ?? 1, state.bands[1] ?? 2];
20103
+ bands[slot] = Number(next);
20104
+ this._setState({ bands });
20105
+ this.render();
20106
+ }, ariaLabel);
20107
+ const grid = el("div", { className: "mlr-band-grid" }, operandSelect(0, "index-band-a"), operandSelect(1, "index-band-b"));
20108
+ wrap.appendChild(field(`Bands (${preset.roleA}, ${preset.roleB})`, grid, HELP.indexBands));
20109
+ return wrap;
20110
+ }
19829
20111
  _buildRescaleField(layer, state, mode) {
19830
20112
  const autoStats = layer.autoStats;
19831
- const channelCount = mode === "single" ? 1 : 3;
20113
+ const channelCount = mode === "rgb" ? 3 : 1;
19832
20114
  const bands = state.bands;
19833
- const perBandStats = Array.from({ length: channelCount }, (_, i) => statsForBand(autoStats, bands[i] ?? bands[0] ?? 1));
20115
+ const isIndex = mode === "index";
20116
+ const perBandStats = Array.from({ length: channelCount }, (_, i) => isIndex ? null : statsForBand(autoStats, bands[i] ?? bands[0] ?? 1));
19834
20117
  const perBandPercentile = perBandStats.map((s) => s ? autoRangeFor(s) : null);
19835
20118
  const perBandMinMax = perBandStats.map((s) => s ? [s.min, s.max] : null);
19836
- const values = Array.from({ length: channelCount }, (_, i) => state.rescale?.[i] ?? perBandPercentile[i] ?? [0, 1]);
20119
+ const defaultRange = isIndex ? [DEFAULT_INDEX_RANGE[0], DEFAULT_INDEX_RANGE[1]] : [0, 1];
20120
+ const values = Array.from({ length: channelCount }, (_, i) => state.rescale?.[i] ?? perBandPercentile[i] ?? defaultRange);
19837
20121
  const setChannel = (i, next) => {
19838
20122
  const out = values.map((v) => [...v]);
19839
20123
  out[i] = next;
@@ -19845,11 +20129,11 @@ var SettingsSection = class {
19845
20129
  const wrap = el("div", { className: "mlr-rescale" });
19846
20130
  const rows = [];
19847
20131
  for (let i = 0; i < channelCount; i++) {
19848
- const channel = mode === "single" ? null : RGB_CHANNELS[i];
20132
+ const channel = mode === "rgb" ? RGB_CHANNELS[i] : null;
19849
20133
  const row = new RescaleRow({
19850
20134
  color: channel?.color ?? "var(--mlr-histogram-neutral)",
19851
20135
  label: channel?.label,
19852
- ariaPrefix: mode === "single" ? "rescale" : `rescale-${channel.label.toLowerCase()}`,
20136
+ ariaPrefix: channel ? `rescale-${channel.label.toLowerCase()}` : "rescale",
19853
20137
  onChange: (next) => setChannel(i, next),
19854
20138
  onDragStart: () => {
19855
20139
  this._dragCount++;
@@ -20839,7 +21123,7 @@ var RasterControl = class {
20839
21123
  if (!map || !manager) return;
20840
21124
  const seen = /* @__PURE__ */ new Set();
20841
21125
  for (const layer of manager.getLayers()) {
20842
- if (!layer.state.colorbar?.visible || !layer.state.visible || layer.state.mode !== "single" || layer.state.colormap === "palette") continue;
21126
+ if (!layer.state.colorbar?.visible || !layer.state.visible || layer.state.mode !== "single" && layer.state.mode !== "index" || layer.state.mode === "single" && layer.state.colormap === "palette") continue;
20843
21127
  seen.add(layer.id);
20844
21128
  const options = this._colorbarOptionsFor(layer);
20845
21129
  const existing = this._colorbars.get(layer.id);
@@ -20864,7 +21148,8 @@ var RasterControl = class {
20864
21148
  _colorbarOptionsFor(layer) {
20865
21149
  const band = layer.state.bands[0] ?? 1;
20866
21150
  const stats = statsForBand(layer.autoStats, band);
20867
- const range = layer.state.rescale?.[0] ?? (stats ? autoRangeFor(stats) : [0, 1]);
21151
+ const autoRange = layer.state.mode === "index" ? [-1, 1] : stats ? autoRangeFor(stats) : [0, 1];
21152
+ const range = layer.state.rescale?.[0] ?? autoRange;
20868
21153
  const cb = layer.state.colorbar;
20869
21154
  return {
20870
21155
  colormap: layer.state.colormap,
@@ -21137,6 +21422,6 @@ var RasterControl = class {
21137
21422
  }
21138
21423
  };
21139
21424
  //#endregion
21140
- export { autoRangeFor as C, percentileFromHistogram as D, computeAutoStats as E, readBandNames as O, colormaps_default as S, MAX_SAMPLE_TILES as T, COLORMAP_DISPLAY_NAMES as _, LayerManager as a, COLORMAP_ROW_COUNT as b, loadColormapSprite as c, classNames as d, debounce as f, loadGeoTIFF as g, throttle as h, DEFAULT_ENGINE as i, createResilientEpsgResolver as k, sampleColormapStops as l, generateId as m, Colorbar as n, PALETTE_COLORMAP as o, formatNumericValue as p, readPixelValues as r, isKnownColormap as s, RasterControl as t, clamp as u, COLORMAP_NAMES as v, statsForBand as w, colormapDisplayName as x, COLORMAP_OPTIONS as y };
21425
+ 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 };
21141
21426
 
21142
- //# sourceMappingURL=RasterControl-CNxONQGm.js.map
21427
+ //# sourceMappingURL=RasterControl-BWZxkUQh.js.map