mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Stop scene attachments of Bhote Koshi, GhostMaps, cables and infrastructure waiting once their scene is released
This commit is contained in:
1 parent
cf6d04b7db
commit
9cc34e324f
7 files changed
+209
-14
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { waitForSignal } from '../sources/requestDeadline.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { isPointerFree } from './inputOwnership.js';
|
||||
import {
|
||||
@@ -3521,10 +3522,19 @@ export function createBhoteKoshiEventLayer({
|
||||
// the release that follows it owns whatever it had already created.
|
||||
let _attachGeneration = 0;
|
||||
|
||||
// Settle when `promise` does, or as soon as the scene signal aborts. The
|
||||
// promise keeps running; the stale checks then ignore what it brings.
|
||||
function untilSceneReleased(promise, signal) {
|
||||
return waitForSignal(promise, signal).catch((error) => {
|
||||
if (!signal?.aborted) throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async function attachRenderer(viewer, { engine = null, signal = null } = {}) {
|
||||
const generation = ++_attachGeneration;
|
||||
const current = () =>
|
||||
generation === _attachGeneration && signal?.aborted !== true;
|
||||
const stack = _mapStackController;
|
||||
_viewer = viewer;
|
||||
_engine = engine;
|
||||
if (!_viewer && _engine) _native = createNativeRenderer(engine);
|
||||
@@ -3539,10 +3549,13 @@ export function createBhoteKoshiEventLayer({
|
||||
? 'preserve'
|
||||
: 'esri',
|
||||
});
|
||||
await _comparisonLease.ready;
|
||||
// The incoming map can take long to switch; a released scene stops
|
||||
// waiting (its release returns the lease).
|
||||
await untilSceneReleased(_comparisonLease.ready, signal);
|
||||
if (!current()) return;
|
||||
}
|
||||
await createImagery(_event, current);
|
||||
// createImagery checks `current` itself before it adds anything.
|
||||
await untilSceneReleased(createImagery(_event, current), signal);
|
||||
if (!current()) return;
|
||||
if (_floodDataSource) {
|
||||
if (_native) _nativeFlood = _native.draw(_floodDataSource);
|
||||
@@ -3552,8 +3565,21 @@ export function createBhoteKoshiEventLayer({
|
||||
registerDynamicCredit(engine ?? viewer, BHOTE_KOSHI_CREDIT);
|
||||
_scenePathPositionCache.clear();
|
||||
_lastCinematicProgress = null;
|
||||
await syncTerrainComparison();
|
||||
if (!current()) return;
|
||||
const terrain = syncTerrainComparison();
|
||||
await untilSceneReleased(terrain, signal);
|
||||
if (!current()) {
|
||||
// The release already cleared the comparison terrain; one this
|
||||
// attachment set afterwards is removed when it lands, unless a newer
|
||||
// attachment owns the same map stack again.
|
||||
terrain.then(
|
||||
() => {
|
||||
if (stack && stack !== _mapStackController)
|
||||
stack.clearTerrainComparison?.();
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
return;
|
||||
}
|
||||
createSplitControl(_event);
|
||||
syncPresentationMode();
|
||||
attachCameraInputRelease();
|
||||
|
||||
@@ -3051,3 +3051,49 @@ for (const stop of ['detach', 'scene-abort']) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('Bhote Koshi stops waiting for a stalled map switch when its scene is released', 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() {} },
|
||||
});
|
||||
layer.attachMapStackController({
|
||||
getActiveId: () => 'esri-imagery',
|
||||
getSwitchGeneration: () => 1,
|
||||
async setStack() {},
|
||||
});
|
||||
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's switch to Esri never finishes; restoring returns.
|
||||
let active = 'photoreal';
|
||||
layer.attachMapStackController({
|
||||
getActiveId: () => active,
|
||||
getSwitchGeneration: () => 1,
|
||||
setStack: (id) => (id === 'esri-imagery' ? new Promise(() => {}) : Promise.resolve()),
|
||||
});
|
||||
const scene = new AbortController();
|
||||
const attaching = layer.attachRenderer(second, { signal: scene.signal });
|
||||
scene.abort();
|
||||
await attaching;
|
||||
await layer.detachRenderer();
|
||||
assert.deepEqual(second._test.imagery, []);
|
||||
assert.deepEqual(second._test.dataSources, []);
|
||||
} finally {
|
||||
await manager.destroyLayer(layer.id);
|
||||
globalThis.document = originalDocument;
|
||||
globalThis.window = originalWindow;
|
||||
}
|
||||
});
|
||||
@@ -159,7 +159,10 @@ export function createGhostMapsLayer({
|
||||
|
||||
const notifyRow = () => _rowListener?.();
|
||||
const render = () => _display?.requestRender();
|
||||
const canSelect = () => overlayHost && picking && (screenSpaceEventHandlerFactory || createRenderer);
|
||||
const canSelect = () =>
|
||||
overlayHost &&
|
||||
picking &&
|
||||
(screenSpaceEventHandlerFactory || createRenderer);
|
||||
|
||||
function wanted(product) {
|
||||
return _selection ? _selection.has(product.id) : product.defaultOn;
|
||||
@@ -207,7 +210,8 @@ export function createGhostMapsLayer({
|
||||
entries.clear();
|
||||
signal.throwIfAborted();
|
||||
const features = parsed.features.map((feature) => ({
|
||||
...feature, anchor: anchorOf(feature.geometries[0]),
|
||||
...feature,
|
||||
anchor: anchorOf(feature.geometries[0]),
|
||||
}));
|
||||
return { version: product.version, urls, features, paint: null };
|
||||
} catch (error) {
|
||||
@@ -219,7 +223,12 @@ export function createGhostMapsLayer({
|
||||
async function paint(id, item, signal) {
|
||||
while (_display && !signal.aborted && !item.removed) {
|
||||
const owner = _display;
|
||||
const next = owner.draw(id, item.features, PRODUCT_COLORS[id] || ACCENT, markerImage);
|
||||
const next = owner.draw(
|
||||
id,
|
||||
item.features,
|
||||
PRODUCT_COLORS[id] || ACCENT,
|
||||
markerImage,
|
||||
);
|
||||
try {
|
||||
await next.ready;
|
||||
if (owner === _display && !signal.aborted && !item.removed) {
|
||||
@@ -477,15 +486,33 @@ export function createGhostMapsLayer({
|
||||
if (viewer || options.engine) return this.attachRenderer(viewer, options);
|
||||
},
|
||||
|
||||
attachRenderer(viewer, { engine = null } = {}) {
|
||||
attachRenderer(viewer, { engine = null, signal = null } = {}) {
|
||||
if (_display) throw new Error('GhostMaps display is already attached');
|
||||
_display = viewer
|
||||
? createGhostMapsCesiumRenderer(viewer, screenSpaceEventHandlerFactory, markerImage)
|
||||
? createGhostMapsCesiumRenderer(
|
||||
viewer,
|
||||
screenSpaceEventHandlerFactory,
|
||||
markerImage,
|
||||
)
|
||||
: createRenderer(engine);
|
||||
if (!_enabled) return;
|
||||
installClickHandler();
|
||||
publishCard();
|
||||
return Promise.all([..._loaded].map(([id, item]) => paint(id, item, _lifetime.signal)));
|
||||
// A paint that outlives its scene destroys what it drew (see paint).
|
||||
// The parsed products stay loaded for the next scene.
|
||||
const paintSignal = signal
|
||||
? AbortSignal.any([_lifetime.signal, signal])
|
||||
: _lifetime.signal;
|
||||
const painted = Promise.all(
|
||||
[..._loaded].map(([id, item]) => paint(id, item, paintSignal)),
|
||||
);
|
||||
if (!signal) return painted;
|
||||
// Released scene: stop waiting; the paints keep their own clean-up.
|
||||
const released = new Promise((resolve) => {
|
||||
if (signal.aborted) resolve();
|
||||
else signal.addEventListener('abort', resolve, { once: true });
|
||||
});
|
||||
return Promise.race([painted, released]);
|
||||
},
|
||||
|
||||
detachRenderer() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { waitForSignal } from '../../sources/requestDeadline.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import {
|
||||
mapAnalystRecord,
|
||||
@@ -131,7 +132,7 @@ export function createInfrastructureSessionLayer(
|
||||
init(viewer, options = {}) {
|
||||
if (viewer || options.engine) return this.attachRenderer(viewer, options);
|
||||
},
|
||||
attachRenderer(viewer, { engine = null } = {}) {
|
||||
attachRenderer(viewer, { engine = null, signal = null } = {}) {
|
||||
if (display) throw Error('Infrastructure display already attached');
|
||||
if (viewer) {
|
||||
const legacy = createLocalGeoJsonLayer(
|
||||
@@ -149,7 +150,13 @@ export function createInfrastructureSessionLayer(
|
||||
lod: legacy.getLodDiagnostics,
|
||||
};
|
||||
} else display = nativeFactory(engine, config, services);
|
||||
if (enabled) return show();
|
||||
// The dataset load belongs to the session and keeps running; only this
|
||||
// scene's wait for it ends when the scene is released. A released
|
||||
// display is destroyed and ignores what arrives later.
|
||||
if (enabled)
|
||||
return waitForSignal(show(), signal).catch((error) => {
|
||||
if (!signal?.aborted) throw error;
|
||||
});
|
||||
},
|
||||
detachRenderer() {
|
||||
if (!display) return;
|
||||
|
||||
@@ -61,3 +61,48 @@ test('a bundled file delivered between planets keeps multipart IDs and is read o
|
||||
assert.equal(reads, 1);
|
||||
layer.destroy();
|
||||
});
|
||||
|
||||
test('a released scene stops waiting for a stalled dataset load, which stays in the session', async (t) => {
|
||||
let deliver;
|
||||
t.mock.method(globalThis, 'fetch', async () => ({
|
||||
ok: true,
|
||||
text: () =>
|
||||
new Promise((resolve) => {
|
||||
deliver = resolve;
|
||||
}),
|
||||
}));
|
||||
const captures = [];
|
||||
const layer = createInfrastructureSessionLayer(
|
||||
{ id: 'local-dams', name: 'Dams', url: '/dams' },
|
||||
{},
|
||||
() => ({
|
||||
show: (data) => captures.push(data),
|
||||
hide() {},
|
||||
destroy() {},
|
||||
lod() {},
|
||||
}),
|
||||
);
|
||||
layer.init(null, { engine: {} });
|
||||
const enabled = layer.enable();
|
||||
await new Promise(setImmediate);
|
||||
layer.detachRenderer();
|
||||
const scene = new AbortController();
|
||||
const attaching = layer.attachRenderer(null, {
|
||||
engine: {},
|
||||
signal: scene.signal,
|
||||
});
|
||||
scene.abort();
|
||||
await attaching;
|
||||
layer.detachRenderer();
|
||||
deliver(
|
||||
JSON.stringify({
|
||||
type: 'Feature',
|
||||
id: 'd',
|
||||
properties: { name: 'Dam' },
|
||||
geometry: { type: 'Point', coordinates: [10, 20] },
|
||||
}),
|
||||
);
|
||||
await enabled;
|
||||
assert.equal(captures.length, 0);
|
||||
assert.equal(layer.getAnalystRecords().length, 1);
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { waitForSignal } from '../../sources/requestDeadline.js';
|
||||
/** The dataset is acquired once per activation, independently of scene attachment. */
|
||||
export function createCableSessionLayer({
|
||||
source,
|
||||
@@ -75,7 +76,7 @@ export function createCableSessionLayer({
|
||||
init(viewer, ctx) {
|
||||
return this.attachRenderer(viewer, ctx);
|
||||
},
|
||||
attachRenderer(viewer, { engine } = {}) {
|
||||
attachRenderer(viewer, { engine, signal = null } = {}) {
|
||||
if (viewer) {
|
||||
const legacy = createCesium({ label: source.label, fetch: read });
|
||||
legacy.init(viewer);
|
||||
@@ -95,7 +96,13 @@ export function createCableSessionLayer({
|
||||
stats: () => legacy.getStats(),
|
||||
};
|
||||
} else display = createNavara(engine);
|
||||
if (enabled) return show();
|
||||
// The cable data load belongs to the session and keeps running; only
|
||||
// this scene's wait for it ends when the scene is released. show()
|
||||
// draws nothing into a display that was released meanwhile.
|
||||
if (enabled)
|
||||
return waitForSignal(show(), signal).catch((error) => {
|
||||
if (!signal?.aborted) throw error;
|
||||
});
|
||||
},
|
||||
detachRenderer() {
|
||||
const owner = display;
|
||||
|
||||
@@ -33,3 +33,40 @@ test('cables arriving during handoff belong to the incoming display', async () =
|
||||
assert.equal(reads, 1);
|
||||
layer.destroy();
|
||||
});
|
||||
|
||||
test('a released scene stops waiting for a stalled cable load, which stays in the session', async () => {
|
||||
let resolve;
|
||||
const shown = [];
|
||||
const layer = createCableSessionLayer({
|
||||
source: {
|
||||
label: 'fixture',
|
||||
fetch: () =>
|
||||
new Promise((done) => {
|
||||
resolve = done;
|
||||
}),
|
||||
},
|
||||
createNavara: (engine) => ({
|
||||
show: (dataset) => shown.push([engine, dataset]),
|
||||
destroy() {},
|
||||
hide() {},
|
||||
}),
|
||||
});
|
||||
layer.init(null, { engine: 'outgoing' });
|
||||
const pending = layer.enable();
|
||||
layer.detachRenderer();
|
||||
const scene = new AbortController();
|
||||
const attaching = layer.attachRenderer(null, {
|
||||
engine: 'released',
|
||||
signal: scene.signal,
|
||||
});
|
||||
scene.abort();
|
||||
await attaching;
|
||||
layer.detachRenderer();
|
||||
const dataset = { cables: { features: [] }, landingPoints: { features: [] } };
|
||||
resolve(dataset);
|
||||
await pending;
|
||||
assert.deepEqual(shown, []);
|
||||
await layer.attachRenderer(null, { engine: 'next' });
|
||||
assert.deepEqual(shown, [['next', dataset]]);
|
||||
layer.destroy();
|
||||
});
|
||||
Reference in new issue
Block a user