Keep tracked-entity state in Core records instead of on Cesium entities

Flights, military, satellites, installations, local ADS-B and ALPR hung
gevTrackedId, gevDisplayPosition, gevVisualPosition, gevSelectionOrigin and
gevLabelModel on the Cesium entities they draw, and the tracked readout,
follow camera, cockpit, share links and awareness read them back. They now
live in src/data/entityRecords.js, a Core-owned record per entity: layers
write entityRecord(entity), readers use readEntityRecord(entity). Nothing
changes on screen. This is the first slice of the renderer plan's state
mirror (P1).

The module is exported as ./data/entity-records so hosted code can read the
same records, and window.__godsEyeView.entityRecords gives the QA scripts
the same access from the page.
This commit is contained in:
Sameh Khamis committed 2026-11-03 08:09:00 -08:00
1 parent ea69bb4ce2
commit 95c0070412
53 files changed
+408 -291

No files matched your search

+6 -4
View File
@@ -3513,7 +3513,7 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
> ambient-label host source. Its getter reads the already-propagated point
> cache, preserving the 1 Hz fleet epoch and eliminating the former second ISS
> propagation. Satellite tracking continues to publish exactly one protected
> tracked-lane card through `gevLabelModel` and `_trackedDisplayCached`; the
> tracked-lane card through its entity record's `labelModel` and `_trackedDisplayCached`; the
> tracked entity is point-only. Tracking ISS suppresses the ambient entry and
> untracking restores it, preventing duplicate ISS text. Disable, orbit-text
> preference changes, catalog rebuild, and destroy clear/hide the source.
@@ -3528,7 +3528,7 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
> - **Phase 5 tracked civil aircraft label:** A tracked flight's Cesium entity
> is a billboard-only camera target with no native label. The flight source
> retains callsign/registration fallback, flight-level/altitude, speed, stale
> state, airline/type, and plausible-route formatting in `gevLabelModel`; the
> state, airline/type, and plausible-route formatting in the entity record's `labelModel`; the
> one protected tracked-lane host card renders that complete model. Its
> position getter reads only `_trackedDisplayCached`, the same authoritative
> Cartesian already consumed by the tracked visual and camera, and never
@@ -3635,8 +3635,10 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
> - **Tracked-readout ownership:** `trackedReadout.js` is now a presentation-model
> bridge only; it owns no canvas, projection, post-render listener, layout,
> keyhole fade, or paint path. Civilian flights, military flights, satellites,
> and mapped installations write explicit `gevLabelModel` objects and expose
> `gevDisplayPosition` getters backed by their layer-owned frame/display cache.
> and mapped installations write explicit `labelModel` objects and
> `displayPosition` getters, backed by their layer-owned frame/display cache,
> into Core's entity records (`src/data/entityRecords.js`), never onto the
> Cesium entity.
> Selected AIS vessels continue using the vessel source's protected selected
> card, so they do not create a duplicate tracked readout. The host registers
> the active readout in the protected tracked lane with a zero ambient quota;
+1
View File
@@ -227,6 +227,7 @@
"./standalone/catalog": "./src/standalone/catalog.js",
"./application/layers": "./src/app/constructCatalog.js",
"./application/operations": "./src/app/operations.js",
"./data/entity-records": "./src/data/entityRecords.js",
"./data/lifecycle": "./src/data/lifecycle.js",
"./ui/navigation": "./src/ui/navigationController.js",
"./ui/share-restoration": "./src/ui/shareRestoration.js",
+27 -11
View File
@@ -871,7 +871,8 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js"
"src/ui/unitLabels.js",
"src/data/entityRecords.js"
],
"external": ["cesium"]
},
@@ -1124,7 +1125,8 @@
"src/ui/kit/UiBadge.vue",
"src/ui/stores/globeActions.js",
"src/ui/stores/recording.js",
"src/ui/i18n.js"
"src/ui/i18n.js",
"src/data/entityRecords.js"
],
"external": ["cesium", "vue", "pinia", "vue-i18n"]
},
@@ -1335,7 +1337,8 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js"
"src/ui/unitLabels.js",
"src/data/entityRecords.js"
],
"external": ["cesium"]
},
@@ -1391,7 +1394,8 @@
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json"
"src/i18n/locales/zh-Hant.json",
"src/data/entityRecords.js"
],
"external": ["cesium"]
},
@@ -1576,7 +1580,8 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js"
"src/ui/unitLabels.js",
"src/data/entityRecords.js"
],
"external": ["cesium", "satellite.js"]
},
@@ -1667,7 +1672,8 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/layers/installations/classLabel.js"
"src/layers/installations/classLabel.js",
"src/data/entityRecords.js"
],
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
},
@@ -1713,7 +1719,8 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js"
"src/ui/unitLabels.js",
"src/data/entityRecords.js"
],
"external": ["cesium"]
},
@@ -1973,7 +1980,8 @@
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json"
"src/i18n/locales/zh-Hant.json",
"src/data/entityRecords.js"
],
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
},
@@ -3184,7 +3192,8 @@
"src/data/placeBoundaries.js",
"src/data/placeNames.js",
"src/data/local_data/us_census_places/files.js",
"src/data/local_data/wof_neighborhoods/files.js"
"src/data/local_data/wof_neighborhoods/files.js",
"src/data/entityRecords.js"
],
"external": [
"hls.js",
@@ -3792,7 +3801,8 @@
"src/data/placeBoundaries.js",
"src/data/placeNames.js",
"src/data/local_data/us_census_places/files.js",
"src/data/local_data/wof_neighborhoods/files.js"
"src/data/local_data/wof_neighborhoods/files.js",
"src/data/entityRecords.js"
],
"external": [
"hls.js",
@@ -3890,6 +3900,11 @@
"mgrs"
]
},
"entity-records": {
"exports": ["./data/entity-records"],
"modules": ["src/data/entityRecords.js"],
"external": []
},
"layer-lifecycle": {
"exports": ["./data/lifecycle"],
"modules": ["src/data/lifecycle.js", "src/data/cameraArrival.js"],
@@ -3944,7 +3959,8 @@
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json"
"src/i18n/locales/zh-Hant.json",
"src/data/entityRecords.js"
],
"external": []
},
+2 -2
View File
@@ -763,7 +763,7 @@ try {
} : null,
navigation: snapshot?.navigation || null,
afterId: trackedInfo?.icao24 || null,
viewerTrackedId: viewer.trackedEntity?.gevTrackedId || null,
viewerTrackedId: window.__godsEyeView.entityRecords.read(viewer.trackedEntity)?.trackedId || null,
contextVisible: !document.getElementById('military-awareness-panel')?.hidden,
entryAvailableAfterSettlement: Boolean(entry && !entry.hidden),
blockerStillRegistered: dataManager.layers.has(blockerId),
@@ -1362,7 +1362,7 @@ try {
contextMode: styleManager._contextControls._contextMode,
contextChanging: styleManager._contextControls._contextModeChanging,
subject: context?.subject ? `${context.subject.layerId}:${context.subject.id}` : null,
tracked: tracker?.gevTrackedId || null,
tracked: window.__godsEyeView.entityRecords.read(tracker)?.trackedId || null,
density: styleManager.getDetectionState().densityPct,
detectionMode: styleManager.getDetectionState().detectionMode,
installations: dataManager.getLayerLifecycleState('military-installations'),
+6 -3
View File
@@ -360,7 +360,9 @@ async function contacts() {
entities: ds?.entities.values.length ?? 0,
created,
tracked:
window.__godsEyeView.viewer.trackedEntity?.gevTrackedId ?? null,
window.__godsEyeView.entityRecords.read(
window.__godsEyeView.viewer.trackedEntity,
)?.trackedId ?? null,
cockpit: document.body.classList.contains('cockpit-mode'),
});
if (samples.length > 5000) samples.splice(0, 1000);
@@ -375,8 +377,9 @@ async function contacts() {
await page
.waitForFunction(
() =>
window.__godsEyeView.viewer.trackedEntity?.gevTrackedId ===
'flights:aaa051',
window.__godsEyeView.entityRecords.read(
window.__godsEyeView.viewer.trackedEntity,
)?.trackedId === 'flights:aaa051',
{ timeout: 60_000 },
)
.catch(() => {});
+3 -3
View File
@@ -169,8 +169,8 @@ async function observeTrackedHostPaint(page, timeoutMs = 5000) {
const tracked = viewer?.trackedEntity;
const current = {
elapsedMs: Math.round(performance.now() - startedAt),
entryId: tracked?.gevTrackedId || null,
hasPresentationModel: Boolean(tracked?.gevLabelModel?.title),
entryId: window.__godsEyeView?.entityRecords?.read(tracked)?.trackedId || null,
hasPresentationModel: Boolean(window.__godsEyeView?.entityRecords?.read(tracked)?.labelModel?.title),
candidateCount: diagnostics.candidateCount || 0,
projectedCount: diagnostics.projectedCount || 0,
selectedCount: diagnostics.selectedCount || 0,
@@ -610,7 +610,7 @@ async function main() {
if (!pool.length) return null;
const ent = v.trackedEntity;
let tracked = null;
if (ent && typeof ent.gevDisplayPosition === 'function') tracked = ent.gevDisplayPosition();
if (ent && typeof window.__godsEyeView.entityRecords.read(ent)?.displayPosition === 'function') tracked = window.__godsEyeView.entityRecords.read(ent)?.displayPosition();
if (!tracked) return pool[0];
let best = null, bestD = Infinity;
for (const m of pool) {
+1 -1
View File
@@ -333,7 +333,7 @@ export async function checkInstallationSelection(
for (let y = 250; y < 650; y += 75)
for (let x = 400; x < 850; x += 75) {
const pick = g.viewer.scene.pick({ x, y });
if (!pick?.id?.installationId && !pick?.id?.gevTrackedId)
if (!pick?.id?.installationId && !window.__godsEyeView.entityRecords.read(pick?.id)?.trackedId)
return { x, y };
}
throw new Error('No empty map point');
+6 -6
View File
@@ -3817,7 +3817,7 @@ async function main() {
id: `qa:${ownerPrefix}:radio-camera-owner`,
position: viewer.camera.positionWC.clone(),
});
owner.gevTrackedId = `${ownerPrefix}:qa-radio-camera-owner`;
window.__godsEyeView.entityRecords.write(owner).trackedId = `${ownerPrefix}:qa-radio-camera-owner`;
viewer.trackedEntity = owner;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const beforeStationId = radio.getUIState().selected?.id || null;
@@ -3831,7 +3831,7 @@ async function main() {
afterStationId: radio.getUIState().selected?.id || null,
flyToDelta: window.__qaRadioFlyToCalls.length - beforeFlyToCalls,
trackedIdentityPreserved: viewer.trackedEntity === owner,
trackedId: viewer.trackedEntity?.gevTrackedId || null,
trackedId: window.__godsEyeView.entityRecords.read(viewer.trackedEntity)?.trackedId || null,
};
viewer.trackedEntity = undefined;
viewer.entities.remove(owner);
@@ -3867,11 +3867,11 @@ async function main() {
id: 'qa:flights:delayed-radio-fallback-owner',
position: viewer.camera.positionWC.clone(),
});
owner.gevTrackedId = 'flights:qa-delayed-radio-fallback-owner';
window.__godsEyeView.entityRecords.write(owner).trackedId = 'flights:qa-delayed-radio-fallback-owner';
viewer.trackedEntity = owner;
window.__qaDelayedRadioTrackingOwner = owner;
window.__qaRejectRadioPlay();
return owner.gevTrackedId;
return window.__godsEyeView.entityRecords.read(owner)?.trackedId;
});
await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing');
const delayedFallbackOwnership = await page.evaluate((beforeFlyToCalls) => {
@@ -3880,7 +3880,7 @@ async function main() {
const result = {
flyToDelta: window.__qaRadioFlyToCalls.length - beforeFlyToCalls,
trackedIdentityPreserved: viewer.trackedEntity === owner,
trackedId: viewer.trackedEntity?.gevTrackedId || null,
trackedId: window.__godsEyeView.entityRecords.read(viewer.trackedEntity)?.trackedId || null,
selectedId: window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().selected?.id || null,
};
viewer.trackedEntity = undefined;
@@ -4222,7 +4222,7 @@ async function main() {
id: 'qa:flights:radio-recenter-stage-owner',
position: viewer.camera.positionWC.clone(),
});
owner.gevTrackedId = 'flights:qa-radio-recenter-stage-owner';
window.__godsEyeView.entityRecords.write(owner).trackedId = 'flights:qa-radio-recenter-stage-owner';
viewer.trackedEntity = owner;
completeFlight(0);
const trackingBetweenStagesCalls = calls.length;
+14 -14
View File
@@ -701,7 +701,7 @@ async function main() {
const tracked = manager?.layers?.get('flights')?.module?.getTrackedInfo?.();
return manager?.isEnabled?.('flights')
&& tracked?.icao24 === 'aaa001'
&& window.__godsEyeView.viewer.trackedEntity?.gevTrackedId === 'flights:aaa001';
&& window.__godsEyeView.entityRecords.read(window.__godsEyeView.viewer.trackedEntity)?.trackedId === 'flights:aaa001';
},
{ timeout: 30000, polling: 100 },
);
@@ -712,7 +712,7 @@ async function main() {
flightsEnabled: manager.isEnabled('flights'),
trackedId: flights.getTrackedInfo()?.icao24 || null,
durableId: flights.getParams()?.selectedFlightsTrackingId || null,
viewerTrackedId: window.__godsEyeView.viewer.trackedEntity?.gevTrackedId || null,
viewerTrackedId: window.__godsEyeView.entityRecords.read(window.__godsEyeView.viewer.trackedEntity)?.trackedId || null,
};
});
record(
@@ -742,7 +742,7 @@ async function main() {
const tracked = manager?.layers?.get('flights')?.module?.getTrackedInfo?.();
return manager?.isEnabled?.('flights')
&& tracked?.icao24 === 'aaa001'
&& window.__godsEyeView.viewer.trackedEntity?.gevTrackedId === 'flights:aaa001';
&& window.__godsEyeView.entityRecords.read(window.__godsEyeView.viewer.trackedEntity)?.trackedId === 'flights:aaa001';
},
{ timeout: 30000, polling: 100 },
);
@@ -753,7 +753,7 @@ async function main() {
flightsEnabled: manager.isEnabled('flights'),
trackedId: flights.getTrackedInfo()?.icao24 || null,
durableId: flights.getParams()?.selectedFlightsTrackingId || null,
viewerTrackedId: window.__godsEyeView.viewer.trackedEntity?.gevTrackedId || null,
viewerTrackedId: window.__godsEyeView.entityRecords.read(window.__godsEyeView.viewer.trackedEntity)?.trackedId || null,
};
});
record(
@@ -815,7 +815,7 @@ async function main() {
const latchedShare = await evalPage(() => ({
tracked: window.__godsEyeView.dataManager.layers
.get('flights').module.getTrackedInfo()?.icao24 || null,
viewerTrackedId: window.__godsEyeView.viewer.trackedEntity?.gevTrackedId || null,
viewerTrackedId: window.__godsEyeView.entityRecords.read(window.__godsEyeView.viewer.trackedEntity)?.trackedId || null,
}));
record(
'share-v2: the pending subject latches on when it arrives on a later poll',
@@ -1380,7 +1380,7 @@ async function main() {
if (exactPickModel) return exactPickModel;
const ent = window.__godsEyeView.viewer.trackedEntity;
let tracked = null;
if (ent && typeof ent.gevDisplayPosition === 'function') tracked = ent.gevDisplayPosition();
if (ent && typeof window.__godsEyeView.entityRecords.read(ent)?.displayPosition === 'function') tracked = window.__godsEyeView.entityRecords.read(ent)?.displayPosition();
if (!tracked) return pool[0];
let best = null, bestD = Infinity;
for (const m of pool) {
@@ -1427,7 +1427,7 @@ async function main() {
const dm = window.__godsEyeView.dataManager;
const fl = dm.layers.get('flights').module;
const ent = window.__godsEyeView.viewer.trackedEntity;
const disp = ent && typeof ent.gevDisplayPosition === 'function' ? ent.gevDisplayPosition() : null;
const disp = ent && typeof window.__godsEyeView.entityRecords.read(ent)?.displayPosition === 'function' ? window.__godsEyeView.entityRecords.read(ent)?.displayPosition() : null;
const dets = fl.getDetectableObjects({ maxCount: 1000 });
const me = dets.find((d) => d.id === icao || d.id === (fl.getTrackedInfo() && fl.getTrackedInfo().callsign));
if (!disp || !me || !me.position) return null;
@@ -1656,7 +1656,7 @@ async function main() {
const ti = fl.getTrackedInfo();
if (!ti) return null;
const ent = v.trackedEntity;
const disp = ent && typeof ent.gevDisplayPosition === 'function' ? ent.gevDisplayPosition() : null;
const disp = ent && typeof window.__godsEyeView.entityRecords.read(ent)?.displayPosition === 'function' ? window.__godsEyeView.entityRecords.read(ent)?.displayPosition() : null;
const pos = disp || ti.position;
if (!pos) return null;
const center = v.scene.cartesianToCanvasCoordinates(pos);
@@ -1908,7 +1908,7 @@ async function main() {
// presentation model (entities are asserted native-label-free);
// the native read stays as a fallback so this probe still fails
// loudly if BOTH surfaces are ever absent.
const model = ent.gevLabelModel;
const model = window.__godsEyeView.entityRecords.read(ent)?.labelModel;
if (model) return [model.title, ...(model.details || [])].filter(Boolean).join('\n');
if (!ent.label || !ent.label.text) return null;
const t = ent.label.text;
@@ -2553,7 +2553,7 @@ async function main() {
}
// WELD (tracked): the tracked CARD anchors to the model you can see.
// `gevVisualPosition` is a SEPARATE accessor from `gevDisplayPosition` on
// the record's `visualPosition` is a SEPARATE accessor from its `displayPosition` on
// purpose — the latter carries the follow-camera anti-jitter contract and
// must keep returning the cached dead-reckoned value the camera settled
// on. So this asserts both halves: the visual accessor is welded to the
@@ -2580,10 +2580,10 @@ async function main() {
const at = (p) => (p ? Math.hypot(
p.x - anchor.x, p.y - anchor.y, p.z - anchor.z,
) : null);
const visual = typeof ent?.gevVisualPosition === 'function' ? ent.gevVisualPosition() : null;
const display = typeof ent?.gevDisplayPosition === 'function' ? ent.gevDisplayPosition() : null;
const visual = typeof window.__godsEyeView.entityRecords.read(ent)?.visualPosition === 'function' ? window.__godsEyeView.entityRecords.read(ent)?.visualPosition() : null;
const display = typeof window.__godsEyeView.entityRecords.read(ent)?.displayPosition === 'function' ? window.__godsEyeView.entityRecords.read(ent)?.displayPosition() : null;
return {
hasVisualAccessor: typeof ent?.gevVisualPosition === 'function',
hasVisualAccessor: typeof window.__godsEyeView.entityRecords.read(ent)?.visualPosition === 'function',
visualDelta: at(visual),
displayDelta: at(display),
};
@@ -2599,7 +2599,7 @@ async function main() {
// anti-jitter accessor has been repurposed and the follow camera is at
// risk. Skipped (not failed) when the DR cache is momentarily invalid.
const displayDelta = trackedWeld.displayDelta;
record('weld: gevDisplayPosition still reports the dead-reckoned position (anti-jitter intact)',
record('weld: the record displayPosition still reports the dead-reckoned position (anti-jitter intact)',
!Number.isFinite(displayDelta) || displayDelta > ACCESSOR_SEPARATION_EPSILON_M,
Number.isFinite(displayDelta)
? `displayΔ=${displayDelta.toFixed(3)} m (must stay > ${ACCESSOR_SEPARATION_EPSILON_M} m — the ground-snap split)`
+3 -3
View File
@@ -8,6 +8,7 @@ import { t } from '../i18n/index.js';
import { nextTick } from 'vue';
import { useScreenAnnotationsStore } from '../ui/stores/screenAnnotations.js';
import { applicationPinia } from '../ui/vueRuntime.js';
import { readEntityRecord } from '../data/entityRecords.js';
/** A label whose detailed outline could not be resolved. */
function outlineUnavailableLabel(label) {
@@ -424,10 +425,9 @@ export function createScreenAnnotationRenderer(
const ent = viewer.trackedEntity;
const now = Cesium.JulianDate.now();
const displayPosition = readEntityRecord(ent)?.displayPosition;
const world =
typeof ent?.gevDisplayPosition === 'function'
? ent.gevDisplayPosition()
: null;
typeof displayPosition === 'function' ? displayPosition() : null;
const win = world
? Cesium.SceneTransforms.worldToWindowCoordinates(
scene,
+2
View File
@@ -17,6 +17,7 @@ import {
holdContinuousRender,
releaseContinuousRender,
} from '../renderGovernor.js';
import { entityRecord, readEntityRecord } from '../data/entityRecords.js';
/** Attach scene tools, rendering listeners and the application debug handle. */
export function createApplicationTools({
@@ -194,6 +195,7 @@ export function createApplicationTools({
getRenderGovernorDiagnostics,
surfaceServices: operations.surface,
requestRender: governorRequestRender,
entityRecords: { read: readEntityRecord, write: entityRecord },
};
const debug = window.__godsEyeView;
defer(() => {
+2 -2
View File
@@ -227,7 +227,7 @@ test('Cockpit Escape handling precedes form-control shortcut suppression and foc
assert.doesNotMatch(exit, /applyTrackedCameraFrame/);
assert.match(
ui,
/restoreTrackingFrame: \(entity\) => \{[\s\S]*?gevTrackedId[\s\S]*?flightsLayer\.refocusTrackedById[\s\S]*?militaryFlightsLayer\.refocusTrackedById/,
/restoreTrackingFrame: \(entity\) => \{[\s\S]*?readEntityRecord\(entity\)\?\.trackedId[\s\S]*?flightsLayer\.refocusTrackedById[\s\S]*?militaryFlightsLayer\.refocusTrackedById/,
'Cockpit exit must return camera-frame ownership through the source layer so Contact Focus cannot accumulate a second owner',
);
});
@@ -299,7 +299,7 @@ test('the cockpit reads its aircraft from the layer that owns Cesium tracking',
const read = readAircraftInfo.toString();
assert.ok(read, 'readAircraftInfo is missing');
assert.match(read, /resolveTrackedAircraftInfo\(\{/);
assert.match(read, /gevTrackedId/);
assert.match(read, /readEntityRecord\([^)]*\)\?\.trackedId/);
// In cockpit mode the controller moves the entity off viewer.trackedEntity,
// so its own handle is the tracked identity there.
assert.match(read, /this\.viewer\?\.trackedEntity \|\| this\.trackedEntity/);
+1 -1
View File
@@ -306,7 +306,7 @@ export function relativeBearing(bearing, heading) {
* Resolve which flight layer owns the cockpit's tracked aircraft.
*
* Both layers can briefly describe a tracked aircraft during a cross-layer
* handoff, so the viewer's normalized tracked identity (`gevTrackedId`) is the
* handoff, so the viewer's normalized tracked identity (the entity record's `trackedId`) is the
* tie-breaker — the same rule `militaryAwareness.currentTrackedFlightSubject`
* uses. Layer precedence remains the fallback when no normalized identity is
* available, so tracking paths that never stamp one keep working.
+1 -1
View File
@@ -365,7 +365,7 @@ test('tracked-identity resolution falls back to layer precedence', () => {
const civilian = { icao24: 'aaa077' };
const military = { icao24: 'ae01ce' };
// No stamped identity (or one that matches neither layer) keeps the historic
// civilian-first order so tracking paths without gevTrackedId still work.
// civilian-first order so tracking paths without an entity record trackedId still work.
assert.equal(resolveTrackedAircraftInfo({ civilian, military }).layerId, 'flights');
assert.equal(
resolveTrackedAircraftInfo({ civilian, military, trackedId: 'satellites:25544' }).layerId,
+1 -1
View File
@@ -1,7 +1,7 @@
/*
* Cockpit aircraft metadata itself is resolved by `resolveTrackedAircraftInfo`
* in `cockpitMath.js` — the landed owner of that rule. It keeps layer
* precedence as the fallback when no normalized `gevTrackedId` is available,
* precedence as the fallback when the entity record has no normalized `trackedId`,
* so tracking paths that never stamp one keep working; `readAircraftInfo` is
* pinned to it by `cockpitMarkup.test.mjs`. This module owns only the
* multi-step ENTRY TRANSACTION (adopt → enter → roll back).
+3 -2
View File
@@ -5,6 +5,7 @@ import {
enterCockpitWithTracking,
restoreAircraftTrackingOwner,
} from './cockpitTracking.js';
import { readEntityRecord, withEntityRecord } from './data/entityRecords.js';
/**
* Model the two production stamping seams that sit under this transaction:
@@ -22,7 +23,7 @@ function stampingWorld({ entryAllowed = true } = {}) {
layerId,
trackById(id) {
if (!trackable) return false;
world.trackedEntity = { gevTrackedId: `${layerId}:${id}` };
world.trackedEntity = withEntityRecord({}, { trackedId: `${layerId}:${id}` });
stamp('trackedEntityChanged'); // the ui.js listener
return true;
},
@@ -80,7 +81,7 @@ test('a refused Cockpit entry still stamps for the tracker it already moved', ()
// Adoption stamped, and the rollback re-track stamped again.
assert.deepEqual(world.stamps, ['trackedEntityChanged', 'trackedEntityChanged']);
assert.equal(world.generation, 2);
assert.equal(world.trackedEntity.gevTrackedId, 'military:ae01ce');
assert.equal(readEntityRecord(world.trackedEntity)?.trackedId, 'military:ae01ce');
});
test('a Cockpit entry that mutates nothing takes no camera authority', () => {
+10 -9
View File
@@ -31,6 +31,7 @@ import {
isAlprSurveillanceType,
normalizeAlprNode,
} from './alprCameras.js';
import { readEntityRecord } from './entityRecords.js';
test('normalizeAlprNode: maps a full OSM node to a plain record', () => {
const record = normalizeAlprNode({
@@ -443,25 +444,25 @@ test('all manufacturers share one ALPR title and color; only supplied metadata a
const entities = h.source.entities.values;
assert.equal(alprCamerasLayer.name, 'ALPR Cameras');
assert.deepEqual(
entities.map((entity) => entity.gevLabelModel.title),
entities.map((entity) => readEntityRecord(entity)?.labelModel.title),
['ALPR-0042', 'ALPR-0043', 'ALPR-0044'],
);
assert.equal(
new Set(entities.map((entity) => entity.billboard.image.getValue())).size,
1,
);
assert.deepEqual(entities[0].gevLabelModel.details, [
assert.deepEqual(readEntityRecord(entities[0])?.labelModel.details, [
'OSM MAPPED',
'FLOCK SAFETY · CITY POLICE · FIXED',
'PUBLIC MAP DATA',
]);
assert.deepEqual(entities[1].gevLabelModel.details, [
assert.deepEqual(readEntityRecord(entities[1])?.labelModel.details, [
'OSM MAPPED',
'MOTOROLA SOLUTIONS',
'PUBLIC MAP DATA',
]);
assert.deepEqual(
entities[2].gevLabelModel.details,
readEntityRecord(entities[2])?.labelModel.details,
['OSM MAPPED', 'PUBLIC MAP DATA'],
'unknown metadata is not guessed, but the source is always named',
);
@@ -813,7 +814,7 @@ test('selected readout samples the rendered ground, not zero elevation or the di
h.click('alpr:42');
const entity = h.source.entities.getById('alpr:42');
const anchor = Cesium.Cartographic.fromCartesian(
entity.gevDisplayPosition(),
readEntityRecord(entity)?.displayPosition(),
);
assert.ok(Math.abs(anchor.height - 187) < 0.001);
assert.ok(
@@ -821,7 +822,7 @@ test('selected readout samples the rendered ground, not zero elevation or the di
);
for (let i = 0; i < 60; i++) {
h.viewer.scene.postRender.raiseEvent();
entity.gevDisplayPosition();
readEntityRecord(entity)?.displayPosition();
}
assert.equal(samples, 1, 'no per-frame raycasts');
h.advance(1000);
@@ -921,9 +922,9 @@ test('two factories keep their requests, records and destruction independent', a
assert.equal(b.source.entities.values[0].id, 'directory:99');
assert.equal(second.source, 'Test camera directory');
assert.ok(
b.source.entities.values[0].gevLabelModel.details.includes(
'Source: Test directory',
),
readEntityRecord(
b.source.entities.values[0],
)?.labelModel.details.includes('Source: Test directory'),
);
assert.equal(a.viewer.camera.moveEnd.numberOfListeners, 0);
assert.equal(a.viewer.scene.postRender.numberOfListeners, 0);
+52
View File
@@ -0,0 +1,52 @@
/**
* @module entityRecords
* @description Core-owned records for the entities layers draw, so layer state lives in Core
* rather than on the renderer's objects. A layer writes the record for an entity it draws;
* the tracked readout, follow camera, cockpit, share links and awareness read it back
* through the entity the renderer hands them (a pick or the tracked entity).
*
* Fields a record may hold:
* - `trackedId`: the normalized `layer:id` identity, e.g. `flights:a1b2c3`.
* - `displayPosition()`: the layer's cached display position (the follow camera's
* anti-jitter contract: the value the camera settled on, never recomputed late).
* - `visualPosition()`: where a 3D model is actually drawn, for the tracked card.
* - `labelModel`: the tracked card's `{ title, details, accent, … }`.
* - `selectionOrigin`: what started the selection (`'user'`, …).
*/
const records = new WeakMap();
/**
* The record for `entity`, made empty on first use. For layers writing their own state.
* @param {Object} entity An entity the layer draws.
* @returns {Object}
*/
export function entityRecord(entity) {
let record = records.get(entity);
if (!record) {
record = {};
records.set(entity, record);
}
return record;
}
/**
* The record for `entity`, or null when it has none (or there is no entity). For readers.
* @param {Object|null|undefined} entity
* @returns {Object|null}
*/
export function readEntityRecord(entity) {
if (!entity || typeof entity !== 'object') return null;
return records.get(entity) ?? null;
}
/**
* Write `fields` into `entity`'s record and return the entity.
* @param {Object} entity
* @param {Object} fields Record fields to set.
* @returns {Object} `entity`
*/
export function withEntityRecord(entity, fields) {
Object.assign(entityRecord(entity), fields);
return entity;
}
+31
View File
@@ -0,0 +1,31 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { entityRecord, readEntityRecord, withEntityRecord } from './entityRecords.js';
test('an entity record is made on first write and read back by the same entity', () => {
const entity = {};
assert.equal(readEntityRecord(entity), null);
entityRecord(entity).trackedId = 'flights:a1b2c3';
assert.equal(entityRecord(entity), entityRecord(entity), 'one record per entity');
assert.equal(readEntityRecord(entity)?.trackedId, 'flights:a1b2c3');
assert.equal(readEntityRecord({}), null, 'another entity has its own record');
});
test('the record lives in Core, not on the entity', () => {
const entity = {};
entityRecord(entity).labelModel = { title: 'DAL123', details: [] };
assert.deepEqual(Object.keys(entity), []);
});
test('reading without an entity gives null', () => {
assert.equal(readEntityRecord(null), null);
assert.equal(readEntityRecord(undefined), null);
assert.equal(readEntityRecord('flights:a1b2c3'), null);
});
test('withEntityRecord writes fields into the record and returns the entity', () => {
const entity = withEntityRecord({ id: 'alpr:42' }, { trackedId: 'alpr:42' });
assert.equal(entity.id, 'alpr:42');
assert.equal(readEntityRecord(entity)?.trackedId, 'alpr:42');
assert.equal(Object.hasOwn(entity, 'trackedId'), false);
});
+14 -13
View File
@@ -28,6 +28,7 @@ import {
destroyTrackedReadout,
initTrackedReadout,
} from './trackedReadout.js';
import { readEntityRecord, withEntityRecord } from './entityRecords.js';
test('share-Follow absence requires an accepted OpenSky snapshot', async () => {
_setFlightTrackingRefreshOutcomeForTest({ status: 'source-unavailable' });
@@ -184,7 +185,7 @@ test('flights first update forwards caller cancellation into the feed request',
test('nonempty OpenSky payload with zero usable rows cannot prove share target absence', async () => {
_setTrackedFlightRefreshStateForTest({
icao24: 'abc123',
entity: { gevLabelModel: { title: 'WARM', details: [] } },
entity: withEntityRecord({}, { labelModel: { title: 'WARM', details: [] } }),
billboard: { show: false },
billboardCollection: { show: true, remove() {} },
viewer: { camera: { positionCartographic: null }, scene: {} },
@@ -317,7 +318,7 @@ test('a newly seen aircraft appears at its delayed position, not ahead of it', a
test('flights poll refreshes tracked callsign/FL/kts and marks a missed poll STALE', async () => {
const icao24 = 'a1b2c3';
const entity = { gevLabelModel: { title: 'OLD', details: [] } };
const entity = withEntityRecord({}, { labelModel: { title: 'OLD', details: [] } });
const billboard = {
position: Cesium.Cartesian3.fromDegrees(-97.7, 30.2, 9_000),
color: Cesium.Color.WHITE,
@@ -388,13 +389,13 @@ test('flights poll refreshes tracked callsign/FL/kts and marks a missed poll STA
try {
await flightsLayer.update(viewer);
assert.equal(entity.gevLabelModel.title, 'DAL123');
assert.match(entity.gevLabelModel.details.join(' · '), /FL350/);
assert.match(entity.gevLabelModel.details.join(' · '), /486 kts/);
assert.equal(readEntityRecord(entity)?.labelModel.title, 'DAL123');
assert.match(readEntityRecord(entity)?.labelModel.details.join(' · '), /FL350/);
assert.match(readEntityRecord(entity)?.labelModel.details.join(' · '), /486 kts/);
await flightsLayer.update(viewer);
assert.match(
[entity.gevLabelModel.title, ...entity.gevLabelModel.details].join(' · '),
[readEntityRecord(entity)?.labelModel.title, ...readEntityRecord(entity)?.labelModel.details].join(' · '),
/STALE/,
);
} finally {
@@ -527,7 +528,7 @@ test('real civil track path creates no native label and publishes every cached h
assert.ok(
entities.values.every((candidate) => candidate.label === undefined),
);
assert.deepEqual(entity.gevLabelModel, {
assert.deepEqual(readEntityRecord(entity)?.labelModel, {
title: 'N12345 · FL350 · 486 kts',
details: ['TEST AIR · A320', 'AUS → LAX'],
accent: '#39d0ff',
@@ -546,7 +547,7 @@ test('real civil track path creates no native label and publishes every cached h
'user',
'same-target selection upgrades durable authority',
);
assert.equal(entity.gevSelectionOrigin, 'user');
assert.equal(readEntityRecord(entity)?.selectionOrigin, 'user');
assert.equal(flightsLayer.refocusTrackedById('different-flight'), false);
assert.equal(
flightsLayer.refocusTrackedById(icao24, { origin: 'voice' }),
@@ -579,8 +580,8 @@ test('real civil track path creates no native label and publishes every cached h
const entry = publication.entries[0];
assert.equal(entry.protected, true);
assert.equal(entry.paintLane, 'tracked');
assert.equal(entry.title, entity.gevLabelModel.title);
assert.deepEqual(entry.details, entity.gevLabelModel.details);
assert.equal(entry.title, readEntityRecord(entity)?.labelModel.title);
assert.deepEqual(entry.details, readEntityRecord(entity)?.labelModel.details);
const display = entity.position.getValue(now);
assert.ok(display, 'tracked position callback must seed its frame cache');
@@ -941,7 +942,7 @@ test('civil label chain: the awareness selection EVENT uses the canonical chain'
});
test('civil tracked readout: a callsign-less enriched contact reads as its registration', async () => {
const entity = { gevLabelModel: { title: 'OLD', details: [] } };
const entity = withEntityRecord({}, { labelModel: { title: 'OLD', details: [] } });
seedLabelContact({ callsign: ' ', registration: ' N123AB ', tracked: true });
_setTrackedFlightRefreshStateForTest({
icao24: LABEL_ICAO,
@@ -981,9 +982,9 @@ test('civil tracked readout: a callsign-less enriched contact reads as its regis
camera: { positionCartographic: null },
scene: {},
});
assert.match(entity.gevLabelModel.title, /^N123AB\b/);
assert.match(readEntityRecord(entity)?.labelModel.title, /^N123AB\b/);
assert.doesNotMatch(
[entity.gevLabelModel.title, ...entity.gevLabelModel.details].join(' · '),
[readEntityRecord(entity)?.labelModel.title, ...readEntityRecord(entity)?.labelModel.details].join(' · '),
/ae1fa4/,
);
} finally {
+9 -10
View File
@@ -46,6 +46,7 @@ import {
presentAwarenessDistance,
} from '../layers/awareness/panel.js';
import { NAVIGATION_AUTHORITY_EVENT } from '../navigationPolicy.js';
import { withEntityRecord } from './entityRecords.js';
const militaryAwarenessSource = readLayerSource(
new URL('./militaryAwareness.js', import.meta.url),
@@ -823,9 +824,7 @@ for (const fixture of [
let retrackCalls = 0;
try {
const trackedEntity = {
gevTrackedId: `${subject.layerId}:${subject.id}`,
};
const trackedEntity = withEntityRecord({}, { trackedId: `${subject.layerId}:${subject.id}` });
runtime.viewer.trackedEntity = trackedEntity;
replaceMethod(
fixture.layer,
@@ -927,7 +926,7 @@ test('Contacts activation adopts the production military tracked-subject descrip
),
'mutating one accessor result must not alter the tracked source position',
);
runtime.viewer.trackedEntity = { gevTrackedId: `military:${id}` };
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `military:${id}` });
replaceMethod(flightsLayer, 'getTrackedSubject', () => null, restores);
replaceMethod(
flightsLayer,
@@ -1032,7 +1031,7 @@ test('production military tracked-subject label falls back to registration befor
'getAllPositions id stays the identity hex',
);
runtime.viewer.trackedEntity = { gevTrackedId: `military:${id}` };
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `military:${id}` });
replaceMethod(flightsLayer, 'getTrackedSubject', () => null, restores);
replaceMethod(flightsLayer, 'trackById', () => true, restores);
replaceMethod(militaryFlightsLayer, 'trackById', () => true, restores);
@@ -1104,7 +1103,7 @@ test('a cached Context subject re-reads its label when enrichment lands after se
meta,
});
runtime.viewer.trackedEntity = { gevTrackedId: `flights:${id}` };
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `flights:${id}` });
replaceMethod(
militaryFlightsLayer,
'getTrackedSubject',
@@ -1247,7 +1246,7 @@ test('Contacts activation adopts the production civilian tracked-subject descrip
),
'mutating one accessor result must not alter the tracked source position',
);
runtime.viewer.trackedEntity = { gevTrackedId: `flights:${id}` };
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `flights:${id}` });
replaceMethod(
militaryFlightsLayer,
'getTrackedSubject',
@@ -1334,7 +1333,7 @@ test('Contacts activation reconciles to a newer cross-layer tracked flight after
const restores = [];
try {
runtime.viewer.trackedEntity = { gevTrackedId: 'flights:first' };
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: 'flights:first' });
replaceMethod(
flightsLayer,
'getTrackedSubject',
@@ -1357,7 +1356,7 @@ test('Contacts activation reconciles to a newer cross-layer tracked flight after
civilianTracked = null;
militaryTracked = awarenessSubject('military', 'latest', positions.latest);
runtime.viewer.trackedEntity = { gevTrackedId: 'military:latest' };
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: 'military:latest' });
releaseDependencies();
await nextTurn();
await nextTurn();
@@ -1401,7 +1400,7 @@ test('tracked flight cleared during Contacts activation suppresses fallback auto
const focused = [];
try {
runtime.viewer.trackedEntity = { gevTrackedId: 'flights:selected' };
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: 'flights:selected' });
replaceMethod(flightsLayer, 'getTrackedSubject', () => tracked, restores);
replaceMethod(
militaryFlightsLayer,
+13 -12
View File
@@ -17,6 +17,7 @@ import {
destroyTrackedReadout,
initTrackedReadout,
} from './trackedReadout.js';
import { readEntityRecord, withEntityRecord } from './entityRecords.js';
test('share-Follow absence requires an accepted adsb.lol snapshot', async () => {
_setMilitaryTrackingRefreshOutcomeForTest({ status: 'source-unavailable' });
@@ -147,7 +148,7 @@ test('military first update forwards caller cancellation into the feed request',
test('nonempty adsb.lol payload with zero usable rows cannot prove share target absence', async () => {
_setTrackedMilitaryRefreshStateForTest({
icao24: 'ae1234',
entity: { gevLabelModel: { title: 'WARM', details: [] } },
entity: withEntityRecord({}, { labelModel: { title: 'WARM', details: [] } }),
billboard: { show: false },
billboardCollection: { show: true, remove() {} },
viewer: { camera: { positionCartographic: null }, scene: {} },
@@ -183,7 +184,7 @@ test('nonempty adsb.lol payload with zero usable rows cannot prove share target
test('military poll refreshes tracked callsign/altitude/kts and marks a missed poll STALE', async () => {
const icao24 = 'ae01ce';
const entity = { gevLabelModel: { title: 'OLD', details: [] } };
const entity = withEntityRecord({}, { labelModel: { title: 'OLD', details: [] } });
const billboard = {
position: Cesium.Cartesian3.fromDegrees(-97.0, 31.0, 8_000),
color: Cesium.Color.WHITE,
@@ -246,12 +247,12 @@ test('military poll refreshes tracked callsign/altitude/kts and marks a missed p
try {
await militaryFlightsLayer.update(viewer);
assert.equal(entity.gevLabelModel.title, 'RCH451');
assert.match(entity.gevLabelModel.details.join(' · '), /28000 ft/);
assert.match(entity.gevLabelModel.details.join(' · '), /400 kt/);
assert.equal(readEntityRecord(entity)?.labelModel.title, 'RCH451');
assert.match(readEntityRecord(entity)?.labelModel.details.join(' · '), /28000 ft/);
assert.match(readEntityRecord(entity)?.labelModel.details.join(' · '), /400 kt/);
await militaryFlightsLayer.update(viewer);
assert.match(entity.gevLabelModel.title, /STALE/);
assert.match(readEntityRecord(entity)?.labelModel.title, /STALE/);
} finally {
globalThis.fetch = realFetch;
}
@@ -264,7 +265,7 @@ test('cached military snapshots retain their observation time and expose stale f
const viewer = { camera: { positionCartographic: null }, scene: {} };
_setTrackedMilitaryRefreshStateForTest({
icao24: 'ae01ce',
entity: { gevLabelModel: { title: 'RCH451', details: [] } },
entity: withEntityRecord({}, { labelModel: { title: 'RCH451', details: [] } }),
billboard: {
position: Cesium.Cartesian3.fromDegrees(-97, 31, 8000),
show: false,
@@ -442,7 +443,7 @@ test('real military track path creates no native label and publishes every cache
assert.ok(
entities.values.every((candidate) => candidate.label === undefined),
);
assert.deepEqual(entity.gevLabelModel, {
assert.deepEqual(readEntityRecord(entity)?.labelModel, {
title: 'RCH451',
details: ['C17 · 05-8152', 'United States Air Force · 28000 ft · 450 kt'],
accent: '#ffd166',
@@ -464,7 +465,7 @@ test('real military track path creates no native label and publishes every cache
'user',
'same-target selection upgrades durable authority',
);
assert.equal(entity.gevSelectionOrigin, 'user');
assert.equal(readEntityRecord(entity)?.selectionOrigin, 'user');
assert.equal(
militaryFlightsLayer.refocusTrackedById('different-flight'),
false,
@@ -500,8 +501,8 @@ test('real military track path creates no native label and publishes every cache
const entry = publication.entries[0];
assert.equal(entry.protected, true);
assert.equal(entry.paintLane, 'tracked');
assert.equal(entry.title, entity.gevLabelModel.title);
assert.deepEqual(entry.details, entity.gevLabelModel.details);
assert.equal(entry.title, readEntityRecord(entity)?.labelModel.title);
assert.deepEqual(entry.details, readEntityRecord(entity)?.labelModel.details);
const display = entity.position.getValue(now);
assert.ok(display, 'tracked position callback must seed its frame cache');
@@ -845,7 +846,7 @@ test('the military tracked descriptor publishes the reported vertical rate', ()
const seedRate = (verticalRateMps) =>
_setTrackedMilitaryRefreshStateForTest({
icao24: 'ae7f01',
entity: { gevLabelModel: { title: 'TEST', details: [] } },
entity: withEntityRecord({}, { labelModel: { title: 'TEST', details: [] } }),
billboard: {
position: Cesium.Cartesian3.fromDegrees(-97.0, 31.0, 8_000),
color: Cesium.Color.WHITE,
+5 -4
View File
@@ -25,6 +25,7 @@ import {
} from '../renderGovernor.js';
import * as Cesium from 'cesium';
import { registerEntityContext, selectEntityContext, getSelectedEntityContext, clearSelectedEntityContextForLayer } from './contextStore.js';
import { readEntityRecord } from './entityRecords.js';
test('clicking a selected installation again or empty map clears it through refresh', async () => {
const run = await runInstallationLoad({ elements: [{ type: 'node', id: 42,
@@ -435,7 +436,7 @@ test('a footprint-less relation just outside the viewport still renders', async
});
try {
assert.deepEqual(
harness.entities().map((entity) => entity.gevLabelModel?.title),
harness.entities().map((entity) => readEntityRecord(entity)?.labelModel?.title),
['Straddling Range'],
);
} finally {
@@ -475,7 +476,7 @@ test('a legacy-shaped payload at the cap fires the exact-viewport retry end to e
assert.equal(harness.requests.length, 2, 'a legacy entry must not skip the retry');
assert.equal(harness.requests[1].includes('exact=1'), true);
assert.deepEqual(
harness.entities().map((entity) => entity.gevLabelModel?.title),
harness.entities().map((entity) => readEntityRecord(entity)?.labelModel?.title),
['Rescued From Legacy'],
);
} finally {
@@ -506,7 +507,7 @@ test('off-viewport records from the snapped superset never render or enter conte
],
});
try {
const titles = harness.entities().map((entity) => entity.gevLabelModel?.title);
const titles = harness.entities().map((entity) => readEntityRecord(entity)?.labelModel?.title);
assert.deepEqual(titles, ['In View'], 'only the in-viewport site renders');
assert.equal(harness.contextLabels().includes('Off View'), false, 'and none enters context');
assert.equal(harness.stats().count, 1);
@@ -534,7 +535,7 @@ test('a saturated snapped tile refetches the exact viewport before rendering', a
assert.equal(harness.requests[0].includes('exact=1'), false, 'first ask uses the shared snapped tile');
assert.equal(harness.requests[1].includes('exact=1'), true, 'retry opts out of the snap');
assert.deepEqual(
harness.entities().map((entity) => entity.gevLabelModel?.title),
harness.entities().map((entity) => readEntityRecord(entity)?.labelModel?.title),
['Rescued'],
'the in-viewport site is no longer starved by off-view ones',
);
+4 -3
View File
@@ -52,6 +52,7 @@ import {
stationMatchesRadioCategory,
} from './radio.js';
import { radioStationRow, withRadioLayer } from '../testSupport/radioLayerHarness.mjs';
import { withEntityRecord } from './entityRecords.js';
const stations = [
{ id: 'news', tags: ['News', 'Weather Radio', 'air traffic'] },
@@ -1652,10 +1653,10 @@ test('Radio recovery preserves closer altitude and caps extreme full-globe zoom'
test('Radio camera navigation yields to every live tracked-entity owner', () => {
const viewer = { camera: {} };
assert.equal(radioCameraNavigationAllowed(viewer), true);
for (const gevTrackedId of ['flights:abc123', 'military:def456']) {
const trackedEntity = { gevTrackedId };
for (const trackedId of ['flights:abc123', 'military:def456']) {
const trackedEntity = withEntityRecord({}, { trackedId });
viewer.trackedEntity = trackedEntity;
assert.equal(radioCameraNavigationAllowed(viewer), false, gevTrackedId);
assert.equal(radioCameraNavigationAllowed(viewer), false, trackedId);
assert.equal(viewer.trackedEntity, trackedEntity);
}
viewer.trackedEntity = undefined;
+10 -9
View File
@@ -18,6 +18,7 @@ import {
} from './satellites.js';
import satellitesLayer from './satellites.js';
import { createTrackedOverlayEntry } from './trackedReadout.js';
import { readEntityRecord, withEntityRecord } from './entityRecords.js';
const L1 =
'1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927';
@@ -117,7 +118,7 @@ test('selected satellite params survive delayed arrival and yield to newer expli
satellitesLayer.setParams({ selectedSatTrackingId: 25544 }),
);
assert.equal(satellitesLayer.getParams().selectedSatTrackingId, 25544);
assert.equal(viewer.trackedEntity?.gevTrackedId, 'satellites:25544');
assert.equal(readEntityRecord(viewer.trackedEntity)?.trackedId, 'satellites:25544');
satellitesLayer.stopTracking();
_removeSatelliteTrackingCandidateForTest(25544);
@@ -194,7 +195,7 @@ test('a tracked docked cluster consolidates its companions onto one card', () =>
// so their ambient labels stack underneath the tracked card. Owner ruling:
// consolidate them as secondary info on that card, and suppress only those
// members — never unrelated satellites that merely happen to be nearby.
const entity = { gevLabelModel: { title: 'OLD', details: ['? km'] } };
const entity = withEntityRecord({}, { labelModel: { title: 'OLD', details: ['? km'] } });
const point = { position: new Cesium.Cartesian3() };
const viewer = { camera: null, scene: { frameState: { frameNumber: 1 } } };
let nowMs = Date.UTC(2008, 8, 20, 12, 30);
@@ -229,7 +230,7 @@ test('a tracked docked cluster consolidates its companions onto one card', () =>
viewer.scene.frameState.frameNumber = 2;
_runSatellitePreRenderForTest();
const details = entity.gevLabelModel.details;
const details = readEntityRecord(entity)?.labelModel.details;
// Class leads the detail block; the altitude line follows it, and the
// consolidated companions stay last.
assert.equal(
@@ -252,7 +253,7 @@ test('a tracked docked cluster consolidates its companions onto one card', () =>
assert.doesNotMatch(details[2], /\+2/);
});
test('satellite pre-render refreshes the tracked altitude on each propagated frame', () => {
const entity = { gevLabelModel: { title: 'OLD', details: ['? km'] } };
const entity = withEntityRecord({}, { labelModel: { title: 'OLD', details: ['? km'] } });
const point = { position: new Cesium.Cartesian3() };
const viewer = {
camera: null,
@@ -271,17 +272,17 @@ test('satellite pre-render refreshes the tracked altitude on each propagated fra
});
_runSatellitePreRenderForTest();
assert.equal(entity.gevLabelModel.title, 'ISS (ZARYA)');
assert.equal(entity.gevLabelModel.details[0], 'STATION · ISS');
assert.equal(readEntityRecord(entity)?.labelModel.title, 'ISS (ZARYA)');
assert.equal(readEntityRecord(entity)?.labelModel.details[0], 'STATION · ISS');
// formatQuantity keeps the number and its unit together with a no-break space.
assert.equal(entity.gevLabelModel.details[1], '353\u00a0km · NORAD 25544');
assert.equal(readEntityRecord(entity)?.labelModel.details[1], '353\u00a0km · NORAD 25544');
epochIndex = 1;
viewer.scene.frameState.frameNumber = 2;
_runSatellitePreRenderForTest();
assert.equal(entity.gevLabelModel.details[1], '366\u00a0km · NORAD 25544');
assert.equal(readEntityRecord(entity)?.labelModel.details[1], '366\u00a0km · NORAD 25544');
assert.equal(
entity.gevLabelModel.details[0],
readEntityRecord(entity)?.labelModel.details[0],
'STATION · ISS',
'the class line survives an altitude-only republish',
);
+5 -4
View File
@@ -33,6 +33,7 @@ import militaryFlightsLayer, {
import { findCompatibleHistoryIndex } from './militaryAwareness.js';
import { createGevActionRunner } from '../voice/gevActions.js';
import { ANALYST_LAYERS, createAnalystEngine } from './analystEngine.js';
import { readEntityRecord, withEntityRecord } from './entityRecords.js';
/** Strip block and line comments so source pins scan CODE, not prose. */
function stripComments(source) {
@@ -149,7 +150,7 @@ test('a conversion survives a poll refresh, in both the billboard and the tracke
const icao24 = 'a1b2c3';
setTr3b(icao24, true);
const entity = { gevLabelModel: { title: 'OLD', details: [] } };
const entity = withEntityRecord({}, { labelModel: { title: 'OLD', details: [] } });
const billboard = {
position: Cesium.Cartesian3.fromDegrees(-97.7, 30.2, 9_000),
color: Cesium.Color.WHITE,
@@ -212,11 +213,11 @@ test('a conversion survives a poll refresh, in both the billboard and the tracke
assert.equal(billboard.image, aircraftIcon('tr3b'),
'the poll reconciler re-images through the TR-3B resolver, not the raw class');
// Live telemetry keeps flowing; only the class label is the operator's fiction.
assert.match(entity.gevLabelModel.title, /^DAL123 · FL350 · 486 kts$/);
assert.deepEqual(entity.gevLabelModel.details.slice(0, 1), ['TR-3B'],
assert.match(readEntityRecord(entity)?.labelModel.title, /^DAL123 · FL350 · 486 kts$/);
assert.deepEqual(readEntityRecord(entity)?.labelModel.details.slice(0, 1), ['TR-3B'],
'the tracked card class line reports TR-3B, replacing operator/type');
assert.equal(
[entity.gevLabelModel.title, ...entity.gevLabelModel.details].join(' · ').includes('Southwest'),
[readEntityRecord(entity)?.labelModel.title, ...readEntityRecord(entity)?.labelModel.details].join(' · ').includes('Southwest'),
false,
'the real operator is not shown alongside the TR-3B classification',
);
+2 -1
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import { readEntityRecord } from './entityRecords.js';
const MAX_FRAME_ATTEMPTS = 120;
const MIN_TRACKED_RANGE_M = 150;
@@ -57,7 +58,7 @@ export function trackedModelScaleForPixelCap({
* @returns {Cesium.Cartesian3|undefined} Camera-frame position.
*/
export function trackedDisplayPositionForCamera(entity, time, result) {
const displayedPosition = entity?.gevDisplayPosition?.();
const displayedPosition = readEntityRecord(entity)?.displayPosition?.();
return displayedPosition
? Cesium.Cartesian3.clone(displayedPosition, result)
: entity?.position?.getValue(time, result);
+3 -5
View File
@@ -6,6 +6,7 @@ import {
trackedDisplayPositionForCamera,
trackedModelScaleForPixelCap,
} from './trackedCamera.js';
import { withEntityRecord } from './entityRecords.js';
test('tracked camera origin crossing returns to the prior side at the minimum range', () => {
const camera = {
@@ -74,15 +75,12 @@ test('tracked model shrinks smoothly when close enough to exceed the pixel cap',
test('tracked camera prefers the already-rendered display cache', () => {
let callbackReads = 0;
const cached = new Cesium.Cartesian3(1, 2, 3);
const entity = {
gevDisplayPosition: () => cached,
position: {
const entity = withEntityRecord({ position: {
getValue: () => {
callbackReads += 1;
return new Cesium.Cartesian3(4, 5, 6);
},
},
};
} }, { displayPosition: () => cached });
const result = trackedDisplayPositionForCamera(
entity,
Cesium.JulianDate.now(),
+3 -2
View File
@@ -8,6 +8,7 @@ import {
_trackedFrameCartesianForTest,
} from './satellites.js';
import { setLocale } from '../i18n/index.js';
import { readEntityRecord, withEntityRecord } from './entityRecords.js';
const L1 =
'1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927';
@@ -16,7 +17,7 @@ const L2 =
function run(locale) {
setLocale(locale);
const entity = { gevLabelModel: { title: 'OLD', details: ['? km'] } };
const entity = withEntityRecord({}, { labelModel: { title: 'OLD', details: ['? km'] } });
const point = { position: new Cesium.Cartesian3() };
const viewer = { camera: null, scene: { frameState: { frameNumber: 1 } } };
let nowMs = Date.UTC(2008, 8, 20, 12, 30);
@@ -43,7 +44,7 @@ function run(locale) {
nowMs += 2000;
viewer.scene.frameState.frameNumber = 2;
_runSatellitePreRenderForTest();
const details = entity.gevLabelModel.details;
const details = readEntityRecord(entity)?.labelModel.details;
setLocale('en');
return details;
}
+14 -10
View File
@@ -4,6 +4,7 @@ import {
setOverlaySourceVisible,
} from '../overlays/worldOverlay.js';
import { WORLD_OVERLAY_STYLE } from '../overlays/worldOverlayTokens.js';
import { readEntityRecord } from './entityRecords.js';
/**
* @module trackedReadout
@@ -79,9 +80,10 @@ export function trackedLabelModelFromText(
* @returns {Object|null}
*/
export function cachedTrackedDisplayPosition(entity) {
if (!entity || typeof entity.gevDisplayPosition !== 'function') return null;
const displayPosition = readEntityRecord(entity)?.displayPosition;
if (typeof displayPosition !== 'function') return null;
try {
return entity.gevDisplayPosition() || null;
return displayPosition() || null;
} catch {
return null;
}
@@ -91,22 +93,23 @@ export function cachedTrackedDisplayPosition(entity) {
* Anchor for the tracked CARD: the position the target is visually at.
*
* A layer that renders its tracked contact as a 3D model publishes
* `gevVisualPosition`, which returns the translation that model is actually rendering
* the entity record's `visualPosition`, which returns the translation that model is actually rendering
* with. For a grounded aircraft that differs from the display position by the full
* ground-snap offset (~100 m at an inland airport), so a card anchored to the display
* position sits below the aircraft and climbs slowly as the coarse floor cell warms.
*
* Falls back to the display position, so layers without a 3D visual (and any layer
* before its model is ready) are unchanged. This deliberately does NOT repurpose
* `gevDisplayPosition`: that accessor carries the follow-camera anti-jitter contract
* the entity record's `displayPosition`: that accessor carries the follow-camera anti-jitter contract
* and must keep returning the value the camera settled on.
* @param {Object|null} entity Tracked or selected presentation entity.
* @returns {Object|null}
*/
export function cachedTrackedVisualPosition(entity) {
if (entity && typeof entity.gevVisualPosition === 'function') {
const visualPosition = readEntityRecord(entity)?.visualPosition;
if (typeof visualPosition === 'function') {
try {
const visual = entity.gevVisualPosition();
const visual = visualPosition();
if (visual) return visual;
} catch {
/* fall through to the display position */
@@ -116,7 +119,7 @@ export function cachedTrackedVisualPosition(entity) {
}
function entryIdFor(entity) {
const explicit = String(entity?.gevTrackedId || '').trim();
const explicit = String(readEntityRecord(entity)?.trackedId || '').trim();
if (explicit) return explicit;
const fallback = String(entity?.id || '').trim();
return fallback ? `entity:${fallback}` : null;
@@ -132,9 +135,10 @@ function activeEntity() {
* @returns {Object|null}
*/
export function createTrackedOverlayEntry(entity) {
const model = entity?.gevLabelModel;
const record = readEntityRecord(entity);
const model = record?.labelModel;
const id = entryIdFor(entity);
if (!id || !model || typeof entity.gevDisplayPosition !== 'function') {
if (!id || !model || typeof record.displayPosition !== 'function') {
return null;
}
const title = String(model.title || '').trim();
@@ -204,7 +208,7 @@ function syncActiveEntity() {
}
/**
* Republish a layer's newly assigned `gevLabelModel` when that entity owns the
* Republish an entity record's newly assigned `labelModel` when that entity owns the
* active tracked readout. Layers call this only when presentation text changes;
* position remains the live frame-cache getter already registered with host.
* @param {Object} entity Entity whose model changed.
+26 -52
View File
@@ -13,6 +13,7 @@ import {
refreshTrackedReadout,
trackedLabelModelFromText,
} from './trackedReadout.js';
import { entityRecord, readEntityRecord, withEntityRecord } from './entityRecords.js';
function makeCesiumEvent() {
const listeners = new Set();
@@ -53,15 +54,12 @@ test('tracked label text becomes an explicit title/details/accent model', () =>
);
});
test('tracked position source reads only gevDisplayPosition and never entity.position', () => {
test('tracked position source reads only the record\'s displayPosition and never entity.position', () => {
const cached = { x: 1, y: 2, z: 3 };
let propertyReads = 0;
const entity = {
gevDisplayPosition: () => cached,
position: {
const entity = withEntityRecord({ position: {
getValue() { propertyReads += 1; throw new Error('fresh position read is forbidden'); },
},
};
} }, { displayPosition: () => cached });
assert.equal(cachedTrackedDisplayPosition(entity), cached);
assert.equal(propertyReads, 0);
assert.equal(cachedTrackedDisplayPosition({ position: entity.position }), null);
@@ -71,17 +69,12 @@ test('tracked position source reads only gevDisplayPosition and never entity.pos
test('the tracked card anchors to the visual position without repurposing the display accessor', () => {
// A grounded aircraft's 3D model rides a ground snap while its billboard stays
// at the reported (buried) altitude — ~100 m apart. The card must follow the
// model you can see, while `gevDisplayPosition` keeps returning the cached
// model you can see, while the record's `displayPosition` keeps returning the cached
// dead-reckoned value the follow camera settled on (anti-jitter contract).
const display = { x: 1, y: 2, z: 3 };
const visual = { x: 1, y: 2, z: 103 };
let displayReads = 0;
const entity = {
gevTrackedId: 'flights:aaa077',
gevDisplayPosition: () => { displayReads += 1; return display; },
gevVisualPosition: () => visual,
gevLabelModel: { title: 'SWA143', details: ['ON GROUND'], accent: '#6be8ff' },
};
const entity = withEntityRecord({}, { trackedId: 'flights:aaa077', displayPosition: () => { displayReads += 1; return display; }, visualPosition: () => visual, labelModel: { title: 'SWA143', details: ['ON GROUND'], accent: '#6be8ff' } });
assert.equal(cachedTrackedVisualPosition(entity), visual);
assert.equal(displayReads, 0, 'the visual accessor short-circuits the display read');
assert.equal(cachedTrackedDisplayPosition(entity), display,
@@ -91,15 +84,12 @@ test('the tracked card anchors to the visual position without repurposing the di
// Layers with no 3D visual, and any layer before its model is ready, are
// unchanged: the accessor falls back to the display position.
const spriteOnly = { gevDisplayPosition: () => display };
const spriteOnly = withEntityRecord({}, { displayPosition: () => display });
assert.equal(cachedTrackedVisualPosition(spriteOnly), display);
assert.equal(cachedTrackedVisualPosition({ ...spriteOnly, gevVisualPosition: () => null }), display,
assert.equal(cachedTrackedVisualPosition(withEntityRecord({}, { displayPosition: () => display, visualPosition: () => null })), display,
'a null visual position falls back rather than blanking the card');
assert.equal(
cachedTrackedVisualPosition({
gevDisplayPosition: () => display,
gevVisualPosition: () => { throw new Error('model gone'); },
}),
cachedTrackedVisualPosition(withEntityRecord({}, { displayPosition: () => display, visualPosition: () => { throw new Error('model gone'); } })),
display,
'a throwing visual accessor falls back instead of dropping the card',
);
@@ -108,11 +98,7 @@ test('the tracked card anchors to the visual position without repurposing the di
test('tracked entry factory pins the production protected-lane policy', () => {
const display = { x: 4, y: 5, z: 6 };
const entry = createTrackedOverlayEntry({
gevTrackedId: 'satellites:25544',
gevDisplayPosition: () => display,
gevLabelModel: { title: 'ISS', details: ['420 km · NORAD 25544'], accent: '#ffd84d' },
});
const entry = createTrackedOverlayEntry(withEntityRecord({}, { trackedId: 'satellites:25544', displayPosition: () => display, labelModel: { title: 'ISS', details: ['420 km · NORAD 25544'], accent: '#ffd84d' } }));
assert.equal(entry.id, 'satellites:25544');
assert.equal(entry.position(), display);
assert.equal(entry.protected, true);
@@ -122,15 +108,12 @@ test('tracked entry factory pins the production protected-lane policy', () => {
});
test('selected camera readout carries tactical animation and fixed badge clearance', () => {
const entry = createTrackedOverlayEntry({
id: 'alpr:42', gevDisplayPosition: () => ({ x: 1, y: 2, z: 3 }),
gevLabelModel: {
const entry = createTrackedOverlayEntry(withEntityRecord({ id: 'alpr:42' }, { displayPosition: () => ({ x: 1, y: 2, z: 3 }), labelModel: {
title: 'ALPR-0042', details: ['OSM MAPPED'], accent: '#ff6474',
cardStyle: 'tactical', selected: true, leaderStyle: 'elbow',
leaderAnimationMs: 440, leaderAnimationStartedAt: 100, leaderDrawRatio: 0.68,
anchorRadiusPx: 30, anchorRadiusScale: null,
},
});
} }));
assert.equal(entry.cardStyle, 'tactical');
assert.equal(entry.selected, true);
assert.equal(entry.leaderStyle, 'elbow');
@@ -146,12 +129,7 @@ test('tracked entity publishes a protected host entry backed by the frame cache'
const fakeWindow = new EventTarget();
const changed = makeCesiumEvent();
const display = { x: 10, y: 20, z: 30 };
const entity = {
id: 'generated',
gevTrackedId: 'flights:abc123',
gevDisplayPosition: () => display,
gevLabelModel: { title: 'UAL123', details: ['FL350 · 451 kts'], accent: '#39d0ff' },
};
const entity = withEntityRecord({ id: 'generated' }, { trackedId: 'flights:abc123', displayPosition: () => display, labelModel: { title: 'UAL123', details: ['FL350 · 451 kts'], accent: '#39d0ff' } });
const viewer = { trackedEntity: entity, trackedEntityChanged: changed };
const recorder = makeHostRecorder();
globalThis.window = fakeWindow;
@@ -174,7 +152,7 @@ test('tracked entity publishes a protected host entry backed by the frame cache'
assert.equal(publication.options.hideInCockpit, true);
assert.equal(getActiveTrackedReadoutId(), 'flights:abc123');
entity.gevLabelModel = { ...entity.gevLabelModel, details: ['FL360 · 455 kts'] };
entityRecord(entity).labelModel = { ...readEntityRecord(entity)?.labelModel, details: ['FL360 · 455 kts'] };
refreshTrackedReadout(entity);
assert.deepEqual(recorder.calls.filter(({ op }) => op === 'set').at(-1).entries[0].details, [
'FL360 · 455 kts',
@@ -197,11 +175,7 @@ test('selection lifecycle leaves installations to their own entry and retains AL
const changed = makeCesiumEvent();
const viewer = { trackedEntity: null, trackedEntityChanged: changed };
const recorder = makeHostRecorder();
const installation = {
gevTrackedId: 'installations:fort-test',
gevDisplayPosition: () => ({ x: 1, y: 2, z: 3 }),
gevLabelModel: { title: 'FORT TEST', details: ['AIRFIELD'], accent: '#5aa9ff' },
};
const installation = withEntityRecord({}, { trackedId: 'installations:fort-test', displayPosition: () => ({ x: 1, y: 2, z: 3 }), labelModel: { title: 'FORT TEST', details: ['AIRFIELD'], accent: '#5aa9ff' } });
globalThis.window = fakeWindow;
_setTrackedOverlayHostForTest(recorder.host);
try {
@@ -219,11 +193,7 @@ test('selection lifecycle leaves installations to their own entry and retains AL
assert.equal(recorder.calls.filter(({ op }) => op === 'set').length, setsBefore, 'installation selection cannot create a second overlay');
// ALPR cameras are static context too: a click publishes the same card.
const camera = {
gevTrackedId: 'alpr:42',
gevDisplayPosition: () => ({ x: 4, y: 5, z: 6 }),
gevLabelModel: { title: 'FLOCK SAFETY ALPR', details: ['CITY PD'], accent: '#ff66c4' },
};
const camera = withEntityRecord({}, { trackedId: 'alpr:42', displayPosition: () => ({ x: 4, y: 5, z: 6 }), labelModel: { title: 'FLOCK SAFETY ALPR', details: ['CITY PD'], accent: '#ff66c4' } });
fakeWindow.dispatchEvent(new CustomEvent('gev:entity-selected', {
detail: { layerId: 'alpr-cameras', entity: camera },
}));
@@ -271,7 +241,7 @@ test('trackedReadout cannot resurrect a dedicated canvas or render listener', as
}
});
test('tracking layers write gevLabelModel and expose only their cached display positions', async () => {
test('tracking layers write the record\'s labelModel and expose only their cached display positions', async () => {
const files = await Promise.all([
'flights.js',
'militaryFlights.js',
@@ -280,12 +250,16 @@ test('tracking layers write gevLabelModel and expose only their cached display p
].map(async (name) => [name, readLayerSource(new URL(`./${name}`, import.meta.url))]));
const sources = Object.fromEntries(files);
for (const [name, source] of files) {
assert.ok(source.includes('.gevLabelModel ='), `${name} writes the explicit model directly`);
assert.ok(source.includes('.gevDisplayPosition ='), `${name} exposes a display-position cache`);
assert.match(source, /entityRecord\([^)]*\)\.labelModel\s*=/, `${name} writes the explicit model into the entity record`);
assert.match(source, /entityRecord\([^)]*\)\.displayPosition\s*=/, `${name} exposes a display-position cache in the entity record`);
assert.doesNotMatch(source, /\.gev(LabelModel|DisplayPosition|VisualPosition|TrackedId|SelectionOrigin)\b/, `${name} hangs no tracked state on the entity`);
}
assert.match(sources['flights.js'], /gevDisplayPosition\s*=\s*parts\.motion\._trackedDisplayCached/);
assert.match(sources['militaryFlights.js'], /gevDisplayPosition\s*=\s*parts\.motion\._trackedDisplayCached/);
assert.ok(sources['satellites.js'].includes('gevDisplayPosition = _trackedDisplayCached'));
assert.match(sources['flights.js'], /entityRecord\([^)]*\)\.displayPosition\s*=\s*parts\.motion\._trackedDisplayCached/);
assert.match(sources['militaryFlights.js'], /entityRecord\([^)]*\)\.displayPosition\s*=\s*parts\.motion\._trackedDisplayCached/);
assert.match(
sources['satellites.js'],
/entityRecord\([^)]*\)\.displayPosition\s*=\s*_trackedDisplayCached/,
);
assert.equal(sources['flights.js'].includes('_trackedEntity.label.text'), false);
assert.equal(sources['militaryFlights.js'].includes('_trackedEntity.label.text'), false);
assert.equal(sources['satellites.js'].includes('_trackedEntity.label.text'), false);
+9 -8
View File
@@ -23,6 +23,7 @@ import {
alprLabelDetails,
validAlprGroundHeight,
} from './visuals.js';
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
export function createAlprPresentation({ state, services, source }) {
const { governorRequestRender } = services.render;
@@ -236,11 +237,10 @@ export function createAlprPresentation({ state, services, source }) {
entity.billboard.width = entity.billboard.height = selected
? SELECTED_MARKER_ICON_SIZE
: MARKER_ICON_SIZE;
if (entity.gevLabelModel) {
entity.gevLabelModel.accent = color.toCssColorString();
entity.gevLabelModel.leaderAnimationStartedAt = selected
? selectionStartedAt
: 0;
const labelModel = readEntityRecord(entity)?.labelModel;
if (labelModel) {
labelModel.accent = color.toCssColorString();
labelModel.leaderAnimationStartedAt = selected ? selectionStartedAt : 0;
}
}
@@ -425,13 +425,14 @@ export function createAlprPresentation({ state, services, source }) {
}
}
entity.gevAlprRecord = record;
entity.gevTrackedId = record.id;
entityRecord(entity).trackedId = record.id;
// The mapped camera datum has no elevation; it is not the clamped marker's
// visual anchor. Only the selected marker samples the rendered surface,
// at most once per second, through Cesium's public height APIs.
entity.gevAlprDisplayPosition ??= null;
entity.gevDisplayPosition = () => entity.gevAlprDisplayPosition;
entity.gevLabelModel = {
entityRecord(entity).displayPosition = () =>
entity.gevAlprDisplayPosition;
entityRecord(entity).labelModel = {
title: alprDisplayId(record),
details: alprLabelDetails(record, source),
accent: color.toCssColorString(),
+9 -5
View File
@@ -10,6 +10,7 @@ import {
SUBJECT_PRESENCE,
AWARENESS_PAGE_SIZE,
} from './policy.js';
import { readEntityRecord } from '../../data/entityRecords.js';
// coverageLabel is machine-composed English from installations/controls.js
// (pinned by src/data/militaryInstallations.test.mjs); translate the known
@@ -192,7 +193,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
function currentTrackedFlightSubject() {
const trackedKey = normalizeContextId(
layerState.viewer?.trackedEntity?.gevTrackedId,
readEntityRecord(layerState.viewer?.trackedEntity)?.trackedId,
);
if (!trackedKey) return null;
const subjects = [
@@ -260,16 +261,19 @@ export function createSubject({ state: layerState, services, parts, source }) {
) {
if (!subject?.position) return null;
if (subject.layerId === 'flights' || subject.layerId === 'military') {
const trackedPosition =
layerState.viewer?.trackedEntity?.gevDisplayPosition?.();
const trackedPosition = readEntityRecord(
layerState.viewer?.trackedEntity,
)?.displayPosition?.();
if (trackedPosition) {
return {
position: Cesium.Cartesian3.clone(trackedPosition),
// Only the follow camera's own contact proves presence this way; a
// different tracked entity says nothing about this subject.
presence:
String(layerState.viewer?.trackedEntity?.gevTrackedId || '') ===
subjectKey(subject)
String(
readEntityRecord(layerState.viewer?.trackedEntity)?.trackedId ||
'',
) === subjectKey(subject)
? SUBJECT_PRESENCE.LIVE
: SUBJECT_PRESENCE.UNCHECKED,
};
+1 -1
View File
@@ -515,7 +515,7 @@ export function createMotion({
*
* It reads `modelMatrix`, which `_updateTrackedModel` already wrote this frame — no
* sampling, no `_modelDisplayPosition` call from postRender, and no new dead reckoning,
* so the follow-camera anti-jitter contract on `gevDisplayPosition` is untouched.
* so the follow-camera anti-jitter contract on the entity record's `displayPosition` is untouched.
*/
function _trackedVisualCached() {
+14 -17
View File
@@ -25,6 +25,7 @@ import {
CYAN_TRANSPARENT,
} from './policy.js';
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
export function createTracking({
flightState,
@@ -66,7 +67,7 @@ export function createTracking({
const info = flightState.records.data.get(icao24);
if (!bb?.position || !info) return false;
if (flightState._trackedEntity)
flightState._trackedEntity.gevSelectionOrigin = origin;
entityRecord(flightState._trackedEntity).selectionOrigin = origin;
_emitAwarenessEvent('gev:awareness-subject-selected', {
layerId: 'flights',
id: icao24,
@@ -788,10 +789,8 @@ export function createTracking({
function _updateTrackedLabelModel(icao24) {
if (!flightState._trackedEntity || icao24 !== flightState._trackedIcao)
return;
flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText(
_trackedLabelText(icao24),
'#39d0ff',
);
entityRecord(flightState._trackedEntity).labelModel =
trackedLabelModelFromText(_trackedLabelText(icao24), '#39d0ff');
refreshTrackedReadout(flightState._trackedEntity);
// The readout and the context slot describe the same contact — refresh them
// together so voice never narrates a fix the card has already replaced.
@@ -1007,12 +1006,10 @@ export function createTracking({
}, false),
},
});
flightState._trackedEntity.gevSelectionOrigin = origin;
flightState._trackedEntity.gevTrackedId = `flights:${icao24}`;
flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText(
_trackedLabelText(icao24),
'#39d0ff',
);
entityRecord(flightState._trackedEntity).selectionOrigin = origin;
entityRecord(flightState._trackedEntity).trackedId = `flights:${icao24}`;
entityRecord(flightState._trackedEntity).labelModel =
trackedLabelModelFromText(_trackedLabelText(icao24), '#39d0ff');
// A billboard has a ~zero bounding sphere, so Cesium's default follow distance is
// far too tight (the user had to scroll out to read the plane). Give the entity a
@@ -1035,12 +1032,12 @@ export function createTracking({
// Expose the camera's already-settled position to cross-module HUD consumers (the tracked-target
// readout) so they draw at the SAME spot the camera framed, without recomputing the dead-reckon in
// postRender (which would jitter the label against the now-stable plane).
flightState._trackedEntity.gevDisplayPosition =
entityRecord(flightState._trackedEntity).displayPosition =
parts.motion._trackedDisplayCached;
// Separate accessor on purpose: `gevDisplayPosition` carries the follow-camera
// Separate accessor on purpose: the entity record's `displayPosition` carries the follow-camera
// anti-jitter contract and must keep returning the cached DR position. Presentation
// that should weld to the AIRCRAFT YOU SEE reads `gevVisualPosition` instead.
flightState._trackedEntity.gevVisualPosition =
// that should weld to the AIRCRAFT YOU SEE reads the entity record's `visualPosition` instead.
entityRecord(flightState._trackedEntity).visualPosition =
parts.motion._trackedVisualCached;
refreshTrackedReadout(flightState._trackedEntity);
flightState._viewer.camera.cancelFlight();
@@ -1155,8 +1152,8 @@ export function createTracking({
) {
_clearTracking(true, {
origin:
flightState._viewer.trackedEntity?.gevSelectionOrigin ||
'programmatic',
readEntityRecord(flightState._viewer.trackedEntity)
?.selectionOrigin || 'programmatic',
});
}
});
+5 -4
View File
@@ -2,6 +2,7 @@ import { installationClassLabel, installationTitle } from './classLabel.js';
import * as Cesium from 'cesium';
import { t } from '../../i18n/index.js';
import { LAYER_ID, MAX_RENDERED, COLOR_BY_CLASS } from './policy.js';
import { entityRecord } from '../../data/entityRecords.js';
export function createRendering({
state: layerState,
@@ -221,7 +222,7 @@ export function createRendering({
if (retained.gevInstallationHeightM !== surfaceHeightM) {
retained.gevInstallationHeightM = surfaceHeightM;
retained.position = displayPosition;
retained.gevDisplayPosition = () => displayPosition;
entityRecord(retained).displayPosition = () => displayPosition;
}
if (retained.gevInstallationSelected !== selectedRecord) {
retained.gevInstallationSelected = selectedRecord;
@@ -269,9 +270,9 @@ export function createRendering({
}
entity.gevInstallationHeightM = surfaceHeightM;
entity.gevInstallationSelected = selectedRecord;
entity.gevTrackedId = `installations:${record.id}`;
entity.gevDisplayPosition = () => displayPosition;
entity.gevLabelModel = {
entityRecord(entity).trackedId = `installations:${record.id}`;
entityRecord(entity).displayPosition = () => displayPosition;
entityRecord(entity).labelModel = {
title: installationTitle(record.name) || t('MAPPED INSTALLATION'),
details: [
installationClassLabel(record.class),
+6 -5
View File
@@ -35,6 +35,7 @@ import {
LOCAL_ADSB_UAT_RING_COLOR,
} from './policy.js';
import { localAdsbStatus } from './status.js';
import { entityRecord } from '../../data/entityRecords.js';
export { HEARD_BY_RECEIVER } from './policy.js';
export { localAdsbCardModel, localAdsbReceiverLine } from './card.js';
@@ -458,14 +459,14 @@ export function createLocalAdsbLayer({
),
},
});
marker.entity.gevTrackedId = id;
marker.entity.gevDisplayPosition = () => marker.position;
entityRecord(marker.entity).trackedId = id;
entityRecord(marker.entity).displayPosition = () => marker.position;
}
refreshClass(marker);
const uat = localAdsbIsUat(record);
if (uat !== Boolean(marker.entity.point))
marker.entity.point = uat ? uatRing(marker) : undefined;
marker.entity.gevLabelModel = cardModel(marker, at);
entityRecord(marker.entity).labelModel = cardModel(marker, at);
registerEntityContext(marker.entity, contextMetadata(id, marker));
if (newFix && id === selectedId) refreshTrail();
return id;
@@ -481,7 +482,7 @@ export function createLocalAdsbLayer({
function refreshSelectedCard() {
const marker = selectedId ? markers.get(selectedId) : null;
if (!marker) return;
marker.entity.gevLabelModel = cardModel(marker, now());
entityRecord(marker.entity).labelModel = cardModel(marker, now());
services.overlays?.refreshReadout?.(marker.entity);
}
@@ -567,7 +568,7 @@ export function createLocalAdsbLayer({
const reselect = id === selectedId && selectedTrail?.id === id;
selectedId = id;
enrichment.request(marker.record, { selected: true });
marker.entity.gevLabelModel = cardModel(marker, now());
entityRecord(marker.entity).labelModel = cardModel(marker, now());
selectEntityContext(marker.entity);
if (!reselect) startTrail(marker);
governorRequestRender('local-adsb-selection');
+11 -7
View File
@@ -12,6 +12,7 @@ import {
localAdsbReceiverLine,
localAdsbStatus,
} from './index.js';
import { readEntityRecord } from '../../data/entityRecords.js';
function fakeReceiver(initial = {}) {
const state = {
@@ -252,8 +253,8 @@ test('local aircraft use their class silhouette and scale in magenta', async (t)
),
);
assert.equal(entity.billboard.rotation.getValue(), 0);
assert.equal(entity.gevLabelModel.title, 'LOCAL1');
assert.equal(typeof entity.gevDisplayPosition, 'function');
assert.equal(readEntityRecord(entity)?.labelModel.title, 'LOCAL1');
assert.equal(typeof readEntityRecord(entity)?.displayPosition, 'function');
const heli = sources[0].entities.getById('local-adsb:a0b702');
assert.equal(heli.billboard.image.getValue(), aircraftIcon('helicopter'));
@@ -265,7 +266,10 @@ test('local aircraft use their class silhouette and scale in magenta', async (t)
),
'the helicopter keeps the local magenta',
);
assert.equal(heli.gevLabelModel.details[1], 'Helicopter · A7');
assert.equal(
readEntityRecord(heli)?.labelModel.details[1],
'Helicopter · A7',
);
// On the ground the silhouette shrinks like a grounded public flight.
receiver.set({
@@ -479,14 +483,14 @@ test('adsbdb type, operator and route reach the card for the selected aircraft o
await new Promise((resolve) => setTimeout(resolve, 400));
assert.deepEqual([...queries].sort(), ['route:EVC145', 'type:a0b702']);
const entity = sources[0].entities.getById('local-adsb:a0b702');
assert.deepEqual(entity.gevLabelModel.details.slice(0, 4), [
assert.deepEqual(readEntityRecord(entity)?.labelModel.details.slice(0, 4), [
'ICAO A0B702 · EVC145',
'Helicopter · A7',
'Air Evac · Bell 407',
'AUS → SAT',
]);
assert.equal(
entity.gevLabelModel.details.at(-1),
readEntityRecord(entity)?.labelModel.details.at(-1),
'Heard by your receiver · 1090 MHz · browser SDR',
);
layer.selectAircraft('a0b702');
@@ -760,7 +764,7 @@ test('feed records merge with browser SDR records by ICAO and UAT-only aircraft
);
assert.equal(shared.point, undefined, 'heard on 1090 too: no UAT ring');
assert.equal(
shared.gevLabelModel.details.at(-1),
readEntityRecord(shared)?.labelModel.details.at(-1),
'Heard by your receiver · 1090 MHz + 978 MHz UAT · browser SDR + decoder feed',
);
assert.ok(uat.point, 'a UAT-only aircraft carries the ring');
@@ -776,7 +780,7 @@ test('feed records merge with browser SDR records by ICAO and UAT-only aircraft
'same magenta marker family',
);
assert.equal(
uat.gevLabelModel.details.at(-1),
readEntityRecord(uat)?.labelModel.details.at(-1),
'Heard by your receiver · 978 MHz UAT · decoder feed',
);
const stats = layer.getStats();
+1 -1
View File
@@ -370,7 +370,7 @@ export function createMotion({
* model is actually rendering with when the model owns the visual, otherwise the cached
* dead-reckoned position. Mirror of flights.js; see that copy for the full rationale.
* Reads the modelMatrix the tracked-model update already wrote this frame: no sampling,
* no `_modelDisplayPosition` from postRender, and `gevDisplayPosition` keeps its
* no `_modelDisplayPosition` from postRender, and the entity record's `displayPosition` keeps its
* follow-camera anti-jitter contract untouched.
*/
+17 -17
View File
@@ -26,6 +26,7 @@ import {
TRACKED_ICON_COLOR,
} from './policy.js';
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
export function createTracking({
flightState,
@@ -65,7 +66,7 @@ export function createTracking({
const info = flightState.records.data.get(icao24);
if (!bb?.position || !info) return false;
if (flightState._trackedEntity)
flightState._trackedEntity.gevSelectionOrigin = origin;
entityRecord(flightState._trackedEntity).selectionOrigin = origin;
_emitAwarenessEvent('gev:awareness-subject-selected', {
layerId: 'military',
id: icao24,
@@ -252,10 +253,11 @@ export function createTracking({
function _updateTrackedLabelModel(icao24) {
if (!flightState._trackedEntity || icao24 !== flightState._trackedIcao)
return;
flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText(
_buildTrackedLabel(flightState.records.data.get(icao24), icao24),
'#ffd166',
);
entityRecord(flightState._trackedEntity).labelModel =
trackedLabelModelFromText(
_buildTrackedLabel(flightState.records.data.get(icao24), icao24),
'#ffd166',
);
refreshTrackedReadout(flightState._trackedEntity);
// The readout and the context slot describe the same contact — refresh them
// together so voice never narrates a fix the card has already replaced.
@@ -966,12 +968,10 @@ export function createTracking({
}, false),
},
});
flightState._trackedEntity.gevSelectionOrigin = origin;
flightState._trackedEntity.gevTrackedId = `military:${icao24}`;
flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText(
_buildTrackedLabel(info, icao24),
'#ffd166',
);
entityRecord(flightState._trackedEntity).selectionOrigin = origin;
entityRecord(flightState._trackedEntity).trackedId = `military:${icao24}`;
entityRecord(flightState._trackedEntity).labelModel =
trackedLabelModelFromText(_buildTrackedLabel(info, icao24), '#ffd166');
// A billboard has a ~zero bounding sphere, so Cesium's default follow distance is
// far too tight (the user had to scroll out to read the plane). Give the entity a
@@ -990,12 +990,12 @@ export function createTracking({
// mid-fly_to_location) would follow the plane WITHOUT centering it like a click does.
// Cross-module HUD consumers (tracked-target readout) read the camera's settled position, not a
// postRender recompute, so the label doesn't jitter against the now-stable plane (mirror of flights).
flightState._trackedEntity.gevDisplayPosition =
entityRecord(flightState._trackedEntity).displayPosition =
parts.motion._trackedDisplayCached;
// Separate accessor on purpose (mirror of flights.js): `gevDisplayPosition` keeps the
// Separate accessor on purpose (mirror of flights.js): the entity record's `displayPosition` keeps the
// follow-camera anti-jitter contract; presentation that must weld to the aircraft you
// can see reads `gevVisualPosition`.
flightState._trackedEntity.gevVisualPosition =
// can see reads the entity record's `visualPosition`.
entityRecord(flightState._trackedEntity).visualPosition =
parts.motion._trackedVisualCached;
refreshTrackedReadout(flightState._trackedEntity);
flightState._viewer.camera.cancelFlight();
@@ -1060,8 +1060,8 @@ export function createTracking({
) {
_clearTracking(true, {
origin:
flightState._viewer.trackedEntity?.gevSelectionOrigin ||
'programmatic',
readEntityRecord(flightState._viewer.trackedEntity)
?.selectionOrigin || 'programmatic',
});
}
});
+3 -2
View File
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium';
import { isPointerFree } from '../../data/inputOwnership.js';
import { readEntityRecord } from '../../data/entityRecords.js';
export function createInteraction({
state: layerState,
@@ -36,8 +37,8 @@ export function createInteraction({
) {
parts.tracking._clearTracking(true, {
origin:
layerState._viewer.trackedEntity?.gevSelectionOrigin ||
'programmatic',
readEntityRecord(layerState._viewer.trackedEntity)
?.selectionOrigin || 'programmatic',
});
}
});
+7 -5
View File
@@ -9,6 +9,7 @@ import {
TRACK_VIEW_FROM_HIGH_SCALE,
TRACK_VIEW_FROM_LEO,
} from './policy.js';
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
export function createTracking({ state: layerState, services, parts, source }) {
const { clearFocusTarget, publishFocusTargetFromCachedPosition } =
@@ -384,7 +385,7 @@ export function createTracking({ state: layerState, services, parts, source }) {
: t('DOCKED · {name}', { name: companions[0] }),
);
}
const current = layerState._trackedEntity.gevLabelModel;
const current = readEntityRecord(layerState._trackedEntity)?.labelModel;
// Compare the WHOLE detail array: comparing only `details[0]` swallowed any
// change confined to the companions line, so the card would never republish.
const unchanged =
@@ -392,7 +393,7 @@ export function createTracking({ state: layerState, services, parts, source }) {
current?.details?.length === details.length &&
details.every((line, index) => current.details[index] === line);
if (unchanged) return;
layerState._trackedEntity.gevLabelModel = {
entityRecord(layerState._trackedEntity).labelModel = {
title,
details,
accent: '#ffd84d',
@@ -456,9 +457,10 @@ export function createTracking({ state: layerState, services, parts, source }) {
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
layerState._trackedEntity.gevSelectionOrigin = origin;
layerState._trackedEntity.gevTrackedId = `satellites:${noradId}`;
layerState._trackedEntity.gevDisplayPosition = _trackedDisplayCached;
entityRecord(layerState._trackedEntity).selectionOrigin = origin;
entityRecord(layerState._trackedEntity).trackedId = `satellites:${noradId}`;
entityRecord(layerState._trackedEntity).displayPosition =
_trackedDisplayCached;
_updateTrackedSatelliteLabelModel(initialPos?.altitude ?? null);
_emitAwarenessEvent('gev:awareness-subject-selected', {
+13 -9
View File
@@ -84,6 +84,7 @@ import {
setDetectionTuning,
setMode as setDetectionMode,
} from '../data/detection.js';
import { withEntityRecord } from '../data/entityRecords.js';
/**
* @module worldOverlayAllocation.worker
@@ -650,16 +651,19 @@ function buildPhase3TrackedWorkload(count) {
({ selected }) => selected,
);
const selected = vesselRegistration.entries.splice(selectedIndex, 1)[0];
const trackedEntity = {
gevTrackedId: 'flights:allocation-probe',
gevDisplayPosition: selected.position,
gevLabelModel: {
// i18n-ignore: benchmark fixture
title: 'ALLOC01',
details: ['FL350 · 451 kts', 'TEST AIR · A320'],
accent: '#39d0ff',
const trackedEntity = withEntityRecord(
{},
{
trackedId: 'flights:allocation-probe',
displayPosition: selected.position,
labelModel: {
// i18n-ignore: benchmark fixture
title: 'ALLOC01',
details: ['FL350 · 451 kts', 'TEST AIR · A320'],
accent: '#39d0ff',
},
},
};
);
const tracked = createTrackedOverlayEntry(trackedEntity);
tracked.horizonCull = false;
vesselRegistration.options = {
+2 -1
View File
@@ -1,13 +1,14 @@
import * as Cesium from 'cesium';
import { isPickedWorldPosition } from '../data/scenePick.js';
import { t } from '../i18n/index.js';
import { readEntityRecord } from '../data/entityRecords.js';
function trackedTarget(viewer) {
const entity = viewer.trackedEntity;
// Read the visual's sample first; sampling a flight CallbackProperty twice
// can advance dead reckoning ahead of the displayed aircraft.
return (
entity?.gevDisplayPosition?.() ||
readEntityRecord(entity)?.displayPosition?.() ||
entity?.position?.getValue(viewer.clock?.currentTime)
);
}
+2 -1
View File
@@ -12,6 +12,7 @@ import {
toggleCameraTilt,
} from './cameraOrientationControls.js';
import { NavigationController } from './navigationController.js';
import { entityRecord } from '../data/entityRecords.js';
function createViewer({ cameraPosition, target, pickPosition = null } = {}) {
const calls = [];
@@ -523,7 +524,7 @@ for (const [label, altitude, range] of [
position: new Cesium.CallbackPositionProperty(() => target, false),
trackingReferenceFrame: Cesium.TrackingReferenceFrame.ENU,
});
entity.gevDisplayPosition = () => target;
entityRecord(entity).displayPosition = () => target;
viewer.trackedEntity = entity;
const follow = new Cesium.EntityView(entity, viewer.scene);
follow.update(viewer.clock.currentTime);
+9 -2
View File
@@ -1,6 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { CockpitViewController } from './cockpitController.js';
import { withEntityRecord } from '../data/entityRecords.js';
function environment() {
const original = { document: globalThis.document, window: globalThis.window };
@@ -95,9 +96,15 @@ test('aircraft identity is read from the supplied layer instances', () => {
militaryFlightsLayer: { getTrackedInfo: () => military },
},
});
env.viewer.trackedEntity = { gevTrackedId: 'military:def456' };
env.viewer.trackedEntity = withEntityRecord(
{},
{ trackedId: 'military:def456' },
);
assert.deepEqual(owner.readAircraftInfo(), military);
env.viewer.trackedEntity = { gevTrackedId: 'flights:abc123' };
env.viewer.trackedEntity = withEntityRecord(
{},
{ trackedId: 'flights:abc123' },
);
assert.deepEqual(owner.readAircraftInfo(), civilian);
owner.dispose();
} finally {
+4 -3
View File
@@ -4,6 +4,7 @@ import { cockpitEntryAllowed } from '../contextModePolicy.js';
import { formatAwarenessLabel } from '../data/militaryAwarenessEngine.js';
import { enterCockpitWithTracking } from '../cockpitTracking.js';
import { cockpitVisionModeForStyle } from '../cockpitVisionPolicy.js';
import { readEntityRecord } from '../data/entityRecords.js';
/** Own Cockpit entry/rollback, readouts and the single Display portal. */
export class CockpitCoordinator {
@@ -90,9 +91,9 @@ export class CockpitCoordinator {
onEntered: () => this.enterPanels(),
onExited: () => this.exitPanels(),
restoreTrackingFrame: (entity) => {
const [layerId, ...idParts] = String(entity?.gevTrackedId || '').split(
':',
);
const [layerId, ...idParts] = String(
readEntityRecord(entity)?.trackedId || '',
).split(':');
const trackedId = idParts.join(':');
if (!trackedId) return false;
if (layerId === 'flights')
+2 -1
View File
@@ -5,6 +5,7 @@ import {
normalizeHeading,
resolveTrackedAircraftInfo,
} from '../cockpitMath.js';
import { readEntityRecord } from '../data/entityRecords.js';
export function readAircraftInfo() {
// In cockpit mode the controller takes the entity off `viewer.trackedEntity`
@@ -13,7 +14,7 @@ export function readAircraftInfo() {
return resolveTrackedAircraftInfo({
civilian: this.services.flightsLayer.getTrackedInfo?.() || null,
military: this.services.militaryFlightsLayer.getTrackedInfo?.() || null,
trackedId: trackedEntity?.gevTrackedId || '',
trackedId: readEntityRecord(trackedEntity)?.trackedId || '',
});
}
+2 -1
View File
@@ -10,6 +10,7 @@ import {
nextSheet,
sheetForPanelId,
} from './mobileShell.js';
import { withEntityRecord } from '../data/entityRecords.js';
function element(tag, document) {
const node = new EventTarget();
@@ -824,7 +825,7 @@ test('the tab-open callback reports only a successful manual opening', () => {
test('manual Context opens the existing transaction without disabling the tracked aircraft', async () => {
const calls = [];
const tracked = { layerId: 'flights', icao24: 'abc123', callsign: 'EJA225' };
const viewer = { trackedEntity: { gevTrackedId: 'flights:abc123' } };
const viewer = { trackedEntity: withEntityRecord({}, { trackedId: 'flights:abc123' }) };
const entity = viewer.trackedEntity;
const enabled = new Set(['flights', 'weather']);
const controls = new ContextControls({
+2 -1
View File
@@ -5,6 +5,7 @@ import {
followOffsetFromEnu,
followOffsetToEnu,
} from '../share/follow.js';
import { readEntityRecord } from '../data/entityRecords.js';
/**
* The bridge between the camera following an entity and a share link's thing
@@ -39,7 +40,7 @@ export function readShareFollowSubject({ viewer, nowMs = Date.now() }) {
const cameraPosition = viewer?.camera?.positionWC;
if (!cameraPosition) return null;
const tracked = viewer.trackedEntity;
const trackedId = String(tracked?.gevTrackedId || '');
const trackedId = String(readEntityRecord(tracked)?.trackedId || '');
const separator = trackedId.indexOf(':');
if (separator <= 0) return null;
const kind = KIND_BY_LAYER.get(trackedId.slice(0, separator));
+5 -10
View File
@@ -18,6 +18,7 @@ import {
followCameraFor,
followOffsetToEnu,
} from '../share/viewCodec.js';
import { entityRecord, withEntityRecord } from '../data/entityRecords.js';
const NOW_MS = Date.now();
const FIX = { lat: 37.6213, lon: -122.379, alt: 3200 };
@@ -278,10 +279,7 @@ test('a thing view turns its layer on, follows by id and frames the shared offse
this.position = Cesium.Cartesian3.clone(offset);
},
});
entity = {
gevTrackedId: 'flights:a1b2c3',
gevDisplayPosition: () => target,
};
entity = withEntityRecord({}, { trackedId: 'flights:a1b2c3', displayPosition: () => target });
const parsed = page.manager._parseShareCode(aircraftCode());
assert.deepEqual(parsed.layerState.enabledLayerIds, ['flights']);
@@ -452,10 +450,7 @@ test('the followed entity is read in the share format, and its offset round-trip
const viewer = {
camera: { positionWC: cameraPosition },
clock: { currentTime: Cesium.JulianDate.now() },
trackedEntity: {
gevTrackedId: 'military:ae1234',
gevDisplayPosition: () => target,
},
trackedEntity: withEntityRecord({}, { trackedId: 'military:ae1234', displayPosition: () => target }),
};
const subject = readShareFollowSubject({ viewer, nowMs: NOW_MS });
assert.equal(subject.kind, 'military_aircraft');
@@ -464,7 +459,7 @@ test('the followed entity is read in the share format, and its offset round-trip
assert.ok(Math.abs(subject.lastKnown.lat - FIX.lat) < 1e-9);
assert.ok(Math.abs(subject.lastKnown.alt - FIX.alt) < 1e-3);
// A followed entity the share format has no kind for is not a thing view.
viewer.trackedEntity.gevTrackedId = 'local-adsb:abc';
entityRecord(viewer.trackedEntity).trackedId = 'local-adsb:abc';
assert.equal(readShareFollowSubject({ viewer }), null);
// Nor is a camera that follows nothing (a selected vessel is not followed).
viewer.trackedEntity = undefined;
@@ -475,7 +470,7 @@ test('a shared offset on the far side swings round, never jumping through the en
const target = Cesium.Cartesian3.fromDegrees(FIX.lon, FIX.lat, FIX.alt);
const listeners = [];
const looks = [];
const entity = { gevDisplayPosition: () => target };
const entity = withEntityRecord({}, { displayPosition: () => target });
const viewer = {
clock: { currentTime: Cesium.JulianDate.now() },
scene: {
+2 -1
View File
@@ -4,6 +4,7 @@ import { canPresentDeferredStatusNotice } from '../loadingFeedback.js';
import { t, msg, formatDate } from '../i18n/index.js';
import { SHARE_VIEW_ENTITY_KINDS } from '../share/follow.js';
import { UiLifetime } from './uiLifetime.js';
import { readEntityRecord } from '../data/entityRecords.js';
/** How long a shared vessel may take to appear in the vessel feed. */
export const SHARED_VESSEL_WAIT_MS = 120_000;
@@ -424,7 +425,7 @@ export class ShareRestoration {
// a long link's follow keeps the app's own framing.
if (
!view?.lastKnown ||
String(entity?.gevTrackedId || '') !==
String(readEntityRecord(entity)?.trackedId || '') !==
`${result.layerId}:${result.targetId}`
)
return;
+2 -1
View File
@@ -23,6 +23,7 @@ import { MAP_STACKS } from '../mapStackController.js';
import { GEV_REALTIME_TOOLS } from '../../server/providers/openai/tools.js';
import { presentResult } from './resultDisplay.js';
import { attachVoiceResult } from './speech.js';
import { withEntityRecord } from '../data/entityRecords.js';
test('every live basemap is reachable by its own id — no enum value without a voice alias', () => {
// B1 regression: a stack added to MAP_STACKS (and the set_map_stack enum)
@@ -474,7 +475,7 @@ test('voice Stop Tracking reports exact layers whose active or durable clear fai
getAll: () => [],
};
const viewer = {
trackedEntity: { gevTrackedId: 'flights:active' },
trackedEntity: withEntityRecord({}, { trackedId: 'flights:active' }),
scene: {
canvas: { addEventListener() {}, removeEventListener() {} },
preRender: { addEventListener() {} },