diff --git a/src/data/bhoteKoshiEvent.js b/src/data/bhoteKoshiEvent.js index 062e3fd8d..58667094c 100644 --- a/src/data/bhoteKoshiEvent.js +++ b/src/data/bhoteKoshiEvent.js @@ -1177,7 +1177,7 @@ export function createBhoteKoshiEventLayer({ return _event; } - async function createImagery(event) { + async function createImagery(event, current = () => true) { const rectangle = Cesium.Rectangle.fromDegrees( event.imagery.rectangle.west, event.imagery.rectangle.south, @@ -1194,13 +1194,13 @@ export function createBhoteKoshiEventLayer({ credit: 'Vantor WorldView Open Data ยท CC BY-NC 4.0', }), ])); - if (!_enabled) return; + if (!_enabled || !current()) return; if (_native) { await _native.loadImages(event.imagery.rectangle, [ beforeProvider, afterProvider, ]); - setSplit(_split); + if (current()) setSplit(_split); return; } _imageryLayerCollection = _viewer.imageryLayers; @@ -3516,7 +3516,15 @@ export function createBhoteKoshiEventLayer({ _imageryLayerCollection = null; } - async function attachRenderer(viewer, { engine = null } = {}) { + // Each attachment and release advances this. An attachment that resumes + // after a release, or after its scene signal aborted, stops right there; + // the release that follows it owns whatever it had already created. + let _attachGeneration = 0; + + async function attachRenderer(viewer, { engine = null, signal = null } = {}) { + const generation = ++_attachGeneration; + const current = () => + generation === _attachGeneration && signal?.aborted !== true; _viewer = viewer; _engine = engine; if (!_viewer && _engine) _native = createNativeRenderer(engine); @@ -3532,16 +3540,20 @@ export function createBhoteKoshiEventLayer({ : 'esri', }); await _comparisonLease.ready; + if (!current()) return; } - await createImagery(_event); + await createImagery(_event, current); + if (!current()) return; if (_floodDataSource) { if (_native) _nativeFlood = _native.draw(_floodDataSource); else await _viewer.dataSources.add(_floodDataSource); + if (!current()) return; } registerDynamicCredit(engine ?? viewer, BHOTE_KOSHI_CREDIT); _scenePathPositionCache.clear(); _lastCinematicProgress = null; await syncTerrainComparison(); + if (!current()) return; createSplitControl(_event); syncPresentationMode(); attachCameraInputRelease(); @@ -3551,6 +3563,7 @@ export function createBhoteKoshiEventLayer({ } async function detachRenderer() { + ++_attachGeneration; detachCameraInputRelease(); detachEvidenceCardActivation(); _splitControl?.destroy(); diff --git a/src/data/bhoteKoshiEvent.test.mjs b/src/data/bhoteKoshiEvent.test.mjs index 51e5f11f5..9bc84fa40 100644 --- a/src/data/bhoteKoshiEvent.test.mjs +++ b/src/data/bhoteKoshiEvent.test.mjs @@ -2992,3 +2992,62 @@ test('Bhote Koshi corridor remains ordered downstream and inside the imagery rec priorChainage = point.chainageM; } }); + +for (const stop of ['detach', 'scene-abort']) { + test(`Bhote Koshi attach resumed after a ${stop} leaves the next scene untouched`, async () => { + const event = JSON.parse(await readFile(eventUrl, 'utf8')); + const originalDocument = globalThis.document; + const originalWindow = globalThis.window; + installEventDom(); + globalThis.window = { setTimeout: () => 1, clearTimeout() {} }; + const first = eventViewer(); + const second = eventViewer(); + const layer = createBhoteKoshiEventLayer({ + eventLoader: async () => event, + imageryProviderFactory: async (url) => ({ url }), + embeddedMediaFactory: () => ({ show() {}, hide() {}, pause() {}, destroy() {} }), + overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} }, + renderHost: { request() {}, hold() {}, release() {} }, + }); + const stack = (initial, gate = null) => { + let active = initial; + let generation = 1; + return { + getActiveId: () => active, + getSwitchGeneration: () => generation, + async setStack(id) { + await gate; + active = id; + generation += 1; + }, + }; + }; + layer.attachMapStackController(stack('esri-imagery')); + const manager = new LayerLifecycle(first); + manager.register(layer); + try { + assert.equal(await manager.setEnabled(layer.id, true, { origin: 'local-restore' }), true); + await layer.detachRenderer(); + + // The incoming map has to switch first: the attachment waits on it. + let openGate; + const gate = new Promise((resolve) => { openGate = resolve; }); + layer.attachMapStackController(stack('photoreal', gate)); + const scene = new AbortController(); + const attaching = layer.attachRenderer(second, { signal: scene.signal }); + let detaching = null; + if (stop === 'detach') detaching = layer.detachRenderer(); + else scene.abort(); + openGate(); + await attaching; + await detaching; + assert.deepEqual(second._test.imagery, []); + assert.deepEqual(second._test.dataSources, []); + if (stop === 'scene-abort') await layer.detachRenderer(); + } finally { + await manager.destroyLayer(layer.id); + globalThis.document = originalDocument; + globalThis.window = originalWindow; + } + }); +}