Keep the tracked aircraft through a failed engine switch on Cesium

Cesium follows a display's sprite through a proxy Entity that mirrors the
native's readout fields, and gevRestoring was not one of them. A follow
the flights layer resumed on attach therefore read as new navigation to
the shell's tracked-entity watcher, which made the share restore of the
same attach count itself superseded and clear the selection. That is why
the aircraft was lost after a failed switch's rollback onto Cesium, and
after the retry. The proxy now shows the restoring mark while its follow
starts.

The flights renderer test runs a detach, a failed attach to a candidate,
the rollback and a retry on headless Cesium engines, and checks that the
aircraft stays tracked and no navigation is stamped.
This commit is contained in:
German Khokhlov committed 2026-11-07 03:44:38 +04:00
1 parent f06b93fbc9
commit 145adc08e0
3 files changed
+110 -1

No files matched your search

+6 -1
View File
@@ -23,13 +23,18 @@ import { toCesium3 } from './convert.js';
const MAX_FRAME_ATTEMPTS = 120;
const entityName = (entity) => (entity ? entity.name || entity.id : undefined);
// What viewer.trackedEntity readers take from a tracked Entity, read through
// a proxy from the layer's own handle.
// a proxy from the layer's own handle. gevRestoring marks a follow that
// rebinds the session's own subject (a resumed renderer, a share restore)
// while it starts: trackedEntityChanged readers (the shell's navigation
// stamp) must see it on the proxy, or a resumed follow supersedes the
// restore it belongs to.
const READOUT_FIELDS = [
'gevTrackedId',
'gevLabelModel',
'gevDisplayPosition',
'gevVisualPosition',
'gevSelectionOrigin',
'gevRestoring',
];
const ZOOM_INERTIA_STATES = new WeakMap();
+20
View File
@@ -109,6 +109,26 @@ test('a native that is not an Entity is echoed and followed through an ENU proxy
assert.equal(viewer.entities.contains(proxy), false);
});
test("the proxy shows a restoring native's mark while its follow starts", () => {
const { viewer, camera } = setup();
const native = { id: 'flights-tracked-1', gevTrackedId: 'flights:abc123' };
// What trackedEntityChanged readers see as the follow starts.
const seen = [];
viewer.trackedEntityChanged.addEventListener((entity) =>
seen.push(entity?.gevRestoring),
);
native.gevRestoring = true;
const handle = camera.follow({
native,
position: () => TARGET,
viewFrom: () => ({ x: 0, y: -500, z: 200 }),
});
delete native.gevRestoring;
assert.deepEqual(seen, [true]);
assert.equal(viewer.trackedEntity.gevRestoring, undefined, 'read through');
handle.stop();
});
test('a camera command or another tracked entity ends the follow', () => {
const { viewer, camera, changes } = setup();
const options = { position: () => TARGET, viewFrom: { x: 0, y: -500, z: 0 } };
+84
View File
@@ -293,3 +293,87 @@ test('the flights logic draws, tracks and follows on the Cesium engine through t
assert.equal(engine.lines.list().length, 0, 'no line left behind');
engine.destroy();
});
test('a failed engine switch, its rollback and the retry re-follow the tracked aircraft as a restore', async () => {
const { createCesiumMoversEngine } =
await import('../../testSupport/cesiumMoversEngine.mjs');
const { createFlightsDisplay } = await import('./render/display.js');
const { aircraftAtlasLayout } = await import('../aircraft/render/display.js');
const layout = aircraftAtlasLayout();
// The shell's navigation stamp (LayerBindings.observeCamera): a follow
// Cesium announces through trackedEntityChanged is new navigation unless
// it rebinds the session's own subject. A new navigation supersedes the
// switch's share restore, which then clears the selection.
const navigations = [];
const cesium = () => {
const made = createCesiumMoversEngine();
made.engine.camera.setView({
destination: geo.cartesian3.fromDegrees(-97.5, 30.3, 400_000),
});
made.viewer.trackedEntityChanged.addEventListener((entity) => {
if (entity && !entity.gevRestoring)
navigations.push(entity.gevTrackedId);
});
return made;
};
const working = cesium();
let display = null;
const layer = createApplicationFlights({
surface: defaultSurface,
source: {
label: 'Fixture aircraft',
getSnapshot: async () => snapshotOf(['abc123', 'def456']),
},
militaryRegistry,
renderer: {
create: ({ engine: given }) =>
(display = createFlightsDisplay(given, {
drawAtlas: async () => ({
canvas: { width: layout.width, height: layout.height },
cells: layout.cells,
}),
})),
},
});
layer.init(working.viewer, { engine: working.engine });
layer.enable();
const candidate = createFakeEngine();
let retried = null;
try {
await display.whenReady();
await layer.update(null);
await new Promise((resolve) => setTimeout(resolve, 0));
// The person's own selection is navigation.
assert.equal(layer.trackById('def456', { origin: 'user' }), true);
assert.deepEqual(navigations, ['flights:def456']);
navigations.length = 0;
// The switch: the working engine detaches; the candidate attaches the
// layer, then fails and releases what it attached.
layer.detachRenderer(working.viewer);
layer.attachRenderer(null, { engine: candidate });
assert.equal(candidate.camera.following()?.label?.id, 'flights:def456');
layer.detachRenderer(null);
// The rollback: the working engine attaches again and follows the same
// aircraft, announced as a restore.
layer.attachRenderer(working.viewer, { engine: working.engine });
assert.equal(layer.getTrackedInfo()?.icao24, 'def456');
assert.equal(working.viewer.trackedEntity?.gevTrackedId, 'flights:def456');
assert.deepEqual(navigations, [], 'the rollback is not new navigation');
// The retry onto a new Cesium engine.
layer.detachRenderer(working.viewer);
retried = cesium();
layer.attachRenderer(retried.viewer, { engine: retried.engine });
assert.equal(layer.getTrackedInfo()?.icao24, 'def456');
assert.equal(retried.viewer.trackedEntity?.gevTrackedId, 'flights:def456');
assert.equal(
retried.engine.camera.following()?.label?.id,
'flights:def456',
);
assert.deepEqual(navigations, [], 'the retry is not new navigation');
} finally {
layer.disable();
layer.destroy(retried?.viewer ?? working.viewer);
}
for (const engine of [working.engine, candidate, retried?.engine])
engine?.destroy();
});