maplibre-gl-raster 0.7.0 → 0.9.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 +6 -3
- package/dist/{RasterControl-CNxONQGm.js → RasterControl-BfTDWAnW.js} +333 -42
- package/dist/RasterControl-BfTDWAnW.js.map +1 -0
- package/dist/index.mjs +2 -2
- package/dist/react.mjs +1 -1
- package/dist/types/index.d.ts +76 -4
- package/dist/types/react.d.ts +10 -4
- package/package.json +1 -1
- package/dist/RasterControl-CNxONQGm.js.map +0 -1
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
|
|
194
|
-
bands: number[]; // 1-indexed band selection
|
|
195
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
};
|
|
@@ -19044,8 +19136,8 @@ var PixelInspector = class {
|
|
|
19044
19136
|
//#region src/lib/ui/AddDataSection.ts
|
|
19045
19137
|
/**
|
|
19046
19138
|
* "Add data" section: URL input + Load button, plus a drop zone that accepts
|
|
19047
|
-
* `.tif` / `.tiff` files via click-to-browse
|
|
19048
|
-
* cog-viewer's EmptyState.
|
|
19139
|
+
* one or more `.tif` / `.tiff` files via click-to-browse (multi-select) or
|
|
19140
|
+
* drag-and-drop. Ported from cog-viewer's EmptyState.
|
|
19049
19141
|
*/
|
|
19050
19142
|
var AddDataSection = class {
|
|
19051
19143
|
/** Root element to insert into the panel. */
|
|
@@ -19057,6 +19149,11 @@ var AddDataSection = class {
|
|
|
19057
19149
|
*/
|
|
19058
19150
|
constructor(options) {
|
|
19059
19151
|
const isTiff = (file) => /\.tiff?$/i.test(file.name);
|
|
19152
|
+
const addFiles = (files) => {
|
|
19153
|
+
if (!files) return;
|
|
19154
|
+
const beforeId = currentBeforeId();
|
|
19155
|
+
for (const file of Array.from(files)) if (isTiff(file)) options.onAddFile(file, beforeId);
|
|
19156
|
+
};
|
|
19060
19157
|
const input = el("input", {
|
|
19061
19158
|
className: "mlr-input",
|
|
19062
19159
|
type: "text",
|
|
@@ -19158,17 +19255,19 @@ var AddDataSection = class {
|
|
|
19158
19255
|
const fileInput = el("input", {
|
|
19159
19256
|
type: "file",
|
|
19160
19257
|
ariaLabel: "raster-file",
|
|
19161
|
-
attrs: {
|
|
19258
|
+
attrs: {
|
|
19259
|
+
accept: ".tif,.tiff",
|
|
19260
|
+
multiple: ""
|
|
19261
|
+
}
|
|
19162
19262
|
});
|
|
19163
19263
|
fileInput.style.display = "none";
|
|
19164
19264
|
fileInput.addEventListener("change", () => {
|
|
19165
|
-
|
|
19166
|
-
if (f && isTiff(f)) options.onAddFile(f, currentBeforeId());
|
|
19265
|
+
addFiles(fileInput.files);
|
|
19167
19266
|
fileInput.value = "";
|
|
19168
19267
|
});
|
|
19169
19268
|
const dropZone = el("div", {
|
|
19170
19269
|
className: "mlr-drop-zone",
|
|
19171
|
-
text: "Drop
|
|
19270
|
+
text: "Drop .tif files here, or click to browse",
|
|
19172
19271
|
attrs: {
|
|
19173
19272
|
role: "button",
|
|
19174
19273
|
tabindex: "0"
|
|
@@ -19191,8 +19290,7 @@ var AddDataSection = class {
|
|
|
19191
19290
|
dropZone.addEventListener("drop", (e) => {
|
|
19192
19291
|
e.preventDefault();
|
|
19193
19292
|
dropZone.classList.remove("dragover");
|
|
19194
|
-
|
|
19195
|
-
if (f && isTiff(f)) options.onAddFile(f, currentBeforeId());
|
|
19293
|
+
addFiles(e.dataTransfer?.files);
|
|
19196
19294
|
});
|
|
19197
19295
|
this.el = el("div", { className: "mlr-section mlr-add-data" }, el("div", {
|
|
19198
19296
|
className: "mlr-section-title",
|
|
@@ -19337,6 +19435,135 @@ var LayerList = class {
|
|
|
19337
19435
|
}
|
|
19338
19436
|
};
|
|
19339
19437
|
//#endregion
|
|
19438
|
+
//#region src/lib/raster/indices.ts
|
|
19439
|
+
/** The `id` used for a free-form normalized difference (generic Band A / Band
|
|
19440
|
+
* B, no role hints). */
|
|
19441
|
+
var CUSTOM_INDEX_ID = "custom";
|
|
19442
|
+
/**
|
|
19443
|
+
* Built-in normalized-difference index presets, in menu order. Each maps two
|
|
19444
|
+
* band roles onto the shared `(A - B) / (A + B)` formula.
|
|
19445
|
+
*/
|
|
19446
|
+
var NORMALIZED_DIFFERENCE_INDICES = [
|
|
19447
|
+
{
|
|
19448
|
+
id: "ndvi",
|
|
19449
|
+
label: "NDVI",
|
|
19450
|
+
name: "Normalized Difference Vegetation Index — (NIR - Red) / (NIR + Red)",
|
|
19451
|
+
roleA: "NIR",
|
|
19452
|
+
roleB: "Red",
|
|
19453
|
+
colormap: "rdylgn"
|
|
19454
|
+
},
|
|
19455
|
+
{
|
|
19456
|
+
id: "ndwi",
|
|
19457
|
+
label: "NDWI",
|
|
19458
|
+
name: "Normalized Difference Water Index — (Green - NIR) / (Green + NIR)",
|
|
19459
|
+
roleA: "Green",
|
|
19460
|
+
roleB: "NIR",
|
|
19461
|
+
colormap: "blues"
|
|
19462
|
+
},
|
|
19463
|
+
{
|
|
19464
|
+
id: "ndmi",
|
|
19465
|
+
label: "NDMI",
|
|
19466
|
+
name: "Normalized Difference Moisture Index — (NIR - SWIR1) / (NIR + SWIR1)",
|
|
19467
|
+
roleA: "NIR",
|
|
19468
|
+
roleB: "SWIR1",
|
|
19469
|
+
colormap: "brbg"
|
|
19470
|
+
},
|
|
19471
|
+
{
|
|
19472
|
+
id: "nbr",
|
|
19473
|
+
label: "NBR",
|
|
19474
|
+
name: "Normalized Burn Ratio — (NIR - SWIR2) / (NIR + SWIR2)",
|
|
19475
|
+
roleA: "NIR",
|
|
19476
|
+
roleB: "SWIR2",
|
|
19477
|
+
colormap: "rdylgn"
|
|
19478
|
+
},
|
|
19479
|
+
{
|
|
19480
|
+
id: "ndbi",
|
|
19481
|
+
label: "NDBI",
|
|
19482
|
+
name: "Normalized Difference Built-up Index — (SWIR1 - NIR) / (SWIR1 + NIR)",
|
|
19483
|
+
roleA: "SWIR1",
|
|
19484
|
+
roleB: "NIR",
|
|
19485
|
+
colormap: "inferno"
|
|
19486
|
+
},
|
|
19487
|
+
{
|
|
19488
|
+
id: "ndsi",
|
|
19489
|
+
label: "NDSI",
|
|
19490
|
+
name: "Normalized Difference Snow Index — (Green - SWIR1) / (Green + SWIR1)",
|
|
19491
|
+
roleA: "Green",
|
|
19492
|
+
roleB: "SWIR1",
|
|
19493
|
+
colormap: "blues"
|
|
19494
|
+
}
|
|
19495
|
+
];
|
|
19496
|
+
/** The generic "custom" preset: two unlabeled bands and a neutral diverging
|
|
19497
|
+
* ramp. Kept separate from the named presets so callers can list it last. */
|
|
19498
|
+
var CUSTOM_NORMALIZED_DIFFERENCE = {
|
|
19499
|
+
id: CUSTOM_INDEX_ID,
|
|
19500
|
+
label: "Custom",
|
|
19501
|
+
name: "Custom normalized difference — (Band A - Band B) / (Band A + Band B)",
|
|
19502
|
+
roleA: "Band A",
|
|
19503
|
+
roleB: "Band B",
|
|
19504
|
+
colormap: "rdylgn"
|
|
19505
|
+
};
|
|
19506
|
+
/** Look up a preset by id (including the custom preset), or null. */
|
|
19507
|
+
function indexById(id) {
|
|
19508
|
+
if (!id) return null;
|
|
19509
|
+
if (id === "custom") return CUSTOM_NORMALIZED_DIFFERENCE;
|
|
19510
|
+
return NORMALIZED_DIFFERENCE_INDICES.find((i) => i.id === id) ?? null;
|
|
19511
|
+
}
|
|
19512
|
+
/**
|
|
19513
|
+
* Guess the 1-based band number that plays a spectral role (e.g. "NIR") for a
|
|
19514
|
+
* raster, matching the role against GDAL band names. Common Sentinel-2 /
|
|
19515
|
+
* Landsat aliases are recognized. Returns null when no band name matches.
|
|
19516
|
+
*
|
|
19517
|
+
* @param role - The role label from a preset (e.g. "Red", "NIR", "SWIR1").
|
|
19518
|
+
* @param bandNames - 1-indexed band-number → name map, when the COG carries one.
|
|
19519
|
+
*/
|
|
19520
|
+
function guessBandForRole(role, bandNames) {
|
|
19521
|
+
if (!bandNames || bandNames.size === 0) return null;
|
|
19522
|
+
const aliases = ROLE_ALIASES[role.toLowerCase()] ?? [role.toLowerCase()];
|
|
19523
|
+
for (const [band, rawName] of bandNames) {
|
|
19524
|
+
const name = rawName.toLowerCase();
|
|
19525
|
+
if (aliases.some((alias) => name.includes(alias))) return band;
|
|
19526
|
+
}
|
|
19527
|
+
return null;
|
|
19528
|
+
}
|
|
19529
|
+
/** Substrings that identify a spectral role inside a GDAL band name. */
|
|
19530
|
+
var ROLE_ALIASES = {
|
|
19531
|
+
red: [
|
|
19532
|
+
"red",
|
|
19533
|
+
"b04",
|
|
19534
|
+
"b4"
|
|
19535
|
+
],
|
|
19536
|
+
green: [
|
|
19537
|
+
"green",
|
|
19538
|
+
"b03",
|
|
19539
|
+
"b3"
|
|
19540
|
+
],
|
|
19541
|
+
blue: [
|
|
19542
|
+
"blue",
|
|
19543
|
+
"b02",
|
|
19544
|
+
"b2"
|
|
19545
|
+
],
|
|
19546
|
+
nir: [
|
|
19547
|
+
"nir",
|
|
19548
|
+
"near infrared",
|
|
19549
|
+
"b08",
|
|
19550
|
+
"b8",
|
|
19551
|
+
"b8a"
|
|
19552
|
+
],
|
|
19553
|
+
swir1: [
|
|
19554
|
+
"swir1",
|
|
19555
|
+
"swir 1",
|
|
19556
|
+
"swir_1",
|
|
19557
|
+
"b11"
|
|
19558
|
+
],
|
|
19559
|
+
swir2: [
|
|
19560
|
+
"swir2",
|
|
19561
|
+
"swir 2",
|
|
19562
|
+
"swir_2",
|
|
19563
|
+
"b12"
|
|
19564
|
+
]
|
|
19565
|
+
};
|
|
19566
|
+
//#endregion
|
|
19340
19567
|
//#region src/lib/ui/BandHistogram.ts
|
|
19341
19568
|
var HANDLE_SIZE = 12;
|
|
19342
19569
|
/**
|
|
@@ -19558,9 +19785,11 @@ var RGB_CHANNELS = [
|
|
|
19558
19785
|
}
|
|
19559
19786
|
];
|
|
19560
19787
|
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.",
|
|
19788
|
+
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
19789
|
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
19790
|
bandSingle: "Which band's pixel values feed the colormap.",
|
|
19791
|
+
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.",
|
|
19792
|
+
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
19793
|
rescale: "Maps a window of source values to the colormap input. Drag the histogram handles, type values, or pick a preset.",
|
|
19565
19794
|
colormap: "Color lookup applied to the rescaled value (after the curve, before nodata).",
|
|
19566
19795
|
reversed: "Sample the colormap from end to start, equivalent to a reversed variant of the ramp.",
|
|
@@ -19758,13 +19987,14 @@ var SettingsSection = class {
|
|
|
19758
19987
|
const bandOptions = Array.from({ length: Math.max(1, layer.bandCount ?? 1) }, (_, i) => i + 1);
|
|
19759
19988
|
const paletteActive = mode === "single" && state.colormap === "palette" && layer.palette !== null;
|
|
19760
19989
|
this._body.appendChild(this._buildModeField(state, mode, bandOptions));
|
|
19761
|
-
this._body.appendChild(this.
|
|
19990
|
+
if (mode === "index") this._body.appendChild(this._buildIndexField(layer, state, bandOptions));
|
|
19991
|
+
else this._body.appendChild(this._buildBandsField(layer, state, mode, bandOptions));
|
|
19762
19992
|
if (!paletteActive) this._body.appendChild(this._buildRescaleField(layer, state, mode));
|
|
19763
|
-
if (mode === "single") {
|
|
19993
|
+
if (mode === "single" || mode === "index") {
|
|
19764
19994
|
const picker = new ColormapPicker({
|
|
19765
19995
|
value: state.colormap,
|
|
19766
|
-
palette: layer.palette,
|
|
19767
|
-
stats: statsForBand(layer.autoStats, state.bands[0] ?? 1),
|
|
19996
|
+
palette: mode === "single" ? layer.palette : null,
|
|
19997
|
+
stats: mode === "single" ? statsForBand(layer.autoStats, state.bands[0] ?? 1) : null,
|
|
19768
19998
|
onChange: (name) => {
|
|
19769
19999
|
this._setState({ colormap: name });
|
|
19770
20000
|
this.render();
|
|
@@ -19791,14 +20021,22 @@ var SettingsSection = class {
|
|
|
19791
20021
|
2,
|
|
19792
20022
|
3
|
|
19793
20023
|
].map((b) => Math.min(b, maxBand));
|
|
19794
|
-
return field("Mode", select([
|
|
19795
|
-
|
|
19796
|
-
|
|
19797
|
-
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
20024
|
+
return field("Mode", select([
|
|
20025
|
+
{
|
|
20026
|
+
value: "rgb",
|
|
20027
|
+
label: "RGB / composite"
|
|
20028
|
+
},
|
|
20029
|
+
{
|
|
20030
|
+
value: "single",
|
|
20031
|
+
label: "Single band + colormap"
|
|
20032
|
+
},
|
|
20033
|
+
{
|
|
20034
|
+
value: "index",
|
|
20035
|
+
label: "Index (normalized difference)"
|
|
20036
|
+
}
|
|
20037
|
+
], mode, (next) => {
|
|
20038
|
+
if (next === "index") this._setState(this._indexPatchFor(indexById(state.index)));
|
|
20039
|
+
else this._setState({
|
|
19802
20040
|
mode: next,
|
|
19803
20041
|
bands: next === "single" ? [state.bands[0] ?? 1] : rgbDefault,
|
|
19804
20042
|
rescale: null
|
|
@@ -19806,6 +20044,26 @@ var SettingsSection = class {
|
|
|
19806
20044
|
this.render();
|
|
19807
20045
|
}, "mode"), HELP.mode);
|
|
19808
20046
|
}
|
|
20047
|
+
/** Builds the state patch for entering / switching an index preset: assigns
|
|
20048
|
+
* each operand a band (guessed from band names, else 1 and 2), applies the
|
|
20049
|
+
* preset's default colormap, and resets rescale to the [-1, 1] auto range. */
|
|
20050
|
+
_indexPatchFor(preset) {
|
|
20051
|
+
const index = preset ?? NORMALIZED_DIFFERENCE_INDICES[0];
|
|
20052
|
+
const layer = this._getLayer();
|
|
20053
|
+
const bandCount = Math.max(1, layer?.bandCount ?? 1);
|
|
20054
|
+
const names = layer?.bandNames ?? null;
|
|
20055
|
+
const clamp = (b) => Math.min(Math.max(1, b), bandCount);
|
|
20056
|
+
const a = clamp(guessBandForRole(index.roleA, names) ?? 1);
|
|
20057
|
+
const b = clamp(guessBandForRole(index.roleB, names) ?? (a === 2 ? 1 : 2));
|
|
20058
|
+
return {
|
|
20059
|
+
mode: "index",
|
|
20060
|
+
index: index.id,
|
|
20061
|
+
bands: [a, b],
|
|
20062
|
+
colormap: index.colormap,
|
|
20063
|
+
reversed: false,
|
|
20064
|
+
rescale: null
|
|
20065
|
+
};
|
|
20066
|
+
}
|
|
19809
20067
|
_buildBandsField(layer, state, mode, bandOptions) {
|
|
19810
20068
|
const options = bandOptions.map((n) => ({
|
|
19811
20069
|
value: String(n),
|
|
@@ -19826,14 +20084,46 @@ var SettingsSection = class {
|
|
|
19826
20084
|
});
|
|
19827
20085
|
return field("Bands (R, G, B)", row, HELP.bandsRgb);
|
|
19828
20086
|
}
|
|
20087
|
+
/** Index mode UI: a preset selector plus a band picker for each operand of
|
|
20088
|
+
* `(A - B) / (A + B)`, labelled with the preset's roles (e.g. NIR / Red). */
|
|
20089
|
+
_buildIndexField(layer, state, bandOptions) {
|
|
20090
|
+
const preset = indexById(state.index) ?? NORMALIZED_DIFFERENCE_INDICES[0];
|
|
20091
|
+
const wrap = el("div");
|
|
20092
|
+
const presetSelect = select([...NORMALIZED_DIFFERENCE_INDICES.map((i) => ({
|
|
20093
|
+
value: i.id,
|
|
20094
|
+
label: i.label
|
|
20095
|
+
})), {
|
|
20096
|
+
value: CUSTOM_NORMALIZED_DIFFERENCE.id,
|
|
20097
|
+
label: CUSTOM_NORMALIZED_DIFFERENCE.label
|
|
20098
|
+
}], preset.id, (next) => {
|
|
20099
|
+
this._setState(this._indexPatchFor(indexById(next)));
|
|
20100
|
+
this.render();
|
|
20101
|
+
}, "index-preset");
|
|
20102
|
+
wrap.appendChild(field("Index", presetSelect, preset.name));
|
|
20103
|
+
const bandChoices = bandOptions.map((n) => ({
|
|
20104
|
+
value: String(n),
|
|
20105
|
+
label: bandLabel(n, layer.bandNames)
|
|
20106
|
+
}));
|
|
20107
|
+
const operandSelect = (slot, ariaLabel) => select(bandChoices, String(state.bands[slot] ?? (slot === 0 ? 1 : 2)), (next) => {
|
|
20108
|
+
const bands = [state.bands[0] ?? 1, state.bands[1] ?? 2];
|
|
20109
|
+
bands[slot] = Number(next);
|
|
20110
|
+
this._setState({ bands });
|
|
20111
|
+
this.render();
|
|
20112
|
+
}, ariaLabel);
|
|
20113
|
+
const grid = el("div", { className: "mlr-band-grid" }, operandSelect(0, "index-band-a"), operandSelect(1, "index-band-b"));
|
|
20114
|
+
wrap.appendChild(field(`Bands (${preset.roleA}, ${preset.roleB})`, grid, HELP.indexBands));
|
|
20115
|
+
return wrap;
|
|
20116
|
+
}
|
|
19829
20117
|
_buildRescaleField(layer, state, mode) {
|
|
19830
20118
|
const autoStats = layer.autoStats;
|
|
19831
|
-
const channelCount = mode === "
|
|
20119
|
+
const channelCount = mode === "rgb" ? 3 : 1;
|
|
19832
20120
|
const bands = state.bands;
|
|
19833
|
-
const
|
|
20121
|
+
const isIndex = mode === "index";
|
|
20122
|
+
const perBandStats = Array.from({ length: channelCount }, (_, i) => isIndex ? null : statsForBand(autoStats, bands[i] ?? bands[0] ?? 1));
|
|
19834
20123
|
const perBandPercentile = perBandStats.map((s) => s ? autoRangeFor(s) : null);
|
|
19835
20124
|
const perBandMinMax = perBandStats.map((s) => s ? [s.min, s.max] : null);
|
|
19836
|
-
const
|
|
20125
|
+
const defaultRange = isIndex ? [DEFAULT_INDEX_RANGE[0], DEFAULT_INDEX_RANGE[1]] : [0, 1];
|
|
20126
|
+
const values = Array.from({ length: channelCount }, (_, i) => state.rescale?.[i] ?? perBandPercentile[i] ?? defaultRange);
|
|
19837
20127
|
const setChannel = (i, next) => {
|
|
19838
20128
|
const out = values.map((v) => [...v]);
|
|
19839
20129
|
out[i] = next;
|
|
@@ -19845,11 +20135,11 @@ var SettingsSection = class {
|
|
|
19845
20135
|
const wrap = el("div", { className: "mlr-rescale" });
|
|
19846
20136
|
const rows = [];
|
|
19847
20137
|
for (let i = 0; i < channelCount; i++) {
|
|
19848
|
-
const channel = mode === "
|
|
20138
|
+
const channel = mode === "rgb" ? RGB_CHANNELS[i] : null;
|
|
19849
20139
|
const row = new RescaleRow({
|
|
19850
20140
|
color: channel?.color ?? "var(--mlr-histogram-neutral)",
|
|
19851
20141
|
label: channel?.label,
|
|
19852
|
-
ariaPrefix:
|
|
20142
|
+
ariaPrefix: channel ? `rescale-${channel.label.toLowerCase()}` : "rescale",
|
|
19853
20143
|
onChange: (next) => setChannel(i, next),
|
|
19854
20144
|
onDragStart: () => {
|
|
19855
20145
|
this._dragCount++;
|
|
@@ -20839,7 +21129,7 @@ var RasterControl = class {
|
|
|
20839
21129
|
if (!map || !manager) return;
|
|
20840
21130
|
const seen = /* @__PURE__ */ new Set();
|
|
20841
21131
|
for (const layer of manager.getLayers()) {
|
|
20842
|
-
if (!layer.state.colorbar?.visible || !layer.state.visible || layer.state.mode !== "single" || layer.state.colormap === "palette") continue;
|
|
21132
|
+
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
21133
|
seen.add(layer.id);
|
|
20844
21134
|
const options = this._colorbarOptionsFor(layer);
|
|
20845
21135
|
const existing = this._colorbars.get(layer.id);
|
|
@@ -20864,7 +21154,8 @@ var RasterControl = class {
|
|
|
20864
21154
|
_colorbarOptionsFor(layer) {
|
|
20865
21155
|
const band = layer.state.bands[0] ?? 1;
|
|
20866
21156
|
const stats = statsForBand(layer.autoStats, band);
|
|
20867
|
-
const
|
|
21157
|
+
const autoRange = layer.state.mode === "index" ? [-1, 1] : stats ? autoRangeFor(stats) : [0, 1];
|
|
21158
|
+
const range = layer.state.rescale?.[0] ?? autoRange;
|
|
20868
21159
|
const cb = layer.state.colorbar;
|
|
20869
21160
|
return {
|
|
20870
21161
|
colormap: layer.state.colormap,
|
|
@@ -21137,6 +21428,6 @@ var RasterControl = class {
|
|
|
21137
21428
|
}
|
|
21138
21429
|
};
|
|
21139
21430
|
//#endregion
|
|
21140
|
-
export {
|
|
21431
|
+
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
21432
|
|
|
21142
|
-
//# sourceMappingURL=RasterControl-
|
|
21433
|
+
//# sourceMappingURL=RasterControl-BfTDWAnW.js.map
|