Skip to content

Say so when a COG's tiles can't be drawn - #161

Merged
thatbudakguy merged 1 commit into
mainfrom
cog-tile-errors
Aug 12, 2026
Merged

Say so when a COG's tiles can't be drawn#161
thatbudakguy merged 1 commit into
mainfrom
cog-tile-errors

Conversation

@thatbudakguy

Copy link
Copy Markdown
Member

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 it couldn't build 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's what #158 looks like from the outside. Diagnosis of the file in that issue, for the record:

CRS EPSG:3857
Bands 4 × Byte, photometric RGB, band 4 ColorInterp=Alpha, ExtraSamples=2
Interleave BAND / PlanarConfiguration=2 ← the trigger
Compression JPEG, including the alpha band
Transparency a real 4th alpha band, no internal TIFF mask

Transparency isn't the problem; interleaving is. @developmentseed/deck.gl-geotiff throws Band-separate images not yet implemented. for that layout, so every tile fails the same way. The fix for that file is upstream — developmentseed/deck.gl-raster#635, open and green. This PR is the other half: the viewer having something to say when a COG can't be drawn, whatever the reason.

What changed

  • MapPreviewer.onError — where a failure that arrives after preview() has resolved goes. Nothing MapLibre draws needs it; it fires those on the map itself, which <ogm-map> has listened to since alerts existed. 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, so a viewport's worth of failing tiles produces one alert rather than forty — that dedupe already existed. Bound to the previewer it came from rather than to whichever is current, so a tile of the record the user just left can't report against the one that replaced it.
  • DeckCogPreviewer takes onTileError and onTileLoad.

Two judgement calls

Only the first failure of a COG that has drawn nothing reaches the 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 working preview with an error about a hole in it. Those are logged and left alone. Without this the change would be a regression for sparse COGs.

Aborted reads are dropped. deck.gl discards a cancelled tile before calling back, so a pan that abandons its reads never arrives here; but a decoder that notices the abort itself can still throw one, and that isn't a failed preview.

What the user sees

The message is deck.gl's own, through referenceError:

The Cloud Optimized GeoTIFF preview couldn't be read
Band-separate images not yet implemented.
https://geobtaa-assets-prod.s3.us-east-2.amazonaws.com/store/asset/04d-02/mdu-057027-0001-croputm31_cog.tif

Cryptic for this case in particular. Translating it would mean matching on an upstream string that developmentseed/deck.gl-raster#635 deletes, so it stays as it is — happy to add a mapping in errors.ts if you'd rather.

Testing

Four cases in cog-deck.test.ts covering all of the above: reports when nothing has drawn, stays quiet once some of the COG is on screen, ignores an AbortError, and reports again after a second load attempt. Full suite green (612 unit, 98 component) and npm run lint clean.

Checked in the browser against the record from #158 — one alert, not one per tile — and against the Tibet COG, which still draws with no alert and clears the previous one.

Note that #158 stays open until the upstream fix ships and we bump @developmentseed/deck.gl-geotiff and @developmentseed/geotiff (we're on 0.7.0; that PR targets 0.8.0-beta.2, so expect other API changes in the bump).

🤖 Generated with Claude Code

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>
@thatbudakguy
thatbudakguy merged commit 72ec32c into main Aug 12, 2026
2 checks passed
@thatbudakguy
thatbudakguy deleted the cog-tile-errors branch August 12, 2026 21:12
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