Skip to content

feat: Vermont open data example#502

Draft
kylebarron wants to merge 17 commits intomainfrom
kyle/vt-opendata-example
Draft

feat: Vermont open data example#502
kylebarron wants to merge 17 commits intomainfrom
kyle/vt-opendata-example

Conversation

@kylebarron
Copy link
Copy Markdown
Member

@kylebarron kylebarron commented May 1, 2026

Shows usage of a slider to show before and after comparisons:

Screen.Recording.2026-05-01.at.5.27.34.PM.mov

kylebarron and others added 17 commits May 1, 2026 13:54
Design for a new pure-deck.gl example that uses _SplitterWidget to
compare two years of Vermont Open Data statewide aerial COGs side by
side, with synchronized viewports and per-side render-mode controls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First pure-deck.gl example in the repo. DeckGL + _SplitterWidget with
two MapViews whose viewState is mirrored, so panning one side moves
both. CARTO dark raster tiles via TileLayer + BitmapLayer per side.
COG layers, file picker, and render-mode UI come in later commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original spec used _SplitterWidget, which composes views into
non-overlapping sub-rectangles. That meant each pane re-centered its
own viewport in its own width, so a fixed world point landed at
different screen pixels on each side and the imagery slid as the
handle moved — the opposite of what a comparison view should do.

Replaces with the classic swipe-map pattern: one shared MapView, two
COGLayers covering the full viewport, ClipExtension on each driven
by a custom swipe-handle component. The handle moves left/right; the
geography under any pixel stays fixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_SplitterWidget composes views into non-overlapping sub-rectangles, so
each pane recenters its viewport in its own width — the same world
point lands at different screen pixels on each side. That breaks the
comparison illusion as soon as the handle moves.

Replace with the classic swipe-map shape (mapbox-gl-compare style):
one MapView, one shared viewState, a custom SwipeHandle React
component that drags a vertical line over the canvas via pointer
events (rAF-throttled). COG-layer clipping comes in a later commit
using ClipExtension; this commit only sets up the shell with the
basemap and the handle.

Drops @deck.gl/widgets; adds @deck.gl/extensions for the upcoming
ClipExtension wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds vt-imagery.ts with the 9 STATEWIDE Vermont composites (1974-2025)
and band metadata, plus proj.ts with hardcoded EPSG:32145 (Vermont
State Plane) and EPSG:26918 (UTM 18N) so COG loads don't hit epsg.io.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gResolver

proj4.defs(code) returns a ProjectionDefinition-shaped object, not a
string — and that is exactly what COGLayer expects. Let TS infer the
return type to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls SetAlpha1 / setFalseColorInfrared / NDVI shader modules out of
naip-mosaic into shaders.ts so they can be reused. Adds tile-loaders.ts
(rgba8unorm + r8unorm variants) and render-pipelines.ts (RGB, false
color IR, grayscale, NDVI). All four render modes available; no UI yet.

The 3-band tile loader uses addAlphaChannel from
@developmentseed/deck.gl-geotiff to expand RGB → RGBA before texture
upload (WebGL2 has no rgb-only 8-bit format). Also exports
addAlphaChannel from the package's public index so example consumers
don't need to reimplement it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Browsers' createImageBitmap always returns RGBA, even for grayscale
JPEGs (R = G = B = gray value). The Canvas codec previously only
handled the 3- and 4-channel cases and threw on 1-channel input,
which blocked Vermont-style historical aerial imagery (panchromatic
JPEG-compressed COGs).

Take the red channel as the single sample for SamplesPerPixel === 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final wire-up of the swipe map. App.tsx mounts a single MapView with
shared viewState, two COGLayers (one per side) on top of the CARTO
basemap, each carrying ClipExtension with clipBounds in EPSG:3857
mercator meters — the coordinate space COGLayer's sub-tile geometry
is rendered in (its sub-layer uses CARTESIAN with a modelMatrix that
scales mercator → deck.gl world units).

Two non-obvious bits worth flagging:

- clipBounds for ClipExtension on COGLayer must be in mercator meters,
  not lng/lat. The extension calls projectPosition on each corner,
  which for a CARTESIAN sub-layer treats the input as already-cartesian
  (i.e. meters). lng/lat or normalized mercator [0..1] both produce
  bounds far outside the geometry and discard everything.

- clipByInstance must be explicitly false. ClipExtension's auto-detect
  sees the instancePositions attribute on the underlying SimpleMeshLayer
  and defaults to vertex-shader mode, which interpolates a 0/1 visibility
  flag across each triangle and produces chunky cuts. Per-pixel
  fragment-shader mode is what the swipe needs.

Per-side floating panels expose the year picker and render-mode picker
(grayscale / true color / false color IR / NDVI), with valid modes
derived from the source's band count. NDVI uses the fixed RdYlGn
colormap. Adds README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pure-deck.gl architecture was originally chosen because we
expected to use _SplitterWidget, which only works without external
view configuration. Once the swipe pivoted to a single MapView with
ClipExtension-based clipping, the multi-view constraint disappeared.

Swap DeckGL for MaplibreMap + MapboxOverlay({ interleaved: true }) so
vector text labels (place names, road labels) from the CARTO dark
basemap render *above* the satellite COGs via beforeId. We use
'waterway_label' (the first label layer in the dark-matter style) as
the anchor so everything labelled stays readable over imagery.

The swipe handle and per-side panels are unchanged — they're DOM
overlays that don't care which mapping library backs the canvas.

Drops @deck.gl/react; adds @deck.gl/mapbox + maplibre-gl + react-map-gl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hunks

Instead of passing URLs to COGLayer (which constructs a fresh
GeoTIFF.fromUrl with the package defaults of 32KB chunks each time),
the App now opens each VT file itself with much larger chunk and
prefetch sizes — 4MB chunks, 32MB cache, 1MB prefetch — so the IFD
walk for these multi-hundred-GB files finishes in a handful of
requests instead of dozens.

GeoTIFF instances are cached in a state Map keyed by URL and reused
across renders. Switching the dropdown back to a previously loaded
year is now instant (no re-fetch). An inFlightRef set dedupes
concurrent loads of the same URL while the first request is in flight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant