Skip to content

Add deck.gl-raster as a client-side COG renderer option (deck.gl missions)#172

Draft
slesaad wants to merge 14 commits into
developmentfrom
feat/deckgl-raster
Draft

Add deck.gl-raster as a client-side COG renderer option (deck.gl missions)#172
slesaad wants to merge 14 commits into
developmentfrom
feat/deckgl-raster

Conversation

@slesaad

@slesaad slesaad commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

Adds an optional client-side COG renderer for deck.gl-based missions, so a Cloud-Optimized GeoTIFF can be decoded and colorized in the browser on the GPU via @developmentseed/deck.gl-raster / deck.gl-geotiff — with no tile server in the loop. TiTiler remains the default and is unchanged.

Implements #158.

What it does

  • New per-layer option cogRendererMode on the COG tab (titiler default / deckRaster). It only takes effect in deck.gl missions; in Leaflet missions the layer falls back to TiTiler, and the field is hidden entirely from the configure UI (not just disabled).
  • When set to deckRaster in a deck.gl mission, the COG is read directly (HTTP range requests) and rendered on the GPU:
    • Single-band: named colormap + min/max stretch. Colors come from the repo's existing js-colormaps evaluate_cmap, so the client render matches TiTiler's colormap_name (including _r reversed variants). NaN nodata is discarded (transparent).
    • Multi-band uint (RGB/RGBA): displayed as true color directly (no colormap). This goes beyond the single-band scope in Add deck.gl-raster as a client-side COG renderer option (deck missions only) #158 — it's a small addition so true-color COGs (e.g. Sentinel-2 TCI) also render client-side; noted as pulling that follow-up forward.
  • Live updates: changing the colormap or the min/max re-renders in place with no tile-server request (GPU module props via updateTriggers).
  • The existing colormap/rescale controls drive it — no new UI.

How it works

  • shouldUseDeckRaster(engine, type, layer) gates the deck branch of makeTileLayer(); when false, the existing TiTiler path runs untouched.
  • ColormappedCOGLayer (a COGLayer subclass) supplies its own getTileData/renderTile. This is required because the library's default pipeline auto-inference only supports unsigned-integer COGs and throws for float (the common scientific case). The custom loader uploads single-band data as r32float and composes [CreateTexture, FilterNaN, LinearRescale, Colormap]; multi-band uint is uploaded as rgba8unorm and displayed via [CreateTexture, FilterBlack]. Overview selection (finer tiles at higher zoom) is handled by the base layer.
  • deck.gl bumped to ^9.3 and luma.gl to ^9.3.2 (peer requirements of the new packages); adds @developmentseed/deck.gl-geotiff, @developmentseed/deck.gl-raster, @developmentseed/morecantile. A pruned focus-trap-react (USWDS build dep) is restored and .npmrc pins legacy-peer-deps for a pre-existing peer conflict.

Testing

  • Unit tests (Playwright, tests/unit/): colormap LUT parity with evaluate_cmap, the routing decision, the configure-UI field gating, the colormap-override precedence, the pipeline composition, and the live-update handlers.
  • Verified end-to-end in a running deck.gl mission (browser, network panel):
    • A single-band float COG (CMIP6 tasmax) renders with the configured colormap + min/max; oceans (NaN) transparent; zero tile-server tile requests.
    • Changing colormap and min/max both re-render live with zero tile-server requests.
    • A multi-band true-color COG (Sentinel-2 TCI) renders in RGB with correct zoom refinement across its overview pyramid.
    • Existing TiTiler COG layers and Leaflet missions are unaffected.

Known follow-ups (out of scope here)

  • The colormap legend swatch is still fetched from TiTiler (/colorMaps) — shared legend/UI infra, independent of the renderer; could be rendered from local js-colormaps for full decoupling.
  • Multi-band pass-through assumes 8-bit samples.
  • Single-band float uses nearest-neighbor sampling (blocky when magnified past native resolution) because float linear filtering isn't guaranteed in WebGL2; could opt into linear filtering where OES_texture_float_linear is available.
  • RGB nodata uses a black-pixel discard heuristic.

🤖 Generated with Claude Code

slesaad added 14 commits June 30, 2026 15:48
- Bump @deck.gl/{core,geo-layers,layers,mapbox} from 9.2.x to ^9.3.0
- Add @deck.gl/extensions ^9.3.0 (required peer dep for geo-layers 9.3)
- Add @deck.gl/mesh-layers ^9.3.0 (new explicit dep)
- Add @developmentseed/deck.gl-geotiff, deck.gl-raster, morecantile at ^0.7.0
- Add @luma.gl/core ^9.3.2 and @luma.gl/shadertools ^9.3.2 (explicit peers)
- Add leaflet ^1.9.4 (missing peer dep for terra-draw-leaflet-adapter)
Adds composeColormapPipeline (pure, unit-tested), ColormappedCOGLayer
(COGLayer subclass that appends LinearRescale + Colormap GPU modules),
and buildDeckCOGLayer factory. Map_.js makeTileLayer now routes
cogRendererMode=deckRaster layers to the new path with the raw COG
URL, bypassing TiTiler entirely.
…cedence

Part A: applyCogFieldsToUrl now prefers currentCogColormap ?? cogColormap,
matching the existing currentCogMin/Max override pattern. TDD: failing test
added, then implementation made it green (24/24 pass).

Part B: layers:refresh handler gains a deckRaster branch — rebuilds the
ColormappedCOGLayer with updated current* values via buildDeckCOGLayer and
re-registers it through engine.addLayer so deck.gl diffs GPU resources in
place. Leaflet .refresh() path remains the unchanged fallback.
…xture pipeline

COGLayer's default inferRenderPipeline only supports unsigned-integer COGs
(throws 'non-unsigned integers not yet supported' for SampleFormat 3/float).
Supply our own getTileData (uploads the band as r32float) and renderTile
([CreateTexture, FilterNoDataVal?, LinearRescale, Colormap]) so _parseGeoTIFF
skips that inference. Verified live: CMIP6 tasmax (float32) renders with
gist_heat_r + rescale 225-315, no tile-server render requests.
Float scientific COGs encode nodata as NaN (GDAL_NODATA=nan); FilterNoDataVal's
== equality never matches NaN. Add a FilterNaN GPU module (color.r != color.r
self-inequality test) after CreateTexture so NaN fill pixels are discarded.
Verified live on CMIP6 tasmax: oceans render transparent (basemap shows through).
… follow-up forward)

Band-count branch in the tile loader: single-band -> r32float + colormap (as
before); multi-band uint -> rgba8unorm displayed directly via CreateTexture, no
colormap/rescale. Adds FilterBlack to discard black nodata edges (Sentinel UTM
tile corners) -> transparent. Verified live: Sentinel-2 TCI renders in true
color client-side with correct zoom refinement across its 5-level overview
pyramid. Assumes 8-bit multi-band samples.
@slesaad slesaad force-pushed the feat/deckgl-raster branch from 93a649e to d1efd40 Compare July 2, 2026 15:16
@slesaad slesaad marked this pull request as draft July 6, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant