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