@@ -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" ;
911import type { Device , Texture } from "@luma.gl/core" ;
1012import type { ShaderModule } from "@luma.gl/shadertools" ;
13+ import * as Slider from "@radix-ui/react-slider" ;
1114import "maplibre-gl/dist/maplibre-gl.css" ;
1215import { useEffect , useRef , useState } from "react" ;
1316import type { MapRef } from "react-map-gl/maplibre" ;
1417import { Map as MaplibreMap , useControl } from "react-map-gl/maplibre" ;
1518import 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" ;
1919import colormap from "./cfastie" ;
20+ import "./proj" ;
2021import STAC_DATA from "./minimal_stac.json" ;
2122import { 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. */
4148type STACFeatureCollection = {
42- features : STACItem [ ] ;
49+ features : PartialSTACItem [ ] ;
4350} ;
4451
4552type 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+ */
8193const 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+ */
91107const 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+ */
152179function 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+ */
167200function 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+ */
185225function 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
254294export 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