Format military-flight and classification components

This commit is contained in:
Sameh Khamis committed 2026-09-13 10:23:17 -07:00
1 parent cb790ddea1
commit 1edebcb6f8
17 files changed
+1759 -640

No files matched your search

+45 -12
View File
@@ -17,18 +17,51 @@ import * as render from '../renderGovernor.js';
import * as groundSnap from './groundSnap.js'; import * as groundSnap from './groundSnap.js';
import * as recession from './aircraftRecession.js'; import * as recession from './aircraftRecession.js';
const militaryFlightsLayer = createMilitaryFlightLayer({ source: createAdsbLolSource(), resolveAsset: (url) => `${import.meta.env?.BASE_URL || '/'}${url.replace(/^\//, '')}`, services: { picking, sprites, trails, aircraftPresentation, camera, militaryRegistry, labels, groundFloor, meshFloor, geoid, focus, readout, context, render, groundSnap, recession } }); const militaryFlightsLayer = createMilitaryFlightLayer({
source: createAdsbLolSource(),
resolveAsset: (url) =>
`${import.meta.env?.BASE_URL || '/'}${url.replace(/^\//, '')}`,
services: {
picking,
sprites,
trails,
aircraftPresentation,
camera,
militaryRegistry,
labels,
groundFloor,
meshFloor,
geoid,
focus,
readout,
context,
render,
groundSnap,
recession,
},
});
export { TRACKED_MODEL_MAX_PX } from '../layers/military/policy.js'; export { TRACKED_MODEL_MAX_PX } from '../layers/military/policy.js';
export const _setTrackedMilitaryRefreshStateForTest = militaryFlightsLayer.testing._setTrackedMilitaryRefreshStateForTest; export const _setTrackedMilitaryRefreshStateForTest =
export const _setMilitaryTrackingRefreshOutcomeForTest = militaryFlightsLayer.testing._setMilitaryTrackingRefreshOutcomeForTest; militaryFlightsLayer.testing._setTrackedMilitaryRefreshStateForTest;
export const _addMilitaryTrackingCandidateForTest = militaryFlightsLayer.testing._addMilitaryTrackingCandidateForTest; export const _setMilitaryTrackingRefreshOutcomeForTest =
export const _pendingMilitaryTrackingRestoreForTest = militaryFlightsLayer.testing._pendingMilitaryTrackingRestoreForTest; militaryFlightsLayer.testing._setMilitaryTrackingRefreshOutcomeForTest;
export const _applyPendingMilitaryTrackingRestoreForTest = militaryFlightsLayer.testing._applyPendingMilitaryTrackingRestoreForTest; export const _addMilitaryTrackingCandidateForTest =
export const _setCockpitDetectionSubjectForTest = militaryFlightsLayer.testing._setCockpitDetectionSubjectForTest; militaryFlightsLayer.testing._addMilitaryTrackingCandidateForTest;
export const _trackedModelRegimeActiveForTest = militaryFlightsLayer.testing._trackedModelRegimeActiveForTest; export const _pendingMilitaryTrackingRestoreForTest =
export const _updateTrackedModelForTest = militaryFlightsLayer.testing._updateTrackedModelForTest; militaryFlightsLayer.testing._pendingMilitaryTrackingRestoreForTest;
export const _trackedBillboardColorForTest = militaryFlightsLayer.testing._trackedBillboardColorForTest; export const _applyPendingMilitaryTrackingRestoreForTest =
export const _driveFleetModelHandoffForTest = militaryFlightsLayer.testing._driveFleetModelHandoffForTest; militaryFlightsLayer.testing._applyPendingMilitaryTrackingRestoreForTest;
export const _ensureFleetModelForTest = militaryFlightsLayer.testing._ensureFleetModelForTest; export const _setCockpitDetectionSubjectForTest =
militaryFlightsLayer.testing._setCockpitDetectionSubjectForTest;
export const _trackedModelRegimeActiveForTest =
militaryFlightsLayer.testing._trackedModelRegimeActiveForTest;
export const _updateTrackedModelForTest =
militaryFlightsLayer.testing._updateTrackedModelForTest;
export const _trackedBillboardColorForTest =
militaryFlightsLayer.testing._trackedBillboardColorForTest;
export const _driveFleetModelHandoffForTest =
militaryFlightsLayer.testing._driveFleetModelHandoffForTest;
export const _ensureFleetModelForTest =
militaryFlightsLayer.testing._ensureFleetModelForTest;
export const mapAnalystRecord = militaryFlightsLayer.mapAnalystRecord; export const mapAnalystRecord = militaryFlightsLayer.mapAnalystRecord;
export default militaryFlightsLayer; export default militaryFlightsLayer;
+2 -1
View File
@@ -7,4 +7,5 @@ export const setMilitaryLayerActive = registry.setMilitaryLayerActive;
export const onMilitaryLayerActiveChange = registry.onMilitaryLayerActiveChange; export const onMilitaryLayerActiveChange = registry.onMilitaryLayerActiveChange;
export const registerMilitaryIcaos = registry.registerMilitaryIcaos; export const registerMilitaryIcaos = registry.registerMilitaryIcaos;
export const isMilitaryIcao = registry.isMilitaryIcao; export const isMilitaryIcao = registry.isMilitaryIcao;
export const refreshMilitaryRegistryIfStale = registry.refreshMilitaryRegistryIfStale; export const refreshMilitaryRegistryIfStale =
registry.refreshMilitaryRegistryIfStale;
+22 -5
View File
@@ -19,7 +19,8 @@ let releaseSource = null;
/** Replace the classification source and cancel its outstanding work. */ /** Replace the classification source and cancel its outstanding work. */
function configureSource(nextSource, { signal } = {}) { function configureSource(nextSource, { signal } = {}) {
if (typeof nextSource?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required'); if (typeof nextSource?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required');
releaseSource?.(); releaseSource?.();
const controller = new AbortController(); const controller = new AbortController();
lifetime = controller; lifetime = controller;
@@ -95,7 +96,9 @@ function onMilitaryLayerActiveChange(listener) {
*/ */
function registerMilitaryIcaos(icaos) { function registerMilitaryIcaos(icaos) {
for (const icao of icaos || []) { for (const icao of icaos || []) {
const hex = String(icao || '').trim().toLowerCase(); const hex = String(icao || '')
.trim()
.toLowerCase();
if (hex) _milIcaos.add(hex); if (hex) _milIcaos.add(hex);
} }
_lastRefreshMs = now(); _lastRefreshMs = now();
@@ -113,7 +116,13 @@ function isMilitaryIcao(icao24) {
/** Refresh known identities while the dedicated layer is disabled. /** Refresh known identities while the dedicated layer is disabled.
* A failed or cancelled request retains the current set and never swaps sources. */ * A failed or cancelled request retains the current set and never swaps sources. */
async function refreshMilitaryRegistryIfStale() { async function refreshMilitaryRegistryIfStale() {
if (!currentSource || !lifetime || lifetime.signal.aborted || _militaryLayerActive) return; if (
!currentSource ||
!lifetime ||
lifetime.signal.aborted ||
_militaryLayerActive
)
return;
if (_polling || now() - _lastRefreshMs < MIL_POLL_INTERVAL_MS) return; if (_polling || now() - _lastRefreshMs < MIL_POLL_INTERVAL_MS) return;
const owner = lifetime; const owner = lifetime;
const requestSource = currentSource; const requestSource = currentSource;
@@ -138,6 +147,14 @@ function dispose() {
_militaryLayerActive = false; _militaryLayerActive = false;
} }
if (source) configureSource(source); if (source) configureSource(source);
return { configureSource, dispose, isMilitaryLayerActive, setMilitaryLayerActive, return {
onMilitaryLayerActiveChange, registerMilitaryIcaos, isMilitaryIcao, refreshMilitaryRegistryIfStale }; configureSource,
dispose,
isMilitaryLayerActive,
setMilitaryLayerActive,
onMilitaryLayerActiveChange,
registerMilitaryIcaos,
isMilitaryIcao,
refreshMilitaryRegistryIfStale,
};
} }
+43 -13
View File
@@ -4,7 +4,11 @@ import { createMilitaryRegistry } from './classification.js';
test('classification instances have independent identities, listeners and active states', () => { test('classification instances have independent identities, listeners and active states', () => {
let requests = 0; let requests = 0;
const source = { getSnapshot() { requests++; } }; const source = {
getSnapshot() {
requests++;
},
};
const first = createMilitaryRegistry({ source }); const first = createMilitaryRegistry({ source });
const second = createMilitaryRegistry({ source }); const second = createMilitaryRegistry({ source });
let firstEvents = 0; let firstEvents = 0;
@@ -26,9 +30,15 @@ test('classification instances have independent identities, listeners and active
test('classification uses normalized identities and respects its polling interval and active layer', async () => { test('classification uses normalized identities and respects its polling interval and active layer', async () => {
let time = 120000; let time = 120000;
let calls = 0; let calls = 0;
const registry = createMilitaryRegistry({ now: () => time, source: { const registry = createMilitaryRegistry({
async getSnapshot() { calls++; return { records: [{ id: 'abc123' }] }; }, now: () => time,
} }); source: {
async getSnapshot() {
calls++;
return { records: [{ id: 'abc123' }] };
},
},
});
await registry.refreshMilitaryRegistryIfStale(); await registry.refreshMilitaryRegistryIfStale();
await registry.refreshMilitaryRegistryIfStale(); await registry.refreshMilitaryRegistryIfStale();
assert.equal(calls, 1); assert.equal(calls, 1);
@@ -48,15 +58,29 @@ test('source replacement rejects an old response and disposal cancels the curren
let resolveNew; let resolveNew;
let oldSignal; let oldSignal;
let newSignal; let newSignal;
const registry = createMilitaryRegistry({ source: { const registry = createMilitaryRegistry({
getSnapshot(query, { signal }) { oldSignal = signal; return new Promise((resolve) => { resolveOld = resolve; }); }, source: {
} }); getSnapshot(query, { signal }) {
oldSignal = signal;
return new Promise((resolve) => {
resolveOld = resolve;
});
},
},
});
const old = registry.refreshMilitaryRegistryIfStale(); const old = registry.refreshMilitaryRegistryIfStale();
const application = new AbortController(); const application = new AbortController();
registry.configureSource({ getSnapshot(query, { signal }) { registry.configureSource(
{
getSnapshot(query, { signal }) {
newSignal = signal; newSignal = signal;
return new Promise((resolve) => { resolveNew = resolve; }); return new Promise((resolve) => {
} }, { signal: application.signal }); resolveNew = resolve;
});
},
},
{ signal: application.signal },
);
const current = registry.refreshMilitaryRegistryIfStale(); const current = registry.refreshMilitaryRegistryIfStale();
resolveOld({ records: [{ id: 'abc123' }] }); resolveOld({ records: [{ id: 'abc123' }] });
await old; await old;
@@ -73,9 +97,15 @@ test('source replacement rejects an old response and disposal cancels the curren
test('a failed classification source preserves known identities and makes no fallback request', async () => { test('a failed classification source preserves known identities and makes no fallback request', async () => {
let time = 120000; let time = 120000;
let calls = 0; let calls = 0;
const registry = createMilitaryRegistry({ now: () => time, source: { const registry = createMilitaryRegistry({
async getSnapshot() { calls++; throw new Error('fixture source unavailable'); }, now: () => time,
} }); source: {
async getSnapshot() {
calls++;
throw new Error('fixture source unavailable');
},
},
});
registry.registerMilitaryIcaos(['abc123']); registry.registerMilitaryIcaos(['abc123']);
time += 60000; time += 60000;
await assert.doesNotReject(registry.refreshMilitaryRegistryIfStale()); await assert.doesNotReject(registry.refreshMilitaryRegistryIfStale());
+9 -6
View File
@@ -1,10 +1,13 @@
export function createController({
export function createController({ flightState, services, parts, layer, resolveAsset }) { flightState,
services,
parts,
layer,
resolveAsset,
}) {
function _abortActiveUpdates() { function _abortActiveUpdates() {
for (const controller of flightState._activeUpdateControllers) controller.abort(); for (const controller of flightState._activeUpdateControllers)
controller.abort();
flightState._activeUpdateControllers.clear(); flightState._activeUpdateControllers.clear();
} }
return { _abortActiveUpdates }; return { _abortActiveUpdates };
+11 -2
View File
@@ -8,7 +8,11 @@ import { createLifecycle } from './lifecycle.js';
import { createTesting } from './testing.js'; import { createTesting } from './testing.js';
import { createQueries } from './queries.js'; import { createQueries } from './queries.js';
/** Compose one military-flight layer with application-owned scene services. */ /** Compose one military-flight layer with application-owned scene services. */
export function createMilitaryFlightLayer({ source, services, resolveAsset = (url) => url } = {}) { export function createMilitaryFlightLayer({
source,
services,
resolveAsset = (url) => url,
} = {}) {
const flightState = createFlightState({ source, services }); const flightState = createFlightState({ source, services });
const parts = {}; const parts = {};
const layer = {}; const layer = {};
@@ -21,7 +25,12 @@ parts.ingestion = createIngestion(context);
parts.lifecycle = createLifecycle(context); parts.lifecycle = createLifecycle(context);
parts.testing = createTesting(context); parts.testing = createTesting(context);
parts.queries = createQueries(context); parts.queries = createQueries(context);
Object.assign(layer, parts.queries.methods, parts.lifecycle.methods, parts.ingestion.methods); Object.assign(
layer,
parts.queries.methods,
parts.lifecycle.methods,
parts.ingestion.methods,
);
Object.defineProperty(layer, 'testing', { value: parts.testing }); Object.defineProperty(layer, 'testing', { value: parts.testing });
return layer; return layer;
} }
+152 -44
View File
@@ -2,20 +2,43 @@ import { pickRenderAltitudeM } from '../../data/renderAltitude.js';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { stickyText, stickyNumber } from '../../data/aircraftMeta.js'; import { stickyText, stickyNumber } from '../../data/aircraftMeta.js';
import { classifyAircraft, CLASS_SCALE_2D } from '../../data/aircraftClass.js'; import { classifyAircraft, CLASS_SCALE_2D } from '../../data/aircraftClass.js';
import { turnRateFromFixHistory, liftRepeatedGroundFix, synthesizeForwardKinematicsFix } from '../../data/motionModel.js'; import {
turnRateFromFixHistory,
liftRepeatedGroundFix,
synthesizeForwardKinematicsFix,
} from '../../data/motionModel.js';
import { aircraftIcon } from '../../data/aircraftIcons.js'; import { aircraftIcon } from '../../data/aircraftIcons.js';
import { GROUND_FLOOR_WARM_MAX_ALT_M, POSITION_HISTORY_LIMIT, BILLBOARD_SCALE, GROUND_SCALE, TRACKED_ICON_COLOR, MIL_ICON_COLOR, LANDED_MISSING_POLL_LIMIT, MISSING_POLL_LIMIT, ERROR_BACKOFF_INTERVAL } from './policy.js'; import {
GROUND_FLOOR_WARM_MAX_ALT_M,
POSITION_HISTORY_LIMIT,
BILLBOARD_SCALE,
GROUND_SCALE,
TRACKED_ICON_COLOR,
MIL_ICON_COLOR,
LANDED_MISSING_POLL_LIMIT,
MISSING_POLL_LIMIT,
ERROR_BACKOFF_INTERVAL,
} from './policy.js';
export function createIngestion({ flightState, services, parts, layer, resolveAsset }) { export function createIngestion({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
const { geoidHeight } = services.geoid; const { geoidHeight } = services.geoid;
const { floorAltitudeM, cachedGroundFloor, warmGroundFloor, GROUND_FLOOR_LIFT_M } = services.groundFloor; const {
floorAltitudeM,
cachedGroundFloor,
warmGroundFloor,
GROUND_FLOOR_LIFT_M,
} = services.groundFloor;
const { sampleMeshFloorCells } = services.meshFloor; const { sampleMeshFloorCells } = services.meshFloor;
const { registerMilitaryIcaos } = services.militaryRegistry; const { registerMilitaryIcaos } = services.militaryRegistry;
const militaryFlightsLayer = layer; const militaryFlightsLayer = layer;
const methods = { const methods = {
/** /**
* Fetch the latest military aircraft positions from adsb.lol and reconcile * Fetch the latest military aircraft positions from adsb.lol and reconcile
* with the billboard collection. Handles error backoff, feed-time-stamped * with the billboard collection. Handles error backoff, feed-time-stamped
@@ -44,15 +67,23 @@ const methods = {
: resourceController.signal; : resourceController.signal;
try { try {
updateSignal.throwIfAborted(); updateSignal.throwIfAborted();
const snapshot = await flightState._source.getSnapshot({}, { signal: updateSignal }); const snapshot = await flightState._source.getSnapshot(
{},
{ signal: updateSignal },
);
updateSignal.throwIfAborted(); updateSignal.throwIfAborted();
flightState._lastStatus = snapshot.status ?? 200; flightState._lastStatus = snapshot.status ?? 200;
flightState._lastSource = snapshot.source; flightState._lastSource = snapshot.source;
militaryFlightsLayer.source = flightState._lastSource; militaryFlightsLayer.source = flightState._lastSource;
const usableAircraft = snapshot.records; const usableAircraft = snapshot.records;
flightState._backoff = snapshot.stale || snapshot.freshness === 'unknown'; flightState._backoff =
snapshot.stale || snapshot.freshness === 'unknown';
flightState._retryAt = 0; flightState._retryAt = 0;
flightState._lastError = snapshot.reason || (snapshot.freshness === 'unknown' ? 'Source snapshot time unavailable' : null); flightState._lastError =
snapshot.reason ||
(snapshot.freshness === 'unknown'
? 'Source snapshot time unavailable'
: null);
const currentIcaos = new Set(); const currentIcaos = new Set();
const receiptNowMs = snapshot.observedAtMs; const receiptNowMs = snapshot.observedAtMs;
// Field-test fix (RS46): coarse floor cells to warm for the below-ground // Field-test fix (RS46): coarse floor cells to warm for the below-ground
@@ -72,9 +103,17 @@ const methods = {
// altitude (field elevation is unknowable here), else 0 m — never the // altitude (field elevation is unknowable here), else 0 m — never the
// 3 km airborne default (a parked plane must not float). // 3 km airborne default (a parked plane must not float).
const onGround = aircraft.onGround; const onGround = aircraft.onGround;
const altitudeFt = aircraft.baroAltitudeM == null ? null : aircraft.baroAltitudeM / 0.3048; const altitudeFt =
const altitudeM = aircraft.baroAltitudeM aircraft.baroAltitudeM == null
?? (onGround ? (Number.isFinite(prevMeta?.altitudeFt) ? prevMeta.altitudeFt * 0.3048 : 0) : 3048); ? null
: aircraft.baroAltitudeM / 0.3048;
const altitudeM =
aircraft.baroAltitudeM ??
(onGround
? Number.isFinite(prevMeta?.altitudeFt)
? prevMeta.altitudeFt * 0.3048
: 0
: 3048);
const track = aircraft.courseDeg || 0; const track = aircraft.courseDeg || 0;
const speedMps = aircraft.speedMps ?? 0; const speedMps = aircraft.speedMps ?? 0;
const verticalRateMps = aircraft.verticalRateMps; const verticalRateMps = aircraft.verticalRateMps;
@@ -136,7 +175,10 @@ const methods = {
// their billboard is depth-test-free (see the surfaceM:null block // their billboard is depth-test-free (see the surfaceM:null block
// above — same one-shot-invariant reasoning). // above — same one-shot-invariant reasoning).
if (!onGround && renderAltitudeM < GROUND_FLOOR_WARM_MAX_ALT_M) { if (!onGround && renderAltitudeM < GROUND_FLOOR_WARM_MAX_ALT_M) {
renderAltitudeM = floorAltitudeM(renderAltitudeM, cachedGroundFloor(lat, lon)); renderAltitudeM = floorAltitudeM(
renderAltitudeM,
cachedGroundFloor(lat, lon),
);
_floorWarmPoints.push({ lat, lon }); _floorWarmPoints.push({ lat, lon });
} else if (onGround) { } else if (onGround) {
// Grounded contacts: warm the floor cell, and — round 4 — when NO // Grounded contacts: warm the floor cell, and — round 4 — when NO
@@ -157,9 +199,14 @@ const methods = {
} }
} }
const position = Cesium.Cartesian3.fromDegrees(lon, lat, renderAltitudeM); const position = Cesium.Cartesian3.fromDegrees(
lon,
lat,
renderAltitudeM,
);
// Landing/takeoff transition: the ground flip restyles IN PLACE. // Landing/takeoff transition: the ground flip restyles IN PLACE.
const groundFlipped = !!prevMeta && (prevMeta.onGround === true) !== onGround; const groundFlipped =
!!prevMeta && (prevMeta.onGround === true) !== onGround;
// Either flip direction retires the model's ground snap: a departing plane // Either flip direction retires the model's ground snap: a departing plane
// flies free of it, a landing plane earns a fresh sample where it rolls out. // flies free of it, a landing plane earns a fresh sample where it rolls out.
if (groundFlipped) flightState._groundSnap.forget(icao24); if (groundFlipped) flightState._groundSnap.forget(icao24);
@@ -173,7 +220,10 @@ const methods = {
callsign: stickyText(callsign, prevMeta?.callsign), callsign: stickyText(callsign, prevMeta?.callsign),
type: stickyType, type: stickyType,
// Type outranks category automatically inside classifyAircraft. // Type outranks category automatically inside classifyAircraft.
klass: classifyAircraft({ typeCode: stickyType, category: aircraft?.category }), klass: classifyAircraft({
typeCode: stickyType,
category: aircraft?.category,
}),
registration: stickyText(registration, prevMeta?.registration), registration: stickyText(registration, prevMeta?.registration),
operator: stickyText(operator, prevMeta?.operator), operator: stickyText(operator, prevMeta?.operator),
altitudeFt: stickyNumber(altitudeFt, prevMeta?.altitudeFt, null), altitudeFt: stickyNumber(altitudeFt, prevMeta?.altitudeFt, null),
@@ -186,7 +236,11 @@ const methods = {
speedMps: stickyNumber(speedMps, prevMeta?.speedMps, null), speedMps: stickyNumber(speedMps, prevMeta?.speedMps, null),
track: stickyNumber(track, prevMeta?.track, null), track: stickyNumber(track, prevMeta?.track, null),
// Analyst seam (additive): sticky like the other kinematics. // Analyst seam (additive): sticky like the other kinematics.
verticalRateMps: stickyNumber(verticalRateMps, prevMeta?.verticalRateMps, null), verticalRateMps: stickyNumber(
verticalRateMps,
prevMeta?.verticalRateMps,
null,
),
lastContactEpochMs: stickyNumber( lastContactEpochMs: stickyNumber(
aircraft.contactTimeMs, aircraft.contactTimeMs,
prevMeta?.lastContactEpochMs, prevMeta?.lastContactEpochMs,
@@ -198,7 +252,10 @@ const methods = {
wasAirborne: prevMeta?.wasAirborne === true || !onGround, wasAirborne: prevMeta?.wasAirborne === true || !onGround,
// Round 6: lifted occlusion-test point for at/below-ellipsoid // Round 6: lifted occlusion-test point for at/below-ellipsoid
// renders (mirror of flights.js). // renders (mirror of flights.js).
cullPosition: renderAltitudeM < 10 ? Cesium.Cartesian3.fromDegrees(lon, lat, 12) : null, cullPosition:
renderAltitudeM < 10
? Cesium.Cartesian3.fromDegrees(lon, lat, 12)
: null,
// Raw poll-fix coords (pre-dead-reckon) — the stale-grounded // Raw poll-fix coords (pre-dead-reckon) — the stale-grounded
// re-floor sweep keys floors off these (mirror of flights.js). // re-floor sweep keys floors off these (mirror of flights.js).
rawLat: lat, rawLat: lat,
@@ -244,12 +301,13 @@ const methods = {
// the surface so the ground leg drapes instead of diving. // the surface so the ground leg drapes instead of diving.
if (isTracked) parts.tracking._appendTrailFix(position.clone()); if (isTracked) parts.tracking._appendTrailFix(position.clone());
} else { } else {
const modelOwnsGroundVisual = parts.rendering._modelOwnsVisual(icao24); const modelOwnsGroundVisual =
parts.rendering._modelOwnsVisual(icao24);
if (!modelOwnsGroundVisual) { if (!modelOwnsGroundVisual) {
liftRepeatedGroundFix(newest, position, meta.onGround); liftRepeatedGroundFix(newest, position, meta.onGround);
} }
const kinematicsChanged = newest.velocity !== meta.speedMps const kinematicsChanged =
|| newest.track !== meta.track; newest.velocity !== meta.speedMps || newest.track !== meta.track;
if (kinematicsChanged) { if (kinematicsChanged) {
const synthetic = synthesizeForwardKinematicsFix(newest, { const synthetic = synthesizeForwardKinematicsFix(newest, {
epochMs: Date.now(), epochMs: Date.now(),
@@ -261,7 +319,8 @@ const methods = {
history.push(synthetic); history.push(synthetic);
if (history.length > POSITION_HISTORY_LIMIT) history.shift(); if (history.length > POSITION_HISTORY_LIMIT) history.shift();
meta.turnRateDps = turnRateFromFixHistory(history); meta.turnRateDps = turnRateFromFixHistory(history);
if (isTracked) parts.tracking._appendTrailFix(synthetic.position.clone()); if (isTracked)
parts.tracking._appendTrailFix(synthetic.position.clone());
} }
} }
} }
@@ -272,28 +331,39 @@ const methods = {
// course changes land on the next rotation pass (forced below). // course changes land on the next rotation pass (forced below).
// Ground flips (landing/takeoff) restyle this SAME billboard in // Ground flips (landing/takeoff) restyle this SAME billboard in
// place — the transition is never a removal. // place — the transition is never a removal.
if (prevMeta?.klass !== meta.klass || groundFlipped || flightState._cockpitContactMode) { if (
prevMeta?.klass !== meta.klass ||
groundFlipped ||
flightState._cockpitContactMode
) {
parts.rendering._applyFleetBillboardPresentation(icao24, bb); parts.rendering._applyFleetBillboardPresentation(icao24, bb);
} }
// Per-class GLBs: a class change can mean a different asset OR scale — // Per-class GLBs: a class change can mean a different asset OR scale —
// resync the live model, any in-flight load, and the tracked // resync the live model, any in-flight load, and the tracked
// standalone (mirror of flights.js). // standalone (mirror of flights.js).
if (prevMeta?.klass !== meta.klass) parts.rendering._syncModelToClass(icao24); if (prevMeta?.klass !== meta.klass)
parts.rendering._syncModelToClass(icao24);
// Round 5: depth policy is uniform (always depth-test-free, see // Round 5: depth policy is uniform (always depth-test-free, see
// _groundDepthDistance) — nothing to flip on landing/takeoff. // _groundDepthDistance) — nothing to flip on landing/takeoff.
} else { } else {
const bb = flightState._billboardCollection.add({ const bb = flightState._billboardCollection.add({
position, position,
image: aircraftIcon(parts.rendering._iconKind(icao24, meta.klass)), image: aircraftIcon(
parts.rendering._iconKind(icao24, meta.klass),
),
width: isTracked ? 24 : 20, width: isTracked ? 24 : 20,
height: isTracked ? 24 : 20, height: isTracked ? 24 : 20,
scale: BILLBOARD_SCALE * (CLASS_SCALE_2D[meta.klass] || 1) * (meta.onGround ? GROUND_SCALE : 1), scale:
BILLBOARD_SCALE *
(CLASS_SCALE_2D[meta.klass] || 1) *
(meta.onGround ? GROUND_SCALE : 1),
// Screen-projected rotation lands on the next fleet tick. // Screen-projected rotation lands on the next fleet tick.
rotation: 0, rotation: 0,
alignedAxis: Cesium.Cartesian3.ZERO, alignedAxis: Cesium.Cartesian3.ZERO,
color: isTracked ? TRACKED_ICON_COLOR : MIL_ICON_COLOR, color: isTracked ? TRACKED_ICON_COLOR : MIL_ICON_COLOR,
sizeInMeters: false, sizeInMeters: false,
scaleByDistance: parts.rendering._normalBillboardScaleByDistance(), scaleByDistance:
parts.rendering._normalBillboardScaleByDistance(),
// Grounded/near-surface planes sit at/below the photoreal tile // Grounded/near-surface planes sit at/below the photoreal tile
// surface — render them depth-test-free so they never vanish up // surface — render them depth-test-free so they never vanish up
// close (_groundDepthDistance). // close (_groundDepthDistance).
@@ -308,7 +378,8 @@ const methods = {
// Takeoff while TRACKED: ground traffic drew no trail, so start one // Takeoff while TRACKED: ground traffic drew no trail, so start one
// from the fresh airborne history (touchdown needs no action — the // from the fresh airborne history (touchdown needs no action — the
// append gate above simply freezes the existing trail). // append gate above simply freezes the existing trail).
if (isTracked && groundFlipped && !meta.onGround) parts.tracking._startTrail(icao24); if (isTracked && groundFlipped && !meta.onGround)
parts.tracking._startTrail(icao24);
// If this is the tracked aircraft, update label text // If this is the tracked aircraft, update label text
// (position updates automatically via dead-reckoning CallbackProperty) // (position updates automatically via dead-reckoning CallbackProperty)
@@ -331,17 +402,30 @@ const methods = {
for (const [icao24, info] of flightState._flightData) { for (const [icao24, info] of flightState._flightData) {
if (!info?.onGround || currentIcaos.has(icao24)) continue; if (!info?.onGround || currentIcaos.has(icao24)) continue;
if (parts.rendering._modelOwnsVisual(icao24)) continue; if (parts.rendering._modelOwnsVisual(icao24)) continue;
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon)) continue; if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon))
continue;
const floor = cachedGroundFloor(info.rawLat, info.rawLon); const floor = cachedGroundFloor(info.rawLat, info.rawLon);
if (!Number.isFinite(floor)) continue; if (!Number.isFinite(floor)) continue;
const lifted = floor + GROUND_FLOOR_LIFT_M; const lifted = floor + GROUND_FLOOR_LIFT_M;
if (Number.isFinite(info.renderAltitudeM) && info.renderAltitudeM >= floor - 1) continue; if (
Number.isFinite(info.renderAltitudeM) &&
info.renderAltitudeM >= floor - 1
)
continue;
info.renderAltitudeM = lifted; info.renderAltitudeM = lifted;
info.cullPosition = null; info.cullPosition = null;
const liftedPos = Cesium.Cartesian3.fromDegrees(info.rawLon, info.rawLat, lifted); const liftedPos = Cesium.Cartesian3.fromDegrees(
info.rawLon,
info.rawLat,
lifted,
);
const hist = flightState._positionHistory.get(icao24); const hist = flightState._positionHistory.get(icao24);
const newest = hist?.[hist.length - 1]; const newest = hist?.[hist.length - 1];
if (newest) newest.position = Cesium.Cartesian3.clone(liftedPos, newest.position); if (newest)
newest.position = Cesium.Cartesian3.clone(
liftedPos,
newest.position,
);
const bbStale = flightState._billboards.get(icao24); const bbStale = flightState._billboards.get(icao24);
if (bbStale) bbStale.position = liftedPos; if (bbStale) bbStale.position = liftedPos;
} }
@@ -349,11 +433,20 @@ const methods = {
// budget-capped, viewer-proximate, google-3d regime only), excluding // budget-capped, viewer-proximate, google-3d regime only), excluding
// this layer's own billboards/models from the probe. // this layer's own billboards/models from the probe.
{ {
const viewerCarto = flightState._viewer?.camera?.positionCartographic || null; const viewerCarto =
flightState._viewer?.camera?.positionCartographic || null;
sampleMeshFloorCells(flightState._viewer?.scene, _floorWarmPoints, { sampleMeshFloorCells(flightState._viewer?.scene, _floorWarmPoints, {
excludeObjects: [...flightState._billboards.values(), ...flightState._models.values(), flightState._trackedModel].filter(Boolean), excludeObjects: [
viewerLat: viewerCarto ? Cesium.Math.toDegrees(viewerCarto.latitude) : undefined, ...flightState._billboards.values(),
viewerLon: viewerCarto ? Cesium.Math.toDegrees(viewerCarto.longitude) : undefined, ...flightState._models.values(),
flightState._trackedModel,
].filter(Boolean),
viewerLat: viewerCarto
? Cesium.Math.toDegrees(viewerCarto.latitude)
: undefined,
viewerLon: viewerCarto
? Cesium.Math.toDegrees(viewerCarto.longitude)
: undefined,
}); });
} }
@@ -366,12 +459,23 @@ const methods = {
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, bb] of flightState._billboards) {
if (currentIcaos.has(icao24)) continue; if (currentIcaos.has(icao24)) continue;
// Partial admissions do not prove absence, but stale retention is bounded. // Partial admissions do not prove absence, but stale retention is bounded.
if (!snapshot.complete && Date.now() - (flightState._flightData.get(icao24)?.observedReceiptMs ?? 0) < 300000) continue; if (
!snapshot.complete &&
Date.now() -
(flightState._flightData.get(icao24)?.observedReceiptMs ?? 0) <
300000
)
continue;
const misses = (flightState._missingPolls.get(icao24) || 0) + 1; const misses = (flightState._missingPolls.get(icao24) || 0) + 1;
const limit = parts.queries._likelyLanded(icao24) ? LANDED_MISSING_POLL_LIMIT : MISSING_POLL_LIMIT; const limit = parts.queries._likelyLanded(icao24)
? LANDED_MISSING_POLL_LIMIT
: MISSING_POLL_LIMIT;
if (misses < limit) { if (misses < limit) {
flightState._missingPolls.set(icao24, misses); flightState._missingPolls.set(icao24, misses);
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) { if (
icao24 === flightState._trackedIcao &&
flightState._trackedEntity
) {
// Honest readout: the tracked plane has no faded billboard (its // Honest readout: the tracked plane has no faded billboard (its
// entity owns the visual), so refresh the label — with icao24 now // entity owns the visual), so refresh the label — with icao24 now
// in _missingPolls, _buildTrackedLabel appends the STALE cue so // in _missingPolls, _buildTrackedLabel appends the STALE cue so
@@ -418,21 +522,25 @@ const methods = {
}; };
console.log(`[Data:Military] Updated: ${flightState._count} aircraft`); console.log(`[Data:Military] Updated: ${flightState._count} aircraft`);
parts.tracking._applyPendingTrackingRestore(); parts.tracking._applyPendingTrackingRestore();
} catch (e) { } catch (e) {
if (updateSignal.aborted || e?.name === 'AbortError') { if (updateSignal.aborted || e?.name === 'AbortError') {
throw new DOMException('Military update aborted', 'AbortError'); throw new DOMException('Military update aborted', 'AbortError');
} }
console.warn('[Data:Military] Fetch error:', e); console.warn('[Data:Military] Fetch error:', e);
flightState._backoff = true; flightState._backoff = true;
flightState._retryAt = Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL); flightState._retryAt =
Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
flightState._lastStatus = e?.status ?? null; flightState._lastStatus = e?.status ?? null;
if (e?.source) { flightState._lastSource = e.source; this.source = flightState._lastSource; } if (e?.source) {
flightState._lastError = e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable'; flightState._lastSource = e.source;
this.source = flightState._lastSource;
}
flightState._lastError =
e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
} finally { } finally {
flightState._activeUpdateControllers.delete(resourceController); flightState._activeUpdateControllers.delete(resourceController);
} }
} },
}; };
return { methods }; return { methods };
+87 -35
View File
@@ -1,7 +1,13 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { JET_MODEL_URL } from './policy.js'; import { JET_MODEL_URL } from './policy.js';
export function createLifecycle({ flightState, services, parts, layer, resolveAsset }) { export function createLifecycle({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
const { clearFocusTarget } = services.focus; const { clearFocusTarget } = services.focus;
const { registerSpriteCollection, restoreSpriteOrder } = services.sprites; const { registerSpriteCollection, restoreSpriteOrder } = services.sprites;
const { holdContinuousRender, releaseContinuousRender } = services.render; const { holdContinuousRender, releaseContinuousRender } = services.render;
@@ -10,27 +16,29 @@ const { registerPickOwner, unregisterPickOwner } = services.picking;
const { setMilitaryLayerActive } = services.militaryRegistry; const { setMilitaryLayerActive } = services.militaryRegistry;
const methods = { const methods = {
/** Configure the source before initialization; an active layer keeps its owner. */ /** Configure the source before initialization; an active layer keeps its owner. */
setSource(source) { setSource(source) {
if (flightState._viewer) throw new Error('Configure the source before layer initialization'); if (flightState._viewer)
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required'); throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required');
flightState._source = source; flightState._source = source;
flightState._lastSource = source.label || flightState._lastSource; flightState._lastSource = source.label || flightState._lastSource;
this.source = flightState._lastSource; this.source = flightState._lastSource;
}, },
/** /**
* Initialize the layer: create the billboard collection, reset all state, * Initialize the layer: create the billboard collection, reset all state,
* and install the click-to-track handler. * and install the click-to-track handler.
* @param {Cesium.Viewer} viewer - The Cesium viewer instance * @param {Cesium.Viewer} viewer - The Cesium viewer instance
*/ */
init(viewer) { init(viewer) {
if (flightState._viewer) throw new Error('Flight layer is already initialized'); if (flightState._viewer)
if (typeof flightState._source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required'); throw new Error('Flight layer is already initialized');
if (flightState.lifetime.signal.aborted) flightState.lifetime = new AbortController(); if (typeof flightState._source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required');
if (flightState.lifetime.signal.aborted)
flightState.lifetime = new AbortController();
clearFocusTarget('militaryFlights'); clearFocusTarget('militaryFlights');
flightState._viewer = viewer; flightState._viewer = viewer;
flightState._billboardCollection = new Cesium.BillboardCollection(); flightState._billboardCollection = new Cesium.BillboardCollection();
@@ -43,12 +51,25 @@ const methods = {
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle. // destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
if (!flightState._preloadModel) { if (!flightState._preloadModel) {
const epoch = flightState._modelEpoch; const epoch = flightState._modelEpoch;
Cesium.Model.fromGltfAsync({ url: resolveAsset(JET_MODEL_URL), asynchronous: false }) Cesium.Model.fromGltfAsync({
.then((m) => { url: resolveAsset(JET_MODEL_URL),
if (epoch === flightState._modelEpoch) { flightState._preloadModel = m; flightState._planeModelLoaded = true; } asynchronous: false,
else { try { m.destroy(); } catch { /* gone */ } }
}) })
.catch(() => { /* tracked plane just stays a billboard a beat longer */ }); .then((m) => {
if (epoch === flightState._modelEpoch) {
flightState._preloadModel = m;
flightState._planeModelLoaded = true;
} else {
try {
m.destroy();
} catch {
/* gone */
}
}
})
.catch(() => {
/* tracked plane just stays a billboard a beat longer */
});
} }
flightState._billboards = new Map(); flightState._billboards = new Map();
flightState._detectionObjects = new Map(); flightState._detectionObjects = new Map();
@@ -66,11 +87,16 @@ const methods = {
parts.tracking._resetTrackedSelectionState(); parts.tracking._resetTrackedSelectionState();
flightState._trackedEntity = null; flightState._trackedEntity = null;
flightState._cockpitSubjectId = null; flightState._cockpitSubjectId = null;
flightState._cockpitContactMode = document.body.classList.contains('cockpit-mode'); flightState._cockpitContactMode =
document.body.classList.contains('cockpit-mode');
flightState._cockpitNearContacts = new Set(); flightState._cockpitNearContacts = new Set();
if (!flightState._cockpitModeListener) { if (!flightState._cockpitModeListener) {
flightState._cockpitModeListener = (event) => parts.tracking._applyCockpitState(event?.detail); flightState._cockpitModeListener = (event) =>
window.addEventListener('gev:cockpit-mode-changed', flightState._cockpitModeListener); parts.tracking._applyCockpitState(event?.detail);
window.addEventListener(
'gev:cockpit-mode-changed',
flightState._cockpitModeListener,
);
} }
parts.tracking._installClickHandler(viewer); parts.tracking._installClickHandler(viewer);
@@ -80,36 +106,50 @@ const methods = {
console.log('[Data:Military] Initialized with billboard icons'); console.log('[Data:Military] Initialized with billboard icons');
}, },
/** /**
* Show the layer and re-install the click handler. * Show the layer and re-install the click handler.
* @param {Cesium.Viewer} viewer - The Cesium viewer instance * @param {Cesium.Viewer} viewer - The Cesium viewer instance
*/ */
enable(viewer) { enable(viewer) {
if (flightState._billboardCollection) flightState._billboardCollection.show = true; if (flightState._billboardCollection)
flightState._billboardCollection.show = true;
holdContinuousRender('military'); // per-frame animator (perf wave 2) holdContinuousRender('military'); // per-frame animator (perf wave 2)
if (flightState._modelCollection) flightState._modelCollection.show = true; if (flightState._modelCollection)
parts.tracking._setCockpitContactMode(document.body.classList.contains('cockpit-mode')); flightState._modelCollection.show = true;
parts.tracking._setCockpitContactMode(
document.body.classList.contains('cockpit-mode'),
);
// Height-datum fix: warm the geoid grid once per layer-enable. The poll loop // Height-datum fix: warm the geoid grid once per layer-enable. The poll loop
// only reads geoidHeight() synchronously after this resolves (guarded by // only reads geoidHeight() synchronously after this resolves (guarded by
// _geoidReady) — never awaited per-aircraft, never blocking a poll tick. // _geoidReady) — never awaited per-aircraft, never blocking a poll tick.
if (!flightState._geoidReady) { if (!flightState._geoidReady) {
const lifetime = flightState.lifetime; const lifetime = flightState.lifetime;
ensureGeoidReady() ensureGeoidReady()
.then(() => { if (!lifetime.signal.aborted) flightState._geoidReady = true; }) .then(() => {
.catch(() => { /* geoid grid failed to load — baro path stays un-geoid-corrected until retried */ }); if (!lifetime.signal.aborted) flightState._geoidReady = true;
})
.catch(() => {
/* geoid grid failed to load — baro path stays un-geoid-corrected until retried */
});
} }
parts.tracking._installClickHandler(viewer); parts.tracking._installClickHandler(viewer);
registerPickOwner('military', (pickedId) => flightState._billboards.has(pickedId)); registerPickOwner('military', (pickedId) =>
flightState._billboards.has(pickedId),
);
// The flights layer suppresses its military duplicates while we render them // The flights layer suppresses its military duplicates while we render them
setMilitaryLayerActive(true); setMilitaryLayerActive(true);
// Force a fresh rotation pass on the first tick after re-enable // Force a fresh rotation pass on the first tick after re-enable
flightState._lastCamPoseSig = ''; flightState._lastCamPoseSig = '';
if (!flightState._preRenderRemove && viewer?.scene) { if (!flightState._preRenderRemove && viewer?.scene) {
flightState._preRenderRemove = viewer.scene.preRender.addEventListener(parts.rendering._fleetTick); flightState._preRenderRemove = viewer.scene.preRender.addEventListener(
parts.rendering._fleetTick,
);
} }
if (!flightState._trackedModelPreUpdateRemove && viewer?.scene) { if (!flightState._trackedModelPreUpdateRemove && viewer?.scene) {
flightState._trackedModelPreUpdateRemove = viewer.scene.preUpdate.addEventListener(parts.rendering._updateTrackedModel); flightState._trackedModelPreUpdateRemove =
viewer.scene.preUpdate.addEventListener(
parts.rendering._updateTrackedModel,
);
} }
if (!flightState._moveEndRemove && viewer?.camera) { if (!flightState._moveEndRemove && viewer?.camera) {
// Arrival polish (mirror of flights.js — see the comment there): a settled // Arrival polish (mirror of flights.js — see the comment there): a settled
@@ -117,15 +157,16 @@ const methods = {
// the pose-signature gate can eat the settle (final easing frames land // the pose-signature gate can eat the settle (final easing frames land
// inside one quantization bucket) and leave stale noses for up to // inside one quantization bucket) and leave stale noses for up to
// ROTATION_REFRESH_MS. One extra pass per gesture — nothing per-frame. // ROTATION_REFRESH_MS. One extra pass per gesture — nothing per-frame.
flightState._moveEndRemove = viewer.camera.moveEnd.addEventListener(() => { flightState._moveEndRemove = viewer.camera.moveEnd.addEventListener(
() => {
flightState._lastCamPoseSig = ''; flightState._lastCamPoseSig = '';
flightState._lastFleetTickMs = 0; flightState._lastFleetTickMs = 0;
}); },
);
} }
restoreSpriteOrder(viewer); restoreSpriteOrder(viewer);
}, },
/** /**
* Hide the layer, clear any active tracking, and remove input handlers * Hide the layer, clear any active tracking, and remove input handlers
* so clicks do not get intercepted while the layer is off. * so clicks do not get intercepted while the layer is off.
@@ -134,10 +175,12 @@ const methods = {
disable(viewer) { disable(viewer) {
parts.controller._abortActiveUpdates(); parts.controller._abortActiveUpdates();
parts.tracking._cancelPendingTrackingRestore(); parts.tracking._cancelPendingTrackingRestore();
if (flightState._billboardCollection) flightState._billboardCollection.show = false; if (flightState._billboardCollection)
flightState._billboardCollection.show = false;
releaseContinuousRender('military'); releaseContinuousRender('military');
parts.rendering._releaseModels(); parts.rendering._releaseModels();
if (flightState._modelCollection) flightState._modelCollection.show = false; if (flightState._modelCollection)
flightState._modelCollection.show = false;
parts.tracking._clearTracking(); parts.tracking._clearTracking();
parts.tracking._destroyTrail(); parts.tracking._destroyTrail();
// Remove click handler + keydown listener while disabled to avoid // Remove click handler + keydown listener while disabled to avoid
@@ -168,7 +211,6 @@ const methods = {
} }
}, },
/** /**
* Tear down the layer: clear tracking, remove handlers, remove the billboard * Tear down the layer: clear tracking, remove handlers, remove the billboard
* collection from the scene, and release all state. * collection from the scene, and release all state.
@@ -191,7 +233,10 @@ const methods = {
} }
document.removeEventListener('keydown', parts.tracking._onKeyDown); document.removeEventListener('keydown', parts.tracking._onKeyDown);
if (flightState._cockpitModeListener) { if (flightState._cockpitModeListener) {
window.removeEventListener('gev:cockpit-mode-changed', flightState._cockpitModeListener); window.removeEventListener(
'gev:cockpit-mode-changed',
flightState._cockpitModeListener,
);
flightState._cockpitModeListener = null; flightState._cockpitModeListener = null;
} }
unregisterPickOwner('military'); unregisterPickOwner('military');
@@ -219,7 +264,14 @@ const methods = {
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy) flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
flightState._modelPending.clear(); flightState._modelPending.clear();
flightState._modelGen.clear(); flightState._modelGen.clear();
if (flightState._preloadModel) { try { flightState._preloadModel.destroy(); } catch { /* gone */ } flightState._preloadModel = null; } if (flightState._preloadModel) {
try {
flightState._preloadModel.destroy();
} catch {
/* gone */
}
flightState._preloadModel = null;
}
flightState._planeModelLoaded = false; flightState._planeModelLoaded = false;
flightState._billboards.clear(); flightState._billboards.clear();
flightState._detectionObjects.clear(); flightState._detectionObjects.clear();
@@ -242,7 +294,7 @@ const methods = {
}; };
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
flightState._viewer = null; flightState._viewer = null;
} },
}; };
return { methods }; return { methods };
+202 -62
View File
@@ -1,13 +1,43 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { lerpAngleDeg, speedRamp, courseBetweenCartesians, COURSE_HOLD_SPEED_MPS, staleCoastLimitSeconds, arcOffsetEnu, courseSlewCapDps, limitCourseStep } from '../../data/motionModel.js'; import {
lerpAngleDeg,
speedRamp,
courseBetweenCartesians,
COURSE_HOLD_SPEED_MPS,
staleCoastLimitSeconds,
arcOffsetEnu,
courseSlewCapDps,
limitCourseStep,
} from '../../data/motionModel.js';
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js'; import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
import { modelVisualAnchor, modelAnchorWorld } from '../../data/modelVisualAnchor.js'; import {
import { RENDER_DELAY_SEC, TRACKED_MODEL_MAX_PX, TRACKED_MODEL_MIN_PX, BILLBOARD_SCALE, TRACKED_BILLBOARD_SCALE_BY_DISTANCE, DR_CORRECTION_MS, COURSE_SLEW_DT_MAX_SEC, COURSE_MAX_DPS } from './policy.js'; modelVisualAnchor,
modelAnchorWorld,
} from '../../data/modelVisualAnchor.js';
import {
RENDER_DELAY_SEC,
TRACKED_MODEL_MAX_PX,
TRACKED_MODEL_MIN_PX,
BILLBOARD_SCALE,
TRACKED_BILLBOARD_SCALE_BY_DISTANCE,
DR_CORRECTION_MS,
COURSE_SLEW_DT_MAX_SEC,
COURSE_MAX_DPS,
} from './policy.js';
export function createMotion({ flightState, services, parts, layer, resolveAsset }) { export function createMotion({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
const { trackedModelScaleForPixelCap } = services.camera; const { trackedModelScaleForPixelCap } = services.camera;
const { nearFarScalarValueAtDistance, clearFocusTarget, publishFocusTargetFromCachedPosition } = services.focus; const {
nearFarScalarValueAtDistance,
clearFocusTarget,
publishFocusTargetFromCachedPosition,
} = services.focus;
/** /**
* Dead-reckon an aircraft's current position using ENU (East-North-Up) frame math. * Dead-reckon an aircraft's current position using ENU (East-North-Up) frame math.
@@ -27,7 +57,9 @@ function _deadReckon(icao24, result) {
const history = flightState._positionHistory.get(icao24); const history = flightState._positionHistory.get(icao24);
const info = flightState._flightData.get(icao24); const info = flightState._flightData.get(icao24);
if (!history || history.length === 0) { if (!history || history.length === 0) {
flightState._drCourseDeg = null; flightState._drSpeedMps = null; flightState._drCourseHold = false; flightState._drCourseDeg = null;
flightState._drSpeedMps = null;
flightState._drCourseHold = false;
return null; return null;
} }
@@ -35,7 +67,9 @@ function _deadReckon(icao24, result) {
// Render one poll interval behind real time so we interpolate between // Render one poll interval behind real time so we interpolate between
// two KNOWN fixes whenever possible (see RENDER_DELAY_SEC rationale). // two KNOWN fixes whenever possible (see RENDER_DELAY_SEC rationale).
const renderTime = Cesium.JulianDate.addSeconds( const renderTime = Cesium.JulianDate.addSeconds(
Cesium.JulianDate.now(), -RENDER_DELAY_SEC, flightState._scratchRenderTime Cesium.JulianDate.now(),
-RENDER_DELAY_SEC,
flightState._scratchRenderTime,
); );
// Bracketing pair: interpolate — no extrapolation error, no snap-back. // Bracketing pair: interpolate — no extrapolation error, no snap-back.
@@ -47,7 +81,8 @@ function _deadReckon(icao24, result) {
Cesium.JulianDate.lessThanOrEquals(renderTime, b.time) Cesium.JulianDate.lessThanOrEquals(renderTime, b.time)
) { ) {
const span = Cesium.JulianDate.secondsDifference(b.time, a.time); const span = Cesium.JulianDate.secondsDifference(b.time, a.time);
const t = span > 0 const t =
span > 0
? Cesium.JulianDate.secondsDifference(renderTime, a.time) / span ? Cesium.JulianDate.secondsDifference(renderTime, a.time) / span
: 1.0; : 1.0;
// Course of the DISPLAYED motion. The chord is only trustworthy when the // Course of the DISPLAYED motion. The chord is only trustworthy when the
@@ -59,14 +94,19 @@ function _deadReckon(icao24, result) {
// of snapping once per poll. Helicopters always use the reported track // of snapping once per poll. Helicopters always use the reported track
// (rotorcraft chords are noise-dominated at their typical speeds). // (rotorcraft chords are noise-dominated at their typical speeds).
const chordLenM = Cesium.Cartesian3.distance(a.position, b.position); const chordLenM = Cesium.Cartesian3.distance(a.position, b.position);
const segSpeed = span > 0 ? chordLenM / span : ((info && info.speedMps) || 0); const segSpeed =
span > 0 ? chordLenM / span : (info && info.speedMps) || 0;
const fallbackTrack = (info && info.track) || 0; const fallbackTrack = (info && info.track) || 0;
const trackFrom = Number.isFinite(a.track) ? a.track : fallbackTrack; const trackFrom = Number.isFinite(a.track) ? a.track : fallbackTrack;
const trackTo = Number.isFinite(b.track) ? b.track : trackFrom; const trackTo = Number.isFinite(b.track) ? b.track : trackFrom;
const trackCourse = lerpAngleDeg(trackFrom, trackTo, t); const trackCourse = lerpAngleDeg(trackFrom, trackTo, t);
const w = (info && info.klass === 'helicopter') ? 0 : speedRamp(segSpeed); const w = info && info.klass === 'helicopter' ? 0 : speedRamp(segSpeed);
const chordCourse = w > 0 ? courseBetweenCartesians(a.position, b.position) : null; const chordCourse =
flightState._drCourseDeg = chordCourse != null ? lerpAngleDeg(trackCourse, chordCourse, w) : trackCourse; w > 0 ? courseBetweenCartesians(a.position, b.position) : null;
flightState._drCourseDeg =
chordCourse != null
? lerpAngleDeg(trackCourse, chordCourse, w)
: trackCourse;
flightState._drSpeedMps = segSpeed; flightState._drSpeedMps = segSpeed;
flightState._drCourseHold = segSpeed < COURSE_HOLD_SPEED_MPS; flightState._drCourseHold = segSpeed < COURSE_HOLD_SPEED_MPS;
return Cesium.Cartesian3.lerp(a.position, b.position, t, out); return Cesium.Cartesian3.lerp(a.position, b.position, t, out);
@@ -74,7 +114,10 @@ function _deadReckon(icao24, result) {
} }
const newest = history[history.length - 1]; const newest = history[history.length - 1];
const elapsedSec = Cesium.JulianDate.secondsDifference(renderTime, newest.time); const elapsedSec = Cesium.JulianDate.secondsDifference(
renderTime,
newest.time,
);
if (elapsedSec <= 0) { if (elapsedSec <= 0) {
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking // Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
// aircraft, before RENDER_DELAY_SEC of history has accumulated, so no bracketing // aircraft, before RENDER_DELAY_SEC of history has accumulated, so no bracketing
@@ -84,8 +127,17 @@ function _deadReckon(icao24, result) {
// the bracketing interpolation above with NO freeze and NO backward snap. (Holding // the bracketing interpolation above with NO freeze and NO backward snap. (Holding
// the oldest fix froze the icon until enough history accrued, then jumped.) // the oldest fix froze the icon until enough history accrued, then jumped.)
const oldest = history[0]; const oldest = history[0];
const lookbackSec = Cesium.JulianDate.secondsDifference(oldest.time, renderTime); // ≥ 0 const lookbackSec = Cesium.JulianDate.secondsDifference(
return _extrapolateFix(oldest, info, -Math.min(lookbackSec, 60), out, (info && info.turnRateDps) || 0); oldest.time,
renderTime,
); // ≥ 0
return _extrapolateFix(
oldest,
info,
-Math.min(lookbackSec, 60),
out,
(info && info.turnRateDps) || 0,
);
} }
// Continue through fresh adsb.lol contacts even when the position epoch has // Continue through fresh adsb.lol contacts even when the position epoch has
@@ -107,7 +159,6 @@ function _deadReckon(icao24, result) {
); );
} }
/** /**
* Dead-reckon a fix along its own velocity/track by `dt` seconds, integrating a * Dead-reckon a fix along its own velocity/track by `dt` seconds, integrating a
* constant-rate-turn arc when `turnRateDps` is significant (straight line * constant-rate-turn arc when `turnRateDps` is significant (straight line
@@ -120,8 +171,12 @@ function _deadReckon(icao24, result) {
*/ */
function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) { function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) {
const speed = Number.isFinite(fix.velocity) ? fix.velocity : ((info && info.speedMps) || 0); const speed = Number.isFinite(fix.velocity)
const heading = Number.isFinite(fix.track) ? fix.track : ((info && info.track) || 0); ? fix.velocity
: (info && info.speedMps) || 0;
const heading = Number.isFinite(fix.track)
? fix.track
: (info && info.track) || 0;
flightState._drSpeedMps = speed; flightState._drSpeedMps = speed;
flightState._drCourseHold = speed < COURSE_HOLD_SPEED_MPS; flightState._drCourseHold = speed < COURSE_HOLD_SPEED_MPS;
if (speed === 0 || dt === 0) { if (speed === 0 || dt === 0) {
@@ -132,12 +187,20 @@ function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) {
// standard-rate turn is ~90° of arc wrong per 30 s if extrapolated straight. // standard-rate turn is ~90° of arc wrong per 30 s if extrapolated straight.
arcOffsetEnu(speed, heading, turnRateDps, dt, flightState._scratchArc); arcOffsetEnu(speed, heading, turnRateDps, dt, flightState._scratchArc);
flightState._drCourseDeg = flightState._scratchArc.endCourseDeg; flightState._drCourseDeg = flightState._scratchArc.endCourseDeg;
Cesium.Cartesian3.fromElements(flightState._scratchArc.east, flightState._scratchArc.north, 0, flightState._scratchOffset); Cesium.Cartesian3.fromElements(
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(fix.position, Cesium.Ellipsoid.WGS84, flightState._scratchEnu); flightState._scratchArc.east,
flightState._scratchArc.north,
0,
flightState._scratchOffset,
);
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(
fix.position,
Cesium.Ellipsoid.WGS84,
flightState._scratchEnu,
);
return Cesium.Matrix4.multiplyByPoint(enu, flightState._scratchOffset, out); return Cesium.Matrix4.multiplyByPoint(enu, flightState._scratchOffset, out);
} }
/** /**
* True while the tracked aircraft's delayed display time (now − RENDER_DELAY_SEC) * True while the tracked aircraft's delayed display time (now − RENDER_DELAY_SEC)
* predates its oldest real fix — _deadReckon is extrapolating backward with no real * predates its oldest real fix — _deadReckon is extrapolating backward with no real
@@ -150,12 +213,13 @@ function _isTrackWarmingUp() {
const history = flightState._positionHistory.get(flightState._trackedIcao); const history = flightState._positionHistory.get(flightState._trackedIcao);
if (!history || history.length === 0) return true; if (!history || history.length === 0) return true;
const renderTime = Cesium.JulianDate.addSeconds( const renderTime = Cesium.JulianDate.addSeconds(
Cesium.JulianDate.now(), -RENDER_DELAY_SEC, flightState._scratchWarmupTime Cesium.JulianDate.now(),
-RENDER_DELAY_SEC,
flightState._scratchWarmupTime,
); );
return Cesium.JulianDate.lessThan(renderTime, history[0].time); return Cesium.JulianDate.lessThan(renderTime, history[0].time);
} }
/** Resolve the selected aircraft's actual rendered square extent this frame. */ /** Resolve the selected aircraft's actual rendered square extent this frame. */
function _trackedFocusSizePx(icao24, position) { function _trackedFocusSizePx(icao24, position) {
@@ -164,7 +228,9 @@ function _trackedFocusSizePx(icao24, position) {
if (!camera?.positionWC || !position || !scene) return 28; if (!camera?.positionWC || !position || !scene) return 28;
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position); const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
if (parts.rendering._modelOwnsVisual(icao24)) { if (parts.rendering._modelOwnsVisual(icao24)) {
const spec = parts.rendering._modelSpec(flightState._flightData.get(icao24)?.klass); const spec = parts.rendering._modelSpec(
flightState._flightData.get(icao24)?.klass,
);
const scale = trackedModelScaleForPixelCap({ const scale = trackedModelScaleForPixelCap({
baseScale: spec.scale, baseScale: spec.scale,
nativeRadiusM: spec.nativeRadiusM, nativeRadiusM: spec.nativeRadiusM,
@@ -173,24 +239,31 @@ function _trackedFocusSizePx(icao24, position) {
fovyRad: camera.frustum.fovy, fovyRad: camera.frustum.fovy,
maximumPixelSize: TRACKED_MODEL_MAX_PX, maximumPixelSize: TRACKED_MODEL_MAX_PX,
}); });
const focalLengthPx = scene.canvas.clientHeight / (2 * Math.tan(camera.frustum.fovy / 2)); const focalLengthPx =
const projectedDiameterPx = (2 * spec.nativeRadiusM * scale * focalLengthPx) / rangeM; scene.canvas.clientHeight / (2 * Math.tan(camera.frustum.fovy / 2));
return Math.max(TRACKED_MODEL_MIN_PX, Math.min(TRACKED_MODEL_MAX_PX, projectedDiameterPx)); const projectedDiameterPx =
(2 * spec.nativeRadiusM * scale * focalLengthPx) / rangeM;
return Math.max(
TRACKED_MODEL_MIN_PX,
Math.min(TRACKED_MODEL_MAX_PX, projectedDiameterPx),
);
} }
const billboard = flightState._trackedEntity?.billboard; const billboard = flightState._trackedEntity?.billboard;
const time = flightState._viewer.clock.currentTime; const time = flightState._viewer.clock.currentTime;
const width = billboard?.width?.getValue(time) ?? 28; const width = billboard?.width?.getValue(time) ?? 28;
const height = billboard?.height?.getValue(time) ?? 28; const height = billboard?.height?.getValue(time) ?? 28;
const scale = billboard?.scale?.getValue(time) const scale =
?? (BILLBOARD_SCALE * (CLASS_SCALE_2D[flightState._flightData.get(icao24)?.klass] || 1)); billboard?.scale?.getValue(time) ??
const scaleByDistance = billboard?.scaleByDistance?.getValue(time) BILLBOARD_SCALE *
?? TRACKED_BILLBOARD_SCALE_BY_DISTANCE; (CLASS_SCALE_2D[flightState._flightData.get(icao24)?.klass] || 1);
const scaleByDistance =
billboard?.scaleByDistance?.getValue(time) ??
TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM); const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM);
return Math.max(width, height) * scale * distanceScale; return Math.max(width, height) * scale * distanceScale;
} }
/** /**
* Per-frame-cached, discontinuity-smoothed tracked DISPLAY position. Cached by Cesium * Per-frame-cached, discontinuity-smoothed tracked DISPLAY position. Cached by Cesium
* frame number so the position / rotation / trail-head callbacks share ONE computation * frame number so the position / rotation / trail-head callbacks share ONE computation
@@ -202,11 +275,16 @@ function _trackedFocusSizePx(icao24, position) {
function _trackedDisplayPosition(icao24) { function _trackedDisplayPosition(icao24) {
const frame = flightState._viewer?.scene?.frameState?.frameNumber ?? -1; const frame = flightState._viewer?.scene?.frameState?.frameNumber ?? -1;
if (frame === flightState._cachedDRFrame && icao24 === flightState._drReconcileIcao) return flightState._cachedDRPosition; if (
frame === flightState._cachedDRFrame &&
icao24 === flightState._drReconcileIcao
)
return flightState._cachedDRPosition;
// Does the reconciliation state belong to THIS aircraft? (Capture before overwriting // Does the reconciliation state belong to THIS aircraft? (Capture before overwriting
// _drReconcileIcao, so a track switch doesn't inherit the old plane's _drPrevRaw.) // _drReconcileIcao, so a track switch doesn't inherit the old plane's _drPrevRaw.)
const sameTrack = flightState._drReconcileValid && flightState._drReconcileIcao === icao24; const sameTrack =
flightState._drReconcileValid && flightState._drReconcileIcao === icao24;
const raw = _deadReckon(icao24, flightState._scratchDrRaw); const raw = _deadReckon(icao24, flightState._scratchDrRaw);
flightState._cachedDRCourse = flightState._drCourseDeg; flightState._cachedDRCourse = flightState._drCourseDeg;
flightState._cachedDRSpeedMps = flightState._drSpeedMps; flightState._cachedDRSpeedMps = flightState._drSpeedMps;
@@ -229,7 +307,11 @@ function _trackedDisplayPosition(icao24) {
const plausible = speed * dtSec * 4 + 25; const plausible = speed * dtSec * 4 + 25;
if (Cesium.Cartesian3.distance(raw, flightState._drPrevRaw) > plausible) { if (Cesium.Cartesian3.distance(raw, flightState._drPrevRaw) > plausible) {
// Discontinuity — re-anchor so the DISPLAYED position stays continuous, then decay. // Discontinuity — re-anchor so the DISPLAYED position stays continuous, then decay.
Cesium.Cartesian3.subtract(flightState._drPrevDisplay, raw, flightState._drCorrection); Cesium.Cartesian3.subtract(
flightState._drPrevDisplay,
raw,
flightState._drCorrection,
);
flightState._drCorrectionStartMs = nowMs; flightState._drCorrectionStartMs = nowMs;
} }
} else { } else {
@@ -238,8 +320,13 @@ function _trackedDisplayPosition(icao24) {
} }
const elapsed = nowMs - flightState._drCorrectionStartMs; const elapsed = nowMs - flightState._drCorrectionStartMs;
const factor = elapsed >= DR_CORRECTION_MS ? 0 : 1 - elapsed / DR_CORRECTION_MS; const factor =
const display = Cesium.Cartesian3.multiplyByScalar(flightState._drCorrection, factor, flightState._trackedPosHolder); elapsed >= DR_CORRECTION_MS ? 0 : 1 - elapsed / DR_CORRECTION_MS;
const display = Cesium.Cartesian3.multiplyByScalar(
flightState._drCorrection,
factor,
flightState._trackedPosHolder,
);
Cesium.Cartesian3.add(raw, display, display); Cesium.Cartesian3.add(raw, display, display);
Cesium.Cartesian3.clone(raw, flightState._drPrevRaw); Cesium.Cartesian3.clone(raw, flightState._drPrevRaw);
@@ -247,7 +334,10 @@ function _trackedDisplayPosition(icao24) {
flightState._drPrevMs = nowMs; flightState._drPrevMs = nowMs;
flightState._drReconcileValid = true; flightState._drReconcileValid = true;
flightState._cachedDRPosition = display; flightState._cachedDRPosition = display;
const focusSizePx = _trackedFocusSizePx(icao24, flightState._cachedDRPosition); const focusSizePx = _trackedFocusSizePx(
icao24,
flightState._cachedDRPosition,
);
// Publish the exact frame cache shared by entity + follow camera. A second // Publish the exact frame cache shared by entity + follow camera. A second
// DR sample here would advance reconciliation in a different frame phase // DR sample here would advance reconciliation in a different frame phase
// and visibly jitter the focus rectangle against the tracked aircraft. // and visibly jitter the focus rectangle against the tracked aircraft.
@@ -263,17 +353,18 @@ function _trackedDisplayPosition(icao24) {
return display; return display;
} }
/** The tracked plane's display position WITHOUT recomputing — the value the follow-camera already /** The tracked plane's display position WITHOUT recomputing — the value the follow-camera already
* settled on this frame. Mirror of flights.js: getDetectableObjects + the readout run in postRender at * settled on this frame. Mirror of flights.js: getDetectableObjects + the readout run in postRender at
* a later frameNumber, so recomputing _trackedDisplayPosition there jitters the label against the * a later frameNumber, so recomputing _trackedDisplayPosition there jitters the label against the
* now-stable plane. Null when no valid fix (callers fall back to the billboard position). */ * now-stable plane. Null when no valid fix (callers fall back to the billboard position). */
function _trackedDisplayCached() { function _trackedDisplayCached() {
return (flightState._drReconcileValid && flightState._drReconcileIcao === flightState._trackedIcao) ? flightState._cachedDRPosition : null; return flightState._drReconcileValid &&
flightState._drReconcileIcao === flightState._trackedIcao
? flightState._cachedDRPosition
: null;
} }
/** /**
* The position the tracked aircraft is VISUALLY at this frame — the translation its 3D * The position the tracked aircraft is VISUALLY at this frame — the translation its 3D
* 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
@@ -284,19 +375,25 @@ function _trackedDisplayCached() {
*/ */
function _trackedVisualCached() { function _trackedVisualCached() {
if (flightState._trackedIcao && parts.rendering._modelOwnsVisual(flightState._trackedIcao)) { if (
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass); flightState._trackedIcao &&
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
) {
const spec = parts.rendering._modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass,
);
return modelVisualAnchor( return modelVisualAnchor(
flightState._trackedModel.modelMatrix, flightState._trackedModel.modelMatrix,
spec.visualCenterNative, spec.visualCenterNative,
Number.isFinite(flightState._trackedModel.computedScale) ? flightState._trackedModel.computedScale : spec.scale, Number.isFinite(flightState._trackedModel.computedScale)
? flightState._trackedModel.computedScale
: spec.scale,
flightState._trackedVisualPos, flightState._trackedVisualPos,
); );
} }
return _trackedDisplayCached(); return _trackedDisplayCached();
} }
/** The tracked model's rendered bounding radius (m), or 0 when no model of this /** The tracked model's rendered bounding radius (m), or 0 when no model of this
* contact is drawing. Carries Cesium's effective `computedScale`, which may be * contact is drawing. Carries Cesium's effective `computedScale`, which may be
* above `scale` to satisfy minimumPixelSize — the trail head has to be judged * above `scale` to satisfy minimumPixelSize — the trail head has to be judged
@@ -306,37 +403,57 @@ function _trackedVisualCached() {
* measured from, so the trail clip and the envelope agree on one frame. */ * measured from, so the trail clip and the envelope agree on one frame. */
function _trackedModelCenterWorld() { function _trackedModelCenterWorld() {
if (!flightState._trackedIcao || !flightState._trackedModel || !parts.rendering._modelOwnsVisual(flightState._trackedIcao)) return null; if (
return Cesium.Matrix4.getTranslation(flightState._trackedModel.modelMatrix, flightState._scratchTrailClip); !flightState._trackedIcao ||
!flightState._trackedModel ||
!parts.rendering._modelOwnsVisual(flightState._trackedIcao)
)
return null;
return Cesium.Matrix4.getTranslation(
flightState._trackedModel.modelMatrix,
flightState._scratchTrailClip,
);
} }
function _trackedModelEnvelopeM() { function _trackedModelEnvelopeM() {
if (!flightState._trackedIcao || !flightState._trackedModel || !parts.rendering._modelOwnsVisual(flightState._trackedIcao)) return 0; if (
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass); !flightState._trackedIcao ||
!flightState._trackedModel ||
!parts.rendering._modelOwnsVisual(flightState._trackedIcao)
)
return 0;
const spec = parts.rendering._modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass,
);
const scale = Number.isFinite(flightState._trackedModel.computedScale) const scale = Number.isFinite(flightState._trackedModel.computedScale)
? flightState._trackedModel.computedScale ? flightState._trackedModel.computedScale
: spec.scale; : spec.scale;
return spec.nativeRadiusM * scale; return spec.nativeRadiusM * scale;
} }
/** Trail endpoint for the rendered tracked owner. Brackets/readouts stay on /** Trail endpoint for the rendered tracked owner. Brackets/readouts stay on
* the visual centre; only the trail moves to the model's aft-belly hardpoint. */ * the visual centre; only the trail moves to the model's aft-belly hardpoint. */
function _trackedTrailCached() { function _trackedTrailCached() {
if (flightState._trackedIcao && parts.rendering._modelOwnsVisual(flightState._trackedIcao)) { if (
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass); flightState._trackedIcao &&
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
) {
const spec = parts.rendering._modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass,
);
// Through the model's OWN render chain (modelVisualAnchor's hand-rolled // Through the model's OWN render chain (modelVisualAnchor's hand-rolled
// half-correction put this offset on the lateral axis — see // half-correction put this offset on the lateral axis — see
// modelAnchorWorld). // modelAnchorWorld).
return modelAnchorWorld(flightState._trackedModel, spec.trailAnchorNative, flightState._trackedTrailPos); return modelAnchorWorld(
flightState._trackedModel,
spec.trailAnchorNative,
flightState._trackedTrailPos,
);
} }
return _trackedDisplayCached(); return _trackedDisplayCached();
} }
/** Smoothed world course for the tracked aircraft this frame. Reads the /** Smoothed world course for the tracked aircraft this frame. Reads the
* frame-cached course (set by _trackedDisplayPosition — the follow-camera's * frame-cached course (set by _trackedDisplayPosition — the follow-camera's
* own computation), NEVER re-runs _deadReckon. Safe to call more than once * own computation), NEVER re-runs _deadReckon. Safe to call more than once
@@ -351,24 +468,34 @@ function _trackedTrailCached() {
function _trackedDisplayCourse() { function _trackedDisplayCourse() {
const info = flightState._flightData.get(flightState._trackedIcao); const info = flightState._flightData.get(flightState._trackedIcao);
const fallback = (info && info.track) || 0; const fallback = (info && info.track) || 0;
const cacheValid = flightState._drReconcileValid && flightState._drReconcileIcao === flightState._trackedIcao && flightState._cachedDRCourse != null; const cacheValid =
flightState._drReconcileValid &&
flightState._drReconcileIcao === flightState._trackedIcao &&
flightState._cachedDRCourse != null;
const raw = cacheValid ? flightState._cachedDRCourse : fallback; const raw = cacheValid ? flightState._cachedDRCourse : fallback;
const nowMs = Date.now(); const nowMs = Date.now();
const dt = flightState._trackedCourseMs const dt = flightState._trackedCourseMs
? Math.min(COURSE_SLEW_DT_MAX_SEC, (nowMs - flightState._trackedCourseMs) / 1000) ? Math.min(
COURSE_SLEW_DT_MAX_SEC,
(nowMs - flightState._trackedCourseMs) / 1000,
)
: 0; : 0;
flightState._trackedCourseMs = nowMs; flightState._trackedCourseMs = nowMs;
const prev = flightState._displayCourse.get(flightState._trackedIcao); const prev = flightState._displayCourse.get(flightState._trackedIcao);
// Hover hold: at near-zero displayed speed both the chord and the reported // Hover hold: at near-zero displayed speed both the chord and the reported
// track are noise — keep the last stable nose direction instead of chasing. // track are noise — keep the last stable nose direction instead of chasing.
if (cacheValid && flightState._cachedDRHold && prev != null) return prev; if (cacheValid && flightState._cachedDRHold && prev != null) return prev;
const cap = courseSlewCapDps(cacheValid ? flightState._cachedDRSpeedMps : ((info && info.speedMps) ?? NaN), COURSE_MAX_DPS); const cap = courseSlewCapDps(
cacheValid
? flightState._cachedDRSpeedMps
: ((info && info.speedMps) ?? NaN),
COURSE_MAX_DPS,
);
const course = limitCourseStep(prev, raw, cap, dt); const course = limitCourseStep(prev, raw, cap, dt);
flightState._displayCourse.set(flightState._trackedIcao, course); flightState._displayCourse.set(flightState._trackedIcao, course);
return course; return course;
} }
/** Reset reconciliation + per-frame cache when tracking stops or switches /** Reset reconciliation + per-frame cache when tracking stops or switches
* target. Deliberately does NOT touch _displayCourse: the smoothed course * target. Deliberately does NOT touch _displayCourse: the smoothed course
* entry belongs to the AIRCRAFT (not to the tracked session) and must * entry belongs to the AIRCRAFT (not to the tracked session) and must
@@ -384,5 +511,18 @@ function _resetTrackedDisplay() {
flightState._cachedDRHold = false; flightState._cachedDRHold = false;
flightState._trackedCourseMs = 0; flightState._trackedCourseMs = 0;
} }
return { _deadReckon, _extrapolateFix, _isTrackWarmingUp, _trackedFocusSizePx, _trackedDisplayPosition, _trackedDisplayCached, _trackedVisualCached, _trackedModelCenterWorld, _trackedModelEnvelopeM, _trackedTrailCached, _trackedDisplayCourse, _resetTrackedDisplay }; return {
_deadReckon,
_extrapolateFix,
_isTrackWarmingUp,
_trackedFocusSizePx,
_trackedDisplayPosition,
_trackedDisplayCached,
_trackedVisualCached,
_trackedModelCenterWorld,
_trackedModelEnvelopeM,
_trackedTrailCached,
_trackedDisplayCourse,
_resetTrackedDisplay,
};
} }
+62 -16
View File
@@ -4,24 +4,51 @@ import { createMilitaryFlightLayer } from './index.js';
import { createFlightState } from './state.js'; import { createFlightState } from './state.js';
function services() { function services() {
const names = ['picking', 'sprites', 'trails', 'aircraftPresentation', 'camera', const names = [
'militaryRegistry', 'labels', 'groundFloor', 'meshFloor', 'geoid', 'focus', 'picking',
'readout', 'context', 'render', 'recession']; 'sprites',
return { ...Object.fromEntries(names.map((name) => [name, {}])), 'trails',
groundSnap: { createGroundSnap: () => ({ clear() {} }) } }; 'aircraftPresentation',
'camera',
'militaryRegistry',
'labels',
'groundFloor',
'meshFloor',
'geoid',
'focus',
'readout',
'context',
'render',
'recession',
];
return {
...Object.fromEntries(names.map((name) => [name, {}])),
groundSnap: { createGroundSnap: () => ({ clear() {} }) },
};
} }
test('military layer instances isolate policy and restoration state without requesting a source', async () => { test('military layer instances isolate policy and restoration state without requesting a source', async () => {
let requests = 0; let requests = 0;
const source = { label: 'Fixture aircraft', getSnapshot() { requests++; } }; const source = {
label: 'Fixture aircraft',
getSnapshot() {
requests++;
},
};
const first = createMilitaryFlightLayer({ source, services: services() }); const first = createMilitaryFlightLayer({ source, services: services() });
const second = createMilitaryFlightLayer({ source, services: services() }); const second = createMilitaryFlightLayer({ source, services: services() });
first.setParams({ models3dMode: 'all' }); first.setParams({ models3dMode: 'all' });
assert.equal(first.getParams().models3dMode, 'all'); assert.equal(first.getParams().models3dMode, 'all');
assert.equal(second.getParams().models3dMode, 'proximity'); assert.equal(second.getParams().models3dMode, 'proximity');
first.testing._setMilitaryTrackingRefreshOutcomeForTest({ ids: [] }); first.testing._setMilitaryTrackingRefreshOutcomeForTest({ ids: [] });
assert.equal((await first.resolveTrackingRestoreTarget('abc123')).status, 'missing'); assert.equal(
assert.equal((await second.resolveTrackingRestoreTarget('abc123')).status, 'source-unavailable'); (await first.resolveTrackingRestoreTarget('abc123')).status,
'missing',
);
assert.equal(
(await second.resolveTrackingRestoreTarget('abc123')).status,
'source-unavailable',
);
assert.equal(requests, 0); assert.equal(requests, 0);
}); });
@@ -33,8 +60,16 @@ test('military source omission fails before viewer initialization', () => {
test('military state owns separate contact maps, motion scratch and ground sampling', () => { test('military state owns separate contact maps, motion scratch and ground sampling', () => {
const first = createFlightState({ services: services() }); const first = createFlightState({ services: services() });
const second = createFlightState({ services: services() }); const second = createFlightState({ services: services() });
for (const key of ['_flightData', '_billboards', '_positionHistory', '_groundSnap', for (const key of [
'_scratchCarto', '_models', '_activeUpdateControllers', 'lifetime']) { '_flightData',
'_billboards',
'_positionHistory',
'_groundSnap',
'_scratchCarto',
'_models',
'_activeUpdateControllers',
'lifetime',
]) {
assert.notEqual(first[key], second[key], key); assert.notEqual(first[key], second[key], key);
} }
}); });
@@ -45,12 +80,23 @@ test('a normalized source can retain its stale reason without changing standalon
supplied.groundFloor.warmGroundFloor = async () => {}; supplied.groundFloor.warmGroundFloor = async () => {};
supplied.meshFloor.sampleMeshFloorCells = () => {}; supplied.meshFloor.sampleMeshFloorCells = () => {};
supplied.militaryRegistry.registerMilitaryIcaos = () => {}; supplied.militaryRegistry.registerMilitaryIcaos = () => {};
const layer = createMilitaryFlightLayer({ services: supplied, source: { const layer = createMilitaryFlightLayer({
label: 'Fixture aircraft', async getSnapshot() { return { services: supplied,
source: 'Fixture aircraft', records: [], complete: true, source: {
observedAtMs: 123000, stale: true, freshness: 'stale', reason, label: 'Fixture aircraft',
}; }, async getSnapshot() {
} }); return {
source: 'Fixture aircraft',
records: [],
complete: true,
observedAtMs: 123000,
stale: true,
freshness: 'stale',
reason,
};
},
},
});
await layer.update({}); await layer.update({});
assert.equal(layer.getStats().error, reason); assert.equal(layer.getStats().error, reason);
assert.equal(layer.getStats().lastUpdate, 123000); assert.equal(layer.getStats().lastUpdate, 123000);
+4 -18
View File
@@ -1,6 +1,5 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
/** /**
* @module militaryFlights * @module militaryFlights
* @description Real-time military flight tracking layer powered by the adsb.lol API. * @description Real-time military flight tracking layer powered by the adsb.lol API.
@@ -30,7 +29,6 @@ export const POSITION_HISTORY_LIMIT = 5;
export const BILLBOARD_SCALE = 0.7; export const BILLBOARD_SCALE = 0.7;
/** @constant {Cesium.Color} Default amber tint for untracked military billboards */ /** @constant {Cesium.Color} Default amber tint for untracked military billboards */
export const MIL_ICON_COLOR = Cesium.Color.fromCssColorString('#FFB800'); export const MIL_ICON_COLOR = Cesium.Color.fromCssColorString('#FFB800');
@@ -39,7 +37,6 @@ export const MIL_ICON_COLOR = Cesium.Color.fromCssColorString('#FFB800');
export const TRACKED_ICON_COLOR = Cesium.Color.fromCssColorString('#FFD166'); export const TRACKED_ICON_COLOR = Cesium.Color.fromCssColorString('#FFD166');
// --- Ground traffic (owner reversal 2026-07-03; mirror of flights.js) --------------- // --- Ground traffic (owner reversal 2026-07-03; mirror of flights.js) ---------------
// adsb.lol/readsb flags ground traffic with alt_baro === "ground" (no separate // adsb.lol/readsb flags ground traffic with alt_baro === "ground" (no separate
// boolean). Such aircraft are RENDERED instead of floating at the 3 km altitude // boolean). Such aircraft are RENDERED instead of floating at the 3 km altitude
@@ -58,7 +55,6 @@ export const TRACKED_ICON_COLOR = Cesium.Color.fromCssColorString('#FFD166');
export const GROUND_SCALE = 0.8; export const GROUND_SCALE = 0.8;
// --- 3D model rendering (mirrors flights.js) ----------------------------------------- // --- 3D model rendering (mirrors flights.js) -----------------------------------------
// When enabled, military aircraft render as 3D glTF jet models once the camera is below // When enabled, military aircraft render as 3D glTF jet models once the camera is below
// MODEL_ALT_CEIL_M (zoomed in); higher up they stay flat billboards. Eligibility is // MODEL_ALT_CEIL_M (zoomed in); higher up they stay flat billboards. Eligibility is
@@ -69,7 +65,6 @@ export const GROUND_SCALE = 0.8;
export const JET_MODEL_URL = '/models/jet.glb'; export const JET_MODEL_URL = '/models/jet.glb';
export const MODEL_ALT_CEIL_M = 800000; export const MODEL_ALT_CEIL_M = 800000;
// m: below this camera altitude, draw 3D models (raised so it's easy to trigger) // m: below this camera altitude, draw 3D models (raised so it's easy to trigger)
@@ -134,13 +129,10 @@ export const MODEL_COLOR_BLEND_AMOUNT = 0.94;
export const PLANE_MODEL_URL = '/models/airplane.glb'; export const PLANE_MODEL_URL = '/models/airplane.glb';
export const PLANE_MODEL_SCALE = 1; export const PLANE_MODEL_SCALE = 1;
export const PLANE_NATIVE_RADIUS_M = 34.41; export const PLANE_NATIVE_RADIUS_M = 34.41;
export const PLANE_BELLY_OFFSET_NATIVE = 6.719; export const PLANE_BELLY_OFFSET_NATIVE = 6.719;
// Grounded-model belly offset: jet.glb's centred origin sits 5.631 native units (= meters — this // Grounded-model belly offset: jet.glb's centred origin sits 5.631 native units (= meters — this
@@ -156,16 +148,15 @@ export const MODEL_BELLY_OFFSET_NATIVE = 5.631;
export const AMBER_TRANSPARENT = MIL_ICON_COLOR.withAlpha(0); export const AMBER_TRANSPARENT = MIL_ICON_COLOR.withAlpha(0);
export const COCKPIT_CONTACT_SIZE_PX = 6; export const COCKPIT_CONTACT_SIZE_PX = 6;
export const TRACKED_BILLBOARD_SCALE_BY_DISTANCE = new Cesium.NearFarScalar( export const TRACKED_BILLBOARD_SCALE_BY_DISTANCE = new Cesium.NearFarScalar(
1000, 3.0, 8000000, 0.5, 1000,
3.0,
8000000,
0.5,
); );
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Track-history trail state (PRD WS-F F2/F4): a fading polyline behind the // Track-history trail state (PRD WS-F F2/F4): a fading polyline behind the
// tracked aircraft. The accumulation array is intentionally SEPARATE from // tracked aircraft. The accumulation array is intentionally SEPARATE from
@@ -181,7 +172,6 @@ export const TRAIL_COLOR = '#FFB800';
export const TRAIL_MAX_POINTS = 400; export const TRAIL_MAX_POINTS = 400;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Render-behind smoothing (mirrors flights.js): the fleet renders at // Render-behind smoothing (mirrors flights.js): the fleet renders at
// now - RENDER_DELAY_SEC so positions interpolate BETWEEN two known fixes // now - RENDER_DELAY_SEC so positions interpolate BETWEEN two known fixes
@@ -229,7 +219,6 @@ export const LANDED_MISSING_POLL_LIMIT = 1;
export const GROUND_FLOOR_WARM_MAX_ALT_M = 4500; export const GROUND_FLOOR_WARM_MAX_ALT_M = 4500;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Nadir-stable icon orientation (mirrors flights.js): surface-normal alignment // Nadir-stable icon orientation (mirrors flights.js): surface-normal alignment
// degenerates when the camera looks straight down (the normal is parallel to // degenerates when the camera looks straight down (the normal is parallel to
@@ -258,7 +247,6 @@ export const COURSE_MAX_DPS = 60;
export const COURSE_SLEW_DT_MAX_SEC = 0.25; export const COURSE_SLEW_DT_MAX_SEC = 0.25;
// -- Tracked-display reconciliation (see flights.js for the rationale): absorb a raw // -- Tracked-display reconciliation (see flights.js for the rationale): absorb a raw
// position step (warm-up→interpolation handoff, feed glitch, backfill splice) into a // position step (warm-up→interpolation handoff, feed glitch, backfill splice) into a
// correction offset that decays to zero over DR_CORRECTION_MS, so the tracked icon, // correction offset that decays to zero over DR_CORRECTION_MS, so the tracked icon,
@@ -266,7 +254,6 @@ export const COURSE_SLEW_DT_MAX_SEC = 0.25;
export const DR_CORRECTION_MS = 900; export const DR_CORRECTION_MS = 900;
/** Bounded on-demand loading for the tracked model (mirror of flights.js). The /** Bounded on-demand loading for the tracked model (mirror of flights.js). The
* tracked regime is DEFAULT-ON and its driver runs every `scene.preUpdate`, so * tracked regime is DEFAULT-ON and its driver runs every `scene.preUpdate`, so
* a missing or corrupt GLB — or a dead network — would otherwise spin * a missing or corrupt GLB — or a dead network — would otherwise spin
@@ -279,7 +266,6 @@ export const DR_CORRECTION_MS = 900;
export const TRACKED_MODEL_MAX_LOAD_FAILS = 3; export const TRACKED_MODEL_MAX_LOAD_FAILS = 3;
export const TRACKED_MODEL_RETRY_BACKOFF_MS = 1500; export const TRACKED_MODEL_RETRY_BACKOFF_MS = 1500;
/** Boost flip = BATCHED release through the fleet tick (mirror of flights.js — /** Boost flip = BATCHED release through the fleet tick (mirror of flights.js —
+238 -95
View File
@@ -3,15 +3,24 @@ import * as Cesium from 'cesium';
import { isExplicitLayerStateOrigin } from '../../data/layerState.js'; import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
import { aircraftIncludedInNearby } from '../../data/aircraftNearbyPolicy.js'; import { aircraftIncludedInNearby } from '../../data/aircraftNearbyPolicy.js';
import { modelVisualAnchor } from '../../data/modelVisualAnchor.js'; import { modelVisualAnchor } from '../../data/modelVisualAnchor.js';
import { rankContactMatch, contactMatchWins, CONTACT_MATCH_TIER } from '../../data/contactMatch.js'; import {
rankContactMatch,
contactMatchWins,
CONTACT_MATCH_TIER,
} from '../../data/contactMatch.js';
import { LANDED_ALT_MAX_FT, LANDED_SPEED_MAX_MPS } from './policy.js'; import { LANDED_ALT_MAX_FT, LANDED_SPEED_MAX_MPS } from './policy.js';
export function createQueries({ flightState, services, parts, layer, resolveAsset }) { export function createQueries({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
const { tr3bAircraftClass, tr3bTypeLabel } = services.aircraftPresentation; const { tr3bAircraftClass, tr3bTypeLabel } = services.aircraftPresentation;
const { formatFlightLevel } = services.labels; const { formatFlightLevel } = services.labels;
const { applyTrackedCameraFrame } = services.camera; const { applyTrackedCameraFrame } = services.camera;
/** /**
* Coerce a value to a finite number, returning null if not possible. * Coerce a value to a finite number, returning null if not possible.
* Handles both numeric and string inputs (e.g. API fields that may arrive as strings). * Handles both numeric and string inputs (e.g. API fields that may arrive as strings).
@@ -28,7 +37,6 @@ function _toFiniteNumber(value) {
return null; return null;
} }
/** /**
* Safely convert a value to a trimmed string, defaulting to '' for falsy inputs. * Safely convert a value to a trimmed string, defaulting to '' for falsy inputs.
* @param {*} value - Raw value to stringify * @param {*} value - Raw value to stringify
@@ -39,7 +47,6 @@ function _toCleanText(value) {
return String(value || '').trim(); return String(value || '').trim();
} }
/** /**
* Format an altitude value in feet for display, with fallback text. * Format an altitude value in feet for display, with fallback text.
* @param {number|null|undefined} altitudeFt - Barometric altitude in feet * @param {number|null|undefined} altitudeFt - Barometric altitude in feet
@@ -51,7 +58,6 @@ function _formatAltitude(altitudeFt) {
return `${Math.round(altitudeFt)} ft`; return `${Math.round(altitudeFt)} ft`;
} }
/** /**
* True when the aircraft's latest metadata reads "on or about the runway" * True when the aircraft's latest metadata reads "on or about the runway"
* (low + slow) — see the landed fast-cull rationale above. Both gates must * (low + slow) — see the landed fast-cull rationale above. Both gates must
@@ -68,11 +74,14 @@ function _likelyLanded(icao24) {
// flaps don't churn their identity/floor state. // flaps don't churn their identity/floor state.
if (info.wasAirborne !== true) return false; if (info.wasAirborne !== true) return false;
if (info.onGround) return true; if (info.onGround) return true;
return Number.isFinite(info.altitudeFt) && info.altitudeFt < LANDED_ALT_MAX_FT return (
&& Number.isFinite(info.speedMps) && info.speedMps < LANDED_SPEED_MAX_MPS; Number.isFinite(info.altitudeFt) &&
info.altitudeFt < LANDED_ALT_MAX_FT &&
Number.isFinite(info.speedMps) &&
info.speedMps < LANDED_SPEED_MAX_MPS
);
} }
/** /**
* Build a public descriptor for one aircraft using its best current position * Build a public descriptor for one aircraft using its best current position
* (dead-reckoned when history exists, billboard position otherwise). * (dead-reckoned when history exists, billboard position otherwise).
@@ -84,7 +93,8 @@ function _likelyLanded(icao24) {
function _describeFlight(icao24) { function _describeFlight(icao24) {
const info = flightState._flightData.get(icao24); const info = flightState._flightData.get(icao24);
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
const basePos = parts.motion._deadReckon(icao24) || (bb ? bb.position : null); const basePos =
parts.motion._deadReckon(icao24) || (bb ? bb.position : null);
if (!basePos) return null; if (!basePos) return null;
const displayed = displayedKinematics({ const displayed = displayedKinematics({
derivedSpeedMps: flightState._drSpeedMps, derivedSpeedMps: flightState._drSpeedMps,
@@ -92,7 +102,11 @@ function _describeFlight(icao24) {
reportedSpeedMps: info?.speedMps, reportedSpeedMps: info?.speedMps,
reportedTrackDeg: info?.track, reportedTrackDeg: info?.track,
}); });
const carto = Cesium.Cartographic.fromCartesian(basePos, Cesium.Ellipsoid.WGS84, flightState._scratchCarto); const carto = Cesium.Cartographic.fromCartesian(
basePos,
Cesium.Ellipsoid.WGS84,
flightState._scratchCarto,
);
if (!carto) return null; if (!carto) return null;
return { return {
icao24, icao24,
@@ -106,23 +120,28 @@ function _describeFlight(icao24) {
longitude: Cesium.Math.toDegrees(carto.longitude), longitude: Cesium.Math.toDegrees(carto.longitude),
// Keep the cockpit readout on the reported aviation altitude. Render // Keep the cockpit readout on the reported aviation altitude. Render
// terrain height is a separate visual datum and may be below zero. // terrain height is a separate visual datum and may be below zero.
altitudeM: Number.isFinite(info?.altitudeM) ? info.altitudeM : carto.height, altitudeM: Number.isFinite(info?.altitudeM)
renderAltitudeM: Number.isFinite(info?.renderAltitudeM) ? info.renderAltitudeM : carto.height, ? info.altitudeM
: carto.height,
renderAltitudeM: Number.isFinite(info?.renderAltitudeM)
? info.renderAltitudeM
: carto.height,
onGround: info?.onGround === true, onGround: info?.onGround === true,
velocityMps: displayed.speedMps, velocityMps: displayed.speedMps,
track: displayed.trackDeg, track: displayed.trackDeg,
stale: Boolean(flightState._missingPolls.get(icao24) || flightState._backoff), stale: Boolean(
flightState._missingPolls.get(icao24) || flightState._backoff,
),
}; };
} }
function _normalizeTrackedIcao(candidate) { function _normalizeTrackedIcao(candidate) {
const normalized = String(candidate ?? '').trim().toLowerCase(); const normalized = String(candidate ?? '')
.trim()
.toLowerCase();
return normalized || null; return normalized || null;
} }
/** /**
* Map one military aircraft's internal poll record to a plain JSON-safe * Map one military aircraft's internal poll record to a plain JSON-safe
* analyst record (analyst query engine seam) — same shape as the flights * analyst record (analyst query engine seam) — same shape as the flights
@@ -141,7 +160,10 @@ function _normalizeTrackedIcao(candidate) {
function mapAnalystRecord(icao24, info) { function mapAnalystRecord(icao24, info) {
const num = (v) => (Number.isFinite(v) ? v : null); const num = (v) => (Number.isFinite(v) ? v : null);
const text = (v) => { const t = String(v ?? '').trim(); return t || null; }; const text = (v) => {
const t = String(v ?? '').trim();
return t || null;
};
const callsign = text(info?.callsign); const callsign = text(info?.callsign);
return { return {
id: callsign || text(info?.registration) || icao24, id: callsign || text(info?.registration) || icao24,
@@ -151,7 +173,9 @@ function mapAnalystRecord(icao24, info) {
lon: num(info?.rawLon), lon: num(info?.rawLon),
// altitudeFt is the sticky barometric/MSL aviation field — converted to // altitudeFt is the sticky barometric/MSL aviation field — converted to
// meters here for shape parity with the flights layer. // meters here for shape parity with the flights layer.
altitudeM: Number.isFinite(info?.altitudeFt) ? info.altitudeFt * 0.3048 : null, altitudeM: Number.isFinite(info?.altitudeFt)
? info.altitudeFt * 0.3048
: null,
speedMps: num(info?.speedMps), speedMps: num(info?.speedMps),
heading: num(info?.track), heading: num(info?.track),
verticalRateMps: num(info?.verticalRateMps), verticalRateMps: num(info?.verticalRateMps),
@@ -166,7 +190,8 @@ function mapAnalystRecord(icao24, info) {
routeDestination: null, routeDestination: null,
}; };
} }
const methods = { mapAnalystRecord, const methods = {
mapAnalystRecord,
id: 'military', id: 'military',
@@ -179,19 +204,16 @@ const methods = { mapAnalystRecord,
/** @type {number} Polling interval in ms between API fetches */ /** @type {number} Polling interval in ms between API fetches */
updateInterval: 15000, updateInterval: 15000,
/** @deprecated Compatibility alias for {@link enable}. */ /** @deprecated Compatibility alias for {@link enable}. */
show(viewer) { show(viewer) {
this.enable(viewer); this.enable(viewer);
}, },
/** @deprecated Compatibility alias for {@link disable}. */ /** @deprecated Compatibility alias for {@link disable}. */
hide(viewer) { hide(viewer) {
this.disable(viewer); this.disable(viewer);
}, },
/** /**
* Live layer params. * Live layer params.
* `models3d` toggles 3D glTF jet-model rendering for the FLEET (altitude-gated): when on, * `models3d` toggles 3D glTF jet-model rendering for the FLEET (altitude-gated): when on,
@@ -202,21 +224,33 @@ const methods = { mapAnalystRecord,
* @param {{models3d?: boolean, models3dMode?: 'proximity'|'all', selectedMilitaryTrackingId?: string|null}} params * @param {{models3d?: boolean, models3dMode?: 'proximity'|'all', selectedMilitaryTrackingId?: string|null}} params
*/ */
setParams(params = {}, { origin = 'programmatic' } = {}) { setParams(params = {}, { origin = 'programmatic' } = {}) {
if (isExplicitLayerStateOrigin(origin) if (
&& !Object.hasOwn(params, 'selectedMilitaryTrackingId')) { isExplicitLayerStateOrigin(origin) &&
!Object.hasOwn(params, 'selectedMilitaryTrackingId')
) {
parts.tracking._cancelPendingTrackingRestore(); parts.tracking._cancelPendingTrackingRestore();
} }
if (typeof params.models3d === 'boolean' && params.models3d !== flightState._models3dEnabled) { if (
typeof params.models3d === 'boolean' &&
params.models3d !== flightState._models3dEnabled
) {
flightState._models3dEnabled = params.models3d; flightState._models3dEnabled = params.models3d;
if (!flightState._models3dEnabled) { if (!flightState._models3dEnabled) {
parts.rendering._releaseModels(); parts.rendering._releaseModels();
parts.tracking._syncTracked2dRotation(); parts.tracking._syncTracked2dRotation();
// Restore fleet billboards (horizon-cull re-asserts next tick), but NEVER the tracked // Restore fleet billboards (horizon-cull re-asserts next tick), but NEVER the tracked
// plane's own fleet billboard — its tracked entity is the visual (avoids a double-image). // plane's own fleet billboard — its tracked entity is the visual (avoids a double-image).
if (flightState._billboardCollection) for (const [icao, bb] of flightState._billboards) { if (icao !== flightState._trackedIcao) bb.show = true; } if (flightState._billboardCollection)
for (const [icao, bb] of flightState._billboards) {
if (icao !== flightState._trackedIcao) bb.show = true;
} }
} }
if ((params.models3dMode === 'proximity' || params.models3dMode === 'all') && params.models3dMode !== flightState._models3dMode) { }
if (
(params.models3dMode === 'proximity' ||
params.models3dMode === 'all') &&
params.models3dMode !== flightState._models3dMode
) {
// The next fleet tick re-derives the eligible set under the new cap and releases the overflow. // The next fleet tick re-derives the eligible set under the new cap and releases the overflow.
flightState._models3dMode = params.models3dMode; flightState._models3dMode = params.models3dMode;
if (flightState._cockpitContactMode) { if (flightState._cockpitContactMode) {
@@ -224,7 +258,10 @@ const methods = { mapAnalystRecord,
flightState._lastFleetTickMs = 0; flightState._lastFleetTickMs = 0;
} }
} }
if (typeof params.irBoost === 'boolean' && params.irBoost !== flightState._irBoost) { if (
typeof params.irBoost === 'boolean' &&
params.irBoost !== flightState._irBoost
) {
flightState._irBoost = params.irBoost; flightState._irBoost = params.irBoost;
parts.rendering._reloadModelsForIrBoost(); parts.rendering._reloadModelsForIrBoost();
// Sprites don't reload with the models — swap the TR-3B glyph between its // Sprites don't reload with the models — swap the TR-3B glyph between its
@@ -232,16 +269,24 @@ const methods = { mapAnalystRecord,
parts.tracking._refreshTr3bForStyle(); parts.tracking._refreshTr3bForStyle();
} }
if (Object.hasOwn(params, 'selectedMilitaryTrackingId')) { if (Object.hasOwn(params, 'selectedMilitaryTrackingId')) {
const requested = _normalizeTrackedIcao(params.selectedMilitaryTrackingId); const requested = _normalizeTrackedIcao(
params.selectedMilitaryTrackingId,
);
if (requested === flightState._trackedIcao) { if (requested === flightState._trackedIcao) {
flightState._pendingTrackingRestore = null; flightState._pendingTrackingRestore = null;
} else if (requested === null) { } else if (requested === null) {
parts.tracking._cancelPendingTrackingRestore(); parts.tracking._cancelPendingTrackingRestore();
if (flightState._trackedIcao) parts.tracking._clearTracking(false, { origin }); if (flightState._trackedIcao)
parts.tracking._clearTracking(false, { origin });
} else { } else {
const generation = ++flightState._trackingIntentGeneration; const generation = ++flightState._trackingIntentGeneration;
flightState._pendingTrackingRestore = { id: requested, generation, origin }; flightState._pendingTrackingRestore = {
if (flightState._trackedIcao) parts.tracking._clearTracking(false, { origin }); id: requested,
generation,
origin,
};
if (flightState._trackedIcao)
parts.tracking._clearTracking(false, { origin });
parts.tracking._applyPendingTrackingRestore(); parts.tracking._applyPendingTrackingRestore();
} }
} }
@@ -257,15 +302,15 @@ const methods = { mapAnalystRecord,
}; };
}, },
/** /**
* Re-render a contact whose TR-3B conversion just flipped (Easter egg). * Re-render a contact whose TR-3B conversion just flipped (Easter egg).
* Callers own the registry write; this only re-derives what renders. * Callers own the registry write; this only re-derives what renders.
* @param {string} icao24 - ICAO 24-bit address. * @param {string} icao24 - ICAO 24-bit address.
* @returns {boolean} True when this layer owns the contact. * @returns {boolean} True when this layer owns the contact.
*/ */
refreshTr3b(icao24) { return parts.tracking._refreshTr3bContact(icao24); }, refreshTr3b(icao24) {
return parts.tracking._refreshTr3bContact(icao24);
},
/** /**
* Find military aircraft near a given ECEF position, sorted by distance. * Find military aircraft near a given ECEF position, sorted by distance.
@@ -278,12 +323,18 @@ const methods = { mapAnalystRecord,
* @returns {Array<Object>} Sorted array of nearby aircraft descriptors * @returns {Array<Object>} Sorted array of nearby aircraft descriptors
*/ */
getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) { getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) {
if (!center || !flightState._billboardCollection || !flightState._billboardCollection.show) return []; if (
!center ||
!flightState._billboardCollection ||
!flightState._billboardCollection.show
)
return [];
const limit = Number.isFinite(maxCount) const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount)) ? Math.max(1, Math.floor(maxCount))
: 50; : 50;
const maxRange = Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY; const maxRange =
Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY;
const now = Cesium.JulianDate.now(); const now = Cesium.JulianDate.now();
const nearby = []; const nearby = [];
@@ -292,14 +343,19 @@ const methods = { mapAnalystRecord,
const isTracked = icao24 === flightState._trackedIcao; const isTracked = icao24 === flightState._trackedIcao;
// Keep planes rendered as a 3D model (billboard hidden) so proximity counts // Keep planes rendered as a 3D model (billboard hidden) so proximity counts
// don't drop to zero on the 2D→3D handoff; bb.position stays current while hidden. // don't drop to zero on the 2D→3D handoff; bb.position stays current while hidden.
if (!aircraftIncludedInNearby({ if (
!aircraftIncludedInNearby({
isTracked, isTracked,
billboardShown: bb.show, billboardShown: bb.show,
modelRendering: parts.rendering._modelOwnsVisual(icao24), modelRendering: parts.rendering._modelOwnsVisual(icao24),
includeHidden, includeHidden,
})) continue; })
)
continue;
const trackedPos = isTracked ? parts.motion._trackedDisplayCached() : null; // cached, no recompute (anti-jitter) const trackedPos = isTracked
? parts.motion._trackedDisplayCached()
: null; // cached, no recompute (anti-jitter)
const pos = trackedPos || bb.position; const pos = trackedPos || bb.position;
if (!pos) continue; if (!pos) continue;
@@ -314,7 +370,12 @@ const methods = { mapAnalystRecord,
distance, distance,
// Filter surface (mirror of flights.js): the cockpit next/previous path // Filter surface (mirror of flights.js): the cockpit next/previous path
// matches on this field, so it follows the conversion. // matches on this field, so it follows the conversion.
aircraftClass: tr3bAircraftClass(icao24, String(info?.klass || info?.type || '').trim().toLowerCase() || null), aircraftClass: tr3bAircraftClass(
icao24,
String(info?.klass || info?.type || '')
.trim()
.toLowerCase() || null,
),
track: info?.track ?? null, track: info?.track ?? null,
// Display type — converted too, so a Contacts row can't still name the // Display type — converted too, so a Contacts row can't still name the
// airframe the triangle replaced (the filter matcher reads this as a // airframe the triangle replaced (the filter matcher reads this as a
@@ -330,7 +391,6 @@ const methods = { mapAnalystRecord,
return nearby.slice(0, limit); return nearby.slice(0, limit);
}, },
/** /**
* Return a subset of aircraft suitable for the detection overlay system. * Return a subset of aircraft suitable for the detection overlay system.
* Uses a deterministic stride-based sampling to keep the count manageable * Uses a deterministic stride-based sampling to keep the count manageable
@@ -341,22 +401,33 @@ const methods = { mapAnalystRecord,
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>} * @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
*/ */
getDetectableObjects(options = {}) { getDetectableObjects(options = {}) {
if (!flightState._billboardCollection || !flightState._billboardCollection.show) return []; if (
!flightState._billboardCollection ||
!flightState._billboardCollection.show
)
return [];
const maxCount = Number.isFinite(options.maxCount) const maxCount = Number.isFinite(options.maxCount)
? Math.max(1, Math.floor(options.maxCount)) ? Math.max(1, Math.floor(options.maxCount))
: flightState._billboards.size; : flightState._billboards.size;
const seed = Number.isFinite(options.seed) ? Math.floor(options.seed) : 0; const seed = Number.isFinite(options.seed) ? Math.floor(options.seed) : 0;
// Deterministic stride: evenly space selections across the billboard map // Deterministic stride: evenly space selections across the billboard map
const stride = Math.max(1, Math.ceil(flightState._billboards.size / maxCount)); const stride = Math.max(
1,
Math.ceil(flightState._billboards.size / maxCount),
);
const start = seed % stride; const start = seed % stride;
const result = []; const result = [];
let idx = 0; let idx = 0;
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, bb] of flightState._billboards) {
const shouldTake = ((idx - start) % stride) === 0; const shouldTake = (idx - start) % stride === 0;
idx++; idx++;
if (!shouldTake) continue; if (!shouldTake) continue;
if (flightState._cockpitContactMode && icao24.toLowerCase() === flightState._cockpitSubjectId) continue; if (
flightState._cockpitContactMode &&
icao24.toLowerCase() === flightState._cockpitSubjectId
)
continue;
const isTracked = icao24 === flightState._trackedIcao; const isTracked = icao24 === flightState._trackedIcao;
// Keep planes rendered as a 3D model (billboard hidden) so the detection box // Keep planes rendered as a 3D model (billboard hidden) so the detection box
// doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden. // doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden.
@@ -369,7 +440,10 @@ const methods = { mapAnalystRecord,
let object = flightState._detectionObjects.get(icao24); let object = flightState._detectionObjects.get(icao24);
if (!object) { if (!object) {
object = { object = {
sourceId: icao24, type: 'AIR', tier: 'military', _weldPos: new Cesium.Cartesian3(), sourceId: icao24,
type: 'AIR',
tier: 'military',
_weldPos: new Cesium.Cartesian3(),
}; };
flightState._detectionObjects.set(icao24, object); flightState._detectionObjects.set(icao24, object);
} }
@@ -377,17 +451,21 @@ const methods = { mapAnalystRecord,
// contacts read the translation the fleet tick already wrote into their // contacts read the translation the fleet tick already wrote into their
// modelMatrix; sprite-owned contacts keep `bb.position`, where sprite and bracket // modelMatrix; sprite-owned contacts keep `bb.position`, where sprite and bracket
// are co-located anyway. // are co-located anyway.
const spec = modelOwnsVisual ? parts.rendering._modelSpec(info?.klass) : null; const spec = modelOwnsVisual
? parts.rendering._modelSpec(info?.klass)
: null;
const pos = isTracked const pos = isTracked
? (parts.motion._trackedVisualCached() || bb.position) ? parts.motion._trackedVisualCached() || bb.position
: (modelOwnsVisual : modelOwnsVisual
? modelVisualAnchor( ? modelVisualAnchor(
model.modelMatrix, model.modelMatrix,
spec.visualCenterNative, spec.visualCenterNative,
Number.isFinite(model.computedScale) ? model.computedScale : spec.scale, Number.isFinite(model.computedScale)
? model.computedScale
: spec.scale,
object._weldPos || (object._weldPos = new Cesium.Cartesian3()), object._weldPos || (object._weldPos = new Cesium.Cartesian3()),
) )
: bb.position); : bb.position;
if (!pos) continue; if (!pos) continue;
object.position = pos; object.position = pos;
object.skipLabel = isTracked; object.skipLabel = isTracked;
@@ -407,15 +485,18 @@ const methods = { mapAnalystRecord,
result.push(object); result.push(object);
if (result.length >= maxCount) break; if (result.length >= maxCount) break;
} }
if (flightState._detectionObjects.size > flightState._billboards.size + 512) { if (
flightState._detectionObjects.size >
flightState._billboards.size + 512
) {
for (const icao24 of flightState._detectionObjects.keys()) { for (const icao24 of flightState._detectionObjects.keys()) {
if (!flightState._billboards.has(icao24)) flightState._detectionObjects.delete(icao24); if (!flightState._billboards.has(icao24))
flightState._detectionObjects.delete(icao24);
} }
} }
return result; return result;
}, },
/** /**
* Find a single aircraft by free-text query. * Find a single aircraft by free-text query.
* Match priority: exact icao24 hex, exact callsign, callsign prefix, * Match priority: exact icao24 hex, exact callsign, callsign prefix,
@@ -425,8 +506,11 @@ const methods = { mapAnalystRecord,
* Best match with a cloned, dead-reckoned position, or null if none. * Best match with a cloned, dead-reckoned position, or null if none.
*/ */
findByQuery(query) { findByQuery(query) {
if (!flightState._flightData || flightState._flightData.size === 0) return null; if (!flightState._flightData || flightState._flightData.size === 0)
const q = String(query || '').trim().toLowerCase(); return null;
const q = String(query || '')
.trim()
.toLowerCase();
if (!q) return null; if (!q) return null;
// Registration is searched alongside callsign because it is what the // Registration is searched alongside callsign because it is what the
@@ -457,7 +541,6 @@ const methods = { mapAnalystRecord,
return best ? _describeFlight(best.id) : null; return best ? _describeFlight(best.id) : null;
}, },
/** /**
* Return id/label/position for up to maxCount currently rendered aircraft. * Return id/label/position for up to maxCount currently rendered aircraft.
* Cheap snapshot for voice-tool framing — billboard positions only, no * Cheap snapshot for voice-tool framing — billboard positions only, no
@@ -478,29 +561,49 @@ const methods = { mapAnalystRecord,
* holds no data and therefore cannot answer. * holds no data and therefore cannot answer.
*/ */
hasContact(icao24) { hasContact(icao24) {
if (!flightState._billboardCollection || !flightState._billboardCollection.show || flightState._billboards.size === 0) return null; if (
!flightState._billboardCollection ||
!flightState._billboardCollection.show ||
flightState._billboards.size === 0
)
return null;
if (!icao24) return false; if (!icao24) return false;
const id = String(icao24).trim(); const id = String(icao24).trim();
return flightState._billboards.has(id) || flightState._billboards.has(id.toLowerCase()); return (
flightState._billboards.has(id) ||
flightState._billboards.has(id.toLowerCase())
);
}, },
getAllPositions(maxCount = 500) { getAllPositions(maxCount = 500) {
if (!flightState._billboardCollection || flightState._billboards.size === 0) return []; if (
const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 500; !flightState._billboardCollection ||
flightState._billboards.size === 0
)
return [];
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
: 500;
const result = []; const result = [];
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, bb] of flightState._billboards) {
const pos = bb.position; const pos = bb.position;
if (!pos) continue; if (!pos) continue;
const carto = Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84, flightState._scratchCarto); const carto = Cesium.Cartographic.fromCartesian(
pos,
Cesium.Ellipsoid.WGS84,
flightState._scratchCarto,
);
if (!carto) continue; if (!carto) continue;
const info = flightState._flightData.get(icao24); const info = flightState._flightData.get(icao24);
result.push({ result.push({
id: icao24, // identity (trackById/Context resolve this) id: icao24, // identity (trackById/Context resolve this)
// Last surface still reading as the raw hex for a callsign-less // Last surface still reading as the raw hex for a callsign-less
// contact — same chain as getNearby/getDetectableObjects/getTrackedSubject. // contact — same chain as getNearby/getDetectableObjects/getTrackedSubject.
label: _toCleanText(info?.callsign) || _toCleanText(info?.registration) || icao24, label:
_toCleanText(info?.callsign) ||
_toCleanText(info?.registration) ||
icao24,
position: pos, position: pos,
latitude: Cesium.Math.toDegrees(carto.latitude), latitude: Cesium.Math.toDegrees(carto.latitude),
longitude: Cesium.Math.toDegrees(carto.longitude), longitude: Cesium.Math.toDegrees(carto.longitude),
@@ -511,7 +614,6 @@ const methods = { mapAnalystRecord,
return result; return result;
}, },
/** /**
* Snapshot the layer's in-memory records as plain JSON-safe objects for * Snapshot the layer's in-memory records as plain JSON-safe objects for
* the analyst query engine. On-demand only (called at most once per * the analyst query engine. On-demand only (called at most once per
@@ -521,8 +623,15 @@ const methods = { mapAnalystRecord,
* @returns {Array<Object>} See mapAnalystRecord for the record shape. * @returns {Array<Object>} See mapAnalystRecord for the record shape.
*/ */
getAnalystRecords(maxCount = 2000) { getAnalystRecords(maxCount = 2000) {
if (!flightState._billboardCollection || !flightState._billboardCollection.show || flightState._flightData.size === 0) return []; if (
const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 2000; !flightState._billboardCollection ||
!flightState._billboardCollection.show ||
flightState._flightData.size === 0
)
return [];
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
: 2000;
const result = []; const result = [];
for (const [icao24, info] of flightState._flightData) { for (const [icao24, info] of flightState._flightData) {
result.push(mapAnalystRecord(icao24, info)); result.push(mapAnalystRecord(icao24, info));
@@ -531,7 +640,6 @@ const methods = { mapAnalystRecord,
return result; return result;
}, },
/** /**
* Start camera-tracking an aircraft by ICAO hex identifier. * Start camera-tracking an aircraft by ICAO hex identifier.
* @param {string} icao24 - ICAO hex identifier of the aircraft. * @param {string} icao24 - ICAO hex identifier of the aircraft.
@@ -542,17 +650,27 @@ const methods = { mapAnalystRecord,
let id = String(icao24).trim(); let id = String(icao24).trim();
if (!flightState._billboards.has(id)) id = id.toLowerCase(); if (!flightState._billboards.has(id)) id = id.toLowerCase();
if (!flightState._billboards.has(id)) return false; if (!flightState._billboards.has(id)) return false;
if (parts.tracking._isExplicitTrackingOrigin(origin)) parts.tracking._cancelPendingTrackingRestore(); if (parts.tracking._isExplicitTrackingOrigin(origin))
if (flightState._trackedIcao === id) return parts.tracking._publishTrackedSelection(id, origin); parts.tracking._cancelPendingTrackingRestore();
if (flightState._trackedIcao === id)
return parts.tracking._publishTrackedSelection(id, origin);
parts.tracking._trackFlight(id, { origin }); parts.tracking._trackFlight(id, { origin });
return true; return true;
}, },
/** Resolve a shared Follow target only against the latest accepted refresh. */ /** Resolve a shared Follow target only against the latest accepted refresh. */
async resolveTrackingRestoreTarget(icao24, { signal = null, origin = 'share-restore' } = {}) { async resolveTrackingRestoreTarget(
if (signal?.aborted) return { status: 'cancelled', reason: String(signal.reason || 'aborted') }; icao24,
const id = String(icao24 ?? '').trim().toLowerCase(); { signal = null, origin = 'share-restore' } = {},
) {
if (signal?.aborted)
return {
status: 'cancelled',
reason: String(signal.reason || 'aborted'),
};
const id = String(icao24 ?? '')
.trim()
.toLowerCase();
if (!id) return { status: 'missing', reason: 'invalid-target' }; if (!id) return { status: 'missing', reason: 'invalid-target' };
const outcome = flightState._lastTrackingRefreshOutcome; const outcome = flightState._lastTrackingRefreshOutcome;
if (outcome.status !== 'accepted') { if (outcome.status !== 'accepted') {
@@ -571,28 +689,47 @@ const methods = { mapAnalystRecord,
source: outcome.source, source: outcome.source,
}; };
} }
if (signal?.aborted) return { status: 'cancelled', reason: String(signal.reason || 'aborted') }; if (signal?.aborted)
return {
status: 'cancelled',
reason: String(signal.reason || 'aborted'),
};
const followed = this.trackById(id, { origin }); const followed = this.trackById(id, { origin });
return followed return followed
? { status: 'found', refreshEpoch: outcome.epoch, source: outcome.source } ? {
: { status: 'source-unavailable', reason: 'target-not-renderable', refreshEpoch: outcome.epoch }; status: 'found',
refreshEpoch: outcome.epoch,
source: outcome.source,
}
: {
status: 'source-unavailable',
reason: 'target-not-renderable',
refreshEpoch: outcome.epoch,
};
}, },
/** Reapply the canonical follow frame without recreating the selected flight. */ /** Reapply the canonical follow frame without recreating the selected flight. */
refocusTrackedById(icao24, { origin = 'programmatic' } = {}) { refocusTrackedById(icao24, { origin = 'programmatic' } = {}) {
if (!icao24 || flightState._cockpitContactMode || !flightState._viewer || !flightState._trackedEntity) return false; if (
!icao24 ||
flightState._cockpitContactMode ||
!flightState._viewer ||
!flightState._trackedEntity
)
return false;
let id = String(icao24).trim(); let id = String(icao24).trim();
if (!flightState._billboards.has(id)) id = id.toLowerCase(); if (!flightState._billboards.has(id)) id = id.toLowerCase();
if ( if (
id !== flightState._trackedIcao id !== flightState._trackedIcao ||
|| !flightState._viewer.entities?.contains?.(flightState._trackedEntity) !flightState._viewer.entities?.contains?.(flightState._trackedEntity) ||
|| flightState._viewer.trackedEntity !== flightState._trackedEntity flightState._viewer.trackedEntity !== flightState._trackedEntity
) return false; )
return false;
flightState._trackedCameraFrameStop?.(); flightState._trackedCameraFrameStop?.();
flightState._viewer.camera.cancelFlight(); flightState._viewer.camera.cancelFlight();
flightState._viewer.trackedEntity = flightState._trackedEntity; flightState._viewer.trackedEntity = flightState._trackedEntity;
flightState._trackedCameraFrameStop = applyTrackedCameraFrame( flightState._trackedCameraFrameStop =
applyTrackedCameraFrame(
flightState._viewer, flightState._viewer,
flightState._trackedEntity, flightState._trackedEntity,
flightState._trackedEntity.viewFrom, flightState._trackedEntity.viewFrom,
@@ -601,7 +738,6 @@ const methods = { mapAnalystRecord,
return true; return true;
}, },
/** /**
* Stop tracking the currently followed aircraft (no-op if none). * Stop tracking the currently followed aircraft (no-op if none).
* @returns {boolean} Always true. * @returns {boolean} Always true.
@@ -612,12 +748,10 @@ const methods = { mapAnalystRecord,
return true; return true;
}, },
cancelPendingTrackingRestore() { cancelPendingTrackingRestore() {
parts.tracking._cancelPendingTrackingRestore(); parts.tracking._cancelPendingTrackingRestore();
}, },
/** /**
* Describe the currently tracked aircraft at its dead-reckoned position. * Describe the currently tracked aircraft at its dead-reckoned position.
* @returns {{icao24: string, callsign: string|null, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null} * @returns {{icao24: string, callsign: string|null, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
@@ -631,7 +765,6 @@ const methods = { mapAnalystRecord,
return rest; return rest;
}, },
/** /**
* Return the current aircraft as a Context subject without changing * Return the current aircraft as a Context subject without changing
* tracking or camera ownership. * tracking or camera ownership.
@@ -652,13 +785,14 @@ const methods = { mapAnalystRecord,
}; };
}, },
/** /**
* Return current layer health/status for the HUD status chip. * Return current layer health/status for the HUD status chip.
* @returns {{count: number, lastUpdate: number|null, stale: boolean, error: string|null, status: number|null, retryInSec: number}} * @returns {{count: number, lastUpdate: number|null, stale: boolean, error: string|null, status: number|null, retryInSec: number}}
*/ */
getStats() { getStats() {
const retryInSec = flightState._retryAt ? Math.max(0, Math.ceil((flightState._retryAt - Date.now()) / 1000)) : 0; const retryInSec = flightState._retryAt
? Math.max(0, Math.ceil((flightState._retryAt - Date.now()) / 1000))
: 0;
return { return {
count: flightState._count, count: flightState._count,
lastUpdate: flightState._lastUpdate, lastUpdate: flightState._lastUpdate,
@@ -669,8 +803,17 @@ const methods = { mapAnalystRecord,
source: flightState._lastSource, source: flightState._lastSource,
fallback: false, fallback: false,
}; };
} },
}; };
return { _toFiniteNumber, _toCleanText, _formatAltitude, _likelyLanded, _describeFlight, _normalizeTrackedIcao, mapAnalystRecord, methods }; return {
_toFiniteNumber,
_toCleanText,
_formatAltitude,
_likelyLanded,
_describeFlight,
_normalizeTrackedIcao,
mapAnalystRecord,
methods,
};
} }
File diff suppressed because it is too large. Load diff
+7 -50
View File
@@ -4,8 +4,6 @@ export function createFlightState({ source, services }) {
const { createGroundSnap } = services.groundSnap; const { createGroundSnap } = services.groundSnap;
const flightState = { lifetime: new AbortController() }; const flightState = { lifetime: new AbortController() };
flightState._source = source; flightState._source = source;
/** Per-class model spec for THIS layer (2026-08-16, owner playtest ask: /** Per-class model spec for THIS layer (2026-08-16, owner playtest ask:
@@ -24,10 +22,8 @@ flightState._specCache = new Map();
flightState._groundSnap = createGroundSnap(); flightState._groundSnap = createGroundSnap();
flightState._scratchGroundCarto = new Cesium.Cartographic(); flightState._scratchGroundCarto = new Cesium.Cartographic();
flightState._scratchGroundPos = new Cesium.Cartesian3(); flightState._scratchGroundPos = new Cesium.Cartesian3();
/** @type {Cesium.PrimitiveCollection|null} */ /** @type {Cesium.PrimitiveCollection|null} */
@@ -60,7 +56,6 @@ flightState._modelEpoch = 0;
flightState._models3dEnabled = true; flightState._models3dEnabled = true;
flightState._models3dMode = 'proximity'; flightState._models3dMode = 'proximity';
// 'proximity' = nearest MODEL_MAX in view; 'all' = every in-view plane (≤ MODEL_MAX_ALL) // 'proximity' = nearest MODEL_MAX in view; 'all' = every in-view plane (≤ MODEL_MAX_ALL)
@@ -78,20 +73,19 @@ flightState._planeModelLoaded = false;
flightState._preloadModel = null; flightState._preloadModel = null;
flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0); flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0);
flightState._scratchModelMtx = new Cesium.Matrix4(); flightState._scratchModelMtx = new Cesium.Matrix4();
flightState._scratchModelBS = new Cesium.BoundingSphere(
flightState._scratchModelBS = new Cesium.BoundingSphere(new Cesium.Cartesian3(), 1.0); new Cesium.Cartesian3(),
1.0,
);
// frustum-visibility test // frustum-visibility test
/** Last limb taper per billboard, retained across class/ground/cockpit repaints. */ /** Last limb taper per billboard, retained across class/ground/cockpit repaints. */
flightState._billboardLimbScale = new WeakMap(); flightState._billboardLimbScale = new WeakMap();
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Module-level state // Module-level state
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -136,14 +130,12 @@ flightState._retryAt = 0;
flightState._lastError = null; flightState._lastError = null;
flightState._activeUpdateControllers = new Set(); flightState._activeUpdateControllers = new Set();
/** @type {number|null} HTTP status code from the last API response */ /** @type {number|null} HTTP status code from the last API response */
flightState._lastStatus = null; flightState._lastStatus = null;
flightState._lastSource = source?.label || 'Aircraft'; flightState._lastSource = source?.label || 'Aircraft';
/** @type {boolean} True once ensureGeoidReady() has resolved (awaited once at enable()). Mirror of flights.js. */ /** @type {boolean} True once ensureGeoidReady() has resolved (awaited once at enable()). Mirror of flights.js. */
@@ -154,22 +146,17 @@ flightState._geoidReady = false;
flightState._geoidNCache = new Map(); flightState._geoidNCache = new Map();
// -- Click-to-track state -- // -- Click-to-track state --
/** @type {string|null} ICAO hex of the currently tracked aircraft */ /** @type {string|null} ICAO hex of the currently tracked aircraft */
flightState._trackedIcao = null; flightState._trackedIcao = null;
flightState._pendingTrackingRestore = null; flightState._pendingTrackingRestore = null;
flightState._trackingIntentGeneration = 0; flightState._trackingIntentGeneration = 0;
flightState._trackingRefreshEpoch = 0; flightState._trackingRefreshEpoch = 0;
flightState._lastTrackingRefreshOutcome = { flightState._lastTrackingRefreshOutcome = {
epoch: 0, epoch: 0,
status: 'unavailable', status: 'unavailable',
@@ -191,10 +178,8 @@ flightState._trackedCameraFrameStop = null;
flightState._trackedModel = null; flightState._trackedModel = null;
flightState._trackedModelGen = 0; flightState._trackedModelGen = 0;
flightState._trackedModelLoading = false; flightState._trackedModelLoading = false;
/** @type {Cesium.ScreenSpaceEventHandler|null} Click handler for selecting aircraft */ /** @type {Cesium.ScreenSpaceEventHandler|null} Click handler for selecting aircraft */
@@ -265,7 +250,6 @@ flightState._lastTrackedRotation = 0;
flightState._preRenderRemove = null; flightState._preRenderRemove = null;
flightState._trackedModelPreUpdateRemove = null; flightState._trackedModelPreUpdateRemove = null;
/** @type {Cesium.Event.RemoveCallback|null} camera.moveEnd listener disposer (arrival rotation pass) */ /** @type {Cesium.Event.RemoveCallback|null} camera.moveEnd listener disposer (arrival rotation pass) */
@@ -276,36 +260,26 @@ flightState._moveEndRemove = null;
flightState._trackedEntityChangedRemove = null; flightState._trackedEntityChangedRemove = null;
// -- Scratch variables (reused each frame to avoid GC pressure) -- // -- Scratch variables (reused each frame to avoid GC pressure) --
flightState._scratchOffset = new Cesium.Cartesian3(); flightState._scratchOffset = new Cesium.Cartesian3();
flightState._scratchCarto = new Cesium.Cartographic(); flightState._scratchCarto = new Cesium.Cartographic();
flightState._scratchEnu = new Cesium.Matrix4(); flightState._scratchEnu = new Cesium.Matrix4();
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 }; flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
flightState._scratchRenderTime = new Cesium.JulianDate(); flightState._scratchRenderTime = new Cesium.JulianDate();
flightState._scratchFleetPos = new Cesium.Cartesian3(); flightState._scratchFleetPos = new Cesium.Cartesian3();
flightState._scratchDrRaw = new Cesium.Cartesian3(); flightState._scratchDrRaw = new Cesium.Cartesian3();
flightState._scratchWarmupTime = new Cesium.JulianDate(); flightState._scratchWarmupTime = new Cesium.JulianDate();
flightState._trackedPosHolder = new Cesium.Cartesian3(); flightState._trackedPosHolder = new Cesium.Cartesian3();
// -- Per-frame dead-reckoning cache for the tracked entity -- // -- Per-frame dead-reckoning cache for the tracked entity --
/** @type {Cesium.Cartesian3|null} Cached DR position for the current render frame */ /** @type {Cesium.Cartesian3|null} Cached DR position for the current render frame */
@@ -315,7 +289,6 @@ flightState._cachedDRPosition = null;
flightState._cachedDRFrame = -1; flightState._cachedDRFrame = -1;
/** Course (deg) of the position `_deadReckon` most recently returned — set on /** Course (deg) of the position `_deadReckon` most recently returned — set on
* every branch of `_deadReckon`, read IMMEDIATELY by the caller (same * every branch of `_deadReckon`, read IMMEDIATELY by the caller (same
* synchronous flow; module-scratch idiom, like the Cartesian scratches). */ * synchronous flow; module-scratch idiom, like the Cartesian scratches). */
@@ -329,7 +302,6 @@ flightState._drCourseDeg = null;
flightState._drSpeedMps = null; flightState._drSpeedMps = null;
flightState._drCourseHold = false; flightState._drCourseHold = false;
/** Frame-cached course for the tracked aircraft (sibling of _cachedDRPosition). */ /** Frame-cached course for the tracked aircraft (sibling of _cachedDRPosition). */
@@ -340,7 +312,6 @@ flightState._cachedDRCourse = null;
flightState._cachedDRSpeedMps = null; flightState._cachedDRSpeedMps = null;
flightState._cachedDRHold = false; flightState._cachedDRHold = false;
/** Wall-clock of the tracked course limiter's last advance (dt source only — /** Wall-clock of the tracked course limiter's last advance (dt source only —
@@ -359,34 +330,26 @@ flightState._trackedCourseMs = 0;
flightState._displayCourse = new Map(); flightState._displayCourse = new Map();
flightState._drCorrection = new Cesium.Cartesian3(0, 0, 0); flightState._drCorrection = new Cesium.Cartesian3(0, 0, 0);
flightState._drCorrectionStartMs = 0; flightState._drCorrectionStartMs = 0;
flightState._drPrevRaw = new Cesium.Cartesian3(); flightState._drPrevRaw = new Cesium.Cartesian3();
flightState._drPrevDisplay = new Cesium.Cartesian3(); flightState._drPrevDisplay = new Cesium.Cartesian3();
flightState._drPrevMs = 0; flightState._drPrevMs = 0;
flightState._drReconcileValid = false; flightState._drReconcileValid = false;
/** @type {string|null} icao the reconciliation state currently belongs to */ /** @type {string|null} icao the reconciliation state currently belongs to */
flightState._drReconcileIcao = null; flightState._drReconcileIcao = null;
/** @type {Cesium.Cartesian3} Scratch for the tracked model's rendered translation. */ /** @type {Cesium.Cartesian3} Scratch for the tracked model's rendered translation. */
flightState._trackedVisualPos = new Cesium.Cartesian3(); flightState._trackedVisualPos = new Cesium.Cartesian3();
flightState._trackedTrailPos = new Cesium.Cartesian3(); flightState._trackedTrailPos = new Cesium.Cartesian3();
/** Scratch for the tracked model's world-space origin (the envelope centre). */ /** Scratch for the tracked model's world-space origin (the envelope centre). */
@@ -398,26 +361,20 @@ flightState._scratchTrailClip = new Cesium.Cartesian3();
flightState._scratchTrailHead = new Cesium.Cartesian3(); flightState._scratchTrailHead = new Cesium.Cartesian3();
/** Hysteresis latch for the tracked contact's zoom regime, plus the selection it /** Hysteresis latch for the tracked contact's zoom regime, plus the selection it
* belongs to. Scoped per selection so a NEW target re-evaluates against the ENTER * belongs to. Scoped per selection so a NEW target re-evaluates against the ENTER
* ceiling instead of inheriting the previous target's looser EXIT band. */ * ceiling instead of inheriting the previous target's looser EXIT band. */
flightState._trackedZoomLatched = false; flightState._trackedZoomLatched = false;
flightState._trackedZoomLatchIcao = null; flightState._trackedZoomLatchIcao = null;
flightState._trackedModelFailIcao = null; flightState._trackedModelFailIcao = null;
flightState._trackedModelFailCount = 0; flightState._trackedModelFailCount = 0;
flightState._trackedModelRetryAtMs = 0; flightState._trackedModelRetryAtMs = 0;
/** IR hot-target mode — mirror of flights.js: under the luminance-mapped /** IR hot-target mode — mirror of flights.js: under the luminance-mapped
* NVG/FLIR post-styles every model renders flat white (hottest); the dominant * NVG/FLIR post-styles every model renders flat white (hottest); the dominant
* amber/tracked tint restores on style exit. */ * amber/tracked tint restores on style exit. */
@@ -430,12 +387,12 @@ flightState._irBoost = false;
* customShader on a READY model is a silent no-op (field-verified), so the * customShader on a READY model is a silent no-op (field-verified), so the
* boost flips by release-and-reload — creation-time options carry it. */ * boost flips by release-and-reload — creation-time options carry it. */
flightState._IR_UNLIT_SHADER = new Cesium.CustomShader({ lightingModel: Cesium.LightingModel.UNLIT }); flightState._IR_UNLIT_SHADER = new Cesium.CustomShader({
lightingModel: Cesium.LightingModel.UNLIT,
});
flightState._irReloadQueue = null; flightState._irReloadQueue = null;
/** @type {Cesium.Cartographic} Scratch for _trailFloorPosition (per-frame safe). */ /** @type {Cesium.Cartographic} Scratch for _trailFloorPosition (per-frame safe). */
flightState._scratchTrailCarto = new Cesium.Cartographic(); flightState._scratchTrailCarto = new Cesium.Cartographic();
+29 -16
View File
@@ -1,8 +1,12 @@
import { AMBER_TRANSPARENT, TRACKED_ICON_COLOR } from './policy.js'; import { AMBER_TRANSPARENT, TRACKED_ICON_COLOR } from './policy.js';
export function createTesting({ flightState, services, parts, layer, resolveAsset }) { export function createTesting({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
/** /**
* Seed only the mutable state needed to exercise tracked-card refreshes through * Seed only the mutable state needed to exercise tracked-card refreshes through
* the production military poll reconciler. Tests still call * the production military poll reconciler. Tests still call
@@ -57,7 +61,6 @@ function _setTrackedMilitaryRefreshStateForTest({
flightState._retryAt = 0; flightState._retryAt = 0;
} }
/** Seed the authoritative snapshot outcome used by share-Follow tests. */ /** Seed the authoritative snapshot outcome used by share-Follow tests. */
function _setMilitaryTrackingRefreshOutcomeForTest({ function _setMilitaryTrackingRefreshOutcomeForTest({
@@ -74,37 +77,37 @@ function _setMilitaryTrackingRefreshOutcomeForTest({
}; };
} }
/** Add a cached contact so tests can model a target arriving on a later feed. */ /** Add a cached contact so tests can model a target arriving on a later feed. */
function _addMilitaryTrackingCandidateForTest({ icao24, meta, billboard, history = [] }) { function _addMilitaryTrackingCandidateForTest({
icao24,
meta,
billboard,
history = [],
}) {
flightState._billboards.set(icao24, billboard); flightState._billboards.set(icao24, billboard);
flightState._flightData.set(icao24, meta); flightState._flightData.set(icao24, meta);
flightState._positionHistory.set(icao24, history); flightState._positionHistory.set(icao24, history);
} }
/** Return the deferred restore target held by the production tracker. */ /** Return the deferred restore target held by the production tracker. */
function _pendingMilitaryTrackingRestoreForTest() { function _pendingMilitaryTrackingRestoreForTest() {
return flightState._pendingTrackingRestore?.id ?? null; return flightState._pendingTrackingRestore?.id ?? null;
} }
/** Exercise the production deferred-restore retry after a simulated feed refresh. */ /** Exercise the production deferred-restore retry after a simulated feed refresh. */
function _applyPendingMilitaryTrackingRestoreForTest() { function _applyPendingMilitaryTrackingRestoreForTest() {
return parts.tracking._applyPendingTrackingRestore(); return parts.tracking._applyPendingTrackingRestore();
} }
/** Set the exact Cockpit subject through the production state transition for focused tests. */ /** Set the exact Cockpit subject through the production state transition for focused tests. */
function _setCockpitDetectionSubjectForTest(active, subjectId = null) { function _setCockpitDetectionSubjectForTest(active, subjectId = null) {
parts.tracking._applyCockpitState({ active, subjectId }); parts.tracking._applyCockpitState({ active, subjectId });
} }
/** Evaluate the TRACKED contact's zoom regime through the production predicate. /** Evaluate the TRACKED contact's zoom regime through the production predicate.
* The decision is latch-bearing (default-on, hysteretic, cockpit/TR-3B-suppressed) * The decision is latch-bearing (default-on, hysteretic, cockpit/TR-3B-suppressed)
* and otherwise only observable through a live scene, so tests drive it here. */ * and otherwise only observable through a live scene, so tests drive it here. */
@@ -113,7 +116,6 @@ function _trackedModelRegimeActiveForTest() {
return parts.tracking._trackedModelRegimeActive(); return parts.tracking._trackedModelRegimeActive();
} }
/** Run one frame of the production tracked-model driver (normally a /** Run one frame of the production tracked-model driver (normally a
* `scene.preUpdate` listener) so tests can pin its bounded load retries. */ * `scene.preUpdate` listener) so tests can pin its bounded load retries. */
@@ -121,14 +123,14 @@ function _updateTrackedModelForTest() {
return parts.rendering._updateTrackedModel(); return parts.rendering._updateTrackedModel();
} }
/** Evaluate the production tracked-billboard handoff colour for focused tests. */ /** Evaluate the production tracked-billboard handoff colour for focused tests. */
function _trackedBillboardColorForTest() { function _trackedBillboardColorForTest() {
return parts.rendering._modelOwnsVisual(flightState._trackedIcao) ? AMBER_TRANSPARENT : TRACKED_ICON_COLOR; return parts.rendering._modelOwnsVisual(flightState._trackedIcao)
? AMBER_TRANSPARENT
: TRACKED_ICON_COLOR;
} }
/** Drive the exact fleet billboard-to-model handoff used by `_fleetTick`. */ /** Drive the exact fleet billboard-to-model handoff used by `_fleetTick`. */
function _driveFleetModelHandoffForTest({ icao24, position, course = 0 }) { function _driveFleetModelHandoffForTest({ icao24, position, course = 0 }) {
@@ -141,12 +143,23 @@ function _driveFleetModelHandoffForTest({ icao24, position, course = 0 }) {
); );
} }
/** Exercise the exact asynchronous fleet loader and return its admitted model. */ /** Exercise the exact asynchronous fleet loader and return its admitted model. */
async function _ensureFleetModelForTest(icao24) { async function _ensureFleetModelForTest(icao24) {
await parts.rendering._ensureModel(icao24); await parts.rendering._ensureModel(icao24);
return flightState._models.get(icao24) || null; return flightState._models.get(icao24) || null;
} }
return { _setTrackedMilitaryRefreshStateForTest, _setMilitaryTrackingRefreshOutcomeForTest, _addMilitaryTrackingCandidateForTest, _pendingMilitaryTrackingRestoreForTest, _applyPendingMilitaryTrackingRestoreForTest, _setCockpitDetectionSubjectForTest, _trackedModelRegimeActiveForTest, _updateTrackedModelForTest, _trackedBillboardColorForTest, _driveFleetModelHandoffForTest, _ensureFleetModelForTest }; return {
_setTrackedMilitaryRefreshStateForTest,
_setMilitaryTrackingRefreshOutcomeForTest,
_addMilitaryTrackingCandidateForTest,
_pendingMilitaryTrackingRestoreForTest,
_applyPendingMilitaryTrackingRestoreForTest,
_setCockpitDetectionSubjectForTest,
_trackedModelRegimeActiveForTest,
_updateTrackedModelForTest,
_trackedBillboardColorForTest,
_driveFleetModelHandoffForTest,
_ensureFleetModelForTest,
};
} }
+339 -126
View File
@@ -2,41 +2,76 @@ import * as Cesium from 'cesium';
import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js'; import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js';
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js'; import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
import { trackedModelZoomActive } from '../../data/trackedModelRegime.js'; import { trackedModelZoomActive } from '../../data/trackedModelRegime.js';
import { screenProjectedRotation, stabilizeScreenRotation } from '../../data/iconOrientation.js'; import {
screenProjectedRotation,
stabilizeScreenRotation,
} from '../../data/iconOrientation.js';
import { trailHeadStart } from '../../data/modelVisualAnchor.js'; import { trailHeadStart } from '../../data/modelVisualAnchor.js';
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js'; import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
import { bindTrackingClickGesture, isTrackingSelectionGesture, isTrackingClickGesture } from '../../data/trackingClickGesture.js'; import {
import { TRACKED_MODEL_MAX_LOAD_FAILS, TRACKED_MODEL_RETRY_BACKOFF_MS, TRAIL_MAX_POINTS, RENDER_DELAY_SEC, TRAIL_COLOR, BILLBOARD_SCALE, GROUND_SCALE, MIL_ICON_COLOR, AMBER_TRANSPARENT, TRACKED_ICON_COLOR } from './policy.js'; bindTrackingClickGesture,
isTrackingSelectionGesture,
isTrackingClickGesture,
} from '../../data/trackingClickGesture.js';
import {
TRACKED_MODEL_MAX_LOAD_FAILS,
TRACKED_MODEL_RETRY_BACKOFF_MS,
TRAIL_MAX_POINTS,
RENDER_DELAY_SEC,
TRAIL_COLOR,
BILLBOARD_SCALE,
GROUND_SCALE,
MIL_ICON_COLOR,
AMBER_TRANSPARENT,
TRACKED_ICON_COLOR,
} from './policy.js';
export function createTracking({ flightState, services, parts, layer, resolveAsset }) { export function createTracking({
const { selectTrackedSubjectContext, refreshTrackedSubjectContext, clearTrackedSubjectContext } = services.context; flightState,
const { tr3bTypeLabel, isTr3b, tr3bConvertedIds } = services.aircraftPresentation; services,
parts,
layer,
resolveAsset,
}) {
const {
selectTrackedSubjectContext,
refreshTrackedSubjectContext,
clearTrackedSubjectContext,
} = services.context;
const { tr3bTypeLabel, isTr3b, tr3bConvertedIds } =
services.aircraftPresentation;
const { trackedLabelModelFromText, refreshTrackedReadout } = services.readout; const { trackedLabelModelFromText, refreshTrackedReadout } = services.readout;
const { floorAltitudeM, cachedGroundFloor, resolveGroundFloorCellsBounded } = services.groundFloor; const { floorAltitudeM, cachedGroundFloor, resolveGroundFloorCellsBounded } =
services.groundFloor;
const { createTrail } = services.trails; const { createTrail } = services.trails;
const { ensureGeoidReady, geoidHeight } = services.geoid; const { ensureGeoidReady, geoidHeight } = services.geoid;
const { clearFocusTarget } = services.focus; const { clearFocusTarget } = services.focus;
const { applyTrackedCameraFrame } = services.camera; const { applyTrackedCameraFrame } = services.camera;
const { resolvePickId, isOwnedByOtherLayer } = services.picking; const { resolvePickId, isOwnedByOtherLayer } = services.picking;
function _emitAwarenessEvent(type, detail) { function _emitAwarenessEvent(type, detail) {
if (typeof window === 'undefined' || !window.dispatchEvent || typeof CustomEvent === 'undefined') return; if (
typeof window === 'undefined' ||
!window.dispatchEvent ||
typeof CustomEvent === 'undefined'
)
return;
window.dispatchEvent(new CustomEvent(type, { detail })); window.dispatchEvent(new CustomEvent(type, { detail }));
} }
function _publishTrackedSelection(icao24, origin = 'programmatic') { function _publishTrackedSelection(icao24, origin = 'programmatic') {
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
const info = flightState._flightData.get(icao24); const info = flightState._flightData.get(icao24);
if (!bb?.position || !info) return false; if (!bb?.position || !info) return false;
if (flightState._trackedEntity) flightState._trackedEntity.gevSelectionOrigin = origin; if (flightState._trackedEntity)
flightState._trackedEntity.gevSelectionOrigin = origin;
_emitAwarenessEvent('gev:awareness-subject-selected', { _emitAwarenessEvent('gev:awareness-subject-selected', {
layerId: 'military', layerId: 'military',
id: icao24, id: icao24,
label: parts.queries._toCleanText(info.callsign) || parts.queries._toCleanText(info.registration) || icao24, label:
parts.queries._toCleanText(info.callsign) ||
parts.queries._toCleanText(info.registration) ||
icao24,
position: Cesium.Cartesian3.clone(bb.position), position: Cesium.Cartesian3.clone(bb.position),
origin, origin,
}); });
@@ -44,7 +79,6 @@ function _publishTrackedSelection(icao24, origin = 'programmatic') {
return true; return true;
} }
/** /**
* Describe the selected contact for the shared context slot the voice tools * Describe the selected contact for the shared context slot the voice tools
* and Cockpit read. Values are the LIVE descriptor, not a selection-time * and Cockpit read. Values are the LIVE descriptor, not a selection-time
@@ -75,12 +109,19 @@ function _contextSubjectMetadata(icao24) {
registration: described.registration || '', registration: described.registration || '',
// Converted contacts report their class as TR-3B — the same override // Converted contacts report their class as TR-3B — the same override
// the readout and Contacts card show. // the readout and Contacts card show.
type: tr3bTypeLabel(icao24, parts.queries._toCleanText(info?.type) || ''), type: tr3bTypeLabel(
altitude: described.onGround ? 'on ground' : parts.queries._formatAltitude(info?.altitudeFt), icao24,
parts.queries._toCleanText(info?.type) || '',
),
altitude: described.onGround
? 'on ground'
: parts.queries._formatAltitude(info?.altitudeFt),
speed: Number.isFinite(described.velocityMps) speed: Number.isFinite(described.velocityMps)
? `${Math.round(described.velocityMps * 1.944)} kt` ? `${Math.round(described.velocityMps * 1.944)} kt`
: '', : '',
heading: Number.isFinite(described.track) ? `${Math.round(described.track)}°` : '', heading: Number.isFinite(described.track)
? `${Math.round(described.track)}°`
: '',
icao24, icao24,
// Honesty cue: the contact is coasting on dead reckoning, so the // Honesty cue: the contact is coasting on dead reckoning, so the
// narrated position/velocity are last-known rather than live. // narrated position/velocity are last-known rather than live.
@@ -89,18 +130,19 @@ function _contextSubjectMetadata(icao24) {
}; };
} }
function _isExplicitTrackingOrigin(origin) { function _isExplicitTrackingOrigin(origin) {
return origin === 'user' || origin === 'voice' || origin === 'tool'; return origin === 'user' || origin === 'voice' || origin === 'tool';
} }
/** Refresh the AIR-only Cockpit near band independently from model admission. */ /** Refresh the AIR-only Cockpit near band independently from model admission. */
function _refreshCockpitNearContacts() { function _refreshCockpitNearContacts() {
if (!flightState._cockpitContactMode || !flightState._viewer?.camera?.positionWC) { if (
if (flightState._cockpitNearContacts.size) flightState._cockpitNearContacts = new Set(); !flightState._cockpitContactMode ||
!flightState._viewer?.camera?.positionWC
) {
if (flightState._cockpitNearContacts.size)
flightState._cockpitNearContacts = new Set();
return; return;
} }
const previous = flightState._cockpitNearContacts; const previous = flightState._cockpitNearContacts;
@@ -109,7 +151,10 @@ function _refreshCockpitNearContacts() {
if (icao24 === flightState._trackedIcao || !bb?.position) continue; if (icao24 === flightState._trackedIcao || !bb?.position) continue;
distancesSquared.push([ distancesSquared.push([
icao24, icao24,
Cesium.Cartesian3.distanceSquared(flightState._viewer.camera.positionWC, bb.position), Cesium.Cartesian3.distanceSquared(
flightState._viewer.camera.positionWC,
bb.position,
),
]); ]);
} }
const next = nextCockpitNearContacts( const next = nextCockpitNearContacts(
@@ -128,7 +173,6 @@ function _refreshCockpitNearContacts() {
if (presentationChanged) flightState._lastCamPoseSig = ''; if (presentationChanged) flightState._lastCamPoseSig = '';
} }
/** Switch all current and future ambient contacts between silhouettes and cockpit pips. */ /** Switch all current and future ambient contacts between silhouettes and cockpit pips. */
function _setCockpitContactMode(active) { function _setCockpitContactMode(active) {
@@ -143,22 +187,22 @@ function _setCockpitContactMode(active) {
if (flightState._modelCollection) flightState._modelCollection.show = true; if (flightState._modelCollection) flightState._modelCollection.show = true;
flightState._trail?.setVisible(!next); flightState._trail?.setVisible(!next);
if (flightState._trailHeadEntity) flightState._trailHeadEntity.show = !next; if (flightState._trailHeadEntity) flightState._trailHeadEntity.show = !next;
for (const [icao24, bb] of flightState._billboards) parts.rendering._applyFleetBillboardPresentation(icao24, bb); for (const [icao24, bb] of flightState._billboards)
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
flightState._lastCamPoseSig = ''; flightState._lastCamPoseSig = '';
flightState._lastFleetTickMs = 0; flightState._lastFleetTickMs = 0;
} }
function _applyCockpitState(detail = {}) { function _applyCockpitState(detail = {}) {
const active = detail?.active === true; const active = detail?.active === true;
flightState._cockpitSubjectId = active flightState._cockpitSubjectId = active
? String(detail?.subjectId || '').trim().toLowerCase() || null ? String(detail?.subjectId || '')
.trim()
.toLowerCase() || null
: null; : null;
_setCockpitContactMode(active); _setCockpitContactMode(active);
} }
/** /**
* Build the multi-line presentation text for the protected tracked host card. * Build the multi-line presentation text for the protected tracked host card.
* Lines: callsign, type/registration, operator/altitude. * Lines: callsign, type/registration, operator/altitude.
@@ -173,13 +217,24 @@ function _applyCockpitState(detail = {}) {
*/ */
function _buildTrackedLabel(info, icao24) { function _buildTrackedLabel(info, icao24) {
const stale = (flightState._missingPolls.get(icao24) || flightState._backoff) ? ' · STALE' : ''; const stale =
const callsign = (parts.queries._toCleanText(info?.callsign) || parts.queries._toCleanText(info?.registration) || icao24) + stale; flightState._missingPolls.get(icao24) || flightState._backoff
? ' · STALE'
: '';
const callsign =
(parts.queries._toCleanText(info?.callsign) ||
parts.queries._toCleanText(info?.registration) ||
icao24) + stale;
// Converted contacts report their class as TR-3B — that override is exactly // Converted contacts report their class as TR-3B — that override is exactly
// what the Easter egg replaces the real type with. // what the Easter egg replaces the real type with.
const type = tr3bTypeLabel(icao24, parts.queries._toCleanText(info?.type) || 'Type unknown'); const type = tr3bTypeLabel(
const registration = parts.queries._toCleanText(info?.registration) || 'Reg unknown'; icao24,
const operator = parts.queries._toCleanText(info?.operator) || 'Operator unknown'; parts.queries._toCleanText(info?.type) || 'Type unknown',
);
const registration =
parts.queries._toCleanText(info?.registration) || 'Reg unknown';
const operator =
parts.queries._toCleanText(info?.operator) || 'Operator unknown';
const altitude = parts.queries._formatAltitude(info?.altitudeFt); const altitude = parts.queries._formatAltitude(info?.altitudeFt);
const speedKt = info?.speedMps ? Math.round(info.speedMps * 1.944) : null; const speedKt = info?.speedMps ? Math.round(info.speedMps * 1.944) : null;
const tail = speedKt ? `${altitude} · ${speedKt} kt` : altitude; const tail = speedKt ? `${altitude} · ${speedKt} kt` : altitude;
@@ -190,11 +245,11 @@ function _buildTrackedLabel(info, icao24) {
].join('\n'); ].join('\n');
} }
/** Write the explicit tracked presentation model and refresh its host entry. */ /** Write the explicit tracked presentation model and refresh its host entry. */
function _updateTrackedLabelModel(icao24) { function _updateTrackedLabelModel(icao24) {
if (!flightState._trackedEntity || icao24 !== flightState._trackedIcao) return; if (!flightState._trackedEntity || icao24 !== flightState._trackedIcao)
return;
flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText( flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText(
_buildTrackedLabel(flightState._flightData.get(icao24), icao24), _buildTrackedLabel(flightState._flightData.get(icao24), icao24),
'#ffd166', '#ffd166',
@@ -205,18 +260,20 @@ function _updateTrackedLabelModel(icao24) {
refreshTrackedSubjectContext(_contextSubjectMetadata(icao24)); refreshTrackedSubjectContext(_contextSubjectMetadata(icao24));
} }
/** Re-image the tracked entity's billboard from the current class/conversion. */ /** Re-image the tracked entity's billboard from the current class/conversion. */
function _syncTrackedBillboardImage() { function _syncTrackedBillboardImage() {
if (!flightState._trackedIcao || !flightState._trackedEntity?.billboard) return; if (!flightState._trackedIcao || !flightState._trackedEntity?.billboard)
return;
flightState._trackedEntity.billboard.image = aircraftIcon( flightState._trackedEntity.billboard.image = aircraftIcon(
parts.rendering._iconKind(flightState._trackedIcao, flightState._flightData.get(flightState._trackedIcao)?.klass), parts.rendering._iconKind(
flightState._trackedIcao,
flightState._flightData.get(flightState._trackedIcao)?.klass,
),
TRACKED_ICON_PX, TRACKED_ICON_PX,
); );
} }
/** /**
* Re-render one contact after its TR-3B conversion (or the active IR style) * Re-render one contact after its TR-3B conversion (or the active IR style)
* changed. Mirror of flights.js: converting drops any 3D model so the triangle * changed. Mirror of flights.js: converting drops any 3D model so the triangle
@@ -226,10 +283,13 @@ function _syncTrackedBillboardImage() {
*/ */
function _refreshTr3bContact(icao24) { function _refreshTr3bContact(icao24) {
const id = String(icao24 || '').trim().toLowerCase(); const id = String(icao24 || '')
.trim()
.toLowerCase();
if (!id) return false; if (!id) return false;
if (isTr3b(id)) { if (isTr3b(id)) {
if (flightState._models.has(id) || flightState._modelPending.has(id)) parts.rendering._releaseModel(id); if (flightState._models.has(id) || flightState._modelPending.has(id))
parts.rendering._releaseModel(id);
const modelled = flightState._billboards.get(id); const modelled = flightState._billboards.get(id);
if (modelled && id !== flightState._trackedIcao) modelled.show = true; // horizon cull re-asserts next tick if (modelled && id !== flightState._trackedIcao) modelled.show = true; // horizon cull re-asserts next tick
if (id === flightState._trackedIcao) { if (id === flightState._trackedIcao) {
@@ -247,16 +307,15 @@ function _refreshTr3bContact(icao24) {
return flightState._billboards.has(id) || id === flightState._trackedIcao; return flightState._billboards.has(id) || id === flightState._trackedIcao;
} }
/** Re-image every converted contact this layer owns (IR style flip). */ /** Re-image every converted contact this layer owns (IR style flip). */
function _refreshTr3bForStyle() { function _refreshTr3bForStyle() {
for (const id of tr3bConvertedIds()) { for (const id of tr3bConvertedIds()) {
if (flightState._billboards.has(id) || id === flightState._trackedIcao) _refreshTr3bContact(id); if (flightState._billboards.has(id) || id === flightState._trackedIcao)
_refreshTr3bContact(id);
} }
} }
/** Clear every per-SELECTION tracked-model latch: the zoom hysteresis band and /** Clear every per-SELECTION tracked-model latch: the zoom hysteresis band and
* the load-failure bound. Called from each path that changes which contact is * the load-failure bound. Called from each path that changes which contact is
* selected — deselect, re-track, cross-layer handoff, init, destroy. * selected — deselect, re-track, cross-layer handoff, init, destroy.
@@ -277,16 +336,16 @@ function _resetTrackedSelectionState() {
flightState._trackedModelRetryAtMs = 0; flightState._trackedModelRetryAtMs = 0;
} }
/** Whether the driver may start another tracked-model load this frame. */ /** Whether the driver may start another tracked-model load this frame. */
function _trackedModelLoadAllowed(nowMs = Date.now()) { function _trackedModelLoadAllowed(nowMs = Date.now()) {
if (flightState._trackedModelFailIcao !== flightState._trackedIcao) return true; // untried selection if (flightState._trackedModelFailIcao !== flightState._trackedIcao)
if (flightState._trackedModelFailCount >= TRACKED_MODEL_MAX_LOAD_FAILS) return false; return true; // untried selection
if (flightState._trackedModelFailCount >= TRACKED_MODEL_MAX_LOAD_FAILS)
return false;
return nowMs >= flightState._trackedModelRetryAtMs; return nowMs >= flightState._trackedModelRetryAtMs;
} }
/** Record a rejected tracked-model load and arm the backoff / give-up latch. */ /** Record a rejected tracked-model load and arm the backoff / give-up latch. */
function _noteTrackedModelLoadFailure(url, err) { function _noteTrackedModelLoadFailure(url, err) {
@@ -295,17 +354,17 @@ function _noteTrackedModelLoadFailure(url, err) {
flightState._trackedModelFailCount = 0; flightState._trackedModelFailCount = 0;
} }
flightState._trackedModelFailCount += 1; flightState._trackedModelFailCount += 1;
flightState._trackedModelRetryAtMs = Date.now() + TRACKED_MODEL_RETRY_BACKOFF_MS; flightState._trackedModelRetryAtMs =
Date.now() + TRACKED_MODEL_RETRY_BACKOFF_MS;
if (flightState._trackedModelFailCount >= TRACKED_MODEL_MAX_LOAD_FAILS) { if (flightState._trackedModelFailCount >= TRACKED_MODEL_MAX_LOAD_FAILS) {
console.warn( console.warn(
`[Data:Military] tracked 3D model gave up after ${flightState._trackedModelFailCount} failed loads of ${url} — ` `[Data:Military] tracked 3D model gave up after ${flightState._trackedModelFailCount} failed loads of ${url} — ` +
+ 'this contact stays 2D until another is selected', 'this contact stays 2D until another is selected',
err, err,
); );
} }
} }
/** /**
* The TRACKED aircraft's own model regime — DEFAULT-ON, camera-distance driven * The TRACKED aircraft's own model regime — DEFAULT-ON, camera-distance driven
* (owner directive 2026-08-19). Mirror of flights.js: this does NOT consult the * (owner directive 2026-08-19). Mirror of flights.js: this does NOT consult the
@@ -327,7 +386,11 @@ function _trackedModelRegimeActive() {
// A converted TR-3B has no 3D asset — suppressing the regime keeps its // A converted TR-3B has no 3D asset — suppressing the regime keeps its
// tracked billboard fully opaque (the colour callback reads this too), so // tracked billboard fully opaque (the colour callback reads this too), so
// the triangle stays the visual all the way in. Mirror of flights.js. // the triangle stays the visual all the way in. Mirror of flights.js.
if (!flightState._trackedIcao || flightState._cockpitContactMode || isTr3b(flightState._trackedIcao)) { if (
!flightState._trackedIcao ||
flightState._cockpitContactMode ||
isTr3b(flightState._trackedIcao)
) {
flightState._trackedZoomLatched = false; flightState._trackedZoomLatched = false;
return false; return false;
} }
@@ -338,12 +401,13 @@ function _trackedModelRegimeActive() {
return flightState._trackedZoomLatched; return flightState._trackedZoomLatched;
} }
/** Seed the tracked billboard's 2D orientation before a 3D→2D handoff. */ /** Seed the tracked billboard's 2D orientation before a 3D→2D handoff. */
function _syncTracked2dRotation() { function _syncTracked2dRotation() {
if (!flightState._trackedIcao || !flightState._viewer) return; if (!flightState._trackedIcao || !flightState._viewer) return;
const pos = parts.motion._trackedDisplayCached() || flightState._billboards.get(flightState._trackedIcao)?.position; const pos =
parts.motion._trackedDisplayCached() ||
flightState._billboards.get(flightState._trackedIcao)?.position;
if (!pos) return; if (!pos) return;
const projected = screenProjectedRotation( const projected = screenProjectedRotation(
flightState._viewer.scene, flightState._viewer.scene,
@@ -351,11 +415,14 @@ function _syncTracked2dRotation() {
parts.motion._trackedDisplayCourse(), parts.motion._trackedDisplayCourse(),
flightState._lastTrackedRotation, flightState._lastTrackedRotation,
); );
const rotation = stabilizeScreenRotation(flightState._lastTrackedRotation, projected, 0); const rotation = stabilizeScreenRotation(
flightState._lastTrackedRotation,
projected,
0,
);
if (rotation !== null) flightState._lastTrackedRotation = rotation; if (rotation !== null) flightState._lastTrackedRotation = rotation;
} }
/** /**
* Field-test round 2 (2026-07-06): floors a trail-bound position at the warm * Field-test round 2 (2026-07-06): floors a trail-bound position at the warm
* coarse ground cell — a pure LIFT (above-floor positions pass through * coarse ground cell — a pure LIFT (above-floor positions pass through
@@ -369,16 +436,22 @@ function _syncTracked2dRotation() {
*/ */
function _trailFloorPosition(position) { function _trailFloorPosition(position) {
const carto = Cesium.Cartographic.fromCartesian(position, Cesium.Ellipsoid.WGS84, flightState._scratchTrailCarto); const carto = Cesium.Cartographic.fromCartesian(
position,
Cesium.Ellipsoid.WGS84,
flightState._scratchTrailCarto,
);
if (!carto) return position; if (!carto) return position;
const latDeg = Cesium.Math.toDegrees(carto.latitude); const latDeg = Cesium.Math.toDegrees(carto.latitude);
const lonDeg = Cesium.Math.toDegrees(carto.longitude); const lonDeg = Cesium.Math.toDegrees(carto.longitude);
const floored = floorAltitudeM(carto.height, cachedGroundFloor(latDeg, lonDeg)); const floored = floorAltitudeM(
carto.height,
cachedGroundFloor(latDeg, lonDeg),
);
if (floored == null || floored === carto.height) return position; if (floored == null || floored === carto.height) return position;
return Cesium.Cartesian3.fromDegrees(lonDeg, latDeg, floored); return Cesium.Cartesian3.fromDegrees(lonDeg, latDeg, floored);
} }
/** /**
* Append one fix to the tracked aircraft's trail accumulation and refresh * Append one fix to the tracked aircraft's trail accumulation and refresh
* the rendered trail. Caller passes an owned (cloned) Cartesian3. * the rendered trail. Caller passes an owned (cloned) Cartesian3.
@@ -387,11 +460,11 @@ function _trailFloorPosition(position) {
function _appendTrailFix(position) { function _appendTrailFix(position) {
flightState._trailPositions.push(_trailFloorPosition(position)); flightState._trailPositions.push(_trailFloorPosition(position));
if (flightState._trailPositions.length > TRAIL_MAX_POINTS) flightState._trailPositions.shift(); if (flightState._trailPositions.length > TRAIL_MAX_POINTS)
flightState._trailPositions.shift();
_refreshTrailDisplay(); _refreshTrailDisplay();
} }
/** /**
* Renders the trail BODY — the accumulated fixes EXCLUDING the newest raw one. That * Renders the trail BODY — the accumulated fixes EXCLUDING the newest raw one. That
* newest fix is at ~now, ~one poll interval AHEAD of the delayed icon (rendered at * newest fix is at ~now, ~one poll interval AHEAD of the delayed icon (rendered at
@@ -403,10 +476,13 @@ function _appendTrailFix(position) {
function _refreshTrailDisplay() { function _refreshTrailDisplay() {
if (!flightState._trail) return; if (!flightState._trail) return;
flightState._trail.setPositions(flightState._trailPositions.length > 1 ? flightState._trailPositions.slice(0, -1) : flightState._trailPositions); flightState._trail.setPositions(
flightState._trailPositions.length > 1
? flightState._trailPositions.slice(0, -1)
: flightState._trailPositions,
);
} }
/** /**
* Start the trail for a newly tracked aircraft: seed it with the short * Start the trail for a newly tracked aircraft: seed it with the short
* dead-reckoning history (chronological), render immediately, then * dead-reckoning history (chronological), render immediately, then
@@ -422,18 +498,25 @@ function _startTrail(icao24) {
// newest ~RENDER_DELAY_SEC of fixes are AHEAD of the displayed icon; including them // newest ~RENDER_DELAY_SEC of fixes are AHEAD of the displayed icon; including them
// would draw the trail in front of the plane. They join via _appendTrailFix as they age. // would draw the trail in front of the plane. They join via _appendTrailFix as they age.
const seedRenderTime = Cesium.JulianDate.addSeconds( const seedRenderTime = Cesium.JulianDate.addSeconds(
Cesium.JulianDate.now(), -RENDER_DELAY_SEC, flightState._scratchWarmupTime Cesium.JulianDate.now(),
-RENDER_DELAY_SEC,
flightState._scratchWarmupTime,
); );
for (const fix of history) { for (const fix of history) {
if (Cesium.JulianDate.lessThanOrEquals(fix.time, seedRenderTime)) { if (Cesium.JulianDate.lessThanOrEquals(fix.time, seedRenderTime)) {
// Floor grounded-history seeds (round 2): a grounded contact's stored // Floor grounded-history seeds (round 2): a grounded contact's stored
// fixes carry the pre-datum default height — the trail copy sits on // fixes carry the pre-datum default height — the trail copy sits on
// the surface instead (pure lift; airborne fixes pass through). // the surface instead (pure lift; airborne fixes pass through).
flightState._trailPositions.push(_trailFloorPosition(Cesium.Cartesian3.clone(fix.position))); flightState._trailPositions.push(
_trailFloorPosition(Cesium.Cartesian3.clone(fix.position)),
);
} }
} }
if (!flightState._trail && flightState._viewer) { if (!flightState._trail && flightState._viewer) {
flightState._trail = createTrail(flightState._viewer, { color: TRAIL_COLOR, width: 2.5 }); flightState._trail = createTrail(flightState._viewer, {
color: TRAIL_COLOR,
width: 2.5,
});
} }
flightState._trail?.setVisible(!flightState._cockpitContactMode); flightState._trail?.setVisible(!flightState._cockpitContactMode);
// Live head segment: last DISPLAYED body point → current dead-reckoned icon, updated // Live head segment: last DISPLAYED body point → current dead-reckoned icon, updated
@@ -453,12 +536,22 @@ function _startTrail(icao24) {
// point would be the sole raw fix — ~now, AHEAD of the delayed icon — so the // point would be the sole raw fix — ~now, AHEAD of the delayed icon — so the
// segment would draw IN FRONT of the plane. Likewise during warm-up the icon // segment would draw IN FRONT of the plane. Likewise during warm-up the icon
// predates all real history, so there is no valid body point behind it. // predates all real history, so there is no valid body point behind it.
if (!flightState._trackedIcao || flightState._trailPositions.length < 2 || parts.motion._isTrackWarmingUp()) return []; if (
const head = parts.motion._trackedTrailCached() || parts.motion._trackedDisplayPosition(flightState._trackedIcao); !flightState._trackedIcao ||
flightState._trailPositions.length < 2 ||
parts.motion._isTrackWarmingUp()
)
return [];
const head =
parts.motion._trackedTrailCached() ||
parts.motion._trackedDisplayPosition(flightState._trackedIcao);
if (!head) return []; if (!head) return [];
// body[n−2] (last displayed body point) → delayed head: runs FORWARD, never a // body[n−2] (last displayed body point) → delayed head: runs FORWARD, never a
// backward/reversing segment. // backward/reversing segment.
const start = flightState._trailPositions[flightState._trailPositions.length - 2]; const start =
flightState._trailPositions[
flightState._trailPositions.length - 2
];
// Floor the head too (round 2): a grounded tracked contact's DR // Floor the head too (round 2): a grounded tracked contact's DR
// display height is the pre-datum default — without this the last // display height is the pre-datum default — without this the last
// segment dives underground while taxiing. // segment dives underground while taxiing.
@@ -471,7 +564,11 @@ function _startTrail(icao24) {
// the FLOORED endpoint, which is the one actually drawn. // the FLOORED endpoint, which is the one actually drawn.
// See trailHeadStart. // See trailHeadStart.
const from = trailHeadStart( const from = trailHeadStart(
start, end, parts.motion._trackedModelCenterWorld(), parts.motion._trackedModelEnvelopeM(), flightState._scratchTrailHead, start,
end,
parts.motion._trackedModelCenterWorld(),
parts.motion._trackedModelEnvelopeM(),
flightState._scratchTrailHead,
); );
if (!from) return []; if (!from) return [];
return [from, end]; return [from, end];
@@ -480,7 +577,8 @@ function _startTrail(icao24) {
material: Cesium.Color.fromCssColorString(TRAIL_COLOR).withAlpha(0.9), material: Cesium.Color.fromCssColorString(TRAIL_COLOR).withAlpha(0.9),
// Round 4: the head must never vanish into the mesh either (dimmed // Round 4: the head must never vanish into the mesh either (dimmed
// when occluded so depth still reads). // when occluded so depth still reads).
depthFailMaterial: Cesium.Color.fromCssColorString(TRAIL_COLOR).withAlpha(0.45), depthFailMaterial:
Cesium.Color.fromCssColorString(TRAIL_COLOR).withAlpha(0.45),
arcType: Cesium.ArcType.GEODESIC, // round 8: consistent with the trail body (no chords) arcType: Cesium.ArcType.GEODESIC, // round 8: consistent with the trail body (no chords)
}, },
}); });
@@ -493,7 +591,6 @@ function _startTrail(icao24) {
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec); _backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
} }
/** /**
* Fire-and-forget adsb.lol readsb trace backfill (PRD F2). Trace points are * Fire-and-forget adsb.lol readsb trace backfill (PRD F2). Trace points are
* stride-thinned to the remaining vertex budget, then spliced strictly older * stride-thinned to the remaining vertex budget, then spliced strictly older
@@ -509,13 +606,25 @@ function _startTrail(icao24) {
async function _backfillTrail(icao24, token, oldestFixEpochSec) { async function _backfillTrail(icao24, token, oldestFixEpochSec) {
let trace = null; let trace = null;
try { try {
const track = await flightState._source.getTrack?.(flightState._flightData.get(icao24)?.sourceReference ?? icao24, { signal: AbortSignal.any([flightState.lifetime.signal, AbortSignal.timeout(8000)]) }); const track = await flightState._source.getTrack?.(
flightState._flightData.get(icao24)?.sourceReference ?? icao24,
{
signal: AbortSignal.any([
flightState.lifetime.signal,
AbortSignal.timeout(8000),
]),
},
);
trace = track?.records ?? null; trace = track?.records ?? null;
} catch { } catch {
return; // silent fallback to the accumulated trail return; // silent fallback to the accumulated trail
} }
if (!trace) return; if (!trace) return;
if (token !== flightState._trailBackfillToken || icao24 !== flightState._trackedIcao) return; if (
token !== flightState._trailBackfillToken ||
icao24 !== flightState._trackedIcao
)
return;
// Height-datum fix (Task 7, mirror of flights.js Task 6 _backfillTrail): a // Height-datum fix (Task 7, mirror of flights.js Task 6 _backfillTrail): a
// readsb trace point's altitude is barometric feet (same datum as the live // readsb trace point's altitude is barometric feet (same datum as the live
@@ -528,7 +637,8 @@ async function _backfillTrail(icao24, token, oldestFixEpochSec) {
const parsed = []; const parsed = [];
for (const point of trace) { for (const point of trace) {
const t = point.observedAtMs / 1000; const t = point.observedAtMs / 1000;
const lat = point.latitude, lon = point.longitude; const lat = point.latitude,
lon = point.longitude;
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue; if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
if (!Number.isFinite(t) || t >= oldestFixEpochSec) continue; if (!Number.isFinite(t) || t >= oldestFixEpochSec) continue;
parsed.push({ lat, lon, baroAltitudeM: point.baroAltitudeM }); parsed.push({ lat, lon, baroAltitudeM: point.baroAltitudeM });
@@ -548,12 +658,17 @@ async function _backfillTrail(icao24, token, oldestFixEpochSec) {
await resolveGroundFloorCellsBounded(parsed); await resolveGroundFloorCellsBounded(parsed);
// Re-check the backfill token after the await (same guard as post-fetch): // Re-check the backfill token after the await (same guard as post-fetch):
// tracking may have moved on while the terrain race was in flight. // tracking may have moved on while the terrain race was in flight.
if (token !== flightState._trailBackfillToken || icao24 !== flightState._trackedIcao) return; if (
token !== flightState._trailBackfillToken ||
icao24 !== flightState._trackedIcao
)
return;
let older = []; let older = [];
let lastAltM = null; // carry-forward for ground points whose cell isn't warm yet let lastAltM = null; // carry-forward for ground points whose cell isn't warm yet
for (const { lat, lon, baroAltitudeM } of parsed) { for (const { lat, lon, baroAltitudeM } of parsed) {
const baroM = baroAltitudeM == null ? null : baroAltitudeM + geoidHeight(lat, lon); const baroM =
baroAltitudeM == null ? null : baroAltitudeM + geoidHeight(lat, lon);
let altM = floorAltitudeM(baroM, cachedGroundFloor(lat, lon)); let altM = floorAltitudeM(baroM, cachedGroundFloor(lat, lon));
// Ground/no-alt point with an unresolved floor: hold the previous // Ground/no-alt point with an unresolved floor: hold the previous
// waypoint's altitude (continuity — never a dive to a made-up depth). // waypoint's altitude (continuity — never a dive to a made-up depth).
@@ -565,7 +680,10 @@ async function _backfillTrail(icao24, token, oldestFixEpochSec) {
} }
// Stride-thin the backfill so it plus the live fine segment fit the cap. // Stride-thin the backfill so it plus the live fine segment fit the cap.
const budget = Math.max(1, TRAIL_MAX_POINTS - flightState._trailPositions.length); const budget = Math.max(
1,
TRAIL_MAX_POINTS - flightState._trailPositions.length,
);
if (older.length > budget) { if (older.length > budget) {
const stride = Math.ceil(older.length / budget); const stride = Math.ceil(older.length / budget);
const thinned = []; const thinned = [];
@@ -575,12 +693,13 @@ async function _backfillTrail(icao24, token, oldestFixEpochSec) {
flightState._trailPositions = older.concat(flightState._trailPositions); flightState._trailPositions = older.concat(flightState._trailPositions);
if (flightState._trailPositions.length > TRAIL_MAX_POINTS) { if (flightState._trailPositions.length > TRAIL_MAX_POINTS) {
flightState._trailPositions = flightState._trailPositions.slice(flightState._trailPositions.length - TRAIL_MAX_POINTS); flightState._trailPositions = flightState._trailPositions.slice(
flightState._trailPositions.length - TRAIL_MAX_POINTS,
);
} }
_refreshTrailDisplay(); _refreshTrailDisplay();
} }
/** /**
* Clear the rendered trail and accumulation; invalidate pending backfills. * Clear the rendered trail and accumulation; invalidate pending backfills.
*/ */
@@ -589,13 +708,20 @@ function _clearTrail() {
flightState._trailBackfillToken += 1; flightState._trailBackfillToken += 1;
flightState._trailPositions = []; flightState._trailPositions = [];
if (flightState._trail) flightState._trail.clear(); if (flightState._trail) flightState._trail.clear();
if (flightState._trailHeadEntity && flightState._viewer && !flightState._viewer.isDestroyed()) { if (
try { flightState._viewer.entities.remove(flightState._trailHeadEntity); } catch { /* already gone */ } flightState._trailHeadEntity &&
flightState._viewer &&
!flightState._viewer.isDestroyed()
) {
try {
flightState._viewer.entities.remove(flightState._trailHeadEntity);
} catch {
/* already gone */
}
} }
flightState._trailHeadEntity = null; flightState._trailHeadEntity = null;
} }
/** /**
* Destroy the trail primitive entirely (layer disable/teardown). * Destroy the trail primitive entirely (layer disable/teardown).
*/ */
@@ -608,7 +734,6 @@ function _destroyTrail() {
} }
} }
/** /**
* Stop tracking the current aircraft and clean up all tracking state. * Stop tracking the current aircraft and clean up all tracking state.
* Restores the original billboard, removes the tracked Entity, invalidates * Restores the original billboard, removes the tracked Entity, invalidates
@@ -630,10 +755,10 @@ function _destroyTrail() {
* only tear down on a deliberate clear. * only tear down on a deliberate clear.
*/ */
function _clearTracking(skipViewerUntrack = false, { function _clearTracking(
evicted = false, skipViewerUntrack = false,
origin = 'programmatic', { evicted = false, origin = 'programmatic' } = {},
} = {}) { ) {
flightState._trackedCameraFrameStop?.(); flightState._trackedCameraFrameStop?.();
flightState._trackedCameraFrameStop = null; flightState._trackedCameraFrameStop = null;
if (!flightState._trackedIcao) { if (!flightState._trackedIcao) {
@@ -656,7 +781,10 @@ function _clearTracking(skipViewerUntrack = false, {
bb.height = 20; bb.height = 20;
// Ground-aware restore (a plane untracked while taxiing comes back at // Ground-aware restore (a plane untracked while taxiing comes back at
// ground scale; tint is full amber on the ground and in the air). // ground scale; tint is full amber on the ground and in the air).
bb.scale = BILLBOARD_SCALE * (CLASS_SCALE_2D[meta?.klass] || 1) * (meta?.onGround ? GROUND_SCALE : 1); bb.scale =
BILLBOARD_SCALE *
(CLASS_SCALE_2D[meta?.klass] || 1) *
(meta?.onGround ? GROUND_SCALE : 1);
bb.color = MIL_ICON_COLOR; bb.color = MIL_ICON_COLOR;
bb.rotation = flightState._lastTrackedRotation; bb.rotation = flightState._lastTrackedRotation;
} }
@@ -676,7 +804,10 @@ function _clearTracking(skipViewerUntrack = false, {
parts.rendering._releaseTrackedModel(); parts.rendering._releaseTrackedModel();
_resetTrackedSelectionState(); // the zoom band + load-failure budget belong to the selection we just dropped _resetTrackedSelectionState(); // the zoom band + load-failure budget belong to the selection we just dropped
flightState._trackedIcao = null; flightState._trackedIcao = null;
parts.rendering._applyFleetBillboardPresentation(clearedIcao, flightState._billboards.get(clearedIcao)); parts.rendering._applyFleetBillboardPresentation(
clearedIcao,
flightState._billboards.get(clearedIcao),
);
clearTrackedSubjectContext('military'); clearTrackedSubjectContext('military');
_emitAwarenessEvent('gev:awareness-subject-cleared', { _emitAwarenessEvent('gev:awareness-subject-cleared', {
layerId: 'military', layerId: 'military',
@@ -690,27 +821,28 @@ function _clearTracking(skipViewerUntrack = false, {
_clearTrail(); _clearTrail();
} }
function _applyPendingTrackingRestore() { function _applyPendingTrackingRestore() {
const pending = flightState._pendingTrackingRestore; const pending = flightState._pendingTrackingRestore;
if (!pending || pending.generation !== flightState._trackingIntentGeneration) return false; if (
if (!flightState._billboardCollection?.show || !flightState._billboards.has(pending.id)) return false; !pending ||
pending.generation !== flightState._trackingIntentGeneration
)
return false;
if (
!flightState._billboardCollection?.show ||
!flightState._billboards.has(pending.id)
)
return false;
flightState._pendingTrackingRestore = null; flightState._pendingTrackingRestore = null;
_trackFlight(pending.id, { origin: pending.origin }); _trackFlight(pending.id, { origin: pending.origin });
return true; return true;
} }
function _cancelPendingTrackingRestore() { function _cancelPendingTrackingRestore() {
flightState._trackingIntentGeneration += 1; flightState._trackingIntentGeneration += 1;
flightState._pendingTrackingRestore = null; flightState._pendingTrackingRestore = null;
} }
/** /**
* Begin tracking a specific aircraft. Clears any previous tracking, hides the * Begin tracking a specific aircraft. Clears any previous tracking, hides the
* billboard, creates a new Entity with dead-reckoning CallbackProperties for * billboard, creates a new Entity with dead-reckoning CallbackProperties for
@@ -742,7 +874,8 @@ function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
// one computation shared by the position/rotation/trail-head callbacks, with // one computation shared by the position/rotation/trail-head callbacks, with
// discontinuity reconciliation). Falls back to the last billboard position when the // discontinuity reconciliation). Falls back to the last billboard position when the
// aircraft has no fix. // aircraft has no fix.
const getTrackedPosition = () => parts.motion._trackedDisplayPosition(icao24) || bb.position; const getTrackedPosition = () =>
parts.motion._trackedDisplayPosition(icao24) || bb.position;
// Dead-reckoning position property evaluated every render frame. // Dead-reckoning position property evaluated every render frame.
// isConstant = false so CesiumJS re-evaluates each frame. // isConstant = false so CesiumJS re-evaluates each frame.
@@ -772,15 +905,29 @@ function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
// camera guard; AUTO may otherwise alternate with a velocity frame. // camera guard; AUTO may otherwise alternate with a velocity frame.
trackingReferenceFrame: Cesium.TrackingReferenceFrame.ENU, trackingReferenceFrame: Cesium.TrackingReferenceFrame.ENU,
billboard: { billboard: {
image: aircraftIcon(parts.rendering._iconKind(flightState._trackedIcao, flightState._flightData.get(flightState._trackedIcao)?.klass), TRACKED_ICON_PX), image: aircraftIcon(
parts.rendering._iconKind(
flightState._trackedIcao,
flightState._flightData.get(flightState._trackedIcao)?.klass,
),
TRACKED_ICON_PX,
),
width: 28, width: 28,
height: 28, height: 28,
scale: BILLBOARD_SCALE * (CLASS_SCALE_2D[flightState._flightData.get(flightState._trackedIcao)?.klass] || 1), scale:
BILLBOARD_SCALE *
(CLASS_SCALE_2D[
flightState._flightData.get(flightState._trackedIcao)?.klass
] || 1),
// Solid amber when the billboard is the visual (zoomed out, 3D off, or model still loading); // Solid amber when the billboard is the visual (zoomed out, 3D off, or model still loading);
// transparent once the STANDALONE tracked model is actually up (ready + shown). // transparent once the STANDALONE tracked model is actually up (ready + shown).
color: new Cesium.CallbackProperty(() => ( color: new Cesium.CallbackProperty(
parts.rendering._modelOwnsVisual(flightState._trackedIcao) ? AMBER_TRANSPARENT : TRACKED_ICON_COLOR () =>
), false), parts.rendering._modelOwnsVisual(flightState._trackedIcao)
? AMBER_TRANSPARENT
: TRACKED_ICON_COLOR,
false,
),
sizeInMeters: false, sizeInMeters: false,
scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5), scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5),
alignedAxis: Cesium.Cartesian3.ZERO, alignedAxis: Cesium.Cartesian3.ZERO,
@@ -794,14 +941,18 @@ function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
rotation: new Cesium.CallbackProperty(() => { rotation: new Cesium.CallbackProperty(() => {
const tracked = flightState._flightData.get(flightState._trackedIcao); const tracked = flightState._flightData.get(flightState._trackedIcao);
const pos = getTrackedPosition(); const pos = getTrackedPosition();
if (!tracked || !pos || !flightState._viewer) return flightState._lastTrackedRotation; if (!tracked || !pos || !flightState._viewer)
return flightState._lastTrackedRotation;
const projected = screenProjectedRotation( const projected = screenProjectedRotation(
flightState._viewer.scene, flightState._viewer.scene,
pos, pos,
parts.motion._trackedDisplayCourse(), parts.motion._trackedDisplayCourse(),
flightState._lastTrackedRotation, flightState._lastTrackedRotation,
); );
const rot = stabilizeScreenRotation(flightState._lastTrackedRotation, projected); const rot = stabilizeScreenRotation(
flightState._lastTrackedRotation,
projected,
);
if (rot !== null) flightState._lastTrackedRotation = rot; if (rot !== null) flightState._lastTrackedRotation = rot;
return flightState._lastTrackedRotation; return flightState._lastTrackedRotation;
}, false), }, false),
@@ -820,23 +971,30 @@ function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
// initial frame with surrounding context. (ENU: east=+X, north=+Y, up=+Z.) // initial frame with surrounding context. (ENU: east=+X, north=+Y, up=+Z.)
const altM = info?.altitudeFt ? info.altitudeFt * 0.3048 : 1500; const altM = info?.altitudeFt ? info.altitudeFt * 0.3048 : 1500;
const followRange = Math.min(Math.max(altM * 1.1 + 2500, 3000), 30000); const followRange = Math.min(Math.max(altM * 1.1 + 2500, 3000), 30000);
flightState._trackedEntity.viewFrom = new Cesium.Cartesian3(0, -followRange * 0.8, followRange * 0.55); flightState._trackedEntity.viewFrom = new Cesium.Cartesian3(
0,
-followRange * 0.8,
followRange * 0.55,
);
// Cancel any in-progress camera flight first — otherwise Cesium won't apply the tracked // Cancel any in-progress camera flight first — otherwise Cesium won't apply the tracked
// entity's viewFrom on the first frame, so voice-initiated tracking (which often fires // entity's viewFrom on the first frame, so voice-initiated tracking (which often fires
// 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 = parts.motion._trackedDisplayCached; flightState._trackedEntity.gevDisplayPosition =
parts.motion._trackedDisplayCached;
// Separate accessor on purpose (mirror of flights.js): `gevDisplayPosition` keeps the // Separate accessor on purpose (mirror of flights.js): `gevDisplayPosition` 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 `gevVisualPosition`.
flightState._trackedEntity.gevVisualPosition = parts.motion._trackedVisualCached; flightState._trackedEntity.gevVisualPosition =
parts.motion._trackedVisualCached;
refreshTrackedReadout(flightState._trackedEntity); refreshTrackedReadout(flightState._trackedEntity);
flightState._viewer.camera.cancelFlight(); flightState._viewer.camera.cancelFlight();
// Camera follows the tracked entity // Camera follows the tracked entity
flightState._viewer.trackedEntity = flightState._trackedEntity; flightState._viewer.trackedEntity = flightState._trackedEntity;
flightState._trackedCameraFrameStop = applyTrackedCameraFrame( flightState._trackedCameraFrameStop =
applyTrackedCameraFrame(
flightState._viewer, flightState._viewer,
flightState._trackedEntity, flightState._trackedEntity,
flightState._trackedEntity.viewFrom, flightState._trackedEntity.viewFrom,
@@ -850,12 +1008,14 @@ function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
// at the surface (_trailFloorPosition), so ground legs drape, never dive. // at the surface (_trailFloorPosition), so ground legs drape, never dive.
_startTrail(icao24); _startTrail(icao24);
const callsign = parts.queries._toCleanText(info.callsign) || parts.queries._toCleanText(info.registration) || icao24; const callsign =
parts.queries._toCleanText(info.callsign) ||
parts.queries._toCleanText(info.registration) ||
icao24;
_publishTrackedSelection(icao24, origin); _publishTrackedSelection(icao24, origin);
console.log(`[Data:Military] Tracking ${callsign} (${icao24})`); console.log(`[Data:Military] Tracking ${callsign} (${icao24})`);
} }
/** /**
* Global keydown handler: pressing Escape clears the active aircraft tracking. * Global keydown handler: pressing Escape clears the active aircraft tracking.
* @param {KeyboardEvent} e - The keyboard event * @param {KeyboardEvent} e - The keyboard event
@@ -868,7 +1028,6 @@ function _onKeyDown(e) {
} }
} }
/** /**
* Install a left-click handler on the scene canvas for aircraft selection. * Install a left-click handler on the scene canvas for aircraft selection.
* Clicking a military billboard starts tracking that aircraft; clicking empty * Clicking a military billboard starts tracking that aircraft; clicking empty
@@ -883,16 +1042,26 @@ function _installClickHandler(viewer) {
// Cross-layer untrack (mirror of flights): if another layer grabs the follow-camera, drop our // Cross-layer untrack (mirror of flights): if another layer grabs the follow-camera, drop our
// tracking without touching viewer.trackedEntity (the new owner controls it). // tracking without touching viewer.trackedEntity (the new owner controls it).
if (!flightState._trackedEntityChangedRemove) { if (!flightState._trackedEntityChangedRemove) {
flightState._trackedEntityChangedRemove = viewer.trackedEntityChanged.addEventListener(() => { flightState._trackedEntityChangedRemove =
if (flightState._trackedIcao && flightState._viewer && flightState._viewer.trackedEntity && flightState._viewer.trackedEntity !== flightState._trackedEntity) { viewer.trackedEntityChanged.addEventListener(() => {
if (
flightState._trackedIcao &&
flightState._viewer &&
flightState._viewer.trackedEntity &&
flightState._viewer.trackedEntity !== flightState._trackedEntity
) {
_clearTracking(true, { _clearTracking(true, {
origin: flightState._viewer.trackedEntity?.gevSelectionOrigin || 'programmatic', origin:
flightState._viewer.trackedEntity?.gevSelectionOrigin ||
'programmatic',
}); });
} }
}); });
} }
flightState._clickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); flightState._clickHandler = new Cesium.ScreenSpaceEventHandler(
viewer.scene.canvas,
);
bindTrackingClickGesture(flightState._clickHandler, (click, gesture) => { bindTrackingClickGesture(flightState._clickHandler, (click, gesture) => {
// Camera drags never select or deselect, even if they finish over a plane. // Camera drags never select or deselect, even if they finish over a plane.
// Duration alone is allowed through so a stationary long press can still // Duration alone is allowed through so a stationary long press can still
@@ -914,20 +1083,33 @@ function _installClickHandler(viewer) {
// above. H1: the model used to have no pick id, so this fell through to // above. H1: the model used to have no pick id, so this fell through to
// "empty space" and deselected the very plane being tracked. // "empty space" and deselected the very plane being tracked.
if (flightState._trackedIcao) { if (flightState._trackedIcao) {
const rawPick = typeof picked.id === 'string' ? picked.id : picked.primitive?.id; const rawPick =
if (picked.primitive === flightState._trackedModel || rawPick === flightState._trackedIcao) return; typeof picked.id === 'string' ? picked.id : picked.primitive?.id;
if (
picked.primitive === flightState._trackedModel ||
rawPick === flightState._trackedIcao
)
return;
} }
// BillboardCollection picks: CesiumJS may expose the billboard as // BillboardCollection picks: CesiumJS may expose the billboard as
// picked.primitive (with .id = icao24) or directly as picked.id // picked.primitive (with .id = icao24) or directly as picked.id
const billboard = picked.primitive; const billboard = picked.primitive;
if (billboard && billboard.id && flightState._billboards.has(billboard.id)) { if (
billboard &&
billboard.id &&
flightState._billboards.has(billboard.id)
) {
_cancelPendingTrackingRestore(); _cancelPendingTrackingRestore();
_trackFlight(billboard.id, { origin: 'user' }); _trackFlight(billboard.id, { origin: 'user' });
return; return;
} }
// Fallback: some CesiumJS versions surface the id string at picked.id // Fallback: some CesiumJS versions surface the id string at picked.id
if (picked.id && typeof picked.id === 'string' && flightState._billboards.has(picked.id)) { if (
picked.id &&
typeof picked.id === 'string' &&
flightState._billboards.has(picked.id)
) {
_cancelPendingTrackingRestore(); _cancelPendingTrackingRestore();
_trackFlight(picked.id, { origin: 'user' }); _trackFlight(picked.id, { origin: 'user' });
return; return;
@@ -955,5 +1137,36 @@ function _installClickHandler(viewer) {
document.addEventListener('keydown', _onKeyDown); document.addEventListener('keydown', _onKeyDown);
} }
return { _emitAwarenessEvent, _publishTrackedSelection, _contextSubjectMetadata, _isExplicitTrackingOrigin, _refreshCockpitNearContacts, _setCockpitContactMode, _applyCockpitState, _buildTrackedLabel, _updateTrackedLabelModel, _syncTrackedBillboardImage, _refreshTr3bContact, _refreshTr3bForStyle, _resetTrackedSelectionState, _trackedModelLoadAllowed, _noteTrackedModelLoadFailure, _trackedModelRegimeActive, _syncTracked2dRotation, _trailFloorPosition, _appendTrailFix, _refreshTrailDisplay, _startTrail, _backfillTrail, _clearTrail, _destroyTrail, _clearTracking, _applyPendingTrackingRestore, _cancelPendingTrackingRestore, _trackFlight, _onKeyDown, _installClickHandler }; return {
_emitAwarenessEvent,
_publishTrackedSelection,
_contextSubjectMetadata,
_isExplicitTrackingOrigin,
_refreshCockpitNearContacts,
_setCockpitContactMode,
_applyCockpitState,
_buildTrackedLabel,
_updateTrackedLabelModel,
_syncTrackedBillboardImage,
_refreshTr3bContact,
_refreshTr3bForStyle,
_resetTrackedSelectionState,
_trackedModelLoadAllowed,
_noteTrackedModelLoadFailure,
_trackedModelRegimeActive,
_syncTracked2dRotation,
_trailFloorPosition,
_appendTrailFix,
_refreshTrailDisplay,
_startTrail,
_backfillTrail,
_clearTrail,
_destroyTrail,
_clearTracking,
_applyPendingTrackingRestore,
_cancelPendingTrackingRestore,
_trackFlight,
_onKeyDown,
_installClickHandler,
};
} }
+8 -2
View File
@@ -5,8 +5,14 @@ import { fileURLToPath } from 'node:url';
/** Read actual component owners for structural regression assertions. */ /** Read actual component owners for structural regression assertions. */
export function readLayerSource(file) { export function readLayerSource(file) {
const path = file instanceof URL ? fileURLToPath(file) : file; const path = file instanceof URL ? fileURLToPath(file) : file;
if (!['flights.js', 'militaryFlights.js'].includes(basename(path))) return readFileSync(path, 'utf8'); if (!['flights.js', 'militaryFlights.js'].includes(basename(path)))
const directory = join(dirname(path), basename(path) === 'flights.js' ? '../layers/flights' : '../layers/military'); return readFileSync(path, 'utf8');
const directory = join(
dirname(path),
basename(path) === 'flights.js'
? '../layers/flights'
: '../layers/military',
);
return readdirSync(directory) return readdirSync(directory)
.filter((name) => name.endsWith('.js')) .filter((name) => name.endsWith('.js'))
.sort() .sort()