Skip to content

Commit 92f49e4

Browse files
authored
feat: add ndvi filter slider to NAIP-mosaic example (#357)
1 parent ceec0ac commit 92f49e4

File tree

3 files changed

+341
-126
lines changed

3 files changed

+341
-126
lines changed

examples/naip-mosaic/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@developmentseed/geotiff": "workspace:^",
1818
"@luma.gl/core": "9.2.6",
1919
"@luma.gl/shadertools": "9.2.6",
20+
"@radix-ui/react-slider": "^1.3.6",
2021
"maplibre-gl": "^5.19.0",
2122
"proj4": "^2.20.4",
2223
"react": "^19.2.4",

examples/naip-mosaic/src/App.tsx

Lines changed: 108 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ import {
66
Colormap,
77
CreateTexture,
88
} from "@developmentseed/deck.gl-raster/gpu-modules";
9+
import type { Overview } from "@developmentseed/geotiff";
10+
import { GeoTIFF } from "@developmentseed/geotiff";
911
import type { Device, Texture } from "@luma.gl/core";
1012
import type { ShaderModule } from "@luma.gl/shadertools";
13+
import * as Slider from "@radix-ui/react-slider";
1114
import "maplibre-gl/dist/maplibre-gl.css";
1215
import { useEffect, useRef, useState } from "react";
1316
import type { MapRef } from "react-map-gl/maplibre";
1417
import { Map as MaplibreMap, useControl } from "react-map-gl/maplibre";
1518
import type { GetTileDataOptions } from "../../../packages/deck.gl-geotiff/dist/cog-layer";
16-
import "./proj";
17-
import type { Overview } from "@developmentseed/geotiff";
18-
import { GeoTIFF } from "@developmentseed/geotiff";
1919
import colormap from "./cfastie";
20+
import "./proj";
2021
import STAC_DATA from "./minimal_stac.json";
2122
import { epsgResolver } from "./proj";
2223

@@ -29,7 +30,12 @@ function DeckGLOverlay(props: DeckProps) {
2930
return null;
3031
}
3132

32-
type STACItem = {
33+
/**
34+
* A subset of STAC Item properties.
35+
*
36+
* These are the only properties we actually care about for this example.
37+
*/
38+
type PartialSTACItem = {
3339
bbox: [number, number, number, number];
3440
assets: {
3541
image: {
@@ -38,8 +44,9 @@ type STACItem = {
3844
};
3945
};
4046

47+
/** A feature collection of STAC items. */
4148
type STACFeatureCollection = {
42-
features: STACItem[];
49+
features: PartialSTACItem[];
4350
};
4451

4552
type TextureDataT = {
@@ -77,7 +84,12 @@ async function getTileData(
7784
};
7885
}
7986

80-
/** Shader module that sets alpha channel to 1.0 */
87+
/** Shader module that sets alpha channel to 1.0.
88+
*
89+
* The input NAIP imagery is 4-band but the 4th band means near-infrared (NIR)
90+
* rather than alpha, so we need to set alpha to 1.0 so that the imagery is
91+
* fully opaque when rendered.
92+
*/
8193
const SetAlpha1 = {
8294
name: "set-alpha-1",
8395
inject: {
@@ -87,7 +99,11 @@ const SetAlpha1 = {
8799
},
88100
} as const satisfies ShaderModule;
89101

90-
/** Shader module that reorders bands to a false color infrared composite. */
102+
/**
103+
* Shader module that reorders bands to a false color infrared composite.
104+
*
105+
* {@see https://www.usgs.gov/media/images/common-landsat-band-combinations}
106+
*/
91107
const setFalseColorInfrared = {
92108
name: "set-false-color-infrared",
93109
inject: {
@@ -126,8 +142,13 @@ uniform ${NDVI_FILTER_MODULE_NAME}Uniforms {
126142
} ${NDVI_FILTER_MODULE_NAME};
127143
`;
128144

129-
// TODO: enable NDVI filtering
130-
const _ndviFilter = {
145+
/**
146+
* A shader module that filters out pixels based on their NDVI value.
147+
*
148+
* It takes in min and max values for the range, and discards pixels outside of
149+
* that range.
150+
*/
151+
const ndviFilter = {
131152
name: NDVI_FILTER_MODULE_NAME,
132153
fs: ndviUniformBlock,
133154
inject: {
@@ -149,6 +170,12 @@ const _ndviFilter = {
149170
},
150171
} as const satisfies ShaderModule<{ ndviMin: number; ndviMax: number }>;
151172

173+
/**
174+
* Create a rendering pipeline for RGB true-color rendering.
175+
*
176+
* Just uploads the texture and overrides the near-infrared (NIR) value in the
177+
* alpha channel to 1.
178+
*/
152179
function renderRGB(tileData: TextureDataT): RasterModule[] {
153180
const { texture } = tileData;
154181
return [
@@ -164,6 +191,12 @@ function renderRGB(tileData: TextureDataT): RasterModule[] {
164191
];
165192
}
166193

194+
/**
195+
* Create a rendering pipeline for false color infrared rendering.
196+
*
197+
* Reorders bands so that NIR is mapped to red, red is mapped to green, and
198+
* green is mapped to blue. Also overrides the alpha channel to 1.
199+
*/
167200
function renderFalseColor(tileData: TextureDataT): RasterModule[] {
168201
const { texture } = tileData;
169202
return [
@@ -182,10 +215,17 @@ function renderFalseColor(tileData: TextureDataT): RasterModule[] {
182215
];
183216
}
184217

218+
/**
219+
* Create a rendering pipeline for NDVI rendering.
220+
*
221+
* Calculates NDVI in a shader module, then applies a color map based on the
222+
* resulting NDVI value. Also applies an NDVI range filter to allow filtering
223+
* out pixels with NDVI values outside of a specified range.
224+
*/
185225
function renderNDVI(
186226
tileData: TextureDataT,
187227
colormapTexture: Texture,
188-
// ndviRange: [number, number],
228+
ndviRange: [number, number],
189229
): RasterModule[] {
190230
const { texture } = tileData;
191231
return [
@@ -198,13 +238,13 @@ function renderNDVI(
198238
{
199239
module: ndvi,
200240
},
201-
// {
202-
// module: ndviFilter,
203-
// props: {
204-
// ndviMin: ndviRange[0],
205-
// ndviMax: ndviRange[1],
206-
// },
207-
// },
241+
{
242+
module: ndviFilter,
243+
props: {
244+
ndviMin: ndviRange[0],
245+
ndviMax: ndviRange[1],
246+
},
247+
},
208248
{
209249
module: Colormap,
210250
props: {
@@ -253,7 +293,7 @@ async function fetchSTACItems(): Promise<STACFeatureCollection> {
253293

254294
export default function App() {
255295
const mapRef = useRef<MapRef>(null);
256-
const [stacItems, setStacItems] = useState<STACItem[]>([]);
296+
const [stacItems, setStacItems] = useState<PartialSTACItem[]>([]);
257297
const [loading, setLoading] = useState(true);
258298
const [error, setError] = useState<string | null>(null);
259299
const [renderMode, setRenderMode] = useState<RenderMode>("trueColor");
@@ -303,7 +343,7 @@ export default function App() {
303343
const layers = [];
304344

305345
if (stacItems.length > 0 && colormapTexture) {
306-
const mosaicLayer = new MosaicLayer<STACItem, GeoTIFF>({
346+
const mosaicLayer = new MosaicLayer<PartialSTACItem, GeoTIFF>({
307347
id: "naip-mosaic-layer",
308348
sources: stacItems,
309349
// For each source, fetch the GeoTIFF instance
@@ -328,7 +368,8 @@ export default function App() {
328368
? renderRGB
329369
: renderMode === "falseColor"
330370
? renderFalseColor
331-
: (tileData) => renderNDVI(tileData, colormapTexture),
371+
: (tileData) =>
372+
renderNDVI(tileData, colormapTexture, ndviRange),
332373
signal,
333374
});
334375
},
@@ -465,136 +506,77 @@ export default function App() {
465506
</select>
466507
</div>
467508

468-
{/* TODO: enable pixel filter */}
469-
{false && renderMode === "ndvi" && (
509+
{renderMode === "ndvi" && (
470510
<div style={{ marginTop: "16px" }}>
471-
<label
472-
htmlFor="ndvi-filter-min"
473-
style={{ fontSize: "14px", fontWeight: 500 }}
474-
>
475-
NDVI Filter
476-
</label>
477-
<div
511+
<span style={{ fontSize: "14px", fontWeight: 500 }}>
512+
NDVI Range
513+
</span>
514+
<Slider.Root
515+
min={-1}
516+
max={1}
517+
step={0.01}
518+
value={ndviRange}
519+
onValueChange={(v) => setNdviRange(v as [number, number])}
478520
style={{
479521
position: "relative",
522+
display: "flex",
523+
alignItems: "center",
524+
userSelect: "none",
525+
touchAction: "none",
480526
height: "20px",
481-
marginTop: "8px",
527+
marginTop: "12px",
482528
}}
483529
>
484-
{/* Background track */}
485-
<div
530+
<Slider.Track
486531
style={{
487-
position: "absolute",
488-
top: "50%",
489-
left: 0,
490-
right: 0,
532+
position: "relative",
533+
flexGrow: 1,
491534
height: "4px",
492-
transform: "translateY(-50%)",
493535
background: "#ddd",
494536
borderRadius: "2px",
495537
}}
496-
/>
497-
{/* Selected range track */}
498-
<div
499-
style={{
500-
position: "absolute",
501-
top: "50%",
502-
left: `${((ndviRange[0] + 1) / 2) * 100}%`,
503-
width: `${((ndviRange[1] - ndviRange[0]) / 2) * 100}%`,
504-
height: "4px",
505-
transform: "translateY(-50%)",
506-
background: "#007bff",
507-
borderRadius: "2px",
508-
}}
509-
/>
510-
<input
511-
id="ndvi-filter-min"
512-
type="range"
513-
min={-1}
514-
max={1}
515-
step={0.01}
516-
value={ndviRange[0]}
517-
onChange={(e) =>
518-
setNdviRange([
519-
Math.min(parseFloat(e.target.value), ndviRange[1] - 0.01),
520-
ndviRange[1],
521-
])
522-
}
523-
style={{
524-
position: "absolute",
525-
width: "100%",
526-
pointerEvents: "none",
527-
background: "transparent",
528-
zIndex: 1,
529-
}}
530-
className="range-thumb"
531-
/>
532-
<input
533-
type="range"
534-
min={-1}
535-
max={1}
536-
step={0.01}
537-
value={ndviRange[1]}
538-
onChange={(e) =>
539-
setNdviRange([
540-
ndviRange[0],
541-
Math.max(parseFloat(e.target.value), ndviRange[0] + 0.01),
542-
])
543-
}
544-
style={{
545-
position: "absolute",
546-
width: "100%",
547-
pointerEvents: "none",
548-
background: "transparent",
549-
zIndex: 2,
550-
}}
551-
className="range-thumb"
552-
/>
553-
</div>
538+
>
539+
<Slider.Range
540+
style={{
541+
position: "absolute",
542+
height: "100%",
543+
background: "#4a7c59",
544+
borderRadius: "2px",
545+
}}
546+
/>
547+
</Slider.Track>
548+
{(["min", "max"] as const).map((key) => (
549+
<Slider.Thumb
550+
key={key}
551+
style={{
552+
display: "block",
553+
width: "16px",
554+
height: "16px",
555+
borderRadius: "50%",
556+
background: "#4a7c59",
557+
border: "2px solid white",
558+
boxShadow: "0 1px 4px rgba(0,0,0,0.3)",
559+
cursor: "pointer",
560+
outline: "none",
561+
}}
562+
/>
563+
))}
564+
</Slider.Root>
554565
<div
555566
style={{
556567
display: "flex",
557568
justifyContent: "space-between",
558-
marginTop: "8px",
569+
marginTop: "6px",
559570
fontSize: "12px",
560571
color: "#666",
561572
}}
562573
>
563574
<span>-1</span>
564575
<span>
565-
{ndviRange[0].toFixed(2)} to {ndviRange[1].toFixed(2)}
576+
{ndviRange[0].toFixed(2)} {ndviRange[1].toFixed(2)}
566577
</span>
567578
<span>+1</span>
568579
</div>
569-
<style>{`
570-
.range-thumb {
571-
-webkit-appearance: none;
572-
appearance: none;
573-
height: 4px;
574-
}
575-
.range-thumb::-webkit-slider-thumb {
576-
-webkit-appearance: none;
577-
appearance: none;
578-
width: 16px;
579-
height: 16px;
580-
border-radius: 50%;
581-
background: #007bff;
582-
cursor: pointer;
583-
pointer-events: auto;
584-
border: 2px solid white;
585-
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
586-
}
587-
.range-thumb::-moz-range-thumb {
588-
width: 16px;
589-
height: 16px;
590-
border-radius: 50%;
591-
background: #007bff;
592-
cursor: pointer;
593-
pointer-events: auto;
594-
border: 2px solid white;
595-
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
596-
}
597-
`}</style>
598580
</div>
599581
)}
600582
</div>

0 commit comments

Comments
 (0)