mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Keep the ridden aircraft's layer when Contacts ends, and clear its rows from Live Signals
Riding an aircraft now adopts its layer the way a direct selection does, and Contacts gives up a dependency the user adopts, so ending Contacts mid-session (one of its layers off, or Contacts itself) no longer turns off the ridden layer and ends the Cockpit. When Contacts goes off, Live Signals drops the rows built from its snapshot instead of leaving stale distances marked CURRENT, and the feed-off warning is no longer duplicated.
This commit is contained in:
1 parent
459b0e8a71
commit
63fd559496
14 files changed
+441
-16
No files matched your search
@@ -98,6 +98,7 @@ function installAwarenessRuntime({
|
||||
// status its module never emits guards nothing. See the settled-but-fetching
|
||||
// tests below, which use militaryInstallations' genuine first-fetch readout.
|
||||
layerStats = {},
|
||||
subscribe = undefined,
|
||||
} = {}) {
|
||||
const originalWindow = globalThis.window;
|
||||
const originalDocument = globalThis.document;
|
||||
@@ -215,6 +216,7 @@ function installAwarenessRuntime({
|
||||
setEnabled,
|
||||
getLayerLifecycleState,
|
||||
layers,
|
||||
...(subscribe ? { subscribe } : {}),
|
||||
});
|
||||
militaryAwarenessLayer.setParams({ passive: true });
|
||||
militaryAwarenessLayer.init(viewer);
|
||||
@@ -383,6 +385,54 @@ test('Contacts does not claim a dependency already enabling for the user', async
|
||||
}
|
||||
});
|
||||
|
||||
test('a dependency the user adopts stays on when Contacts turns off', async () => {
|
||||
// A direct selection, or the Cockpit riding one of its aircraft, promotes a
|
||||
// layer Contacts turned on into the user's own (adoptLayerVisibility). The
|
||||
// coordinator must give it up, or its OFF release tears the layer down —
|
||||
// and the track or the Cockpit with it — before Context restores it.
|
||||
const calls = [];
|
||||
const subscribers = new Set();
|
||||
const runtime = installAwarenessRuntime({
|
||||
isEnabled: () => false,
|
||||
isEffectivelyEnabled: () => false,
|
||||
setEnabled: (layerId, enabled) => {
|
||||
calls.push({ layerId, enabled });
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
subscribe: (listener) => {
|
||||
subscribers.add(listener);
|
||||
return () => subscribers.delete(listener);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
militaryAwarenessLayer.setParams({ passive: false });
|
||||
await nextTurn();
|
||||
for (const listener of subscribers)
|
||||
listener({
|
||||
type: 'visibility',
|
||||
layerId: 'flights',
|
||||
enabled: true,
|
||||
origin: 'user',
|
||||
adopted: true,
|
||||
adoptedFromSelection: true,
|
||||
});
|
||||
await militaryAwarenessLayer.disable();
|
||||
const released = calls
|
||||
.filter(({ enabled }) => !enabled)
|
||||
.map(({ layerId }) => layerId)
|
||||
.sort();
|
||||
assert.deepEqual(
|
||||
released,
|
||||
AWARENESS_DEPENDENCIES.filter((layerId) => layerId !== 'flights').sort(),
|
||||
'the adopted layer is the user\'s now; the rest are still released',
|
||||
);
|
||||
} finally {
|
||||
runtime.restore();
|
||||
}
|
||||
assert.equal(subscribers.size, 0, 'destroy drops the manager subscription');
|
||||
});
|
||||
|
||||
test('operational awareness becomes ready after aircraft while deferred sources keep loading', async () => {
|
||||
let enabled = false;
|
||||
let releaseAircraft;
|
||||
|
||||
Vendored
+1
@@ -23,6 +23,7 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
|
||||
attachDataManager(dataManager) {
|
||||
layerState.dataManager = dataManager;
|
||||
parts.dependencies.watchAdoptions(dataManager);
|
||||
},
|
||||
|
||||
setParams(params = {}) {
|
||||
|
||||
@@ -79,6 +79,25 @@ export function createDependencies({
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Give up a dependency the user adopts. A direct selection, or the Cockpit
|
||||
* riding one of its aircraft, promotes a layer Contacts turned on into the
|
||||
* user's own (`DataLayerManager.adoptLayerVisibility`). Releasing it when
|
||||
* Contacts goes off would tear down that track or Cockpit before the
|
||||
* Context restore could put the layer back.
|
||||
*/
|
||||
function relinquishAdoptedDependency(change) {
|
||||
if (change?.type === 'visibility' && change.adopted && change.enabled)
|
||||
layerState.ownedDependencies.delete(change.layerId);
|
||||
}
|
||||
|
||||
/** Follow one manager's adoptions; null stops following. */
|
||||
function watchAdoptions(dataManager) {
|
||||
layerState.unwatchAdoptions?.();
|
||||
layerState.unwatchAdoptions =
|
||||
dataManager?.subscribe?.(relinquishAdoptedDependency) || null;
|
||||
}
|
||||
|
||||
function activateOperationalContext() {
|
||||
const activationId = ++layerState.activationId;
|
||||
layerState.autoFocusRetryPending = false;
|
||||
@@ -113,6 +132,7 @@ export function createDependencies({
|
||||
refreshAfterDeferredDependency,
|
||||
enableDependencies,
|
||||
releaseOwnedDependencies,
|
||||
watchAdoptions,
|
||||
activateOperationalContext,
|
||||
};
|
||||
}
|
||||
@@ -340,6 +340,7 @@ export function createLifecycle({
|
||||
layerState.clearListener = null;
|
||||
layerState.subjectClearListener = null;
|
||||
layerState.viewer = null;
|
||||
parts.dependencies.watchAdoptions(null);
|
||||
layerState.dataManager = null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -26,6 +26,8 @@ export function createState({ services }) {
|
||||
// layers activate only after Contacts is explicitly selected.
|
||||
passive: true,
|
||||
ownedDependencies: new Set(),
|
||||
/** Stops following the manager's adoptions (see watchAdoptions). */
|
||||
unwatchAdoptions: null,
|
||||
subject: null,
|
||||
results: null,
|
||||
visual: null,
|
||||
|
||||
@@ -75,6 +75,9 @@ export function updateContext(info, heading) {
|
||||
const readout = resolveCockpitContextReadout({ snapshot, info });
|
||||
const partial = readPartialPicture.call(this);
|
||||
if (!readout.visible) {
|
||||
// Contacts just went off: the rows its snapshot built stop being
|
||||
// measured, so they go before the standby rows replace them.
|
||||
if (this.presence.context) this.dropContextSnapshotSignals?.();
|
||||
this.showContextCard(false);
|
||||
this.contextLayoutStamp = null;
|
||||
this.pushCockpitSignal(
|
||||
|
||||
@@ -12,6 +12,13 @@ import {
|
||||
updateContext,
|
||||
} from './cockpitContext.js';
|
||||
import { createContextState } from './cockpitContextState.js';
|
||||
import {
|
||||
dropCockpitSignal,
|
||||
dropContextSnapshotSignals,
|
||||
publishCockpitSignals,
|
||||
pushCockpitSignal,
|
||||
updateCockpitSignals,
|
||||
} from './cockpitSignals.js';
|
||||
import { syncWeatherToggle } from './cockpitInstruments.js';
|
||||
import { setContextCollapsed } from './cockpitLayout.js';
|
||||
import { useCockpitStore } from './stores/cockpit.js';
|
||||
@@ -261,6 +268,62 @@ test('without Contacts, a feed that is off adds a signal until it is back', asyn
|
||||
assert.deepEqual(dropped, ['feed-status']);
|
||||
});
|
||||
|
||||
/** The fixture's owner with the real Live Signals presenters. */
|
||||
function withSignals(owner) {
|
||||
const published = [];
|
||||
Object.assign(owner, {
|
||||
signalItems: [],
|
||||
signalSignatures: new Map(),
|
||||
publishSignals: (rows) => published.push(rows),
|
||||
publishCockpitSignals,
|
||||
pushCockpitSignal,
|
||||
dropCockpitSignal,
|
||||
dropContextSnapshotSignals,
|
||||
updateCockpitSignals,
|
||||
});
|
||||
return () => published.at(-1).map((row) => row.title);
|
||||
}
|
||||
|
||||
test('Contacts going off clears its contact rows and leaves standby', async (t) => {
|
||||
const { owner } = fixture(t, snapshot());
|
||||
const rows = withSignals(owner);
|
||||
owner.services.isLayerEnabled = () => true;
|
||||
updateContext.call(owner, INFO, 0);
|
||||
assert.ok(
|
||||
owner.signalItems.some((item) => item.key.startsWith('flight:')),
|
||||
'Contacts fills the rows',
|
||||
);
|
||||
assert.ok(owner.signalItems.some((item) => item.key === 'input-status'));
|
||||
// Both feeds stay on; only Contacts goes.
|
||||
owner.snapshot = null;
|
||||
updateContext.call(owner, INFO, 0);
|
||||
assert.deepEqual(rows(), ['CONTEXT STANDBY']);
|
||||
updateContext.call(owner, INFO, 0);
|
||||
assert.deepEqual(rows(), ['CONTEXT STANDBY'], 'standby holds still');
|
||||
});
|
||||
|
||||
test('Contacts going off with a feed off keeps the feed row beside standby', async (t) => {
|
||||
const { owner } = fixture(t, snapshot());
|
||||
const rows = withSignals(owner);
|
||||
owner.services.isLayerEnabled = (layerId) => layerId !== 'military';
|
||||
updateContext.call(owner, INFO, 0);
|
||||
owner.snapshot = null;
|
||||
updateContext.call(owner, INFO, 0);
|
||||
assert.deepEqual(rows(), [
|
||||
'MILITARY FEED OFF · PARTIAL PICTURE',
|
||||
'CONTEXT STANDBY',
|
||||
]);
|
||||
});
|
||||
|
||||
test('standby without Contacts keeps the rows the session already shows', async (t) => {
|
||||
const { owner } = fixture(t, null);
|
||||
const rows = withSignals(owner);
|
||||
owner.services.isLayerEnabled = () => true;
|
||||
owner.pushCockpitSignal('track', 'track', 'TRACK ACQUIRED', 'UAL123');
|
||||
updateContext.call(owner, INFO, 0);
|
||||
assert.deepEqual(rows(), ['CONTEXT STANDBY', 'TRACK ACQUIRED']);
|
||||
});
|
||||
|
||||
test('the weather toggle renders its state and the action it offers', async (t) => {
|
||||
const { $, text, owner } = fixture(t);
|
||||
syncWeatherToggle.call(owner, true);
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
} from './cockpitBriefing.js';
|
||||
import {
|
||||
dropCockpitSignal,
|
||||
dropContextSnapshotSignals,
|
||||
handleSignalClick,
|
||||
publishCockpitSignals,
|
||||
pushCockpitSignal,
|
||||
@@ -82,6 +83,7 @@ export class CockpitViewController {
|
||||
isUtilityDisclosureOpen = null,
|
||||
onEntered = null,
|
||||
onExited = null,
|
||||
onRide = null,
|
||||
restoreTrackingFrame = null,
|
||||
publish = {},
|
||||
} = {},
|
||||
@@ -121,6 +123,7 @@ export class CockpitViewController {
|
||||
this.onCameraTakeover = onCameraTakeover;
|
||||
this.onEntered = onEntered;
|
||||
this.onExited = onExited;
|
||||
this.onRide = onRide;
|
||||
this.restoreTrackingFrame =
|
||||
typeof restoreTrackingFrame === 'function'
|
||||
? restoreTrackingFrame
|
||||
@@ -394,6 +397,9 @@ export class CockpitViewController {
|
||||
dropCockpitSignal(key) {
|
||||
return dropCockpitSignal.call(this, key);
|
||||
}
|
||||
dropContextSnapshotSignals() {
|
||||
return dropContextSnapshotSignals.call(this);
|
||||
}
|
||||
updateCockpitSignals(snapshot, unknownCount, partialPicture) {
|
||||
return updateCockpitSignals.call(
|
||||
this,
|
||||
|
||||
@@ -97,6 +97,7 @@ export class CockpitCoordinator {
|
||||
isUtilityDisclosureOpen: () => !!this.readUtilityDisclosureOpen?.(),
|
||||
onEntered: () => this.enterPanels(),
|
||||
onExited: () => this.exitPanels(),
|
||||
onRide: (layerId) => this._adoptRiddenLayer(layerId),
|
||||
restoreTrackingFrame: (entity) => {
|
||||
const [layerId, ...idParts] = String(entity?.gevTrackedId || '').split(
|
||||
':',
|
||||
@@ -127,6 +128,22 @@ export class CockpitCoordinator {
|
||||
_isLayerEnabled(layerId) {
|
||||
return Boolean(layerId) && !!this._dataManager?.isEnabled(layerId);
|
||||
}
|
||||
/**
|
||||
* Make the ridden aircraft's layer the user's own while Contacts is on.
|
||||
* Contacts may have turned that layer on, and it releases the layers it
|
||||
* owns when it goes off; the Cockpit must outlive Contacts, so riding an
|
||||
* aircraft adopts its layer as a direct selection does. Contacts then stops
|
||||
* owning it and the Context restore keeps it (session `userAdded`).
|
||||
* @param {string|null} layerId The ridden aircraft's layer.
|
||||
*/
|
||||
_adoptRiddenLayer(layerId) {
|
||||
if (!COCKPIT_AIRCRAFT_LAYER_IDS.includes(layerId)) return;
|
||||
if (!this._isLayerEnabled('military-awareness')) return;
|
||||
this._dataManager?.adoptLayerVisibility?.(layerId, true, {
|
||||
origin: 'user',
|
||||
adoptedFromSelection: true,
|
||||
});
|
||||
}
|
||||
/** Whether either aircraft layer is on, so there is an aircraft to ride. */
|
||||
get _aircraftLayerEnabled() {
|
||||
return COCKPIT_AIRCRAFT_LAYER_IDS.some((layerId) =>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
// src/ui/cockpitEntryGate.test.mjs — first person needs only the layer of
|
||||
// the vehicle it rides: Contacts and the other feeds may be off before or
|
||||
// during a session, turning the ridden layer off ends it, and the voice gate
|
||||
// and state say so without claiming Contacts is required. Over happy-dom.
|
||||
// during a session, turning the ridden layer off ends it, Contacts ending
|
||||
// keeps the ridden layer it had turned on, and the voice gate and state say
|
||||
// so without claiming Contacts is required. Over happy-dom.
|
||||
import '../testSupport/registerDom.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createPinia } from 'pinia';
|
||||
import { createApp, defineComponent, h, nextTick } from 'vue';
|
||||
import { DataLayerManager } from '../data/manager.js';
|
||||
import { createDependencies } from '../layers/awareness/dependencies.js';
|
||||
import { CockpitCoordinator } from './cockpitCoordinator.js';
|
||||
import { ContextControls } from './contextControls.js';
|
||||
import { _handleContextLayerChange } from './contextLayerChanges.js';
|
||||
import { useCockpitStore } from './stores/cockpit.js';
|
||||
import { useHudStore } from './stores/hud.js';
|
||||
@@ -34,6 +38,8 @@ function fixture(
|
||||
tracked = null,
|
||||
contextMode = null,
|
||||
changing = false,
|
||||
dataManager: suppliedManager = null,
|
||||
readContext = null,
|
||||
} = {},
|
||||
) {
|
||||
document.body.replaceChildren();
|
||||
@@ -45,22 +51,29 @@ function fixture(
|
||||
app.mount(host);
|
||||
const store = useCockpitStore(pinia);
|
||||
const layers = new Set(enabled);
|
||||
const dataManager = { isEnabled: (layerId) => layers.has(layerId) };
|
||||
const dataManager = suppliedManager || {
|
||||
isEnabled: (layerId) => layers.has(layerId),
|
||||
};
|
||||
// A layer that is off has no tracked aircraft, as the flight layers clear
|
||||
// tracking on disable.
|
||||
const trackedFrom = (layerId) => () =>
|
||||
tracked?.layerId === layerId && layers.has(layerId) ? tracked.info : null;
|
||||
const entity = {
|
||||
tracked?.layerId === layerId && dataManager.isEnabled(layerId)
|
||||
? tracked.info
|
||||
: null;
|
||||
const entityFor = (subject) => ({
|
||||
position: { getValue: () => null },
|
||||
show: true,
|
||||
gevTrackedId: tracked ? `${tracked.layerId}:${tracked.info.icao24}` : '',
|
||||
};
|
||||
gevTrackedId: subject ? `${subject.layerId}:${subject.info.icao24}` : '',
|
||||
});
|
||||
const entities = new Set();
|
||||
const entity = entityFor(tracked);
|
||||
entities.add(entity);
|
||||
const listeners = () => ({ addEventListener: () => () => {} });
|
||||
const viewer = {
|
||||
trackedEntity: tracked ? entity : undefined,
|
||||
clock: { currentTime: null },
|
||||
camera: { cancelFlight() {}, frustum: { fov: 1, aspectRatio: 1 } },
|
||||
entities: { contains: (candidate) => candidate === entity },
|
||||
entities: { contains: (candidate) => entities.has(candidate) },
|
||||
scene: {
|
||||
preUpdate: listeners(),
|
||||
screenSpaceCameraController: { enableInputs: true },
|
||||
@@ -86,7 +99,7 @@ function fixture(
|
||||
tracked ? { layerId: tracked.layerId, id: tracked.info.icao24 } : null,
|
||||
},
|
||||
readDataManager: () => dataManager,
|
||||
readContext: () => context,
|
||||
readContext: readContext || (() => context),
|
||||
readActiveStyle: () => 'normal',
|
||||
readUtilityDisclosureOpen: () => false,
|
||||
enterPanels() {},
|
||||
@@ -125,8 +138,14 @@ function fixture(
|
||||
{ type: 'visibility', layerId, enabled: false, origin: 'user' },
|
||||
);
|
||||
};
|
||||
/** Track another aircraft, as Contacts navigation does mid-session. */
|
||||
const retrack = (subject) => {
|
||||
tracked = subject;
|
||||
viewer.trackedEntity = entityFor(subject);
|
||||
entities.add(viewer.trackedEntity);
|
||||
};
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
return { $, coordinator, cockpit, store, layers, disable, pinia };
|
||||
return { $, coordinator, cockpit, store, layers, disable, retrack, pinia };
|
||||
}
|
||||
|
||||
test('a civilian aircraft enters with Live Flights alone: no Contacts, no Military', (t) => {
|
||||
@@ -232,3 +251,217 @@ test('the readout offers the same entry, labelled by kind, and enters on click',
|
||||
assert.equal(cockpit.active, true);
|
||||
assert.equal(readoutEntry.hidden, true, 'inside, there is nothing to enter');
|
||||
});
|
||||
|
||||
const AIR_AND_SEA = [
|
||||
'flights',
|
||||
'military',
|
||||
'ais-live-vessels',
|
||||
'military-installations',
|
||||
];
|
||||
|
||||
/** A source layer that settles at once, as the data manager drives it. */
|
||||
function sourceLayer(id) {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
icon: '',
|
||||
source: 'test',
|
||||
updateInterval: -1,
|
||||
init() {},
|
||||
enable() {},
|
||||
disable() {},
|
||||
update() {},
|
||||
getStats: () => ({ count: 0, lastUpdate: null }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Contacts as the manager sees it: a coordinator whose real dependency
|
||||
* ownership (src/layers/awareness/dependencies.js) turns on the air and sea
|
||||
* layers it finds off and releases them when it goes off.
|
||||
*/
|
||||
function contactsCoordinator() {
|
||||
const state = {
|
||||
dataManager: null,
|
||||
enabled: false,
|
||||
passive: true,
|
||||
activationId: 0,
|
||||
ownedDependencies: new Set(),
|
||||
subject: null,
|
||||
autoFocusRetryPending: false,
|
||||
};
|
||||
const dependencies = createDependencies({
|
||||
state,
|
||||
services: {},
|
||||
parts: { subject: {}, focus: {} },
|
||||
source: null,
|
||||
});
|
||||
return {
|
||||
id: 'military-awareness',
|
||||
name: 'Contacts',
|
||||
icon: '',
|
||||
source: 'test',
|
||||
showInTogglePanel: false,
|
||||
updateInterval: -1,
|
||||
attachDataManager(dataManager) {
|
||||
state.dataManager = dataManager;
|
||||
dependencies.watchAdoptions(dataManager);
|
||||
},
|
||||
setParams(params = {}) {
|
||||
if (typeof params.passive === 'boolean') state.passive = params.passive;
|
||||
},
|
||||
getParams: () => ({ passive: state.passive }),
|
||||
init() {},
|
||||
enable() {
|
||||
state.enabled = true;
|
||||
if (!state.passive) return dependencies.enableDependencies();
|
||||
return true;
|
||||
},
|
||||
disable() {
|
||||
state.enabled = false;
|
||||
state.passive = true;
|
||||
return dependencies.releaseOwnedDependencies(++state.activationId);
|
||||
},
|
||||
update() {},
|
||||
getStats: () => ({ count: 0, lastUpdate: null }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The Cockpit beside the real Context owner and data manager, so a Contacts
|
||||
* exit runs its whole restore: the coordinator's releases, then the snapshot.
|
||||
*/
|
||||
async function liveFixture(t, { enabled = [], tracked = null } = {}) {
|
||||
const manager = new DataLayerManager({});
|
||||
for (const layerId of AIR_AND_SEA) manager.register(sourceLayer(layerId));
|
||||
const coordinatorModule = contactsCoordinator();
|
||||
manager.register(coordinatorModule);
|
||||
coordinatorModule.attachDataManager(manager);
|
||||
for (const layerId of enabled) await manager.setEnabled(layerId, true);
|
||||
let cockpit = null;
|
||||
const controls = new ContextControls({
|
||||
elements: {},
|
||||
installations: {},
|
||||
actions: {
|
||||
getCockpit: () => cockpit,
|
||||
claimVisualAuthority() {},
|
||||
showToast() {},
|
||||
setClearBusy() {},
|
||||
syncDetection() {},
|
||||
scheduleLayout() {},
|
||||
refreshRadio() {},
|
||||
setPanelCollapsed() {},
|
||||
},
|
||||
});
|
||||
controls.connect(manager);
|
||||
t.after(() => {
|
||||
controls.stop();
|
||||
controls.disconnect();
|
||||
});
|
||||
const base = fixture(t, {
|
||||
tracked,
|
||||
dataManager: manager,
|
||||
readContext: () => controls.getContextModeState(),
|
||||
});
|
||||
cockpit = base.cockpit;
|
||||
/** Let every Context reaction the last change started settle. */
|
||||
const settle = async () => {
|
||||
for (let turn = 0; turn < 5; turn += 1) {
|
||||
await controls._waitForContextLayerSettlement();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
};
|
||||
return { ...base, manager, controls, settle };
|
||||
}
|
||||
|
||||
const on = (manager) =>
|
||||
[...AIR_AND_SEA, 'military-awareness'].filter((layerId) =>
|
||||
manager.isEnabled(layerId),
|
||||
);
|
||||
|
||||
test('Contacts ending in a Cockpit session keeps the ridden layer it had turned on', async (t) => {
|
||||
for (const [label, endContacts] of [
|
||||
[
|
||||
'Military Flights off',
|
||||
({ manager }) =>
|
||||
manager.setEnabled('military', false, { origin: 'user' }),
|
||||
],
|
||||
[
|
||||
'Contacts off by its own layer',
|
||||
({ manager }) =>
|
||||
manager.setEnabled('military-awareness', false, { origin: 'user' }),
|
||||
],
|
||||
[
|
||||
'Contacts off from its chooser',
|
||||
({ controls }) => controls._selectContextMode(null),
|
||||
],
|
||||
]) {
|
||||
await t.test(label, async (t) => {
|
||||
// Live Flights is off on the map; Contacts turns it on as its own.
|
||||
const live = await liveFixture(t, {
|
||||
tracked: { layerId: 'flights', info: CIVIL },
|
||||
});
|
||||
const { manager, controls, cockpit, settle } = live;
|
||||
assert.equal(await controls._selectContextMode('flights'), true);
|
||||
await settle();
|
||||
assert.deepEqual(on(manager), [...AIR_AND_SEA, 'military-awareness']);
|
||||
assert.equal(cockpit.enter(), true);
|
||||
await endContacts(live);
|
||||
await settle();
|
||||
assert.equal(controls.getContextModeState().mode, null, 'Contacts ended');
|
||||
assert.equal(cockpit.active, true, 'the Cockpit rides on');
|
||||
assert.deepEqual(
|
||||
on(manager),
|
||||
['flights'],
|
||||
'only the ridden layer outlives the pre-Contacts restore',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('a ride handed to an aircraft on a layer Contacts turned on keeps that layer', async (t) => {
|
||||
const live = await liveFixture(t, {
|
||||
enabled: ['flights'],
|
||||
tracked: { layerId: 'flights', info: CIVIL },
|
||||
});
|
||||
const { manager, controls, cockpit, retrack, settle } = live;
|
||||
assert.equal(await controls._selectContextMode('flights'), true);
|
||||
await settle();
|
||||
assert.equal(cockpit.enter(), true);
|
||||
// NEXT hands the view to a military aircraft; Contacts turned its layer on.
|
||||
retrack({ layerId: 'military', info: MILITARY });
|
||||
assert.equal(cockpit._adoptTrackedEntity(performance.now()), true);
|
||||
assert.equal(cockpit.readSubjectLayerId(), 'military');
|
||||
await manager.setEnabled('ais-live-vessels', false, { origin: 'user' });
|
||||
await settle();
|
||||
assert.equal(controls.getContextModeState().mode, null);
|
||||
assert.equal(cockpit.active, true);
|
||||
assert.deepEqual(on(manager), ['flights', 'military']);
|
||||
});
|
||||
|
||||
test('the ridden layer still ends the Cockpit while Contacts is on', async (t) => {
|
||||
const live = await liveFixture(t, {
|
||||
tracked: { layerId: 'flights', info: CIVIL },
|
||||
});
|
||||
const { manager, controls, cockpit, settle } = live;
|
||||
assert.equal(await controls._selectContextMode('flights'), true);
|
||||
await settle();
|
||||
assert.equal(cockpit.enter(), true);
|
||||
await manager.setEnabled('flights', false, { origin: 'user' });
|
||||
await settle();
|
||||
assert.equal(cockpit.active, false);
|
||||
assert.equal(manager.isEnabled('flights'), false);
|
||||
});
|
||||
|
||||
test('without a Cockpit, ending Contacts restores the layers it found', async (t) => {
|
||||
const live = await liveFixture(t, {
|
||||
tracked: { layerId: 'flights', info: CIVIL },
|
||||
});
|
||||
const { manager, controls, settle } = live;
|
||||
assert.equal(await controls._selectContextMode('flights'), true);
|
||||
await settle();
|
||||
await manager.setEnabled('military', false, { origin: 'user' });
|
||||
await settle();
|
||||
assert.equal(controls.getContextModeState().mode, null);
|
||||
assert.deepEqual(on(manager), [], 'Live Flights goes back off');
|
||||
});
|
||||
@@ -74,6 +74,27 @@ export function dropCockpitSignal(key) {
|
||||
this.publishCockpitSignals();
|
||||
}
|
||||
|
||||
/** Whether a row is one a Contacts snapshot builds, with its status rows. */
|
||||
const isContextSnapshotRow = (key) =>
|
||||
key.startsWith('flight:') ||
|
||||
['input-status', 'feed-status', 'context-status'].includes(key);
|
||||
|
||||
/**
|
||||
* Take off every row a Contacts snapshot built, as when Contacts goes off:
|
||||
* its contacts, distances and CURRENT would otherwise read as live. Rows the
|
||||
* session made without Contacts, such as TRACK ACQUIRED, stay.
|
||||
*/
|
||||
export function dropContextSnapshotSignals() {
|
||||
for (const key of [...this.signalSignatures.keys()])
|
||||
if (isContextSnapshotRow(key)) this.signalSignatures.delete(key);
|
||||
const kept = this.signalItems.filter(
|
||||
(item) => !isContextSnapshotRow(item.key),
|
||||
);
|
||||
if (kept.length === this.signalItems.length) return;
|
||||
this.signalItems = kept;
|
||||
this.publishCockpitSignals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the rows from a Contacts snapshot: the nearest flights, a row for
|
||||
* unknown inputs and, first, a row naming an air feed that is off.
|
||||
|
||||
@@ -155,6 +155,7 @@ export function _adoptTrackedEntity(nowMs, suppliedInfo = null) {
|
||||
this.lastContextUpdateMs = 0;
|
||||
this.lastCameraUpdateMs = 0;
|
||||
this.contextNavigationDeadlineMs = 0;
|
||||
this.onRide?.(info.layerId);
|
||||
this.dispatchCockpitModeChanged(true, info);
|
||||
return true;
|
||||
}
|
||||
@@ -230,6 +231,7 @@ export function enter() {
|
||||
this.mapViewButton?.focus({ preventScroll: true });
|
||||
});
|
||||
this.onEntered?.();
|
||||
this.onRide?.(info.layerId);
|
||||
this.dispatchCockpitModeChanged(true, info);
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user