fix(deck.gl-geotiff): render band-separate (PlanarConfiguration=2) images - #635
Open
thatbudakguy wants to merge 1 commit into
Open
fix(deck.gl-geotiff): render band-separate (PlanarConfiguration=2) images#635thatbudakguy wants to merge 1 commit into
thatbudakguy wants to merge 1 commit into
Conversation
…ages `getTileData` threw "Band-separate images not yet implemented." for any COG stored with `PlanarConfiguration=2` (GDAL's `INTERLEAVE=BAND`), so the layer drew nothing and the reason only reached `onTileError`. Interleave the tile instead, which is what `toPixelInterleaved` in `@developmentseed/geotiff` already does - it just wasn't exported from that package's index. `addAlphaChannel` carried the same throw, and is exported; it now interleaves too, and its return type narrows to `RasterArrayPixelInterleaved`, which it has always satisfied. That lets the pixel-interleaved narrowing in `getTileData` survive the call, and makes the layout guard in the vermont-cog-comparison example dead code. Uploading one texture per band and sampling a `sampler2DArray` would avoid the per-tile copy this adds, and remains the better fix (developmentseed#159); this is the fallback path `packBandsToRGBA` was already documented for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
|
Apologies for the largely AI-generated PR; this came up in some work I was doing and seemed like a straightforward fix. Let me know if these changes aren't desirable! |
thatbudakguy
added a commit
to OpenGeoMetadata/ogm-viewer
that referenced
this pull request
Aug 12, 2026
Every other preview reports a failure through <ogm-preview>'s alert. A COG drawn by deck.gl had one path there - a file that refused to be opened, which rejects preview() - and nothing for the failures that arrive after that. Tiles come in one at a time, and deck.gl reports one that couldn't be built by calling onTileError rather than by rejecting anything. Left unhandled, its own handler logs the tile and the map stays empty: a record zoomed to the right place with no layer on it and no reason given. That is what #158 looks like from the outside. The COG in it is stored band- separate, which the version of @developmentseed/deck.gl-geotiff we build against refuses outright, so every tile of it fails the same way. The fix for that file is upstream (developmentseed/deck.gl-raster#635); this is about the viewer having nothing to say when a COG can't be drawn, whatever the reason. MapPreviewer gains an onError, which is where a failure that arrives after preview() has resolved goes. Nothing MapLibre draws needs it - it fires those on the map itself, and <ogm-map> has listened to that since alerts existed - so this is for the previews that paint with their own WebGL and have no such channel. <ogm-map> binds it to the same reportError() a failed load takes, which already dedupes to one alert per load attempt, so a viewport's worth of failing tiles reports once rather than forty times. It is bound to the previewer it came from rather than to whichever is current, because a tile of the record the user just left would otherwise report against the one that replaced it. Only the first failure of a COG that has drawn nothing reaches that alert. A COG can be sparse by design, and the alert covers the map completely - so a tile that failed among tiles that didn't would replace a preview the user can see with an error about a hole in it. Those are logged and left alone. Aborted reads are dropped rather than logged: deck.gl discards a cancelled tile before calling back, so a pan that abandons its reads doesn't arrive here at all, but a decoder that notices the abort itself can still throw one. The message the user sees is deck.gl's own, through referenceError, which is cryptic for the band-separate case in particular. Naming it here would mean matching on an upstream string that the upstream fix removes, so it stays as it is. Co-Authored-By: Claude Opus 5 <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.
What
getTileDataincreateUnormPipelinethrows for any COG stored withPlanarConfiguration=2(GDAL'sINTERLEAVE=BAND):Because it throws per tile inside the tile loader, a
COGLayerover such a file draws nothing at all and the only sign isonTileErrorfiring once per tile.This interleaves the tile instead.
toPixelInterleavedin@developmentseed/geotiffalready does exactly this — the array helpers just weren't exported from that package's index, although their types were.Why this came up
A librarian at UMN reported a scanned map that wouldn't display in a viewer I'm working on that uses this library. The file is a 4-band RGBA JPEG COG with an alpha collar, written with
-of COG -co COMPRESS=JPEG -co INTERLEAVE=BAND, and the reader handles it fine — only the render pipeline refuses it.Changes
packages/geotiff— exporttoPixelInterleaved,toBandSeparate,reorderBands,packBandsToRGBAandPackBandsToRGBAOptionsfrom the index. TheRasterArray*types were already exported; the functions that produce them weren't.render-pipeline.ts— interleave a band-separate tile before the RGB padding step, rather than throwing. Doing it first means the padding and texture-format logic below see the same shape they would for a pixel-interleaved image, so 3-bandINTERLEAVE=BANDis fixed alongside 4-band.geotiff.ts—addAlphaChannelcarried a second copy of the same throw, and unlike the pipeline it's part of the public API. It now interleaves too. Its return type narrows fromRasterArraytoRasterArrayPixelInterleaved, which every path through it has always satisfied; that's what lets the narrowing ingetTileDatasurvive the call.examples/vermont-cog-comparison— the narrowed return type makes that example'slayout === "band-separate"guard statically unreachable, so it's removed.Tests
tests/render-pipeline.test.ts—getTileDatainterleaves a 4-band tile into onergba8unormtexture, and interleaves and pads a 3-band one. The tile is handed in directly rather than read from a fixture: the only band-separate fixture isint8_3band_zstd_block64, andinferRenderPipelinedoesn't build a pipeline for non-unsigned samples. Happy to add auint8band-separate fixture togeotiff-test-dataas a follow-up if you'd like the coverage to be end-to-end.tests/geotiff.test.ts(new) —addAlphaChannelover band-separate RGB and RGBA input, plus the existing pixel-interleaved path.I also checked the interleave against real data outside the suite: for a band-separate DEFLATE COG built from the reported file,
toPixelInterleavedoutput is byte-identical togeotiff.js's independentreadRasters({ interleave: true })across four full 256×256 tiles (~1M samples), with the alpha channel spanning 0–255 rather than a constant. And with this change the reported COG renders correctly in ogm-viewer, transparent collar included.pnpm build,pnpm -r --filter='!docs' typecheck,biome ci .and thegeotiff/deck.gl-geotiff/deck.gl-rastersuites all pass locally. The 17 failures inpackages/geotiff/tests/integration-rasterio.test.tsare the.npyreference fixtures that CI generates withpixi run generate-npy; I don't have pixi here, and they fail the same way on a clean checkout.Note on approach
Uploading one texture per band and sampling a
sampler2DArray— the idea in #159 — would avoid the per-tile copy this adds, and is the better long-term shape. It needs the render pipeline to know how many textures a tile has, which is a larger change. This is the fallback pathpackBandsToRGBA's docstring already describes ("useful as a fallback path when a single 4-channel texture upload is preferred over one texture per band"), and it makes these files render today. Glad to rework it if you'd rather go straight to the texture-array version.🤖 Generated with Claude Code