From 26597ad25ca826dd6dbb0cf969be24b3b73b178c Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Sat, 7 Nov 2026 17:02:49 +0400 Subject: [PATCH] Decode the Bhote Koshi images once for every engine The event's before and after images went to each engine's display as URLs, so every engine switch fetched and decoded both again: the strict Nepal event scenario saw three reads of each image where Cesium's kept providers made one. The event now decodes the pair once with its own loader and hands the images to each display, which takes a URL or a decoded image. The pair is dropped with the rest of the event state on disable, and a failed load is retried on the next attachment. The wind engine-switch scenario waited for a diagnostics field the wind layer lost when it moved onto engine.imagery; it now waits for imageryActive. Checked in the browser, keyless: the Nepal event, wind and submarine-cable engine switches pass. --- scripts/qa-wind-engine-switch.mjs | 2 +- src/data/bhoteKoshiEvent.js | 22 ++++++++++++++++++---- src/layers/bhoteKoshi/render/display.js | 14 ++++++++------ 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/scripts/qa-wind-engine-switch.mjs b/scripts/qa-wind-engine-switch.mjs index ac155feba..f9b6c7e05 100644 --- a/scripts/qa-wind-engine-switch.mjs +++ b/scripts/qa-wind-engine-switch.mjs @@ -81,7 +81,7 @@ await strict.run({}, async (b) => { }); await p.waitForFunction( () => - window.qaWind.getDiagnostics().surface?.ready && + window.qaWind.getDiagnostics().imageryActive && window.qaWind.getDiagnostics().gpu?.ready, ); await p.evaluate(() => window.qaWind.setParams({ inspect: true })); diff --git a/src/data/bhoteKoshiEvent.js b/src/data/bhoteKoshiEvent.js index bff2e169e..5e32285f0 100644 --- a/src/data/bhoteKoshiEvent.js +++ b/src/data/bhoteKoshiEvent.js @@ -1010,6 +1010,7 @@ export function createBhoteKoshiEventLayer({ imageryProviderFactory = DEFAULT_IMAGERY_PROVIDER_FACTORY, terrainSampler = DEFAULT_TERRAIN_SAMPLER, mediaLoader = DEFAULT_MEDIA_LOADER, + imageLoader = DEFAULT_MEDIA_LOADER, evidenceFrameFactory = createEvidenceFrame, evidenceFrameUpdater = updateEvidenceFrame, videoFactory = DEFAULT_VIDEO_FACTORY, @@ -1022,6 +1023,9 @@ export function createBhoteKoshiEventLayer({ let _native = null; let _nativeFlood = null; let _providers = null; + // The before and after images, decoded once for every engine the event + // is drawn on (as Cesium's providers were kept across switches). + let _imageryImages = null; const cameraForScene = () => _engine?.camera ?? _viewer?.camera; const canvasForScene = () => _engine?.host.canvas ?? _viewer?.scene?.canvas; let _dataManager = null; @@ -1183,12 +1187,21 @@ export function createBhoteKoshiEventLayer({ async function createImagery(event, current = () => true) { if (_native) { // The engine's display draws the images through engine.imagery. + _imageryImages ??= Promise.all( + [event.imagery.before.path, event.imagery.after.path].map((path) => + imageLoader(eventAsset(path)), + ), + ); + let images; + try { + images = await _imageryImages; + } catch { + _imageryImages = null; + throw new Error('Bhote Koshi imagery unavailable'); + } await _native.loadImages( event.imagery.rectangle, - [ - eventAsset(event.imagery.before.path), - eventAsset(event.imagery.after.path), - ], + images, 'Vantor WorldView Open Data ยท CC BY-NC 4.0', ); if (_enabled && current()) setSplit(_split); @@ -3793,6 +3806,7 @@ export function createBhoteKoshiEventLayer({ _nativeFlood = null; _native?.clear(); _providers = null; + _imageryImages = null; if (_floodDataSource && _viewer) _viewer.dataSources.remove(_floodDataSource, true); _floodDataSource = null; diff --git a/src/layers/bhoteKoshi/render/display.js b/src/layers/bhoteKoshi/render/display.js index 05b3ab820..2172bb1db 100644 --- a/src/layers/bhoteKoshi/render/display.js +++ b/src/layers/bhoteKoshi/render/display.js @@ -44,11 +44,11 @@ export function createBhoteKoshiDisplay(engine) { }; }, /** - * The before and after images over `box` (degrees); resolves once both - * draw, rejects when one would not load (unless replaced or cleared - * first). + * The before and after images over `box` (degrees), each a URL or an + * image already decoded; resolves once both draw, rejects when one + * would not load (unless replaced or cleared first). */ - async loadImages(box, urls, credit = null) { + async loadImages(box, images, credit = null) { if (destroyed) return; clear(); const load = loads; @@ -60,7 +60,7 @@ export function createBhoteKoshiDisplay(engine) { }; // Each layer stages its image over an empty one, which tells whether // the image loaded. - layers = urls.map((_url, index) => + layers = images.map((_image, index) => engine.imagery.createLayer({ id: `${id}-${SIDES[index]}`, order: IMAGERY_ORDER, @@ -75,7 +75,9 @@ export function createBhoteKoshiDisplay(engine) { layer.setSource( { kind: 'image', - url: urls[index], + ...(typeof images[index] === 'string' + ? { url: images[index] } + : { image: images[index] }), rectangle, ...(credit ? { credit } : {}), },