mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 07:12:21 +00:00
fix(cockpit): entry to the built-in views is as before: aircraft, satellite and vessel keep their fixed order whenever each was selected, and an application's subject takes entry only when it was selected after every built-in one present, so without providers nothing changes
This commit is contained in:
1 parent
6fbc98952f
commit
a4f321a9ef
6 files changed
+119
-40
No files matched your search
@@ -6,6 +6,7 @@
|
||||
under reduced motion) and a bottom progress line. The Rail and Transit
|
||||
layers expose read and hold APIs so a selected train or vehicle can be
|
||||
ridden, and `./layers/rail/paths` is exported; `docs/APPLICATION.md` shows
|
||||
a minimal provider. GEV itself registers none. Cockpit entry now goes to
|
||||
the most recently selected subject, so a vessel selected after an aircraft
|
||||
is tracked enters BRIDGE (Denis Groshev, @ZloyS, #1686).
|
||||
a minimal provider. GEV itself registers none, and entry to the built-in
|
||||
aircraft, satellite and vessel views is unchanged; a provider's subject
|
||||
takes entry when it was selected after them (Denis Groshev, @ZloyS,
|
||||
#1686).
|
||||
+14
-13
@@ -1696,9 +1696,7 @@ and portal cleanup follows that restoration.
|
||||
|
||||
### Ship bridge view
|
||||
|
||||
Cockpit can also ride the selected AIS vessel; when other subjects are
|
||||
selected too, entry takes the most recently selected one (see Application
|
||||
subjects below).
|
||||
With no aircraft tracked, Cockpit can ride the selected AIS vessel instead.
|
||||
Entry needs only the layer of the vessel it rides, Live AIS Vessels, and a
|
||||
selected vessel; Contacts and every other layer may be off, before or during
|
||||
the bridge (`bridgeEntryAllowed` in `src/contextModePolicy.js`). A Context mode
|
||||
@@ -1784,9 +1782,9 @@ satellite instead. Entry needs only the Satellites layer
|
||||
layer may be off. A Context mode change in flight blocks entry until it
|
||||
settles. Debris (rocket bodies, fragments and dead satellites; see the
|
||||
satellite class toggles) can be tracked but never entered: no ORBIT entry is
|
||||
offered and double-click refuses it. When an aircraft or a vessel is also
|
||||
tracked or selected, entry takes the most recently selected subject (see
|
||||
Application subjects below). Double-clicking the tracked satellite
|
||||
offered and double-click refuses it. A tracked aircraft still takes
|
||||
entry first, and a tracked satellite
|
||||
takes it before a selected vessel. Double-clicking the tracked satellite
|
||||
enters it, and the entry button reads
|
||||
ORBIT with a satellite icon in Contacts (`#cockpit-entry`) and under the Intel
|
||||
HUD readout (`#cockpit-entry-readout`). Voice (`control_cockpit`) stays
|
||||
@@ -2001,13 +1999,16 @@ layer on, no Context change in flight and the provider's own
|
||||
`isEntryAllowed(candidate)`; voice stays aircraft-only and is refused, with
|
||||
the reason, while a provider's view is in use.
|
||||
|
||||
Entry rides the most recently selected subject of all kinds: a tracked
|
||||
aircraft, the tracked satellite, the selected vessel or a provider's
|
||||
candidate, by when the Cockpit first saw it, or saw it selected again (a
|
||||
provider candidate's `selectedAt` changing); subjects first seen together keep
|
||||
the fixed order (aircraft, satellite, vessel, then providers by priority). The
|
||||
newest decides alone: when its gate is shut there is no entry rather than an
|
||||
older subject's.
|
||||
The built-in kinds keep their fixed entry order (a tracked aircraft, then the
|
||||
tracked satellite, then the selected vessel), so without providers entry is
|
||||
unchanged. A provider's candidate takes entry only when it was selected after
|
||||
every built-in subject present, by when the Cockpit first saw it or saw it
|
||||
selected again (its `selectedAt` changing): a train picked after an aircraft
|
||||
was tracked takes entry, and an aircraft tracked after it takes it back.
|
||||
Providers' candidates are ordered most recently selected first, then by
|
||||
priority; one first seen together with a built-in subject comes after it. The
|
||||
first in that order decides alone: when its gate is shut there is no entry
|
||||
rather than the next subject's.
|
||||
|
||||
A frame places the eye `eye.upM` above `heightM` (the shared floor where the
|
||||
frame gives none, or where it last stood while that floor is cold; with
|
||||
|
||||
@@ -273,8 +273,8 @@ export function cockpitSubjectProviders(
|
||||
* A provider's candidate: `{id, name, face, selectedAt}`, or null without a
|
||||
* usable id. `name` falls back to the id; `face` is null unless well formed;
|
||||
* `selectedAt` (a number or a short string, else null) is a token the
|
||||
* provider changes each time its subject is selected, even again: entry
|
||||
* takes the most recently selected subject (entryCandidates).
|
||||
* provider changes each time its subject is selected, even again: a
|
||||
* subject selected after the built-in ones takes entry (entryCandidates).
|
||||
* @param {unknown} raw What `candidate()` returned.
|
||||
*/
|
||||
export function cockpitSubjectCandidate(raw) {
|
||||
|
||||
@@ -603,7 +603,7 @@ test('the readout entry reads BRIDGE for a selected vessel', async (t) => {
|
||||
assert.match(readoutEntry.textContent, /BRIDGE/);
|
||||
});
|
||||
|
||||
test('without providers too, the later selection takes entry: a vessel after an aircraft reads BRIDGE', async (t) => {
|
||||
test('without providers the built-in order decides: a vessel selected after an aircraft still reads COCKPIT', async (t) => {
|
||||
const { $, cockpit, store, selectVessel } = fixture(t, {
|
||||
enabled: ['flights', 'ais-live-vessels'],
|
||||
tracked: { layerId: 'flights', info: CIVIL },
|
||||
@@ -614,8 +614,8 @@ test('without providers too, the later selection takes entry: a vessel after an
|
||||
selectVessel(VESSEL);
|
||||
cockpit.syncEntry();
|
||||
await nextTick();
|
||||
assert.equal(store.presence.entryKind, 'vessel');
|
||||
assert.match($('#cockpit-entry-readout').textContent, /BRIDGE/);
|
||||
assert.equal(store.presence.entryKind, 'aircraft');
|
||||
assert.match($('#cockpit-entry-readout').textContent, /COCKPIT/);
|
||||
// And the other way round: an aircraft tracked after the vessel.
|
||||
const later = fixture(t, {
|
||||
enabled: ['flights', 'ais-live-vessels'],
|
||||
|
||||
@@ -158,6 +158,7 @@ function fixture(t, { providers = [], vessel = null, layerOn = true } = {}) {
|
||||
trackedEntityChanged: listeners(),
|
||||
};
|
||||
let vesselSelected = vessel;
|
||||
let trackedAircraftInfo = null;
|
||||
let floorM = FLOOR_M;
|
||||
const warned = [];
|
||||
t.mock.method(console, 'warn', (message) => warned.push(String(message)));
|
||||
@@ -168,7 +169,7 @@ function fixture(t, { providers = [], vessel = null, layerOn = true } = {}) {
|
||||
const dropped = [];
|
||||
const cockpit = new CockpitViewController(viewer, {
|
||||
services: {
|
||||
flightsLayer: { getTrackedInfo: () => null },
|
||||
flightsLayer: { getTrackedInfo: () => trackedAircraftInfo },
|
||||
militaryFlightsLayer: { getTrackedInfo: () => null },
|
||||
aisLiveVesselsLayer: {
|
||||
getCockpitSubject: () =>
|
||||
@@ -235,6 +236,11 @@ function fixture(t, { providers = [], vessel = null, layerOn = true } = {}) {
|
||||
selectVessel(record) {
|
||||
vesselSelected = record;
|
||||
},
|
||||
/** Track the fixture's aircraft as Live Flights does, or untrack it. */
|
||||
trackAircraft(info) {
|
||||
trackedAircraftInfo = info;
|
||||
viewer.trackedEntity = info ? aircraft : undefined;
|
||||
},
|
||||
setFloor(value) {
|
||||
floorM = value;
|
||||
},
|
||||
@@ -283,6 +289,15 @@ const VESSEL = Object.freeze({
|
||||
missedRefreshes: 0,
|
||||
});
|
||||
|
||||
/** The fixture's aircraft as Live Flights reports it once tracked. */
|
||||
const TRACKED_AIRCRAFT = Object.freeze({
|
||||
icao24: 'abc123',
|
||||
callsign: 'UAL123',
|
||||
track: 92,
|
||||
latitude: LAT,
|
||||
longitude: LON,
|
||||
});
|
||||
|
||||
test('a provider candidate offers its own entry face; a shut gate hides it', async (t) => {
|
||||
const ride = subjectProvider();
|
||||
const { $, cockpit, store } = fixture(t, { providers: [ride] });
|
||||
@@ -869,6 +884,51 @@ test('the most recently selected subject wins entry', async (t) => {
|
||||
assert.equal(cockpit.enter(), false);
|
||||
});
|
||||
|
||||
test('the built-in kinds keep their order; a subject takes entry only when selected after all of them', async (t) => {
|
||||
const clock = manualClock(t, 10_000);
|
||||
const ride = subjectProvider();
|
||||
ride.subject.candidate = null;
|
||||
const { cockpit, selectVessel, trackAircraft } = fixture(t, {
|
||||
providers: [ride],
|
||||
});
|
||||
trackAircraft(TRACKED_AIRCRAFT);
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'aircraft');
|
||||
// A subject selected after the aircraft was tracked takes entry.
|
||||
clock.advance(500);
|
||||
ride.subject.candidate = { id: 'a1', name: 'Example 12' };
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'example-kind');
|
||||
// A vessel selected after it: a built-in kind is newer, so the built-in
|
||||
// order decides, as without providers, and the aircraft comes first.
|
||||
clock.advance(500);
|
||||
selectVessel(VESSEL);
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'aircraft');
|
||||
// The subject selected again is newer than every built-in one.
|
||||
clock.advance(500);
|
||||
ride.subject.candidate = { id: 'a1', name: 'Example 12', selectedAt: 2 };
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'example-kind');
|
||||
// An aircraft tracked after it takes entry back.
|
||||
selectVessel(null);
|
||||
trackAircraft(null);
|
||||
cockpit.syncEntry();
|
||||
clock.advance(500);
|
||||
trackAircraft(TRACKED_AIRCRAFT);
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'aircraft');
|
||||
});
|
||||
|
||||
test('a subject first seen together with a built-in one comes after it', async (t) => {
|
||||
manualClock(t, 10_000);
|
||||
const ride = subjectProvider();
|
||||
const { cockpit, store } = fixture(t, { providers: [ride], vessel: VESSEL });
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'vessel');
|
||||
assert.equal(store.presence.entryKind, 'vessel');
|
||||
});
|
||||
|
||||
test('a provider view with look turns with the arrow keys and words its readout', async (t) => {
|
||||
const clock = manualClock(t, 10_000);
|
||||
const ride = subjectProvider();
|
||||
|
||||
@@ -93,7 +93,7 @@ export function subjectInfo(frame) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Entry's fixed order, for candidates first seen at the same moment. */
|
||||
/** The built-in kinds' fixed entry order. */
|
||||
const BUILT_IN_ENTRY_ORDER = Object.freeze({
|
||||
aircraft: 0,
|
||||
satellite: 1,
|
||||
@@ -101,14 +101,19 @@ const BUILT_IN_ENTRY_ORDER = Object.freeze({
|
||||
});
|
||||
|
||||
/**
|
||||
* Every subject entry could ride now, the most recently selected first: a
|
||||
* tracked aircraft, the tracked satellite, the selected vessel and each
|
||||
* provider's candidate. A candidate's time is when this controller first saw
|
||||
* it (it looks on its entry cadence and on tracking changes), or saw it
|
||||
* selected again: a provider candidate whose `selectedAt` changes, the same
|
||||
* subject clicked again, moves to the front. Candidates first seen together
|
||||
* keep the fixed order (aircraft, satellite, vessel, then providers by
|
||||
* priority). Each carries `allowed()`, its kind's entry gate.
|
||||
* Every subject entry could ride now, in entry order: a tracked aircraft,
|
||||
* the tracked satellite, the selected vessel and each provider's candidate.
|
||||
* The built-in kinds keep their fixed order (aircraft, satellite, vessel),
|
||||
* whenever each was selected. A provider's candidate goes ahead of them only
|
||||
* when it was selected after every built-in one present: a train picked
|
||||
* after an aircraft was tracked takes entry, an aircraft tracked after it
|
||||
* takes it back. Providers' candidates are ordered most recently selected
|
||||
* first, then by priority. A candidate's time is when this controller first
|
||||
* saw it (it looks on its entry cadence and on tracking changes), or saw it
|
||||
* selected again (a provider candidate whose `selectedAt` changes). One
|
||||
* first seen together with a built-in candidate comes after it. With no
|
||||
* provider, entry is the built-in order alone. Each carries `allowed()`, its
|
||||
* kind's entry gate.
|
||||
* @param {number} [nowMs]
|
||||
* @returns {Array<{kind: string, id: string, seenAtMs: number, order: number,
|
||||
* allowed: () => boolean, provider?: object, candidate?: object}>}
|
||||
@@ -167,13 +172,25 @@ export function entryCandidates(nowMs = performance.now()) {
|
||||
// A subject gone and back counts as selected again.
|
||||
for (const kind of [...seen.keys()])
|
||||
if (!list.some((entry) => entry.kind === kind)) seen.delete(kind);
|
||||
return list.sort((a, b) => b.seenAtMs - a.seenAtMs || a.order - b.order);
|
||||
const builtIns = list
|
||||
.filter((entry) => !entry.provider)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
const provided = list
|
||||
.filter((entry) => entry.provider)
|
||||
.sort((a, b) => b.seenAtMs - a.seenAtMs || a.order - b.order);
|
||||
const builtInSeenAtMs = Math.max(
|
||||
-Infinity,
|
||||
...builtIns.map((entry) => entry.seenAtMs),
|
||||
);
|
||||
const later = provided.filter((entry) => entry.seenAtMs > builtInSeenAtMs);
|
||||
const earlier = provided.filter((entry) => entry.seenAtMs <= builtInSeenAtMs);
|
||||
return [...later, ...builtIns, ...earlier];
|
||||
}
|
||||
|
||||
/**
|
||||
* The subject entry would ride: the most recently selected one. It decides
|
||||
* alone, so when its own gate is shut entry is unavailable rather than taken
|
||||
* by an older selection.
|
||||
* The subject entry would ride: the first in entry order (entryCandidates).
|
||||
* It decides alone, so when its own gate is shut entry is unavailable rather
|
||||
* than taken by the next one.
|
||||
* @returns {object|null} An entry of entryCandidates, or null.
|
||||
*/
|
||||
export function resolveEntry(nowMs = performance.now()) {
|
||||
@@ -199,9 +216,9 @@ export function readSubjectLayerId() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind entry would take: the most recently selected subject's
|
||||
* (resolveEntry): a Cesium-tracked aircraft, the tracked satellite, the
|
||||
* selected vessel or a provider's candidate.
|
||||
* The kind entry would take, the first in entry order's (resolveEntry): a
|
||||
* Cesium-tracked aircraft, the tracked satellite, the selected vessel or a
|
||||
* provider's candidate.
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function resolveEntryKind() {
|
||||
@@ -296,7 +313,7 @@ export function syncEntry() {
|
||||
const candidates = this.entryCandidates(performance.now());
|
||||
const tracked = candidates.some(({ kind }) => kind === 'aircraft');
|
||||
this.syncTr3bToggle(tracked ? this.readAircraftInfo() : null);
|
||||
// The most recently selected subject decides (resolveEntry).
|
||||
// The first in entry order decides alone (resolveEntry).
|
||||
const chosen = candidates[0] ?? null;
|
||||
const entryKind = chosen?.allowed() ? chosen.kind : null;
|
||||
const face =
|
||||
@@ -518,7 +535,7 @@ function enterSubject(provider) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter first person on the most recently selected subject (resolveEntry): a
|
||||
* Enter first person on the first subject in entry order (resolveEntry): a
|
||||
* tracked aircraft, the tracked satellite's orbital view, the selected
|
||||
* vessel's bridge or an application provider's subject.
|
||||
* @param {{kind?: string|null}} [options] Restrict entry to one subject kind;
|
||||
|
||||
Reference in new issue
Block a user