feat: Vermont open data example#502
Draft
kylebarron wants to merge 17 commits intomainfrom
Draft
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Shows usage of a slider to show before and after comparisons:
Screen.Recording.2026-05-01.at.5.27.34.PM.mov