Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/components/ogm-map/ogm-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ export class OgmMap {
// that can change without the style document being rebuilt
this.applyViewConstraints();

// A preview that paints with its own WebGL - deck.gl's COG overlay - has no MapLibre source to
// report on, and only finds out it can't be drawn once its tiles start arriving, after preview()
// below has resolved. Give it the same alert a failed load gets. Bound to the previewer it came
// from rather than to whichever is current, so a tile of the record we just left can't report
// against the one that replaced it.
const previewer = this.previewer;
previewer.onError = error => {
if (this.previewer === previewer) this.reportError(error);
};

try {
// The style is only known now: it comes out of the theme, and the theme can change under a
// preview that is already on screen
Expand Down
65 changes: 65 additions & 0 deletions src/lib/previewers/cog-deck.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,71 @@ describe('DeckCogPreviewer', () => {
});
});

// deck.gl only finds out a COG can't be drawn once its tiles start arriving, which is after
// preview() has resolved - so before this the map stayed empty and the reason only reached the
// console. See https://github.com/OpenGeoMetadata/ogm-viewer/issues/158, where a band-separate COG
// failed every tile this way.
describe('a tile that fails', () => {
// One error per prop call, since deck.gl reports each failed tile separately
const failTile = (previewer: TestDeckCogPreviewer, error: unknown = new Error('Band-separate images not yet implemented.')) =>
previewer.overlay.lastLayers[0].props.onTileError(error);

const drawTile = (previewer: TestDeckCogPreviewer) => previewer.overlay.lastLayers[0].props.onTileLoad({});

it('fails the preview when nothing has been drawn', async () => {
const { previewer } = previewFor();
const reported: unknown[] = [];
previewer.onError = error => reported.push(error);
await previewer.preview();

failTile(previewer);

expect(reported).toEqual([new Error('Band-separate images not yet implemented.')]);
});

// A COG can be sparse by design, and a hole in a preview the user can see is not worth replacing
// that preview with an error
it('is left alone once some of the COG is on screen', async () => {
const { previewer } = previewFor();
const reported: unknown[] = [];
previewer.onError = error => reported.push(error);
await previewer.preview();

drawTile(previewer);
failTile(previewer);

expect(reported).toEqual([]);
});

// deck.gl drops a cancelled tile before calling back, but a decoder that notices the abort itself
// can still throw one - and a pan that abandons its reads is not a failed preview
it('ignores an aborted read', async () => {
const { previewer } = previewFor();
const reported: unknown[] = [];
previewer.onError = error => reported.push(error);
await previewer.preview();

failTile(previewer, new DOMException('The user aborted a request.', 'AbortError'));

expect(reported).toEqual([]);
});

// A fresh load attempt starts over: the tiles of the last one are gone from the overlay
it('fails again after the preview is drawn a second time', async () => {
const { map, previewer } = previewFor();
const reported: unknown[] = [];
previewer.onError = error => reported.push(error);
await previewer.preview();
drawTile(previewer);

previewer.attach(map as unknown as maplibregl.Map, style);
await previewer.preview();
failTile(previewer);

expect(reported).toHaveLength(1);
});
});

it('takes its layer off the overlay when cleared', async () => {
const { previewer } = previewFor();
await previewer.preview();
Expand Down
30 changes: 30 additions & 0 deletions src/lib/previewers/cog-deck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,18 @@ export default class DeckCogPreviewer extends MapPreviewer {
// open file instead of reading its header again.
protected geotiff: GeoTIFF | undefined;

// Whether any tile of this COG has been drawn, which is what tells a COG that can't be drawn at
// all from one tile of it that couldn't. Reset per attach, alongside everything else a fresh load
// attempt starts over. See reportTileError.
protected anyTileDrawn = false;

attach(map: maplibregl.Map, style: MapLibreStyle): this {
super.attach(map, style);
this.deckOverlay = this.getDeckOverlay();
this.decoderPool ??= this.createDecoderPool();
this.drawnState = { visible: true, opacity: style.opacity };
this.geotiffBoundsLoaded = new Promise(resolve => (this.resolveGeotiffBounds = resolve));
this.anyTileDrawn = false;
return this;
}

Expand Down Expand Up @@ -130,11 +136,35 @@ export default class DeckCogPreviewer extends MapPreviewer {
[east, north],
]);
},
onTileLoad: () => (this.anyTileDrawn = true),
onTileError: (error: unknown) => this.reportTileError(error),
parameters: { depthCompare: 'always', cullMode: 'back' },
pool: this.decoderPool,
});
}

// A COG can only fail once its tiles start arriving, which is after preview() has resolved - so a
// file deck.gl refuses to draw used to leave the map empty with the reason only in the console.
// deck.gl reports each such tile here; overriding it also takes over from its own handler, which
// logs every one of them.
//
// Only the first failure of a COG that has drawn nothing is worth an alert. A COG can be sparse by
// design, and a tile that failed among tiles that didn't means a hole in a preview the user can
// see rather than a preview that isn't there - not worth replacing with an error. deck.gl drops a
// cancelled tile before calling back, so a pan that abandons its reads never arrives here at all,
// but a decoder that notices the abort itself can still throw one.
protected reportTileError(error: unknown) {
if ((error as { name?: unknown } | null)?.name === 'AbortError') return;

if (this.anyTileDrawn) {
console.warn(`Could not draw a tile of ${this.url}:`, error);
return;
}

console.error(`Error drawing ${this.url}:`, error);
this.onError?.(error);
}

// Disable the web worker decoder pool; it appears to error because it can't find /worker.js.
// See: https://developmentseed.org/deck.gl-raster/api/geotiff/type-aliases/DecoderPoolOptions/
// See also: https://github.com/developmentseed/deck.gl-raster/issues/364
Expand Down
8 changes: 8 additions & 0 deletions src/lib/previewers/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export default abstract class MapPreviewer extends Previewer {
protected style: MapLibreStyle;
protected map: maplibregl.Map;

// Where a failure that arrives after preview() has already resolved goes. Everything MapLibre
// draws itself reports one on the map, which ogm-map is already listening to; a preview that
// paints with its own WebGL has no such channel, so it is handed one. Set by whoever draws this
// preview, and only worth reporting for the failures that mean the preview isn't there - see
// DeckCogPreviewer, which has tiles arriving one at a time and most of a viewport's worth of
// failures to say nothing about.
onError?: (error: unknown) => void;

// Stored state for added MapLibre sources and layers to allow for cleanup
sourceIds: string[] = [];
layerIds: string[] = [];
Expand Down
Loading