Format civil-flight components

This commit is contained in:
Sameh Khamis committed 2026-09-13 09:50:16 -07:00
1 parent 7c7e83c9e3
commit e18aeb8565
16 files changed
+2751 -1402

No files matched your search

+53 -16
View File
@@ -17,22 +17,59 @@ import * as render from '../renderGovernor.js';
import * as groundSnap from './groundSnap.js';
import * as recession from './aircraftRecession.js';
const flightsLayer = createCivilFlightLayer({ source: createOpenSkySource(), 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 flightsLayer = createCivilFlightLayer({
source: createOpenSkySource(),
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/flights/policy.js';
export const _floorGroundedDisplayPositionForTest = flightsLayer.testing._floorGroundedDisplayPositionForTest;
export const _clearDisplayFloorStateForTest = flightsLayer.testing._clearDisplayFloorStateForTest;
export const _setTrackedFlightRefreshStateForTest = flightsLayer.testing._setTrackedFlightRefreshStateForTest;
export const _setFlightTrackingRefreshOutcomeForTest = flightsLayer.testing._setFlightTrackingRefreshOutcomeForTest;
export const _addFlightTrackingCandidateForTest = flightsLayer.testing._addFlightTrackingCandidateForTest;
export const _militaryLayerSuppressesForTest = flightsLayer.testing._militaryLayerSuppressesForTest;
export const _armFlightTrackingRestoreForTest = flightsLayer.testing._armFlightTrackingRestoreForTest;
export const _pendingFlightTrackingRestoreForTest = flightsLayer.testing._pendingFlightTrackingRestoreForTest;
export const _applyPendingFlightTrackingRestoreForTest = flightsLayer.testing._applyPendingFlightTrackingRestoreForTest;
export const _setCockpitDetectionSubjectForTest = flightsLayer.testing._setCockpitDetectionSubjectForTest;
export const _trackedModelRegimeActiveForTest = flightsLayer.testing._trackedModelRegimeActiveForTest;
export const _updateTrackedModelForTest = flightsLayer.testing._updateTrackedModelForTest;
export const _trackedBillboardColorForTest = flightsLayer.testing._trackedBillboardColorForTest;
export const _driveFleetModelHandoffForTest = flightsLayer.testing._driveFleetModelHandoffForTest;
export const _ensureFleetModelForTest = flightsLayer.testing._ensureFleetModelForTest;
export const _floorGroundedDisplayPositionForTest =
flightsLayer.testing._floorGroundedDisplayPositionForTest;
export const _clearDisplayFloorStateForTest =
flightsLayer.testing._clearDisplayFloorStateForTest;
export const _setTrackedFlightRefreshStateForTest =
flightsLayer.testing._setTrackedFlightRefreshStateForTest;
export const _setFlightTrackingRefreshOutcomeForTest =
flightsLayer.testing._setFlightTrackingRefreshOutcomeForTest;
export const _addFlightTrackingCandidateForTest =
flightsLayer.testing._addFlightTrackingCandidateForTest;
export const _militaryLayerSuppressesForTest =
flightsLayer.testing._militaryLayerSuppressesForTest;
export const _armFlightTrackingRestoreForTest =
flightsLayer.testing._armFlightTrackingRestoreForTest;
export const _pendingFlightTrackingRestoreForTest =
flightsLayer.testing._pendingFlightTrackingRestoreForTest;
export const _applyPendingFlightTrackingRestoreForTest =
flightsLayer.testing._applyPendingFlightTrackingRestoreForTest;
export const _setCockpitDetectionSubjectForTest =
flightsLayer.testing._setCockpitDetectionSubjectForTest;
export const _trackedModelRegimeActiveForTest =
flightsLayer.testing._trackedModelRegimeActiveForTest;
export const _updateTrackedModelForTest =
flightsLayer.testing._updateTrackedModelForTest;
export const _trackedBillboardColorForTest =
flightsLayer.testing._trackedBillboardColorForTest;
export const _driveFleetModelHandoffForTest =
flightsLayer.testing._driveFleetModelHandoffForTest;
export const _ensureFleetModelForTest =
flightsLayer.testing._ensureFleetModelForTest;
export const mapAnalystRecord = flightsLayer.mapAnalystRecord;
export default flightsLayer;
+18 -14
View File
@@ -1,22 +1,26 @@
import * as Cesium from 'cesium';
export function createController({ flightState, services, parts, layer, resolveAsset }) {
function _abortActiveUpdates() {
for (const controller of flightState._activeUpdateControllers) controller.abort();
export function createController({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
function _abortActiveUpdates() {
for (const controller of flightState._activeUpdateControllers)
controller.abort();
flightState._activeUpdateControllers.clear();
}
}
function _flightQuery(viewer) {
function _flightQuery(viewer) {
const cartographic = viewer?.camera?.positionCartographic;
return cartographic ? {
return cartographic
? {
latitude: Cesium.Math.toDegrees(cartographic.latitude),
longitude: Cesium.Math.toDegrees(cartographic.longitude),
} : {};
}
return { _abortActiveUpdates, _flightQuery };
}
: {};
}
return { _abortActiveUpdates, _flightQuery };
}
+156 -65
View File
@@ -1,35 +1,52 @@
import { classifyAircraft } from '../../data/aircraftClass.js';
import { horizonOccluder } from '../../data/iconOrientation.js';
import * as Cesium from 'cesium';
import { ENRICH_MAX_INFLIGHT, ENRICH_DISPATCH_GAP_MS, ENRICH_AMBIENT_BUDGET_CEIL, ENRICH_AMBIENT_REFILL_TOKENS, ENRICH_AMBIENT_REFILL_WINDOW_MS, ENRICH_AMBIENT_PER_SWEEP } from './policy.js';
import {
ENRICH_MAX_INFLIGHT,
ENRICH_DISPATCH_GAP_MS,
ENRICH_AMBIENT_BUDGET_CEIL,
ENRICH_AMBIENT_REFILL_TOKENS,
ENRICH_AMBIENT_REFILL_WINDOW_MS,
ENRICH_AMBIENT_PER_SWEEP,
} from './policy.js';
export function createEnrichment({ flightState, services, parts, layer, resolveAsset }) {
function _enqueueEnrich(key, query, onData, priority = false) {
export function createEnrichment({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
function _enqueueEnrich(key, query, onData, priority = false) {
if (flightState.lifetime.signal.aborted) return;
if (flightState._enrichSeen.has(key)) return;
flightState._enrichSeen.add(key);
const job = { query, onData };
// Priority (tracked / model-eligible) goes to the FRONT so a deep ambient
// backlog can never delay the plane the user just clicked or zoomed into.
if (priority) flightState._enrichQueue.unshift(job); else flightState._enrichQueue.push(job);
if (priority) flightState._enrichQueue.unshift(job);
else flightState._enrichQueue.push(job);
_drainEnrich();
}
}
function _drainEnrich() {
function _drainEnrich() {
if (flightState.lifetime.signal.aborted) return;
while (flightState._enrichActive < ENRICH_MAX_INFLIGHT && flightState._enrichQueue.length) {
while (
flightState._enrichActive < ENRICH_MAX_INFLIGHT &&
flightState._enrichQueue.length
) {
// Drip: at most one dispatch per ENRICH_DISPATCH_GAP_MS. When the gap
// hasn't elapsed yet, park a single wake-up timer and stop — completions
// and enqueues in the meantime re-enter here harmlessly.
const wait = ENRICH_DISPATCH_GAP_MS - (Date.now() - flightState._enrichLastDispatchMs);
const wait =
ENRICH_DISPATCH_GAP_MS -
(Date.now() - flightState._enrichLastDispatchMs);
if (wait > 0) {
if (!flightState._enrichDripTimer) {
flightState._enrichDripTimer = setTimeout(() => { flightState._enrichDripTimer = null; _drainEnrich(); }, wait);
flightState._enrichDripTimer = setTimeout(() => {
flightState._enrichDripTimer = null;
_drainEnrich();
}, wait);
}
return;
}
@@ -37,113 +54,187 @@ function _drainEnrich() {
const job = flightState._enrichQueue.shift();
flightState._enrichActive += 1;
const lifetime = flightState.lifetime;
Promise.resolve().then(() => {
Promise.resolve()
.then(() => {
lifetime.signal.throwIfAborted();
return flightState._source.getEnrichment?.(job.query, { signal: lifetime.signal });
return flightState._source.getEnrichment?.(job.query, {
signal: lifetime.signal,
});
})
.then((data) => { if (!lifetime.signal.aborted && data && data.found) job.onData(data); })
.catch(() => { /* enrichment never surfaces errors */ })
.finally(() => { if (lifetime.signal.aborted) return; flightState._enrichActive -= 1; _drainEnrich(); });
.then((data) => {
if (!lifetime.signal.aborted && data && data.found) job.onData(data);
})
.catch(() => {
/* enrichment never surfaces errors */
})
.finally(() => {
if (lifetime.signal.aborted) return;
flightState._enrichActive -= 1;
_drainEnrich();
});
}
}
}
function _requestTypeEnrichment(icao24, priority = false) {
function _requestTypeEnrichment(icao24, priority = false) {
if (!/^[0-9a-f]{6}$/i.test(icao24)) return;
_enqueueEnrich(`t:${icao24}`, { kind: 'type', id: icao24.toLowerCase() }, (data) => {
_enqueueEnrich(
`t:${icao24}`,
{ kind: 'type', id: icao24.toLowerCase() },
(data) => {
const meta = flightState._flightData.get(icao24);
if (!meta) return; // evicted while the lookup was in flight
meta.typeCode = data.typeCode || meta.typeCode;
meta.typeName = data.typeName || meta.typeName;
meta.registration = data.registration || meta.registration;
if (meta.typeCode) {
const klass = classifyAircraft({ typeCode: meta.typeCode, category: meta.category });
const klass = classifyAircraft({
typeCode: meta.typeCode,
category: meta.category,
});
if (klass !== meta.klass) {
meta.klass = klass;
const bb = flightState._billboards.get(icao24);
if (bb) parts.rendering._applyFleetBillboardPresentation(icao24, bb);
if (bb)
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
// Hangar fleet: the class's GLB/scale may have changed — resync the
// live model, any in-flight load, and the tracked standalone model.
parts.rendering._syncModelToClass(icao24);
}
}
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) parts.tracking._updateTrackedLabelModel(icao24);
}, priority);
}
if (icao24 === flightState._trackedIcao && flightState._trackedEntity)
parts.tracking._updateTrackedLabelModel(icao24);
},
priority,
);
}
function _requestRouteEnrichment(icao24) {
const cs = String(flightState._flightData.get(icao24)?.callsign || '').trim().toUpperCase();
function _requestRouteEnrichment(icao24) {
const cs = String(flightState._flightData.get(icao24)?.callsign || '')
.trim()
.toUpperCase();
if (!/^[A-Z]{3}\d/.test(cs)) return; // airline-style callsigns only (LLL + digit); GA tails won't resolve
_enqueueEnrich(`r:${cs}`, { kind: 'route', id: cs }, (data) => {
_enqueueEnrich(
`r:${cs}`,
{ kind: 'route', id: cs },
(data) => {
const meta = flightState._flightData.get(icao24);
if (!meta) return;
meta.airline = data.airline || meta.airline;
if (data.origin && data.destination) meta.route = { origin: data.origin, destination: data.destination };
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) parts.tracking._updateTrackedLabelModel(icao24);
}, true); // route lookups only fire for the TRACKED plane — front of the queue
}
if (data.origin && data.destination)
meta.route = { origin: data.origin, destination: data.destination };
if (icao24 === flightState._trackedIcao && flightState._trackedEntity)
parts.tracking._updateTrackedLabelModel(icao24);
},
true,
); // route lookups only fire for the TRACKED plane — front of the queue
}
/** QA seam: headless harnesses (scripts/qa-enrich-ambient.mjs) shrink the
/** QA seam: headless harnesses (scripts/qa-enrich-ambient.mjs) shrink the
* bucket knobs via window.__GEV_ENRICH_AMBIENT_QA = {ceil, refillTokens,
* windowMs} — they cannot wait out a real 5-minute window. Read lazily each
* refill so a pre-boot override (or a mid-run windowMs swap) applies.
* Production never sets this; the constants above are the defaults. */
function _ambientBudgetKnobs() {
const o = (typeof window !== 'undefined' && window.__GEV_ENRICH_AMBIENT_QA) || null;
function _ambientBudgetKnobs() {
const o =
(typeof window !== 'undefined' && window.__GEV_ENRICH_AMBIENT_QA) || null;
return {
ceil: Number.isFinite(o?.ceil) && o.ceil > 0 ? o.ceil : ENRICH_AMBIENT_BUDGET_CEIL,
refillTokens: Number.isFinite(o?.refillTokens) && o.refillTokens > 0 ? o.refillTokens : ENRICH_AMBIENT_REFILL_TOKENS,
windowMs: Number.isFinite(o?.windowMs) && o.windowMs > 0 ? o.windowMs : ENRICH_AMBIENT_REFILL_WINDOW_MS,
ceil:
Number.isFinite(o?.ceil) && o.ceil > 0
? o.ceil
: ENRICH_AMBIENT_BUDGET_CEIL,
refillTokens:
Number.isFinite(o?.refillTokens) && o.refillTokens > 0
? o.refillTokens
: ENRICH_AMBIENT_REFILL_TOKENS,
windowMs:
Number.isFinite(o?.windowMs) && o.windowMs > 0
? o.windowMs
: ENRICH_AMBIENT_REFILL_WINDOW_MS,
};
}
}
/** Advance the token bucket: add refillTokens per FULLY elapsed window since
/** Advance the token bucket: add refillTokens per FULLY elapsed window since
* the anchor, clamp at the ceiling, and move the anchor forward by the whole
* windows consumed (while the bucket sits full this still advances, so idle
* time never banks more than one bucket's worth of burst). */
function _refillAmbientBudget(nowMs) {
function _refillAmbientBudget(nowMs) {
const { ceil, refillTokens, windowMs } = _ambientBudgetKnobs();
if (!flightState._enrichAmbientRefillAnchorMs) { flightState._enrichAmbientRefillAnchorMs = nowMs; return; }
const windows = Math.floor((nowMs - flightState._enrichAmbientRefillAnchorMs) / windowMs);
if (!flightState._enrichAmbientRefillAnchorMs) {
flightState._enrichAmbientRefillAnchorMs = nowMs;
return;
}
const windows = Math.floor(
(nowMs - flightState._enrichAmbientRefillAnchorMs) / windowMs,
);
if (windows <= 0) return;
flightState._enrichAmbientBudget = Math.min(ceil, flightState._enrichAmbientBudget + windows * refillTokens);
flightState._enrichAmbientBudget = Math.min(
ceil,
flightState._enrichAmbientBudget + windows * refillTokens,
);
flightState._enrichAmbientRefillAnchorMs += windows * windowMs;
}
}
function _sweepAmbientEnrichment() {
function _sweepAmbientEnrichment() {
_refillAmbientBudget(Date.now());
if (flightState._enrichAmbientBudget <= 0 || !flightState._viewer || !flightState._billboardCollection || !flightState._billboardCollection.show) return;
if (
flightState._enrichAmbientBudget <= 0 ||
!flightState._viewer ||
!flightState._billboardCollection ||
!flightState._billboardCollection.show
)
return;
try {
const camera = flightState._viewer.camera;
const camPos = camera.positionWC;
const occluder = horizonOccluder(camera);
const cull = camera.frustum.computeCullingVolume(camPos, camera.directionWC, camera.upWC);
const cull = camera.frustum.computeCullingVolume(
camPos,
camera.directionWC,
camera.upWC,
);
const cand = [];
for (const [icao24, bb] of flightState._billboards) {
if (flightState._enrichSeen.has(`t:${icao24}`)) continue; // answered / queued / negative this session
if (!/^[0-9a-f]{6}$/i.test(icao24)) continue; // adsbdb keys are 6-char hex only
if (flightState._flightData.get(icao24)?.onGround) continue; // ground traffic never spends ambient budget (click-to-enrich still works)
if (!bb.position || !occluder.isPointVisible(bb.position)) continue; // beyond the limb
Cesium.Cartesian3.clone(bb.position, flightState._scratchModelBS.center);
if (cull.computeVisibility(flightState._scratchModelBS) === Cesium.Intersect.OUTSIDE) continue; // off-screen
cand.push([icao24, Cesium.Cartesian3.distanceSquared(camPos, bb.position)]);
Cesium.Cartesian3.clone(
bb.position,
flightState._scratchModelBS.center,
);
if (
cull.computeVisibility(flightState._scratchModelBS) ===
Cesium.Intersect.OUTSIDE
)
continue; // off-screen
cand.push([
icao24,
Cesium.Cartesian3.distanceSquared(camPos, bb.position),
]);
}
cand.sort((a, b) => a[1] - b[1]); // nearest first — what the user is looking at resolves first
const n = Math.min(cand.length, ENRICH_AMBIENT_PER_SWEEP, flightState._enrichAmbientBudget);
const n = Math.min(
cand.length,
ENRICH_AMBIENT_PER_SWEEP,
flightState._enrichAmbientBudget,
);
for (let i = 0; i < n; i++) {
flightState._enrichAmbientBudget -= 1;
_requestTypeEnrichment(cand[i][0]); // non-priority: fills the back of the queue
}
} catch { /* ambient enrichment is best-effort — never disturb the poll loop */ }
}
return { _enqueueEnrich, _drainEnrich, _requestTypeEnrichment, _requestRouteEnrichment, _ambientBudgetKnobs, _refillAmbientBudget, _sweepAmbientEnrichment };
} catch {
/* ambient enrichment is best-effort — never disturb the poll loop */
}
}
return {
_enqueueEnrich,
_drainEnrich,
_requestTypeEnrichment,
_requestRouteEnrichment,
_ambientBudgetKnobs,
_refillAmbientBudget,
_sweepAmbientEnrichment,
};
}
+65 -31
View File
@@ -2,33 +2,53 @@ import * as Cesium from 'cesium';
import { aircraftIcon } from '../../data/aircraftIcons.js';
import { FOCUS_EVIDENCE_DEV } from './policy.js';
export function createEvidence({ flightState, services, parts, layer, resolveAsset }) {
export function createEvidence({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
/** Resolve a JSON-safe evidence position into ECEF. DEV-only caller. */
/** Resolve a JSON-safe evidence position into ECEF. DEV-only caller. */
function _focusEvidencePosition(record) {
function _focusEvidencePosition(record) {
const cartesian = record?.cartesian;
if (Array.isArray(cartesian) && cartesian.length >= 3
&& cartesian.slice(0, 3).every(Number.isFinite)) {
return Cesium.Cartesian3.fromElements(cartesian[0], cartesian[1], cartesian[2]);
if (
Array.isArray(cartesian) &&
cartesian.length >= 3 &&
cartesian.slice(0, 3).every(Number.isFinite)
) {
return Cesium.Cartesian3.fromElements(
cartesian[0],
cartesian[1],
cartesian[2],
);
}
if (!Number.isFinite(record?.longitude) || !Number.isFinite(record?.latitude)) return null;
if (
!Number.isFinite(record?.longitude) ||
!Number.isFinite(record?.latitude)
)
return null;
return Cesium.Cartesian3.fromDegrees(
record.longitude,
record.latitude,
Number.isFinite(record.altitudeM) ? record.altitudeM : 3_000,
);
}
}
/** Replace the real fleet with deterministic explicit-position contacts. */
/** Replace the real fleet with deterministic explicit-position contacts. */
function _setFocusEvidenceAircraft(records = []) {
if (!FOCUS_EVIDENCE_DEV || !flightState._billboardCollection || !flightState._viewer) return { ok: false, count: 0 };
function _setFocusEvidenceAircraft(records = []) {
if (
!FOCUS_EVIDENCE_DEV ||
!flightState._billboardCollection ||
!flightState._viewer
)
return { ok: false, count: 0 };
if (flightState._trackedIcao) parts.tracking._clearTracking();
parts.rendering._releaseModels();
for (const bb of flightState._billboards.values()) flightState._billboardCollection.remove(bb);
for (const bb of flightState._billboards.values())
flightState._billboardCollection.remove(bb);
flightState._billboards.clear();
flightState._flightData.clear();
flightState._positionHistory.clear();
@@ -37,13 +57,15 @@ function _setFocusEvidenceAircraft(records = []) {
flightState._focusEvidenceIds.clear();
for (const record of Array.isArray(records) ? records : []) {
const id = String(record?.id || '').trim().toLowerCase();
const id = String(record?.id || '')
.trim()
.toLowerCase();
const position = _focusEvidencePosition(record);
if (!id || !position) continue;
const klass = record.klass || 'airliner';
const altitudeM = Number.isFinite(record.altitudeM)
? record.altitudeM
: (Cesium.Cartographic.fromCartesian(position)?.height || 3_000);
: Cesium.Cartographic.fromCartesian(position)?.height || 3_000;
const meta = {
callsign: String(record.callsign || id).toUpperCase(),
altitude: altitudeM,
@@ -82,16 +104,17 @@ function _setFocusEvidenceAircraft(records = []) {
flightState._lastFleetTickMs = 0;
flightState._viewer.scene.requestRender?.();
return { ok: true, count: flightState._count };
}
}
/** Update explicit evidence positions without rebuilding billboards. */
/** Update explicit evidence positions without rebuilding billboards. */
function _moveFocusEvidenceAircraft(records = []) {
function _moveFocusEvidenceAircraft(records = []) {
if (!FOCUS_EVIDENCE_DEV) return { ok: false, moved: 0 };
let moved = 0;
for (const record of Array.isArray(records) ? records : []) {
const id = String(record?.id || '').trim().toLowerCase();
const id = String(record?.id || '')
.trim()
.toLowerCase();
if (!flightState._focusEvidenceIds.has(id)) continue;
const position = _focusEvidencePosition(record);
const bb = flightState._billboards.get(id);
@@ -100,24 +123,27 @@ function _moveFocusEvidenceAircraft(records = []) {
const meta = flightState._flightData.get(id);
if (meta) {
if (Number.isFinite(record.trackDeg)) meta.true_track = record.trackDeg;
if (Number.isFinite(record.velocityMps)) meta.velocity = record.velocityMps;
if (Number.isFinite(record.velocityMps))
meta.velocity = record.velocityMps;
}
moved += 1;
}
flightState._lastFleetTickMs = 0;
flightState._viewer?.scene?.requestRender?.();
return { ok: true, moved };
}
}
/** JSON-safe visual snapshot for the evidence report. */
/** JSON-safe visual snapshot for the evidence report. */
function _focusEvidenceSnapshot() {
function _focusEvidenceSnapshot() {
if (!FOCUS_EVIDENCE_DEV || !flightState._viewer) return [];
return [...flightState._focusEvidenceIds].map((id) => {
const bb = flightState._billboards.get(id);
const screen = bb?.position
? Cesium.SceneTransforms.worldToWindowCoordinates(flightState._viewer.scene, bb.position)
? Cesium.SceneTransforms.worldToWindowCoordinates(
flightState._viewer.scene,
bb.position,
)
: null;
return {
id,
@@ -127,10 +153,18 @@ function _focusEvidenceSnapshot() {
x: screen?.x ?? null,
y: screen?.y ?? null,
cameraDistanceM: bb?.position
? Cesium.Cartesian3.distance(flightState._viewer.camera.positionWC, bb.position)
? Cesium.Cartesian3.distance(
flightState._viewer.camera.positionWC,
bb.position,
)
: null,
};
});
}
return { _focusEvidencePosition, _setFocusEvidenceAircraft, _moveFocusEvidenceAircraft, _focusEvidenceSnapshot };
}
return {
_focusEvidencePosition,
_setFocusEvidenceAircraft,
_moveFocusEvidenceAircraft,
_focusEvidenceSnapshot,
};
}
+27 -18
View File
@@ -10,23 +10,32 @@ import { createEvidence } from './evidence.js';
import { createTesting } from './testing.js';
import { createQueries } from './queries.js';
/** Compose one civil-flight layer with application-owned scene services. */
export function createCivilFlightLayer({ source, services, resolveAsset = (url) => url } = {}) {
const flightState = createFlightState({ source, services });
const parts = {};
const layer = {};
const context = { flightState, services, parts, layer, resolveAsset };
parts.rendering = createRendering(context);
parts.motion = createMotion(context);
parts.tracking = createTracking(context);
parts.controller = createController(context);
parts.enrichment = createEnrichment(context);
parts.ingestion = createIngestion(context);
parts.lifecycle = createLifecycle(context);
parts.evidence = createEvidence(context);
parts.testing = createTesting(context);
parts.queries = createQueries(context);
Object.assign(layer, parts.queries.methods, parts.lifecycle.methods, parts.ingestion.methods);
Object.defineProperty(layer, 'testing', { value: parts.testing });
return layer;
export function createCivilFlightLayer({
source,
services,
resolveAsset = (url) => url,
} = {}) {
const flightState = createFlightState({ source, services });
const parts = {};
const layer = {};
const context = { flightState, services, parts, layer, resolveAsset };
parts.rendering = createRendering(context);
parts.motion = createMotion(context);
parts.tracking = createTracking(context);
parts.controller = createController(context);
parts.enrichment = createEnrichment(context);
parts.ingestion = createIngestion(context);
parts.lifecycle = createLifecycle(context);
parts.evidence = createEvidence(context);
parts.testing = createTesting(context);
parts.queries = createQueries(context);
Object.assign(
layer,
parts.queries.methods,
parts.lifecycle.methods,
parts.ingestion.methods,
);
Object.defineProperty(layer, 'testing', { value: parts.testing });
return layer;
}
export { TRACKED_MODEL_MAX_PX } from './policy.js';
+180 -58
View File
@@ -1,21 +1,41 @@
import * as Cesium from 'cesium';
import { stickyNumber, stickyText } from '../../data/aircraftMeta.js';
import { geoidSurfaceLastResortM, pickRenderAltitudeM } from '../../data/renderAltitude.js';
import {
geoidSurfaceLastResortM,
pickRenderAltitudeM,
} from '../../data/renderAltitude.js';
import { classifyAircraft } 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 { GROUND_FLOOR_CLAMP_RADIUS_KM, GROUND_FLOOR_WARM_MAX_ALT_M, POSITION_HISTORY_LIMIT, LANDED_MISSING_POLL_LIMIT, MISSING_POLL_LIMIT, ERROR_BACKOFF_INTERVAL } from './policy.js';
export function createIngestion({ flightState, services, parts, layer, resolveAsset }) {
const { refreshMilitaryRegistryIfStale, isMilitaryIcao } = services.militaryRegistry;
const { geoidHeight } = services.geoid;
const { cachedGroundFloor, floorAltitudeM, warmGroundFloor } = services.groundFloor;
const { sampleMeshFloorCells } = services.meshFloor;
const flightsLayer = layer;
const methods = {
import {
GROUND_FLOOR_CLAMP_RADIUS_KM,
GROUND_FLOOR_WARM_MAX_ALT_M,
POSITION_HISTORY_LIMIT,
LANDED_MISSING_POLL_LIMIT,
MISSING_POLL_LIMIT,
ERROR_BACKOFF_INTERVAL,
} from './policy.js';
export function createIngestion({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
const { refreshMilitaryRegistryIfStale, isMilitaryIcao } =
services.militaryRegistry;
const { geoidHeight } = services.geoid;
const { cachedGroundFloor, floorAltitudeM, warmGroundFloor } =
services.groundFloor;
const { sampleMeshFloorCells } = services.meshFloor;
const flightsLayer = layer;
const methods = {
/**
* Fetch the latest aircraft state vectors from the OpenSky proxy and
* reconcile them with the billboard collection.
@@ -50,7 +70,10 @@ const methods = {
: resourceController.signal;
try {
updateSignal.throwIfAborted();
const snapshot = await flightState._source.getSnapshot(parts.controller._flightQuery(viewer || flightState._viewer), { signal: updateSignal });
const snapshot = await flightState._source.getSnapshot(
parts.controller._flightQuery(viewer || flightState._viewer),
{ signal: updateSignal },
);
updateSignal.throwIfAborted();
flightState._lastStatus = snapshot.status ?? 200;
const usableStates = snapshot.records;
@@ -60,7 +83,8 @@ const methods = {
flightState._backoff = sourceStale;
flightState._retryAt = 0;
flightState._lastError = sourceStale
? sourceAgeMs == null ? 'Source snapshot time unavailable'
? sourceAgeMs == null
? 'Source snapshot time unavailable'
: `Source snapshot ${Math.max(2, Math.round(sourceAgeMs / 60_000))} min old`
: null;
flightState._lastSource = snapshot.source;
@@ -72,18 +96,33 @@ const methods = {
// Field-test round 3 (2026-07-06, Austin fleet-underground): viewer
// subpoint + collected floor cells for the viewer-proximate low-contact
// clamp below — one carto read per poll, one batch warm after the loop.
const viewerCarto = (viewer || flightState._viewer)?.camera?.positionCartographic || null;
const viewerLatDeg = viewerCarto ? Cesium.Math.toDegrees(viewerCarto.latitude) : null;
const viewerLonDeg = viewerCarto ? Cesium.Math.toDegrees(viewerCarto.longitude) : null;
const viewerCarto =
(viewer || flightState._viewer)?.camera?.positionCartographic || null;
const viewerLatDeg = viewerCarto
? Cesium.Math.toDegrees(viewerCarto.latitude)
: null;
const viewerLonDeg = viewerCarto
? Cesium.Math.toDegrees(viewerCarto.longitude)
: null;
const floorWarmPoints = [];
// Classification and display policy consume source-independent observations.
refreshMilitaryRegistryIfStale();
for (const observation of usableStates) {
const { id: icao24, callsign, originCountry: origin_country,
longitude: lon, latitude: lat, baroAltitudeM: baro_alt, onGround: on_ground,
speedMps: velocity, courseDeg: true_track, ellipsoidAltitudeM: geo_alt,
category, verticalRateMps: vertical_rate } = observation;
const {
id: icao24,
callsign,
originCountry: origin_country,
longitude: lon,
latitude: lat,
baroAltitudeM: baro_alt,
onGround: on_ground,
speedMps: velocity,
courseDeg: true_track,
ellipsoidAltitudeM: geo_alt,
category,
verticalRateMps: vertical_rate,
} = observation;
acceptedSnapshotIcaos.add(icao24);
const onGround = on_ground === true;
@@ -126,7 +165,11 @@ const methods = {
// ellipsoidal), else baro+geoid as a visual fallback, else ground
// surface when parked. `Cartesian3.fromDegrees` gets renderAltitudeM,
// never `alt` directly.
const alt = stickyNumber(baro_alt, prevMeta?.altitude, onGround ? 0 : 10000);
const alt = stickyNumber(
baro_alt,
prevMeta?.altitude,
onGround ? 0 : 10000,
);
// geoid undulation N: cached per-aircraft (negligible drift — see
// task brief) once the geoid grid has loaded; unavailable pre-load
@@ -154,14 +197,25 @@ const methods = {
// (warmed by last poll's batch; aprons are flat across adjacent
// 111 m cells). Round-5 verify caught taxiing contacts stuck at
// the geoid without this (round 2's lesson, at cell granularity).
if (surfaceM == null && Number.isFinite(prevMeta?.rawLat) && Number.isFinite(prevMeta?.rawLon)) {
if (
surfaceM == null &&
Number.isFinite(prevMeta?.rawLat) &&
Number.isFinite(prevMeta?.rawLon)
) {
surfaceM = cachedGroundFloor(prevMeta.rawLat, prevMeta.rawLon);
}
// Grounded contacts near the viewer feed the floor warm/sampler
// (the only ones whose exact height is visible; far contacts are
// subpixel and always-on-top anyway).
if (viewerLatDeg != null &&
parts.queries._approxDistanceKm(viewerLatDeg, viewerLonDeg, lat, lon) <= GROUND_FLOOR_CLAMP_RADIUS_KM) {
if (
viewerLatDeg != null &&
parts.queries._approxDistanceKm(
viewerLatDeg,
viewerLonDeg,
lat,
lon,
) <= GROUND_FLOOR_CLAMP_RADIUS_KM
) {
floorWarmPoints.push({ lat, lon });
}
// Last synchronous resort for a BRAND-NEW grounded contact with NO
@@ -228,17 +282,33 @@ const methods = {
// within GROUND_FLOOR_CLAMP_RADIUS_KM of the viewer — the only ones
// whose burial is visible. Cells warm in one batch after the loop.
// Airborne only (grounded planes keep the surface-cache path above).
if (!onGround && renderAltitudeM < GROUND_FLOOR_WARM_MAX_ALT_M &&
if (
!onGround &&
renderAltitudeM < GROUND_FLOOR_WARM_MAX_ALT_M &&
(icao24 === flightState._trackedIcao ||
(viewerLatDeg != null &&
parts.queries._approxDistanceKm(viewerLatDeg, viewerLonDeg, lat, lon) <= GROUND_FLOOR_CLAMP_RADIUS_KM))) {
renderAltitudeM = floorAltitudeM(renderAltitudeM, cachedGroundFloor(lat, lon));
parts.queries._approxDistanceKm(
viewerLatDeg,
viewerLonDeg,
lat,
lon,
) <= GROUND_FLOOR_CLAMP_RADIUS_KM))
) {
renderAltitudeM = floorAltitudeM(
renderAltitudeM,
cachedGroundFloor(lat, lon),
);
floorWarmPoints.push({ lat, lon });
}
const position = Cesium.Cartesian3.fromDegrees(lon, lat, renderAltitudeM);
const position = Cesium.Cartesian3.fromDegrees(
lon,
lat,
renderAltitudeM,
);
// Landing/takeoff transition: the on_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
// flies free of it, a landing plane earns a fresh sample where it rolls out.
if (groundFlipped) flightState._groundSnap.forget(icao24);
@@ -263,17 +333,28 @@ const methods = {
// Round 6: lifted occlusion-test point for contacts rendering
// at/below the ellipsoid (fleet pass reads it — see the occluder
// note there). Null for the overwhelmingly common airborne case.
cullPosition: renderAltitudeM < 10 ? Cesium.Cartesian3.fromDegrees(lon, lat, 12) : null,
cullPosition:
renderAltitudeM < 10
? Cesium.Cartesian3.fromDegrees(lon, lat, 12)
: null,
velocity: stickyNumber(velocity, prevMeta?.velocity, 0),
true_track: stickyNumber(true_track, prevMeta?.true_track, 0),
category: cat,
// An adsbdb-enriched type code outranks the coarse OpenSky category.
klass: classifyAircraft({ typeCode: prevMeta?.typeCode ?? null, category: cat }),
klass: classifyAircraft({
typeCode: prevMeta?.typeCode ?? null,
category: cat,
}),
turnRateDps: prevMeta?.turnRateDps || 0,
verticalRate: stickyNumber(vertical_rate, prevMeta?.verticalRate, null),
verticalRate: stickyNumber(
vertical_rate,
prevMeta?.verticalRate,
null,
),
// Analyst seam: OpenSky origin_country (state[2]) — additive, sticky
// like callsign so a transient blank row doesn't blank the field.
originCountry: stickyText(origin_country, prevMeta?.originCountry) || null,
originCountry:
stickyText(origin_country, prevMeta?.originCountry) || null,
// OpenSky distinguishes the last position epoch from the last
// transponder message. The fleet coast horizon uses this actual
// contact time so a temporarily old position does not hard-freeze
@@ -305,7 +386,9 @@ const methods = {
// 5-15s stale and receipt-time stamping is what caused the
// back/forward oscillation. Only append when the fix actually
// advances, so stale repeats don't create zero-dt segments.
const fixEpochMs = Number.isFinite(observation.positionTimeMs) && observation.positionTimeMs > 0
const fixEpochMs =
Number.isFinite(observation.positionTimeMs) &&
observation.positionTimeMs > 0
? observation.positionTimeMs
: Date.now();
const fixTime = Cesium.JulianDate.fromDate(new Date(fixEpochMs));
@@ -339,15 +422,17 @@ const methods = {
// surface-clamped via the surfaceM chain, so the ground leg drapes).
if (isTracked) parts.tracking._appendTrailFix(position.clone());
} else {
const modelOwnsGroundVisual = parts.rendering._modelOwnsVisual(icao24);
const modelOwnsGroundVisual =
parts.rendering._modelOwnsVisual(icao24);
if (!modelOwnsGroundVisual) {
liftRepeatedGroundFix(newest, position, meta.onGround);
}
// Apply fresh kinematics only from a forward synthetic fix. Mutating
// the historical fix reprojects the entire stale interval and snaps
// the rendered aircraft when course or speed changes late.
const kinematicsChanged = newest.velocity !== meta.velocity
|| newest.track !== meta.true_track;
const kinematicsChanged =
newest.velocity !== meta.velocity ||
newest.track !== meta.true_track;
if (kinematicsChanged) {
const synthetic = synthesizeForwardKinematicsFix(newest, {
epochMs: Date.now(),
@@ -359,7 +444,8 @@ const methods = {
history.push(synthetic);
if (history.length > POSITION_HISTORY_LIMIT) history.shift();
meta.turnRateDps = turnRateFromFixHistory(history);
if (isTracked) parts.tracking._appendTrailFix(synthetic.position.clone());
if (isTracked)
parts.tracking._appendTrailFix(synthetic.position.clone());
}
}
}
@@ -376,27 +462,39 @@ const methods = {
if (!isTracked) {
// Refresh affiliation hue without clobbering the tick-owned
// freshness × focus × horizon alpha composition.
bb.color = parts.rendering._fleetBillboardColor(icao24).withAlpha(bb.color?.alpha ?? 1);
bb.color = parts.rendering
._fleetBillboardColor(icao24)
.withAlpha(bb.color?.alpha ?? 1);
}
if (prevMeta?.klass !== meta.klass || groundFlipped || flightState._cockpitContactMode) {
if (
prevMeta?.klass !== meta.klass ||
groundFlipped ||
flightState._cockpitContactMode
) {
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Poll-path class change (category updates): same model resync rule
// as the enrichment path — the class's GLB/scale may have changed.
if (prevMeta?.klass !== meta.klass) parts.rendering._syncModelToClass(icao24);
if (prevMeta?.klass !== meta.klass)
parts.rendering._syncModelToClass(icao24);
} else {
const bb = flightState._billboardCollection.add({
position,
image: aircraftIcon(parts.rendering._iconKind(icao24, meta.klass)),
image: aircraftIcon(
parts.rendering._iconKind(icao24, meta.klass),
),
width: isTracked ? 24 : 20,
height: isTracked ? 24 : 20,
scale: parts.rendering._fleetBillboardScale(icao24, meta.klass),
// Screen-projected rotation lands on the next fleet tick.
rotation: 0,
alignedAxis: Cesium.Cartesian3.ZERO,
color: isTracked ? Cesium.Color.CYAN : parts.rendering._fleetBillboardColor(icao24),
color: isTracked
? Cesium.Color.CYAN
: parts.rendering._fleetBillboardColor(icao24),
sizeInMeters: false,
scaleByDistance: parts.rendering._normalBillboardScaleByDistance(),
scaleByDistance:
parts.rendering._normalBillboardScaleByDistance(),
// Grounded/near-surface planes sit at/below the photoreal tile
// surface — render them depth-test-free so they never vanish up
// close (_groundDepthDistance).
@@ -411,7 +509,8 @@ const methods = {
// Takeoff while TRACKED: ground traffic drew no trail, so start one
// from the fresh airborne history (touchdown needs no action — the
// 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
// (position updates automatically via dead-reckoning CallbackProperty)
@@ -429,12 +528,23 @@ const methods = {
for (const [icao24, bb] of flightState._billboards) {
if (currentIcaos.has(icao24)) continue;
// 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 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) {
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
// entity owns the visual), so refresh the label — with icao24 now
// in _missingPolls, _trackedLabelText appends the STALE cue so
@@ -477,7 +587,11 @@ const methods = {
// contact renders across every cell its dead-reckoned position drifts
// through. Add those too, so the display clamp has data where the contact
// actually is instead of silently passing.
parts.motion._collectDisplayCorridorCells(floorWarmPoints, viewerLatDeg, viewerLonDeg);
parts.motion._collectDisplayCorridorCells(
floorWarmPoints,
viewerLatDeg,
viewerLonDeg,
);
// Field-test round 3: one batch warm of the viewer-proximate low-contact
// floor cells collected in the loop (fire-and-forget, single-flight;
@@ -488,7 +602,11 @@ const methods = {
// billboards/models are excluded so a vertical probe can't land on an
// aircraft instead of the pavement.
sampleMeshFloorCells(flightState._viewer?.scene, floorWarmPoints, {
excludeObjects: [...flightState._billboards.values(), ...flightState._models.values(), flightState._trackedModel].filter(Boolean),
excludeObjects: [
...flightState._billboards.values(),
...flightState._models.values(),
flightState._trackedModel,
].filter(Boolean),
viewerLat: viewerLatDeg,
viewerLon: viewerLonDeg,
});
@@ -518,22 +636,26 @@ const methods = {
};
console.log(`[Data:Flights] Updated: ${flightState._count} aircraft`);
parts.tracking._applyPendingTrackingRestore();
} catch (e) {
if (updateSignal.aborted || e?.name === 'AbortError') {
throw new DOMException('Flights update aborted', 'AbortError');
}
console.warn('[Data:Flights] Fetch error:', e);
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;
if (e?.source) { flightState._lastSource = e.source; this.source = flightState._lastSource; }
flightState._lastError = e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
if (e?.source) {
flightState._lastSource = e.source;
this.source = flightState._lastSource;
}
flightState._lastError =
e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
} finally {
flightState._activeUpdateControllers.delete(resourceController);
}
}
};
},
};
return { methods };
return { methods };
}
+109 -47
View File
@@ -1,27 +1,36 @@
import * as Cesium from 'cesium';
import { PLANE_MODEL_URL } from './policy.js';
export function createLifecycle({ flightState, services, parts, layer, resolveAsset }) {
const { clearFocusTarget } = services.focus;
const { registerSpriteCollection, restoreSpriteOrder, restoreSpriteOrderOnEnable } = services.sprites;
const { onMilitaryLayerActiveChange } = services.militaryRegistry;
const { holdContinuousRender, releaseContinuousRender } = services.render;
const { ensureGeoidReady } = services.geoid;
const { registerPickOwner, unregisterPickOwner } = services.picking;
const methods = {
export function createLifecycle({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
const { clearFocusTarget } = services.focus;
const {
registerSpriteCollection,
restoreSpriteOrder,
restoreSpriteOrderOnEnable,
} = services.sprites;
const { onMilitaryLayerActiveChange } = services.militaryRegistry;
const { holdContinuousRender, releaseContinuousRender } = services.render;
const { ensureGeoidReady } = services.geoid;
const { registerPickOwner, unregisterPickOwner } = services.picking;
const methods = {
/** Configure the source before initialization; an active layer keeps its owner. */
setSource(source) {
if (flightState._viewer) throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
if (flightState._viewer)
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._lastSource = source.label || flightState._lastSource;
this.source = flightState._lastSource;
},
/**
* Initialize the flights layer.
* Creates the BillboardCollection, resets all state, and installs the
@@ -29,9 +38,12 @@ const methods = {
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
*/
init(viewer) {
if (flightState._viewer) throw new Error('Flight layer is already initialized');
if (typeof flightState._source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
if (flightState.lifetime.signal.aborted) flightState.lifetime = new AbortController();
if (flightState._viewer)
throw new Error('Flight layer is already initialized');
if (typeof flightState._source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required');
if (flightState.lifetime.signal.aborted)
flightState.lifetime = new AbortController();
clearFocusTarget('flights');
flightState._focusEvidenceIds.clear();
flightState._viewer = viewer;
@@ -45,12 +57,25 @@ const methods = {
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
if (!flightState._preloadModel) {
const epoch = flightState._modelEpoch;
Cesium.Model.fromGltfAsync({ url: resolveAsset(PLANE_MODEL_URL), asynchronous: false })
.then((m) => {
if (epoch === flightState._modelEpoch) { flightState._preloadModel = m; flightState._planeModelLoaded = true; }
else { try { m.destroy(); } catch { /* gone */ } }
Cesium.Model.fromGltfAsync({
url: resolveAsset(PLANE_MODEL_URL),
asynchronous: false,
})
.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._detectionObjects = new Map();
@@ -70,14 +95,20 @@ const methods = {
parts.tracking._resetTrackedSelectionState();
flightState._trackedEntity = null;
flightState._cockpitSubjectId = null;
flightState._cockpitContactMode = document.body.classList.contains('cockpit-mode');
flightState._cockpitContactMode =
document.body.classList.contains('cockpit-mode');
flightState._cockpitNearContacts = new Set();
if (!flightState._cockpitModeListener) {
flightState._cockpitModeListener = (event) => parts.tracking._applyCockpitState(event?.detail);
window.addEventListener('gev:cockpit-mode-changed', flightState._cockpitModeListener);
flightState._cockpitModeListener = (event) =>
parts.tracking._applyCockpitState(event?.detail);
window.addEventListener(
'gev:cockpit-mode-changed',
flightState._cockpitModeListener,
);
}
// Fresh session — full bucket, anchor re-seeded on the first sweep.
flightState._enrichAmbientBudget = parts.enrichment._ambientBudgetKnobs().ceil;
flightState._enrichAmbientBudget =
parts.enrichment._ambientBudgetKnobs().ceil;
flightState._enrichAmbientRefillAnchorMs = 0;
parts.tracking._installClickHandler(viewer);
@@ -85,7 +116,9 @@ const methods = {
// React to Military-layer toggles IMMEDIATELY (suppress/restore sweep)
// instead of waiting out the 30 s poll (M2).
if (!flightState._milActiveChangeUnsub) {
flightState._milActiveChangeUnsub = onMilitaryLayerActiveChange(parts.tracking._onMilitaryActiveChange);
flightState._milActiveChangeUnsub = onMilitaryLayerActiveChange(
parts.tracking._onMilitaryActiveChange,
);
}
restoreSpriteOrder(viewer);
@@ -93,34 +126,48 @@ const methods = {
console.log('[Data:Flights] Initialized with billboard icons');
},
/**
* Show the billboard collection and re-install the click handler.
* @param {Cesium.Viewer} viewer
*/
enable(viewer) {
if (flightState._billboardCollection) flightState._billboardCollection.show = true;
if (flightState._billboardCollection)
flightState._billboardCollection.show = true;
holdContinuousRender('flights'); // per-frame animator (perf wave 2)
if (flightState._modelCollection) flightState._modelCollection.show = true;
parts.tracking._setCockpitContactMode(document.body.classList.contains('cockpit-mode'));
if (flightState._modelCollection)
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
// only reads geoidHeight() synchronously after this resolves (guarded by
// _geoidReady) — never awaited per-aircraft, never blocking a poll tick.
if (!flightState._geoidReady) {
const lifetime = flightState.lifetime;
ensureGeoidReady()
.then(() => { if (!lifetime.signal.aborted) flightState._geoidReady = true; })
.catch(() => { /* geoid grid failed to load — baro path stays un-geoid-corrected until retried */ });
.then(() => {
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);
registerPickOwner('flights', (pickedId) => flightState._billboards.has(pickedId));
registerPickOwner('flights', (pickedId) =>
flightState._billboards.has(pickedId),
);
// Force a fresh rotation pass on the first tick after re-enable
flightState._lastCamPoseSig = '';
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) {
flightState._trackedModelPreUpdateRemove = viewer.scene.preUpdate.addEventListener(parts.rendering._updateTrackedModel);
flightState._trackedModelPreUpdateRemove =
viewer.scene.preUpdate.addEventListener(
parts.rendering._updateTrackedModel,
);
}
if (!flightState._moveEndRemove && viewer?.camera) {
// Arrival polish (field test 2026-07-03: "planes look weird when you first
@@ -132,15 +179,16 @@ const methods = {
// ROTATION_REFRESH_MS. Zeroing the tick throttle too means the pass runs
// on the next preRender, not up to FLEET_DR_INTERVAL_MS later. Cost: one
// extra rotation pass per completed camera gesture — nothing per-frame.
flightState._moveEndRemove = viewer.camera.moveEnd.addEventListener(() => {
flightState._moveEndRemove = viewer.camera.moveEnd.addEventListener(
() => {
flightState._lastCamPoseSig = '';
flightState._lastFleetTickMs = 0;
});
},
);
}
restoreSpriteOrderOnEnable('flights', viewer);
},
/**
* Hide all flight billboards and tear down click/keyboard handlers.
* Also clears any active flight tracking so the camera is released.
@@ -149,10 +197,12 @@ const methods = {
disable(viewer) {
parts.controller._abortActiveUpdates();
parts.tracking._cancelPendingTrackingRestore();
if (flightState._billboardCollection) flightState._billboardCollection.show = false;
if (flightState._billboardCollection)
flightState._billboardCollection.show = false;
releaseContinuousRender('flights');
parts.rendering._releaseModels();
if (flightState._modelCollection) flightState._modelCollection.show = false;
if (flightState._modelCollection)
flightState._modelCollection.show = false;
parts.tracking._clearTracking();
parts.tracking._destroyTrail();
// Remove click handler + keydown listener while disabled to avoid
@@ -181,7 +231,6 @@ const methods = {
}
},
/**
* Fully tear down the flights layer — remove primitives, handlers,
* tracked entities, and clear all internal state maps.
@@ -210,7 +259,10 @@ const methods = {
}
document.removeEventListener('keydown', parts.tracking._onKeyDown);
if (flightState._cockpitModeListener) {
window.removeEventListener('gev:cockpit-mode-changed', flightState._cockpitModeListener);
window.removeEventListener(
'gev:cockpit-mode-changed',
flightState._cockpitModeListener,
);
flightState._cockpitModeListener = null;
}
unregisterPickOwner('flights');
@@ -238,7 +290,14 @@ const methods = {
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
flightState._modelPending.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._billboards.clear();
flightState._detectionObjects.clear();
@@ -249,7 +308,10 @@ const methods = {
flightState._displayFloorState.clear();
flightState._enrichQueue.length = 0;
flightState._enrichSeen.clear();
if (flightState._enrichDripTimer) { clearTimeout(flightState._enrichDripTimer); flightState._enrichDripTimer = null; }
if (flightState._enrichDripTimer) {
clearTimeout(flightState._enrichDripTimer);
flightState._enrichDripTimer = null;
}
flightState._missingPolls.clear();
flightState._focusEvidenceIds.clear();
flightState._count = 0;
@@ -267,8 +329,8 @@ const methods = {
};
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
flightState._viewer = null;
}
};
},
};
return { methods };
return { methods };
}
+425 -169
View File
@@ -1,24 +1,74 @@
import * as Cesium from 'cesium';
import { COURSE_HOLD_SPEED_MPS, lerpAngleDeg, speedRamp, courseBetweenCartesians, staleCoastLimitSeconds, arcOffsetEnu, courseSlewCapDps, limitCourseStep, corridorPathLatLon } from '../../data/motionModel.js';
import {
COURSE_HOLD_SPEED_MPS,
lerpAngleDeg,
speedRamp,
courseBetweenCartesians,
staleCoastLimitSeconds,
arcOffsetEnu,
courseSlewCapDps,
limitCourseStep,
corridorPathLatLon,
} from '../../data/motionModel.js';
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
import { modelVisualAnchor, modelAnchorWorld } from '../../data/modelVisualAnchor.js';
import { FOCUS_EVIDENCE_DEV, RENDER_DELAY_SEC, TRACKED_MODEL_MAX_PX, TRACKED_MODEL_MIN_PX, TRACKED_BILLBOARD_SCALE_BY_DISTANCE, DR_CORRECTION_MS, COURSE_SLEW_DT_MAX_SEC, COURSE_MAX_DPS, HELD_FLOOR_MAX_DRIFT_KM, NEIGHBOR_FLOOR_PROBE_MS, FLOOR_SEED_GRACE_MS, FLOOR_EASE_MAX_STEP, FLOOR_EASE_TAU_MS, FLOOR_EASE_EPSILON_M, DISPLAY_CORRIDOR_RADIUS_KM, DISPLAY_CORRIDOR_LOOKAHEAD_SEC, DISPLAY_CORRIDOR_CELL_BUDGET, DISPLAY_CORRIDOR_FAIR_SHARE } from './policy.js';
import {
modelVisualAnchor,
modelAnchorWorld,
} from '../../data/modelVisualAnchor.js';
import {
FOCUS_EVIDENCE_DEV,
RENDER_DELAY_SEC,
TRACKED_MODEL_MAX_PX,
TRACKED_MODEL_MIN_PX,
TRACKED_BILLBOARD_SCALE_BY_DISTANCE,
DR_CORRECTION_MS,
COURSE_SLEW_DT_MAX_SEC,
COURSE_MAX_DPS,
HELD_FLOOR_MAX_DRIFT_KM,
NEIGHBOR_FLOOR_PROBE_MS,
FLOOR_SEED_GRACE_MS,
FLOOR_EASE_MAX_STEP,
FLOOR_EASE_TAU_MS,
FLOOR_EASE_EPSILON_M,
DISPLAY_CORRIDOR_RADIUS_KM,
DISPLAY_CORRIDOR_LOOKAHEAD_SEC,
DISPLAY_CORRIDOR_CELL_BUDGET,
DISPLAY_CORRIDOR_FAIR_SHARE,
} from './policy.js';
export function createMotion({ flightState, services, parts, layer, resolveAsset }) {
const { cachedGroundFloor, GROUND_FLOOR_LIFT_M, neighborFloorM, stickyFloorCell, displayFloorHeightM, coarseFloorCoord, corridorFloorCells, allocateCorridorCells } = services.groundFloor;
const { trackedModelScaleForPixelCap } = services.camera;
const { nearFarScalarValueAtDistance, clearFocusTarget, publishFocusTargetFromCachedPosition } = services.focus;
export function createMotion({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
const {
cachedGroundFloor,
GROUND_FLOOR_LIFT_M,
neighborFloorM,
stickyFloorCell,
displayFloorHeightM,
coarseFloorCoord,
corridorFloorCells,
allocateCorridorCells,
} = services.groundFloor;
const { trackedModelScaleForPixelCap } = services.camera;
const {
nearFarScalarValueAtDistance,
clearFocusTarget,
publishFocusTargetFromCachedPosition,
} = services.focus;
// Round 5: the old `_warmGroundedAircraftSurfaceCache` (global exact-5-decimal
// warm for every grounded contact on Earth) is GONE. Parked-aircraft GPS
// jitter minted fresh keys every poll → thousands of upstream points per
// minute → Re:Earth proxy failures → geoid-fallback POISON cached at
// sea-level heights → sunken sprites/trails and rejected mesh samples. The
// coarse ~111 m floor cells (viewer-proximate, collected in the poll loop)
// are the only DEM warm the layer needs.
// Round 5: the old `_warmGroundedAircraftSurfaceCache` (global exact-5-decimal
// warm for every grounded contact on Earth) is GONE. Parked-aircraft GPS
// jitter minted fresh keys every poll → thousands of upstream points per
// minute → Re:Earth proxy failures → geoid-fallback POISON cached at
// sea-level heights → sunken sprites/trails and rejected mesh samples. The
// coarse ~111 m floor cells (viewer-proximate, collected in the poll loop)
// are the only DEM warm the layer needs.
/**
/**
* Round 6: lifts STALE grounded contacts onto floors that warmed after
* their last feed fix. A parked plane whose transponder went quiet keeps
* coasting on its final meta — if that fix predated the floor warm, it sat
@@ -30,27 +80,36 @@ const { nearFarScalarValueAtDistance, clearFocusTarget, publishFocusTargetFromCa
* floored by the live path — skipped here).
*/
function _refloorStaleGroundedContacts(currentIcaos) {
function _refloorStaleGroundedContacts(currentIcaos) {
for (const [icao24, info] of flightState._flightData) {
if (!info?.onGround || currentIcaos.has(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);
if (!Number.isFinite(floor)) continue;
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.cullPosition = null; // above the ellipsoid now (or floors say otherwise next poll)
const position = Cesium.Cartesian3.fromDegrees(info.rawLon, info.rawLat, lifted);
const position = Cesium.Cartesian3.fromDegrees(
info.rawLon,
info.rawLat,
lifted,
);
const history = flightState._positionHistory.get(icao24);
const newest = history?.[history.length - 1];
if (newest) newest.position = Cesium.Cartesian3.clone(position, newest.position);
if (newest)
newest.position = Cesium.Cartesian3.clone(position, newest.position);
const bb = flightState._billboards.get(icao24);
if (bb) bb.position = position;
}
}
}
/**
/**
* Dead-reckon an aircraft's current position using ENU (East-North-Up) frame math.
*
* Projects forward from the last known API fix using the aircraft's ground
@@ -65,19 +124,25 @@ function _refloorStaleGroundedContacts(currentIcaos) {
* @returns {Cesium.Cartesian3|null} Estimated ECEF position, or null if no history exists.
*/
function _deadReckon(icao24, result) {
function _deadReckon(icao24, result) {
const info = flightState._flightData.get(icao24);
if (FOCUS_EVIDENCE_DEV && flightState._focusEvidenceIds.has(icao24)) {
const position = flightState._billboards.get(icao24)?.position;
flightState._drCourseDeg = info?.true_track || 0;
flightState._drSpeedMps = info?.velocity || 0;
flightState._drCourseHold = flightState._drSpeedMps < COURSE_HOLD_SPEED_MPS;
flightState._drCourseHold =
flightState._drSpeedMps < COURSE_HOLD_SPEED_MPS;
flightState._drExtrapolating = false;
return position ? Cesium.Cartesian3.clone(position, result || new Cesium.Cartesian3()) : null;
return position
? Cesium.Cartesian3.clone(position, result || new Cesium.Cartesian3())
: null;
}
const history = flightState._positionHistory.get(icao24);
if (!history || history.length === 0) {
flightState._drCourseDeg = null; flightState._drSpeedMps = null; flightState._drCourseHold = false; flightState._drExtrapolating = false;
flightState._drCourseDeg = null;
flightState._drSpeedMps = null;
flightState._drCourseHold = false;
flightState._drExtrapolating = false;
return null;
}
@@ -85,7 +150,9 @@ function _deadReckon(icao24, result) {
// Render one poll interval behind real time so we interpolate between
// two KNOWN fixes whenever possible (see RENDER_DELAY_SEC rationale).
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.
@@ -97,7 +164,8 @@ function _deadReckon(icao24, result) {
Cesium.JulianDate.lessThanOrEquals(renderTime, b.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
: 1.0;
// Course of the DISPLAYED motion. The chord is only trustworthy when the
@@ -109,14 +177,19 @@ function _deadReckon(icao24, result) {
// of snapping once per poll. Helicopters always use the reported track
// (rotorcraft chords are noise-dominated at their typical speeds).
const chordLenM = Cesium.Cartesian3.distance(a.position, b.position);
const segSpeed = span > 0 ? chordLenM / span : ((info && info.velocity) || 0);
const segSpeed =
span > 0 ? chordLenM / span : (info && info.velocity) || 0;
const fallbackTrack = (info && info.true_track) || 0;
const trackFrom = Number.isFinite(a.track) ? a.track : fallbackTrack;
const trackTo = Number.isFinite(b.track) ? b.track : trackFrom;
const trackCourse = lerpAngleDeg(trackFrom, trackTo, t);
const w = (info && info.klass === 'helicopter') ? 0 : speedRamp(segSpeed);
const chordCourse = w > 0 ? courseBetweenCartesians(a.position, b.position) : null;
flightState._drCourseDeg = chordCourse != null ? lerpAngleDeg(trackCourse, chordCourse, w) : trackCourse;
const w = info && info.klass === 'helicopter' ? 0 : speedRamp(segSpeed);
const chordCourse =
w > 0 ? courseBetweenCartesians(a.position, b.position) : null;
flightState._drCourseDeg =
chordCourse != null
? lerpAngleDeg(trackCourse, chordCourse, w)
: trackCourse;
flightState._drSpeedMps = segSpeed;
flightState._drCourseHold = segSpeed < COURSE_HOLD_SPEED_MPS;
flightState._drExtrapolating = false;
@@ -125,7 +198,10 @@ function _deadReckon(icao24, result) {
}
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) {
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
// aircraft, before RENDER_DELAY_SEC of history has accumulated, so no bracketing
@@ -136,8 +212,17 @@ function _deadReckon(icao24, result) {
// the oldest fix froze the icon; extrapolating the NEWEST fix to wall-clock now made
// the icon jump back ~one poll interval the instant interpolation took over.)
const oldest = history[0];
const lookbackSec = Cesium.JulianDate.secondsDifference(oldest.time, renderTime); // ≥ 0
return _extrapolateFix(oldest, info, -Math.min(lookbackSec, 60), out, (info && info.turnRateDps) || 0);
const lookbackSec = Cesium.JulianDate.secondsDifference(
oldest.time,
renderTime,
); // ≥ 0
return _extrapolateFix(
oldest,
info,
-Math.min(lookbackSec, 60),
out,
(info && info.turnRateDps) || 0,
);
}
// Newest POSITION is older than renderTime. OpenSky can still be receiving
@@ -164,10 +249,9 @@ function _deadReckon(icao24, result) {
out,
(info && info.turnRateDps) || 0,
);
}
}
/**
/**
* Dead-reckon a fix along its own velocity/track by `dt` seconds, integrating a
* constant-rate-turn arc when `turnRateDps` is significant (straight line
* otherwise). Positive `dt` projects FORWARD (after the fix); negative `dt`
@@ -178,9 +262,13 @@ function _deadReckon(icao24, result) {
* Arc math adapted from skylight (https://github.com/cpaczek/skylight, MIT).
*/
function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) {
const speed = Number.isFinite(fix.velocity) ? fix.velocity : ((info && info.velocity) || 0);
const heading = Number.isFinite(fix.track) ? fix.track : ((info && info.true_track) || 0);
function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) {
const speed = Number.isFinite(fix.velocity)
? fix.velocity
: (info && info.velocity) || 0;
const heading = Number.isFinite(fix.track)
? fix.track
: (info && info.true_track) || 0;
flightState._drSpeedMps = speed;
flightState._drCourseHold = speed < COURSE_HOLD_SPEED_MPS;
flightState._drExtrapolating = true;
@@ -192,13 +280,21 @@ function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) {
// standard-rate turn is ~90° of arc wrong per 30 s if extrapolated straight.
arcOffsetEnu(speed, heading, turnRateDps, dt, flightState._scratchArc);
flightState._drCourseDeg = flightState._scratchArc.endCourseDeg;
Cesium.Cartesian3.fromElements(flightState._scratchArc.east, flightState._scratchArc.north, 0, flightState._scratchOffset);
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(fix.position, Cesium.Ellipsoid.WGS84, flightState._scratchEnu);
Cesium.Cartesian3.fromElements(
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);
}
}
/**
/**
* True while the tracked aircraft's delayed display time (now − RENDER_DELAY_SEC)
* predates its oldest real fix — i.e. _deadReckon is extrapolating backward, with no
* real history yet sitting BEHIND the displayed icon. The trail must draw nothing in
@@ -206,26 +302,29 @@ function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) {
* @returns {boolean}
*/
function _isTrackWarmingUp() {
function _isTrackWarmingUp() {
if (!flightState._trackedIcao) return false;
const history = flightState._positionHistory.get(flightState._trackedIcao);
if (!history || history.length === 0) return true;
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);
}
}
/** 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) {
const camera = flightState._viewer?.camera;
const scene = flightState._viewer?.scene;
if (!camera?.positionWC || !position || !scene) return 28;
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
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({
baseScale: spec.scale,
nativeRadiusM: spec.nativeRadiusM,
@@ -234,25 +333,31 @@ function _trackedFocusSizePx(icao24, position) {
fovyRad: camera.frustum.fovy,
maximumPixelSize: TRACKED_MODEL_MAX_PX,
});
const focalLengthPx = scene.canvas.clientHeight / (2 * Math.tan(camera.frustum.fovy / 2));
const projectedDiameterPx = (2 * spec.nativeRadiusM * scale * focalLengthPx) / rangeM;
return Math.max(TRACKED_MODEL_MIN_PX, Math.min(TRACKED_MODEL_MAX_PX, projectedDiameterPx));
const focalLengthPx =
scene.canvas.clientHeight / (2 * Math.tan(camera.frustum.fovy / 2));
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 time = flightState._viewer.clock.currentTime;
const width = billboard?.width?.getValue(time) ?? 28;
const height = billboard?.height?.getValue(time) ?? 28;
const scale = billboard?.scale?.getValue(time)
?? (CLASS_SCALE_2D[flightState._flightData.get(icao24)?.klass] || 1);
const scaleByDistance = billboard?.scaleByDistance?.getValue(time)
?? TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
const scale =
billboard?.scale?.getValue(time) ??
(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);
return Math.max(width, height) * scale * distanceScale;
}
}
/**
/**
* Per-frame-cached, discontinuity-smoothed tracked DISPLAY position. Cached by Cesium
* frame number so the position / rotation / trail-head callbacks share ONE computation
* (and one reconciliation-state update) per frame. Returns a stable module holder, or
@@ -261,13 +366,18 @@ function _trackedFocusSizePx(icao24, position) {
* @returns {Cesium.Cartesian3|null}
*/
function _trackedDisplayPosition(icao24) {
function _trackedDisplayPosition(icao24) {
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
// _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);
flightState._cachedDRCourse = flightState._drCourseDeg;
flightState._cachedDRSpeedMps = flightState._drSpeedMps;
@@ -290,7 +400,11 @@ function _trackedDisplayPosition(icao24) {
const plausible = speed * dtSec * 4 + 25;
if (Cesium.Cartesian3.distance(raw, flightState._drPrevRaw) > plausible) {
// 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;
}
} else {
@@ -299,8 +413,13 @@ function _trackedDisplayPosition(icao24) {
}
const elapsed = nowMs - flightState._drCorrectionStartMs;
const factor = elapsed >= DR_CORRECTION_MS ? 0 : 1 - elapsed / DR_CORRECTION_MS;
let display = Cesium.Cartesian3.multiplyByScalar(flightState._drCorrection, factor, flightState._trackedPosHolder);
const factor =
elapsed >= DR_CORRECTION_MS ? 0 : 1 - elapsed / DR_CORRECTION_MS;
let display = Cesium.Cartesian3.multiplyByScalar(
flightState._drCorrection,
factor,
flightState._trackedPosHolder,
);
Cesium.Cartesian3.add(raw, display, display);
// Same display floor the fleet pass applies — otherwise selecting a correctly
// floored grounded billboard swapped it for an unfloored tracked entity and
@@ -312,14 +431,23 @@ function _trackedDisplayPosition(icao24) {
// reports sensor truth — see the note there. Skipped while a tracked 3D model
// owns the visual: it rides groundSnap's one-shot sample and moving its input
// would force a re-sample (T7).
display = _floorGroundedDisplayPosition(icao24, info, display, parts.rendering._modelOwnsVisual(icao24), nowMs);
display = _floorGroundedDisplayPosition(
icao24,
info,
display,
parts.rendering._modelOwnsVisual(icao24),
nowMs,
);
Cesium.Cartesian3.clone(raw, flightState._drPrevRaw);
Cesium.Cartesian3.clone(display, flightState._drPrevDisplay);
flightState._drPrevMs = nowMs;
flightState._drReconcileValid = true;
flightState._cachedDRPosition = display;
const focusSizePx = _trackedFocusSizePx(icao24, flightState._cachedDRPosition);
const focusSizePx = _trackedFocusSizePx(
icao24,
flightState._cachedDRPosition,
);
// Publish only the exact per-frame display cache the tracked entity/camera
// consumes. Re-running DR from a later frame phase recreates the historical
// target-vs-camera jitter bug.
@@ -333,22 +461,23 @@ function _trackedDisplayPosition(icao24) {
heightPx: focusSizePx,
});
return display;
}
}
/** The tracked plane's current display position WITHOUT recomputing — the exact value the
/** The tracked plane's current display position WITHOUT recomputing — the exact value the
* follow-camera already settled on this frame (in the Viewer _onTick). getDetectableObjects + the
* readout run in postRender at a LATER frameNumber, so calling _trackedDisplayPosition there would
* re-run the dead-reckon on a fresh sample and double-advance the reconciliation → the label jitters
* against the now-stable plane (the model's jitter fix, resurfacing in the labels). Returns null when
* there's no valid fix for the tracked aircraft, so callers fall back to the billboard position. */
function _trackedDisplayCached() {
return (flightState._drReconcileValid && flightState._drReconcileIcao === flightState._trackedIcao) ? flightState._cachedDRPosition : null;
}
function _trackedDisplayCached() {
return flightState._drReconcileValid &&
flightState._drReconcileIcao === flightState._trackedIcao
? flightState._cachedDRPosition
: null;
}
/**
/**
* 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 dead-reckoned position the billboard uses.
@@ -364,61 +493,86 @@ function _trackedDisplayCached() {
* so the follow-camera anti-jitter contract on `gevDisplayPosition` is untouched.
*/
function _trackedVisualCached() {
if (flightState._trackedIcao && parts.rendering._modelOwnsVisual(flightState._trackedIcao)) {
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass);
function _trackedVisualCached() {
if (
flightState._trackedIcao &&
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
) {
const spec = parts.rendering._modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass,
);
return modelVisualAnchor(
flightState._trackedModel.modelMatrix,
spec.visualCenterNative,
Number.isFinite(flightState._trackedModel.computedScale) ? flightState._trackedModel.computedScale : spec.scale,
Number.isFinite(flightState._trackedModel.computedScale)
? flightState._trackedModel.computedScale
: spec.scale,
flightState._trackedVisualPos,
);
}
return _trackedDisplayCached();
}
}
/** 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 lower-centre hardpoint. */
/** 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
* above `scale` to satisfy minimumPixelSize — the trail head has to be judged
* against the size the operator SEES, not the nominal one. */
/** World-space origin of the tracked model, or null when no model of this
/** World-space origin of the tracked model, or null when no model of this
* contact is drawing. This is the centre the rendered bounding sphere is
* measured from, so the trail clip and the envelope agree on one frame. */
function _trackedModelCenterWorld() {
if (!flightState._trackedIcao || !flightState._trackedModel || !parts.rendering._modelOwnsVisual(flightState._trackedIcao)) return null;
return Cesium.Matrix4.getTranslation(flightState._trackedModel.modelMatrix, flightState._scratchTrailClip);
}
function _trackedModelCenterWorld() {
if (
!flightState._trackedIcao ||
!flightState._trackedModel ||
!parts.rendering._modelOwnsVisual(flightState._trackedIcao)
)
return null;
return Cesium.Matrix4.getTranslation(
flightState._trackedModel.modelMatrix,
flightState._scratchTrailClip,
);
}
function _trackedModelEnvelopeM() {
if (!flightState._trackedIcao || !flightState._trackedModel || !parts.rendering._modelOwnsVisual(flightState._trackedIcao)) return 0;
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass);
function _trackedModelEnvelopeM() {
if (
!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)
? flightState._trackedModel.computedScale
: spec.scale;
return spec.nativeRadiusM * scale;
}
}
function _trackedTrailCached() {
if (flightState._trackedIcao && parts.rendering._modelOwnsVisual(flightState._trackedIcao)) {
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass);
function _trackedTrailCached() {
if (
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
// half-correction put this offset on the lateral axis — see
// modelAnchorWorld).
return modelAnchorWorld(flightState._trackedModel, spec.trailAnchorNative, flightState._trackedTrailPos);
return modelAnchorWorld(
flightState._trackedModel,
spec.trailAnchorNative,
flightState._trackedTrailPos,
);
}
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
* own computation), NEVER re-runs _deadReckon. Safe to call more than once
* per frame: the second call sees dt≈0 and the limiter is a no-op.
@@ -429,33 +583,43 @@ function _trackedTrailCached() {
* whatever nose this path last wrote — the tracked and fleet consumers of
* the same aircraft can never disagree across the handoff. */
function _trackedDisplayCourse() {
function _trackedDisplayCourse() {
const info = flightState._flightData.get(flightState._trackedIcao);
const fallback = (info && info.true_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 nowMs = Date.now();
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;
flightState._trackedCourseMs = nowMs;
const prev = flightState._displayCourse.get(flightState._trackedIcao);
// 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.
if (cacheValid && flightState._cachedDRHold && prev != null) return prev;
const cap = courseSlewCapDps(cacheValid ? flightState._cachedDRSpeedMps : ((info && info.velocity) ?? NaN), COURSE_MAX_DPS);
const cap = courseSlewCapDps(
cacheValid
? flightState._cachedDRSpeedMps
: ((info && info.velocity) ?? NaN),
COURSE_MAX_DPS,
);
const course = limitCourseStep(prev, raw, cap, dt);
flightState._displayCourse.set(flightState._trackedIcao, 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
* entry belongs to the AIRCRAFT (not to the tracked session) and must
* survive the handoff back to the fleet pass. */
function _resetTrackedDisplay() {
function _resetTrackedDisplay() {
flightState._drReconcileValid = false;
flightState._drReconcileIcao = null;
flightState._cachedDRFrame = -1;
@@ -464,9 +628,9 @@ function _resetTrackedDisplay() {
flightState._cachedDRSpeedMps = null;
flightState._cachedDRHold = false;
flightState._trackedCourseMs = 0;
}
}
/**
/**
* The floor to stand a grounded contact on while its own cell is unresolved.
*
* Owner directive (2026-08-21, after a Re:Earth outage buried a parked contact
@@ -510,48 +674,59 @@ function _resetTrackedDisplay() {
* @returns {number|null} Floor to use, or null when nothing anywhere can answer.
*/
function _heldDisplayFloorM(state, cell, nowMs) {
if (state.heldTier && !(Number.isFinite(state.heldM) && state.heldCell
&& parts.queries._approxDistanceKm(cell.lat, cell.lon, state.heldCell.lat, state.heldCell.lon)
<= HELD_FLOOR_MAX_DRIFT_KM)) {
function _heldDisplayFloorM(state, cell, nowMs) {
if (
state.heldTier &&
!(
Number.isFinite(state.heldM) &&
state.heldCell &&
parts.queries._approxDistanceKm(
cell.lat,
cell.lon,
state.heldCell.lat,
state.heldCell.lon,
) <= HELD_FLOOR_MAX_DRIFT_KM
)
) {
// Out of range: the held value is no longer a measurement of anywhere this
// contact has been. Drop it rather than stretch it.
_dropHeldFloor(state);
}
if (state.heldTier === 'own' && !state.seeded) return state.heldM;
if (state.probeMs != null && nowMs - state.probeMs < NEIGHBOR_FLOOR_PROBE_MS) return state.heldM;
if (
state.probeMs != null &&
nowMs - state.probeMs < NEIGHBOR_FLOOR_PROBE_MS
)
return state.heldM;
state.probeMs = nowMs;
const near = neighborFloorM(cell);
if (near != null) return _adoptHeldFloorM(state, near, cell, 'neighbor');
return state.heldM;
}
}
/** Records a held floor and the tier it came from; returns it. Every caller
/** Records a held floor and the tier it came from; returns it. Every caller
* passes a floor measured for the cell the contact is reading NOW, so an
* adoption always clears the seed flag: live evidence has arrived. */
function _adoptHeldFloorM(state, floorM, cell, tier) {
function _adoptHeldFloorM(state, floorM, cell, tier) {
state.heldM = floorM;
state.heldCell = cell;
state.heldTier = tier;
state.seeded = false;
return floorM;
}
}
/** Forgets the held floor and everything that describes where it came from,
/** Forgets the held floor and everything that describes where it came from,
* leaving the rest of the contact's display state alone. */
function _dropHeldFloor(state) {
function _dropHeldFloor(state) {
state.heldM = null;
state.heldCell = null;
state.heldTier = null;
state.seeded = false;
}
}
/** Whether a parked seed has been away longer than the grace window.
/** Whether a parked seed has been away longer than the grace window.
*
* ONE judgement, asked from BOTH sides of the park, because neither side sees
* the whole story on its own. While the contact keeps reporting, the retire
@@ -564,12 +739,13 @@ function _dropHeldFloor(state) {
* before it clears `retiredMs`.
* @param {object} state @param {number} nowMs - Tick clock. */
function _seedExpired(state, nowMs) {
return state.retiredMs != null && nowMs - state.retiredMs > FLOOR_SEED_GRACE_MS;
}
function _seedExpired(state, nowMs) {
return (
state.retiredMs != null && nowMs - state.retiredMs > FLOOR_SEED_GRACE_MS
);
}
/** Retires a contact's display-floor state. Called the moment it stops being a
/** Retires a contact's display-floor state. Called the moment it stops being a
* grounded billboard — airborne, model-owned, or gone.
*
* The floor itself is kept as a rehydration seed for FLOOR_SEED_GRACE_MS (see
@@ -579,7 +755,7 @@ function _seedExpired(state, nowMs) {
* model-ownership gate is unaffected.
* @param {string} icao24 @param {number} nowMs - Tick clock. */
function _retireDisplayFloorState(icao24, nowMs) {
function _retireDisplayFloorState(icao24, nowMs) {
const state = flightState._displayFloorState.get(icao24);
if (!state) return;
if (state.retiredMs == null) {
@@ -595,10 +771,9 @@ function _retireDisplayFloorState(icao24, nowMs) {
} else if (_seedExpired(state, nowMs)) {
flightState._displayFloorState.delete(icao24); // airborne long enough to be anywhere
}
}
}
/**
/**
* Floors a GROUNDED contact's DISPLAYED position onto the local ground.
*
* `renderAltitudeM` is chosen once per poll from the floor of the FIX's coarse
@@ -645,23 +820,45 @@ function _retireDisplayFloorState(icao24, nowMs) {
* case — no allocation, no rebuild), otherwise the lifted position.
*/
function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs = Date.now()) {
function _floorGroundedDisplayPosition(
icao24,
info,
pos,
modelOwnsVisual,
nowMs = Date.now(),
) {
if (!pos || !info?.onGround || modelOwnsVisual) {
_retireDisplayFloorState(icao24, nowMs);
return pos;
}
const state = flightState._displayFloorState.get(icao24);
const carto = Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84, flightState._scratchDisplayCarto);
const carto = Cesium.Cartographic.fromCartesian(
pos,
Cesium.Ellipsoid.WGS84,
flightState._scratchDisplayCarto,
);
// Boundary hysteresis: a position jittering across a cell edge would flip
// floors at fleet-tick rate (see stickyFloorCell).
const cell = stickyFloorCell(
Cesium.Math.toDegrees(carto.latitude), Cesium.Math.toDegrees(carto.longitude), state?.cell,
Cesium.Math.toDegrees(carto.latitude),
Cesium.Math.toDegrees(carto.longitude),
state?.cell,
);
const floor = cachedGroundFloor(cell.lat, cell.lon);
const next = state || {
cell, in: new Cesium.Cartesian3(), out: null, effectiveM: null,
heldM: null, heldCell: null, heldTier: null, heldActive: false, seeded: false,
probeMs: null, easedM: null, easeMs: null, retiredMs: null,
cell,
in: new Cesium.Cartesian3(),
out: null,
effectiveM: null,
heldM: null,
heldCell: null,
heldTier: null,
heldActive: false,
seeded: false,
probeMs: null,
easedM: null,
easeMs: null,
retiredMs: null,
};
// Back on the ground. Judge the seed's AGE here, before `retiredMs` is
// cleared: a contact that made no calls while it was away never reached the
@@ -697,8 +894,13 @@ function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs
// AGAIN while the first approach is still running.
// Scoped to a borrowed floor on purpose: an ordinary cell-to-cell change
// between two resolved floors is the existing path and keeps its timing.
if (next.easedM == null && wasHeld && Number.isFinite(stoodOnM)
&& Number.isFinite(effective) && effective < stoodOnM) {
if (
next.easedM == null &&
wasHeld &&
Number.isFinite(stoodOnM) &&
Number.isFinite(effective) &&
effective < stoodOnM
) {
next.easedM = stoodOnM; // start from where the contact is actually drawn
next.easeMs = nowMs;
}
@@ -713,7 +915,10 @@ function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs
// where it is, so there is no seam to jump across.
const dtMs = Math.max(0, nowMs - next.easeMs);
next.easeMs = nowMs;
const closed = Math.min(FLOOR_EASE_MAX_STEP, 1 - Math.exp(-dtMs / FLOOR_EASE_TAU_MS));
const closed = Math.min(
FLOOR_EASE_MAX_STEP,
1 - Math.exp(-dtMs / FLOOR_EASE_TAU_MS),
);
let value = next.easedM + (effective - next.easedM) * closed;
if (Math.abs(value - effective) <= FLOOR_EASE_EPSILON_M) {
value = effective; // arrive exactly, so a parked contact stops rebuilding
@@ -735,7 +940,11 @@ function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs
// never adopted. One owned entry per grounded
// contact — O(grounded), dropped on eviction, on destroy, and the moment the
// contact stops being a grounded billboard.
if (state && state.effectiveM === effective && Cesium.Cartesian3.equals(pos, state.in)) {
if (
state &&
state.effectiveM === effective &&
Cesium.Cartesian3.equals(pos, state.in)
) {
return state.out || pos;
}
const lifted = displayFloorHeightM(carto.height, effective);
@@ -748,16 +957,18 @@ function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs
// The cache OWNS its output: returning a shared scratch would let the next
// contact in the fleet loop overwrite a position already handed out.
next.out = Cesium.Cartesian3.fromRadians(
carto.longitude, carto.latitude, lifted, Cesium.Ellipsoid.WGS84,
carto.longitude,
carto.latitude,
lifted,
Cesium.Ellipsoid.WGS84,
next.out || new Cesium.Cartesian3(),
);
}
if (!state) flightState._displayFloorState.set(icao24, next);
return next.out || pos;
}
}
/**
/**
* Adds the cells each grounded contact's DISPLAY is about to render over to
* this poll's floor warm/sample batch.
*
@@ -797,7 +1008,7 @@ function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs
* @param {number|null} viewerLat @param {number|null} viewerLon - Viewer subpoint.
*/
function _collectDisplayCorridorCells(out, viewerLat, viewerLon) {
function _collectDisplayCorridorCells(out, viewerLat, viewerLon) {
if (viewerLat == null || viewerLon == null) return;
// Cells the poll already collected (grounded + low-airborne fix cells).
const seen = new Set();
@@ -811,18 +1022,37 @@ function _collectDisplayCorridorCells(out, viewerLat, viewerLon) {
if (!info?.onGround) continue;
// T7: a contact whose 3D model is the visual never reads a display floor.
if (parts.rendering._modelOwnsVisual(icao24)) continue;
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon)) continue;
if (parts.queries._approxDistanceKm(viewerLat, viewerLon, info.rawLat, info.rawLon) > DISPLAY_CORRIDOR_RADIUS_KM) continue;
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon))
continue;
if (
parts.queries._approxDistanceKm(
viewerLat,
viewerLon,
info.rawLat,
info.rawLon,
) > DISPLAY_CORRIDOR_RADIUS_KM
)
continue;
const dr = _deadReckon(icao24, flightState._scratchCorridorPos);
if (!dr) continue;
// Read the sibling scratches IMMEDIATELY, before any other _deadReckon call.
const extrapolating = flightState._drExtrapolating;
const speedMps = Number.isFinite(flightState._drSpeedMps) ? flightState._drSpeedMps : (info.velocity || 0);
const courseDeg = flightState._drCourseDeg != null ? flightState._drCourseDeg : (info.true_track || 0);
const c = Cesium.Cartographic.fromCartesian(dr, Cesium.Ellipsoid.WGS84, flightState._scratchCorridorCarto);
const speedMps = Number.isFinite(flightState._drSpeedMps)
? flightState._drSpeedMps
: info.velocity || 0;
const courseDeg =
flightState._drCourseDeg != null
? flightState._drCourseDeg
: info.true_track || 0;
const c = Cesium.Cartographic.fromCartesian(
dr,
Cesium.Ellipsoid.WGS84,
flightState._scratchCorridorCarto,
);
const lat = Cesium.Math.toDegrees(c.latitude);
const lon = Cesium.Math.toDegrees(c.longitude);
const cells = corridorFloorCells(corridorPathLatLon({
const cells = corridorFloorCells(
corridorPathLatLon({
extrapolating,
displayLat: lat,
displayLon: lon,
@@ -834,7 +1064,8 @@ function _collectDisplayCorridorCells(out, viewerLat, viewerLon) {
fixLat: info.rawLat,
fixLon: info.rawLon,
lookaheadSec: DISPLAY_CORRIDOR_LOOKAHEAD_SEC,
}));
}),
);
let cold = 0;
for (const cell of cells) {
if (cachedGroundFloor(cell.lat, cell.lon) == null) cold += 1;
@@ -843,10 +1074,35 @@ function _collectDisplayCorridorCells(out, viewerLat, viewerLon) {
}
flightState._corridorEpoch += 1;
for (const cell of allocateCorridorCells(
candidates, seen, DISPLAY_CORRIDOR_CELL_BUDGET, DISPLAY_CORRIDOR_FAIR_SHARE, flightState._corridorEpoch,
candidates,
seen,
DISPLAY_CORRIDOR_CELL_BUDGET,
DISPLAY_CORRIDOR_FAIR_SHARE,
flightState._corridorEpoch,
)) {
out.push(cell);
}
}
return { _refloorStaleGroundedContacts, _deadReckon, _extrapolateFix, _isTrackWarmingUp, _trackedFocusSizePx, _trackedDisplayPosition, _trackedDisplayCached, _trackedVisualCached, _trackedModelCenterWorld, _trackedModelEnvelopeM, _trackedTrailCached, _trackedDisplayCourse, _resetTrackedDisplay, _heldDisplayFloorM, _adoptHeldFloorM, _dropHeldFloor, _seedExpired, _retireDisplayFloorState, _floorGroundedDisplayPosition, _collectDisplayCorridorCells };
}
return {
_refloorStaleGroundedContacts,
_deadReckon,
_extrapolateFix,
_isTrackWarmingUp,
_trackedFocusSizePx,
_trackedDisplayPosition,
_trackedDisplayCached,
_trackedVisualCached,
_trackedModelCenterWorld,
_trackedModelEnvelopeM,
_trackedTrailCached,
_trackedDisplayCourse,
_resetTrackedDisplay,
_heldDisplayFloorM,
_adoptHeldFloorM,
_dropHeldFloor,
_seedExpired,
_retireDisplayFloorState,
_floorGroundedDisplayPosition,
_collectDisplayCorridorCells,
};
}
+61 -16
View File
@@ -5,16 +5,37 @@ import { createFlightState } from './state.js';
import { createEnrichment } from './enrichment.js';
function services() {
const names = ['picking', 'sprites', 'trails', 'aircraftPresentation', 'camera',
'militaryRegistry', 'labels', 'groundFloor', 'meshFloor', 'geoid', 'focus',
'readout', 'context', 'render', 'recession'];
return { ...Object.fromEntries(names.map((name) => [name, {}])),
groundSnap: { createGroundSnap: () => ({ clear() {} }) } };
const names = [
'picking',
'sprites',
'trails',
'aircraftPresentation',
'camera',
'militaryRegistry',
'labels',
'groundFloor',
'meshFloor',
'geoid',
'focus',
'readout',
'context',
'render',
'recession',
];
return {
...Object.fromEntries(names.map((name) => [name, {}])),
groundSnap: { createGroundSnap: () => ({ clear() {} }) },
};
}
test('constructing independent flight layers performs no source request and isolates selection', () => {
let requests = 0;
const source = { label: 'Fixture aircraft', getSnapshot() { requests++; } };
const source = {
label: 'Fixture aircraft',
getSnapshot() {
requests++;
},
};
const first = createCivilFlightLayer({ source, services: services() });
const second = createCivilFlightLayer({ source, services: services() });
first.testing._armFlightTrackingRestoreForTest('abc123');
@@ -34,8 +55,17 @@ test('a layer without a source fails before touching the viewer', () => {
test('flight state owns distinct mutable records, scratch objects and floor samplers', () => {
const first = createFlightState({ services: services() });
const second = createFlightState({ services: services() });
for (const key of ['_flightData', '_billboards', '_positionHistory', '_displayFloorState',
'_groundSnap', '_scratchCarto', '_models', '_activeUpdateControllers', 'lifetime']) {
for (const key of [
'_flightData',
'_billboards',
'_positionHistory',
'_displayFloorState',
'_groundSnap',
'_scratchCarto',
'_models',
'_activeUpdateControllers',
'lifetime',
]) {
assert.notEqual(first[key], second[key], key);
}
});
@@ -43,14 +73,22 @@ test('flight state owns distinct mutable records, scratch objects and floor samp
test('aborted enrichment cannot write into a later lifecycle or decrement its active count', async () => {
let resolve;
let signal;
const source = { getEnrichment(query, options) {
const source = {
getEnrichment(query, options) {
signal = options.signal;
return new Promise((done) => { resolve = done; });
} };
return new Promise((done) => {
resolve = done;
});
},
};
const flightState = createFlightState({ source, services: services() });
const enrichment = createEnrichment({ flightState, services: {}, parts: {} });
let writes = 0;
enrichment._enqueueEnrich('test', { kind: 'type', id: 'abc123' }, () => writes++);
enrichment._enqueueEnrich(
'test',
{ kind: 'type', id: 'abc123' },
() => writes++,
);
await Promise.resolve();
assert.equal(flightState._enrichActive, 1);
flightState.lifetime.abort();
@@ -64,11 +102,18 @@ test('aborted enrichment cannot write into a later lifecycle or decrement its ac
});
test('a synchronous enrichment failure stays bounded and releases its slot', async () => {
const flightState = createFlightState({ services: services(), source: {
getEnrichment() { throw new Error('fixture failure'); },
} });
const flightState = createFlightState({
services: services(),
source: {
getEnrichment() {
throw new Error('fixture failure');
},
},
});
const enrichment = createEnrichment({ flightState, services: {}, parts: {} });
assert.doesNotThrow(() => enrichment._enqueueEnrich('test', {}, () => assert.fail()));
assert.doesNotThrow(() =>
enrichment._enqueueEnrich('test', {}, () => assert.fail()),
);
await new Promise((done) => setImmediate(done));
assert.equal(flightState._enrichActive, 0);
});
+22 -43
View File
@@ -1,15 +1,11 @@
import * as Cesium from 'cesium';
export const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
/** Amber tint for known-military aircraft rendered by this layer (matches the military layer's icon color). */
export const MIL_TINT = Cesium.Color.fromCssColorString('#FFB800');
// --- Ground traffic (owner reversal 2026-07-03: "absolutely we should see planes
// taxiing and landing") -----------------------------------------------------------
// Present-but-grounded planes are RENDERED instead of being skipped: same class
@@ -31,7 +27,6 @@ export const MIL_TINT = Cesium.Color.fromCssColorString('#FFB800');
export const GROUND_SCALE = 0.8;
// --- 3D model rendering (B3) ---------------------------------------------------------
// When enabled, aircraft render as 3D glTF models once the camera is below MODEL_ALT_CEIL_M
// (zoomed in); higher up they stay flat billboards. Eligibility is FRUSTUM-based (on-screen), and
@@ -40,33 +35,31 @@ export const GROUND_SCALE = 0.8;
export const PLANE_MODEL_URL = '/models/airplane.glb';
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)
export const MODEL_MIN_PX = 24;
// floor so distant models stay visible WITHOUT ballooning into a giant
// min-pixel blob (was 54 — far planes at the All radius became white
// star-bursts); ~matches the 2D icon size so the model↔billboard read is consistent
// floor so distant models stay visible WITHOUT ballooning into a giant
// min-pixel blob (was 54 — far planes at the All radius became white
// star-bursts); ~matches the 2D icon size so the model↔billboard read is consistent
export const TRACKED_MODEL_MIN_PX = 40;
// keep the glTF silhouette comparable to the selected 2D glyph at handoff
// keep the glTF silhouette comparable to the selected 2D glyph at handoff
export const TRACKED_MODEL_MAX_PX = 200;
// owner-selected close-range tracked-target feel
// owner-selected close-range tracked-target feel
export const MODEL_NATIVE_RADIUS_M = 34.41;
export const MODEL_SCALE = 1;
// airplane.glb is transform-applied and baked to real-world meters
// airplane.glb is transform-applied and baked to real-world meters
// Per-mode caps. Each model is its own draw call (no instancing yet), so these bound the frame cost.
export const MODEL_MAX = 150;
// 'proximity' cap (the planes immediately around you)
// 'proximity' cap (the planes immediately around you)
export const MODEL_MAX_ALL = 350;
// 'all' cap (everything out to ~the horizon)
// 'all' cap (everything out to ~the horizon)
// Per-mode ADD / KEEP radii. The two modes differ by RADIUS, not just cap — otherwise they look
// IDENTICAL whenever fewer than a cap's worth of planes are in range (field bug: Proximity and All
// rendered the same). 'proximity' = a tight ring; 'all' = roughly to the horizon (state-scale). Each
@@ -76,23 +69,22 @@ export const MODEL_MAX_ALL = 350;
// on-screen priority then spend the model slots on planes you can actually see.
export const MODEL_PROX_ADD_M = 150000;
// proximity: model NEW planes within 150 km
// proximity: model NEW planes within 150 km
export const MODEL_PROX_KEEP_M = 185000;
// proximity: KEEP modeled planes out to 185 km
// proximity: KEEP modeled planes out to 185 km
export const COCKPIT_MODEL_MAX = 60;
// max concurrent GLBs in cockpit (never raises the map cap)
// max concurrent GLBs in cockpit (never raises the map cap)
export const MODEL_ALL_ADD_M = 400000;
// all: model NEW planes within 400 km (~to the horizon)
// all: model NEW planes within 400 km (~to the horizon)
export const MODEL_ALL_KEEP_M = 450000;
// all: KEEP modeled planes out to 450 km
// all: KEEP modeled planes out to 450 km
export const MODEL_HEADING_OFFSET_DEG = 180;
// airplane.glb nose is opposite Cesium heading-0
// airplane.glb nose is opposite Cesium heading-0
// Owner launch-polish direction: models should read as clean light silhouettes,
// with only a weak diffuse contribution from the existing approved textures.
@@ -107,31 +99,28 @@ export const MODEL_COLOR_BLEND_AMOUNT = 0.94;
export const MODEL_BELLY_OFFSET_NATIVE = 6.719;
export const CYAN_TRANSPARENT = Cesium.Color.CYAN.withAlpha(0);
/** @constant {number} ERROR_BACKOFF_INTERVAL - Cooldown (ms) after transient errors */
export const ERROR_BACKOFF_INTERVAL = 20000;
// transient error retry
// transient error retry
/** @constant {number} POSITION_HISTORY_LIMIT - Max position samples kept per aircraft for dead reckoning */
export const POSITION_HISTORY_LIMIT = 5;
export const COCKPIT_CONTACT_SIZE_PX = 6;
export const COCKPIT_CIVILIAN_COLOR = Cesium.Color.fromCssColorString('#DCEEFF');
export const COCKPIT_CIVILIAN_COLOR =
Cesium.Color.fromCssColorString('#DCEEFF');
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 F1/F4): a fading polyline behind the
// tracked aircraft. The accumulation array is intentionally SEPARATE from
@@ -147,7 +136,6 @@ export const TRAIL_COLOR = '#00d4ff';
export const TRAIL_MAX_POINTS = 400;
// ---------------------------------------------------------------------------
// Render-behind smoothing (PRD WS-C C2 — approved product decision):
// the fleet renders at now - RENDER_DELAY_SEC so positions interpolate
@@ -202,7 +190,6 @@ export const GROUND_FLOOR_WARM_MAX_ALT_M = 4500;
export const GROUND_FLOOR_CLAMP_RADIUS_KM = 150;
// ---------------------------------------------------------------------------
// Icon orientation (2026-06-10 playtest fix): rotation is computed by
// projecting each aircraft's course vector into WINDOW coordinates
@@ -230,7 +217,6 @@ export const COURSE_MAX_DPS = 60;
export const COURSE_SLEW_DT_MAX_SEC = 0.25;
// ---------------------------------------------------------------------------
// adsbdb enrichment (best-effort, fail-silent). Bounded fan-out: max 4
// concurrent requests, dispatches dripped ≥ENRICH_DISPATCH_GAP_MS apart
@@ -247,7 +233,6 @@ export const ENRICH_MAX_INFLIGHT = 4;
export const ENRICH_DISPATCH_GAP_MS = 200;
// ---------------------------------------------------------------------------
// Ambient fleet type enrichment (2026-07-02 field data: OpenSky's live
// category field is 0/"no info" for ~94% of planes, so ambient classification
@@ -293,7 +278,6 @@ export const ENRICH_AMBIENT_REFILL_WINDOW_MS = 5 * 60 * 1000;
export const ENRICH_AMBIENT_PER_SWEEP = 150;
// ---------------------------------------------------------------------------
// Tracked-display reconciliation. _deadReckon gives the RAW position from real
// fixes; at the warm-up→interpolation handoff (and on feed glitches / backfill
@@ -303,10 +287,8 @@ export const ENRICH_AMBIENT_PER_SWEEP = 150;
// (the correction stays ~0 whenever motion is already continuous).
// ---------------------------------------------------------------------------
export const DR_CORRECTION_MS = 900;
/** Bounded on-demand loading for the tracked model. The 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 load→reject at frame
@@ -318,7 +300,6 @@ export const DR_CORRECTION_MS = 900;
export const TRACKED_MODEL_MAX_LOAD_FAILS = 3;
export const TRACKED_MODEL_RETRY_BACKOFF_MS = 1500;
/** @constant {number} Cap on NEW cells the display corridors may add to one
@@ -345,7 +326,6 @@ export const DISPLAY_CORRIDOR_LOOKAHEAD_SEC = RENDER_DELAY_SEC * 2;
export const DISPLAY_CORRIDOR_RADIUS_KM = 25;
/** @constant {number} How far a contact may travel from the cell that supplied
* its held floor before that floor stops describing the ground under it.
*
@@ -426,7 +406,6 @@ export const FLOOR_EASE_MAX_STEP = 0.22;
export const FLOOR_EASE_EPSILON_M = 0.02;
/** @constant {number} How long a retired hold stays usable as a rehydration
* seed — three poll intervals.
*
+318 -138
View File
@@ -2,20 +2,40 @@ import { displayedKinematics } from '../../data/motionModel.js';
import * as Cesium from 'cesium';
import { isExplicitLayerStateOrigin } from '../../data/layerState.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 { aircraftIncludedInNearby } from '../../data/aircraftNearbyPolicy.js';
import { LANDED_ALT_MAX_M, LANDED_SPEED_MAX_MPS, FOCUS_EVIDENCE_DEV, FLEET_DR_INTERVAL_MS } from './policy.js';
import {
LANDED_ALT_MAX_M,
LANDED_SPEED_MAX_MPS,
FOCUS_EVIDENCE_DEV,
FLEET_DR_INTERVAL_MS,
} from './policy.js';
export function createQueries({ flightState, services, parts, layer, resolveAsset }) {
const { tr3bTypeLabel, tr3bAircraftClass } = services.aircraftPresentation;
const { formatFlightLevel } = services.labels;
const { isMilitaryIcao } = services.militaryRegistry;
const { applyTrackedCameraFrame } = services.camera;
const { setFocusDeemphasisParams, getFocusDeemphasisParams, setFocusEvidenceNowMs, advanceFocusEvidenceNowMs } = services.focus;
const { setAircraftRecessionParams, getAircraftRecessionParams } = services.recession;
export function createQueries({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
const { tr3bTypeLabel, tr3bAircraftClass } = services.aircraftPresentation;
const { formatFlightLevel } = services.labels;
const { isMilitaryIcao } = services.militaryRegistry;
const { applyTrackedCameraFrame } = services.camera;
const {
setFocusDeemphasisParams,
getFocusDeemphasisParams,
setFocusEvidenceNowMs,
advanceFocusEvidenceNowMs,
} = services.focus;
const { setAircraftRecessionParams, getAircraftRecessionParams } =
services.recession;
/**
/**
* Cheap equirectangular distance (km) — plenty accurate for the ~150 km
* ground-floor clamp gate; runs once per contact per poll, so no trig-heavy
* haversine needed.
@@ -23,14 +43,14 @@ const { setAircraftRecessionParams, getAircraftRecessionParams } = services.rece
* @returns {number} Approximate great-circle distance in km.
*/
function _approxDistanceKm(lat1, lon1, lat2, lon2) {
function _approxDistanceKm(lat1, lon1, lat2, lon2) {
const dLat = (lat2 - lat1) * 111.32;
const dLon = (lon2 - lon1) * 111.32 * Math.cos(((lat1 + lat2) / 2) * Math.PI / 180);
const dLon =
(lon2 - lon1) * 111.32 * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180);
return Math.hypot(dLat, dLon);
}
}
/**
/**
* 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
* hold, so a plane missing either datum keeps the normal grace.
@@ -38,7 +58,7 @@ function _approxDistanceKm(lat1, lon1, lat2, lon2) {
* @returns {boolean}
*/
function _likelyLanded(icao24) {
function _likelyLanded(icao24) {
const info = flightState._flightData.get(icao24);
if (!info) return false;
// Round 7 (owner: "fewer planes than OpenSky's own map" + "parked planes
@@ -53,24 +73,26 @@ function _likelyLanded(icao24) {
if (info.wasAirborne !== true) return false;
// Was airborne, then grounded, then VANISHED from the poll — landed ghost.
if (info.onGround) return true;
return Number.isFinite(info.altitude) && info.altitude < LANDED_ALT_MAX_M
&& Number.isFinite(info.velocity) && info.velocity < LANDED_SPEED_MAX_MPS;
}
return (
Number.isFinite(info.altitude) &&
info.altitude < LANDED_ALT_MAX_M &&
Number.isFinite(info.velocity) &&
info.velocity < LANDED_SPEED_MAX_MPS
);
}
/**
/**
* Normalize a value to a trimmed string. A whitespace-only field (" ") is
* truthy, so every label chain must trim FIRST and then fall through.
* @param {*} value - Any value (typically a metadata string or null).
* @returns {string} Trimmed string, or '' if falsy.
*/
function _toCleanText(value) {
function _toCleanText(value) {
return String(value || '').trim();
}
}
/**
/**
* The layer's ONE label convention: spoken callsign → tail registration → raw
* ICAO hex. Mirrors militaryFlights.js so the same aircraft reads identically
* in both layers.
@@ -87,12 +109,13 @@ function _toCleanText(value) {
* @returns {string} Display label; never empty.
*/
function _contactLabel(icao24, info) {
return _toCleanText(info?.callsign) || _toCleanText(info?.registration) || icao24;
}
function _contactLabel(icao24, info) {
return (
_toCleanText(info?.callsign) || _toCleanText(info?.registration) || icao24
);
}
/**
/**
* Build a public descriptor for one aircraft using its best current position
* (dead-reckoned when history exists, billboard position otherwise).
* @param {string} icao24 - ICAO 24-bit transponder address.
@@ -100,7 +123,7 @@ function _contactLabel(icao24, info) {
* Descriptor with a cloned position, or null if the aircraft is unknown.
*/
function _describeFlight(icao24) {
function _describeFlight(icao24) {
// DATA, NOT PIXELS — deliberately UNFLOORED (2026-08-19). The display floor
// lifts grounded contacts onto the visible mesh so the sprite you see is not
// buried; that is a rendering correction, not a measurement. This descriptor
@@ -115,7 +138,8 @@ function _describeFlight(icao24) {
// `_trackedDisplayPosition`). If that ever changes, floor this path too.
const info = flightState._flightData.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;
const displayed = displayedKinematics({
derivedSpeedMps: flightState._drSpeedMps,
@@ -123,7 +147,11 @@ function _describeFlight(icao24) {
reportedSpeedMps: info?.velocity,
reportedTrackDeg: info?.true_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;
return {
icao24,
@@ -135,11 +163,15 @@ function _describeFlight(icao24) {
// ellipsoid height of the camera/ground-clamped render position. The
// latter can be slightly negative over terrain near the surface.
altitudeM: Number.isFinite(info?.altitude) ? info.altitude : carto.height,
renderAltitudeM: Number.isFinite(info?.renderAltitudeM) ? info.renderAltitudeM : carto.height,
renderAltitudeM: Number.isFinite(info?.renderAltitudeM)
? info.renderAltitudeM
: carto.height,
onGround: info?.onGround === true,
velocityMps: displayed.speedMps,
track: displayed.trackDeg,
stale: Boolean(flightState._missingPolls.get(icao24) || flightState._backoff),
stale: Boolean(
flightState._missingPolls.get(icao24) || flightState._backoff,
),
airline: info?.airline ?? null,
// CLASS label follows the TR-3B conversion so every downstream card
// (cockpit, Contacts, analyst) agrees with the triangle on screen.
@@ -152,24 +184,32 @@ function _describeFlight(icao24) {
// narration, getTrackedSubject) can use it as a label link without
// re-guarding a whitespace-only enrichment value.
registration: _toCleanText(info?.registration) || null,
origin: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.origin.code : null,
destination: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.destination.code : null,
route: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? {
origin:
info?.route && parts.tracking._routeIsPlausible(icao24, info.route)
? info.route.origin.code
: null,
destination:
info?.route && parts.tracking._routeIsPlausible(icao24, info.route)
? info.route.destination.code
: null,
route:
info?.route && parts.tracking._routeIsPlausible(icao24, info.route)
? {
origin: { ...info.route.origin },
destination: { ...info.route.destination },
} : null,
}
: null,
};
}
}
function _normalizeTrackedIcao(candidate) {
const normalized = String(candidate ?? '').trim().toLowerCase();
function _normalizeTrackedIcao(candidate) {
const normalized = String(candidate ?? '')
.trim()
.toLowerCase();
return normalized || null;
}
}
/**
/**
* Map one aircraft's internal poll record to a plain JSON-safe analyst
* record (analyst query engine seam). Pure — no Cesium types, no fetches;
* enrichment fields read the CACHED adsbdb values only. Missing/unknown
@@ -187,9 +227,16 @@ function _normalizeTrackedIcao(candidate) {
* operator: string|null, routeOrigin: string|null, routeDestination: string|null}}
*/
function mapAnalystRecord(icao24, info, { military = false, routeOk = false } = {}) {
function mapAnalystRecord(
icao24,
info,
{ military = false, routeOk = false } = {},
) {
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);
return {
// Display identity for the narration layer. `id` is NOT a queryable field
@@ -214,8 +261,9 @@ function mapAnalystRecord(icao24, info, { military = false, routeOk = false } =
routeOrigin: routeOk ? text(info?.route?.origin?.code) : null,
routeDestination: routeOk ? text(info?.route?.destination?.code) : null,
};
}
const methods = { mapAnalystRecord,
}
const methods = {
mapAnalystRecord,
id: 'flights',
@@ -227,12 +275,12 @@ const methods = { mapAnalystRecord,
// Browser-harness seam: isolates synthetic display-floor scenarios without
// changing any production lifecycle or cache policy.
_clearDisplayFloorStateForTest: parts.testing._clearDisplayFloorStateForTest,
_clearDisplayFloorStateForTest:
parts.testing._clearDisplayFloorStateForTest,
/** @type {number} Polling interval (ms) between update() calls */
updateInterval: 30000,
/**
* Live layer params.
* `models3d` toggles 3D glTF model rendering for the FLEET (altitude-gated): when on,
@@ -243,11 +291,16 @@ const methods = { mapAnalystRecord,
* @param {{models3d?: boolean, models3dMode?: 'proximity'|'all', selectedFlightsTrackingId?: string|null}} params
*/
setParams(params = {}, { origin = 'programmatic' } = {}) {
if (isExplicitLayerStateOrigin(origin)
&& !Object.hasOwn(params, 'selectedFlightsTrackingId')) {
if (
isExplicitLayerStateOrigin(origin) &&
!Object.hasOwn(params, 'selectedFlightsTrackingId')
) {
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;
if (!flightState._models3dEnabled) {
parts.rendering._releaseModels();
@@ -255,10 +308,17 @@ const methods = { mapAnalystRecord,
// Restore fleet billboards (the horizon-cull pass re-asserts next tick), but NEVER the
// tracked plane's own fleet billboard — its tracked entity is the visual, so re-showing it
// here would double-image the tracked plane when 3D is turned off mid-track.
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.
flightState._models3dMode = params.models3dMode;
if (flightState._cockpitContactMode) {
@@ -266,7 +326,10 @@ const methods = { mapAnalystRecord,
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;
parts.rendering._reloadModelsForIrBoost();
// Sprites don't reload with the models — swap the TR-3B glyph between its
@@ -275,16 +338,24 @@ const methods = { mapAnalystRecord,
parts.tracking._refreshTr3bForStyle();
}
if (Object.hasOwn(params, 'selectedFlightsTrackingId')) {
const requested = _normalizeTrackedIcao(params.selectedFlightsTrackingId);
const requested = _normalizeTrackedIcao(
params.selectedFlightsTrackingId,
);
if (requested === flightState._trackedIcao) {
flightState._pendingTrackingRestore = null;
} else if (requested === null) {
parts.tracking._cancelPendingTrackingRestore();
if (flightState._trackedIcao) parts.tracking._clearTracking(false, { origin });
if (flightState._trackedIcao)
parts.tracking._clearTracking(false, { origin });
} else {
const generation = ++flightState._trackingIntentGeneration;
flightState._pendingTrackingRestore = { id: requested, generation, origin };
if (flightState._trackedIcao) parts.tracking._clearTracking(false, { origin });
flightState._pendingTrackingRestore = {
id: requested,
generation,
origin,
};
if (flightState._trackedIcao)
parts.tracking._clearTracking(false, { origin });
parts.tracking._applyPendingTrackingRestore();
}
}
@@ -300,15 +371,15 @@ const methods = { mapAnalystRecord,
};
},
/**
* Re-render a contact whose TR-3B conversion just flipped (Easter egg).
* Callers own the registry write; this only re-derives what renders.
* @param {string} icao24 - ICAO 24-bit address.
* @returns {boolean} True when this layer owns the contact.
*/
refreshTr3b(icao24) { return parts.tracking._refreshTr3bContact(icao24); },
refreshTr3b(icao24) {
return parts.tracking._refreshTr3bContact(icao24);
},
/**
* Return a subsample of currently visible aircraft for detection overlay
@@ -323,7 +394,11 @@ const methods = { mapAnalystRecord,
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
*/
getDetectableObjects(options = {}) {
if (!flightState._billboardCollection || !flightState._billboardCollection.show) return [];
if (
!flightState._billboardCollection ||
!flightState._billboardCollection.show
)
return [];
// Compute a stride that evenly samples the billboard map.
// seed shifts the starting offset so successive calls can sample
// different aircraft without shuffling the underlying Map order.
@@ -331,16 +406,23 @@ const methods = { mapAnalystRecord,
? Math.max(1, Math.floor(options.maxCount))
: flightState._billboards.size;
const seed = Number.isFinite(options.seed) ? Math.floor(options.seed) : 0;
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 result = [];
let idx = 0;
for (const [icao24, bb] of flightState._billboards) {
const shouldTake = ((idx - start) % stride) === 0;
const shouldTake = (idx - start) % stride === 0;
idx++;
if (!shouldTake) continue;
if (flightState._cockpitContactMode && icao24.toLowerCase() === flightState._cockpitSubjectId) continue;
if (
flightState._cockpitContactMode &&
icao24.toLowerCase() === flightState._cockpitSubjectId
)
continue;
const isTracked = icao24 === flightState._trackedIcao;
// 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.
@@ -350,7 +432,11 @@ const methods = { mapAnalystRecord,
const info = flightState._flightData.get(icao24);
let object = flightState._detectionObjects.get(icao24);
if (!object) {
object = { sourceId: icao24, type: 'AIR', _weldPos: new Cesium.Cartesian3() };
object = {
sourceId: icao24,
type: 'AIR',
_weldPos: new Cesium.Cartesian3(),
};
flightState._detectionObjects.set(icao24, object);
}
// WELD: anchor to whatever actually owns the visual. A model-owned contact is
@@ -360,17 +446,21 @@ const methods = { mapAnalystRecord,
// and rises only as the coarse ground-floor cell warms. Zero extra sampling and
// no `_modelDisplayPosition` call from postRender. Sprite-owned contacts keep
// `bb.position` — sprite and bracket are co-located there, so association holds.
const spec = modelOwnsVisual ? parts.rendering._modelSpec(info?.klass) : null;
const spec = modelOwnsVisual
? parts.rendering._modelSpec(info?.klass)
: null;
const pos = isTracked
? (parts.motion._trackedVisualCached() || bb.position)
: (modelOwnsVisual
? parts.motion._trackedVisualCached() || bb.position
: modelOwnsVisual
? modelVisualAnchor(
model.modelMatrix,
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()),
)
: bb.position);
: bb.position;
if (!pos) continue;
object.position = pos;
object.skipLabel = isTracked;
@@ -385,15 +475,18 @@ const methods = { mapAnalystRecord,
result.push(object);
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()) {
if (!flightState._billboards.has(icao24)) flightState._detectionObjects.delete(icao24);
if (!flightState._billboards.has(icao24))
flightState._detectionObjects.delete(icao24);
}
}
return result;
},
/**
* Find a single aircraft by free-text query.
* Match priority: exact icao24 hex, exact callsign, callsign prefix,
@@ -403,8 +496,11 @@ const methods = { mapAnalystRecord,
* Best match with a cloned, dead-reckoned position, or null if none.
*/
findByQuery(query) {
if (!flightState._flightData || flightState._flightData.size === 0) return null;
const q = String(query || '').trim().toLowerCase();
if (!flightState._flightData || flightState._flightData.size === 0)
return null;
const q = String(query || '')
.trim()
.toLowerCase();
if (!q) return null;
// Registration is searched alongside callsign because it is what the
@@ -434,7 +530,6 @@ const methods = { mapAnalystRecord,
return best ? _describeFlight(best.id) : null;
},
/**
* Find aircraft near a given ECEF position, sorted ascending by distance.
* Return shape mirrors militaryFlightsLayer.getNearby (id/icao24/position/distance).
@@ -446,12 +541,18 @@ const methods = { mapAnalystRecord,
* @returns {Array<{id: string, icao24: string, callsign: string|null, position: Cesium.Cartesian3, distance: number, aircraftClass: string|null, altitudeM: number|null, velocityMps: number|null, track: number|null}>}
*/
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)
? Math.max(1, Math.floor(maxCount))
: 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 nearby = [];
@@ -460,14 +561,19 @@ const methods = { mapAnalystRecord,
const isTracked = icao24 === flightState._trackedIcao;
// 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.
if (!aircraftIncludedInNearby({
if (
!aircraftIncludedInNearby({
isTracked,
billboardShown: bb.show,
modelRendering: parts.rendering._modelOwnsVisual(icao24),
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;
if (!pos) continue;
@@ -486,7 +592,12 @@ const methods = { mapAnalystRecord,
// Filter surface: the cockpit next/previous path matches on THIS field
// (militaryAwareness.aircraftClassMatchesFilter), so a converted contact
// has to report the class it renders as or a `tr3b` filter skips it.
aircraftClass: tr3bAircraftClass(icao24, String(info?.klass || '').trim().toLowerCase() || null),
aircraftClass: tr3bAircraftClass(
icao24,
String(info?.klass || '')
.trim()
.toLowerCase() || null,
),
altitudeM: info?.altitude ?? null,
velocityMps: info?.velocity ?? null,
track: info?.true_track ?? null,
@@ -497,7 +608,6 @@ const methods = { mapAnalystRecord,
return nearby.slice(0, limit);
},
/**
* Return id/label/position for up to maxCount currently rendered aircraft.
* Cheap snapshot for voice-tool framing — billboard positions only, no
@@ -520,22 +630,39 @@ const methods = { mapAnalystRecord,
* holds no data and therefore cannot answer.
*/
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;
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) {
if (!flightState._billboardCollection || flightState._billboards.size === 0) return [];
const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 500;
if (
!flightState._billboardCollection ||
flightState._billboards.size === 0
)
return [];
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
: 500;
const result = [];
for (const [icao24, bb] of flightState._billboards) {
const pos = bb.position;
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;
const info = flightState._flightData.get(icao24);
result.push({
@@ -549,15 +676,20 @@ const methods = { mapAnalystRecord,
typeName: info?.typeName ?? null,
typeCode: info?.typeCode ?? null,
registration: info?.registration ?? null,
origin: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.origin.code : null,
destination: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.destination.code : null,
origin:
info?.route && parts.tracking._routeIsPlausible(icao24, info.route)
? info.route.origin.code
: null,
destination:
info?.route && parts.tracking._routeIsPlausible(icao24, info.route)
? info.route.destination.code
: null,
});
if (result.length >= limit) break;
}
return result;
},
/**
* 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
@@ -568,18 +700,30 @@ const methods = { mapAnalystRecord,
* @returns {Array<Object>} See mapAnalystRecord for the record shape.
*/
getAnalystRecords(maxCount = 2000) {
if (!flightState._billboardCollection || !flightState._billboardCollection.show || flightState._flightData.size === 0) return [];
const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 2000;
if (
!flightState._billboardCollection ||
!flightState._billboardCollection.show ||
flightState._flightData.size === 0
)
return [];
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
: 2000;
const result = [];
for (const [icao24, info] of flightState._flightData) {
const routeOk = !!info?.route && parts.tracking._routeIsPlausible(icao24, info.route);
result.push(mapAnalystRecord(icao24, info, { military: isMilitaryIcao(icao24), routeOk }));
const routeOk =
!!info?.route && parts.tracking._routeIsPlausible(icao24, info.route);
result.push(
mapAnalystRecord(icao24, info, {
military: isMilitaryIcao(icao24),
routeOk,
}),
);
if (result.length >= limit) break;
}
return result;
},
/**
* Start camera-tracking an aircraft by ICAO24 address.
* @param {string} icao24 - ICAO 24-bit transponder address.
@@ -590,16 +734,24 @@ const methods = { mapAnalystRecord,
let id = String(icao24).trim();
if (!flightState._billboards.has(id)) id = id.toLowerCase();
if (!flightState._billboards.has(id)) return false;
if (parts.tracking._isExplicitTrackingOrigin(origin)) parts.tracking._cancelPendingTrackingRestore();
if (flightState._trackedIcao === id) return parts.tracking._publishTrackedSelection(id, origin);
if (parts.tracking._isExplicitTrackingOrigin(origin))
parts.tracking._cancelPendingTrackingRestore();
if (flightState._trackedIcao === id)
return parts.tracking._publishTrackedSelection(id, origin);
parts.tracking._trackFlight(id, { origin });
return true;
},
/** Resolve a shared Follow target only against the latest accepted refresh. */
async resolveTrackingRestoreTarget(icao24, { signal = null, origin = 'share-restore' } = {}) {
if (signal?.aborted) return { status: 'cancelled', reason: String(signal.reason || 'aborted') };
async resolveTrackingRestoreTarget(
icao24,
{ signal = null, origin = 'share-restore' } = {},
) {
if (signal?.aborted)
return {
status: 'cancelled',
reason: String(signal.reason || 'aborted'),
};
const id = _normalizeTrackedIcao(icao24);
if (!id) return { status: 'missing', reason: 'invalid-target' };
const outcome = flightState._lastTrackingRefreshOutcome;
@@ -621,7 +773,11 @@ const methods = { mapAnalystRecord,
coverage: outcome.coverage,
};
}
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 });
return followed
? {
@@ -630,24 +786,35 @@ const methods = { mapAnalystRecord,
source: outcome.source,
coverage: outcome.coverage,
}
: { status: 'source-unavailable', reason: 'target-not-renderable', refreshEpoch: outcome.epoch };
: {
status: 'source-unavailable',
reason: 'target-not-renderable',
refreshEpoch: outcome.epoch,
};
},
/** Reapply the canonical follow frame without recreating the selected flight. */
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();
if (!flightState._billboards.has(id)) id = id.toLowerCase();
if (
id !== flightState._trackedIcao
|| !flightState._viewer.entities?.contains?.(flightState._trackedEntity)
|| flightState._viewer.trackedEntity !== flightState._trackedEntity
) return false;
id !== flightState._trackedIcao ||
!flightState._viewer.entities?.contains?.(flightState._trackedEntity) ||
flightState._viewer.trackedEntity !== flightState._trackedEntity
)
return false;
flightState._trackedCameraFrameStop?.();
flightState._viewer.camera.cancelFlight();
flightState._viewer.trackedEntity = flightState._trackedEntity;
flightState._trackedCameraFrameStop = applyTrackedCameraFrame(
flightState._trackedCameraFrameStop =
applyTrackedCameraFrame(
flightState._viewer,
flightState._trackedEntity,
flightState._trackedEntity.viewFrom,
@@ -656,7 +823,6 @@ const methods = { mapAnalystRecord,
return true;
},
/**
* Stop tracking the currently followed aircraft (no-op if none).
* @returns {boolean} Always true.
@@ -667,12 +833,10 @@ const methods = { mapAnalystRecord,
return true;
},
cancelPendingTrackingRestore() {
parts.tracking._cancelPendingTrackingRestore();
},
/**
* 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}
@@ -686,7 +850,6 @@ const methods = { mapAnalystRecord,
return rest;
},
/**
* Return the current aircraft as a Context subject without changing
* tracking or camera ownership.
@@ -702,15 +865,16 @@ const methods = { mapAnalystRecord,
id: described.icao24,
// Same label chain as getNearby/getDetectableObjects: a callsign-less
// contact reads as its registration, never as the raw ICAO hex.
label: described.callsign
|| _toCleanText(described.registration)
|| described.icao24,
label:
described.callsign ||
_toCleanText(described.registration) ||
described.icao24,
position: Cesium.Cartesian3.clone(described.position),
};
},
...(FOCUS_EVIDENCE_DEV ? {
...(FOCUS_EVIDENCE_DEV
? {
__focusEvidence: Object.freeze({
setAircraft: parts.evidence._setFocusEvidenceAircraft,
moveAircraft: parts.evidence._moveFocusEvidenceAircraft,
@@ -728,35 +892,42 @@ const methods = { mapAnalystRecord,
};
},
takeFrameClock(startMs = 1_000_000_000) {
if (!flightState._viewer || !Number.isFinite(startMs)) return { ok: false, nowMs: null };
if (!flightState._viewer || !Number.isFinite(startMs))
return { ok: false, nowMs: null };
flightState._viewer.useDefaultRenderLoop = false;
setFocusEvidenceNowMs(startMs);
flightState._lastFleetTickMs = startMs - FLEET_DR_INTERVAL_MS;
// Cross a browser task boundary, then close Cesium's pending-loop
// latch. Any already-queued callback still observes the false gate,
// while manual evidence renders can begin without waiting on VSYNC.
return new Promise((resolve) => setTimeout(() => {
if (flightState._viewer?._cesiumWidget) flightState._viewer._cesiumWidget._renderLoopRunning = false;
return new Promise((resolve) =>
setTimeout(() => {
if (flightState._viewer?._cesiumWidget)
flightState._viewer._cesiumWidget._renderLoopRunning = false;
resolve({ ok: true, nowMs: startMs });
}, 0));
}, 0),
);
},
advanceFrameClock(deltaMs = FLEET_DR_INTERVAL_MS) {
return advanceFocusEvidenceNowMs(deltaMs);
},
releaseFrameClock() {
setFocusEvidenceNowMs(null);
if (flightState._viewer) flightState._viewer.useDefaultRenderLoop = true;
if (flightState._viewer)
flightState._viewer.useDefaultRenderLoop = true;
},
}),
} : {}),
}
: {}),
/**
* Return layer health/status for the HUD stats chip.
* @returns {{count: number, lastUpdate: number|null, stale: boolean, error: string|null, status: number|null, retryInSec: number}}
*/
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 {
count: flightState._count,
lastUpdate: flightState._lastUpdate,
@@ -767,8 +938,17 @@ const methods = { mapAnalystRecord,
source: flightState._lastSource,
coverage: flightState._lastCoverage,
};
}
};
},
};
return { _approxDistanceKm, _likelyLanded, _toCleanText, _contactLabel, _describeFlight, _normalizeTrackedIcao, mapAnalystRecord, methods };
return {
_approxDistanceKm,
_likelyLanded,
_toCleanText,
_contactLabel,
_describeFlight,
_normalizeTrackedIcao,
mapAnalystRecord,
methods,
};
}
File diff suppressed because it is too large. Load diff
+221 -268
View File
@@ -2,14 +2,12 @@ import * as Cesium from 'cesium';
import { ENRICH_AMBIENT_BUDGET_CEIL } from './policy.js';
export function createFlightState({ source, services }) {
const { createGroundSnap } = services.groundSnap;
const flightState = { lifetime: new AbortController() };
const { createGroundSnap } = services.groundSnap;
const flightState = { lifetime: new AbortController() };
flightState._source = source;
flightState._source = source;
/** Per-class model spec. Hangar-fleet classes (CLASS_MODEL_REAL) ship GLBs
/** Per-class model spec. Hangar-fleet classes (CLASS_MODEL_REAL) ship GLBs
* vertex-baked to real-world METERS in the airplane.glb axis convention, so
* they render at scale 1 with their own measured belly lift and bounding
* radius. Every other class keeps the shared-airplane.glb formula
@@ -17,380 +15,355 @@ flightState._source = source;
* math multiplies it by `scale`, so world radius = nativeRadiusM × scale in
* both branches. The code-side MIX tint dominates every existing asset so
* class silhouettes stay light without modifying third-party GLBs/textures. */
/* Specs are static per class, and the detection weld now asks for one per
/* Specs are static per class, and the detection weld now asks for one per
* MODELED contact per frame (up to the fleet cap) on top of the 12 Hz fleet
* pass — so this is memoized, as militaryFlights.js already does. */
flightState._specCache = new Map();
flightState._specCache = new Map();
/** One-shot cached tile-skin heights for MODELED grounded planes (see groundSnap.js). */
/** One-shot cached tile-skin heights for MODELED grounded planes (see groundSnap.js). */
flightState._groundSnap = createGroundSnap();
flightState._groundSnap = createGroundSnap();
flightState._scratchGroundCarto = new Cesium.Cartographic();
flightState._scratchGroundCarto = new Cesium.Cartographic();
flightState._scratchGroundPos = new Cesium.Cartesian3();
/** @type {Cesium.PrimitiveCollection|null} */
flightState._scratchGroundPos = new Cesium.Cartesian3();
flightState._modelCollection = null;
/** @type {Cesium.PrimitiveCollection|null} */
/** @type {Map<string, Cesium.Model>} icao24 → model */
flightState._modelCollection = null;
flightState._models = new Map();
/** @type {Map<string, Cesium.Model>} icao24 → model */
/** @type {Set<string>} icao24 currently loading (async) */
flightState._models = new Map();
flightState._modelPending = new Set();
/** @type {Set<string>} icao24 currently loading (async) */
flightState._modelPending = new Set();
/** @type {Map<string, number>} icao24 → load generation; bumped on release to invalidate
/** @type {Map<string, number>} icao24 → load generation; bumped on release to invalidate
* an in-flight load (so a track/untrack/remove during fromGltfAsync can't add a stale model). */
flightState._modelGen = new Map();
flightState._modelGen = new Map();
/** Lifecycle epoch; bumped on destroy so an in-flight load from a PREVIOUS init can't settle
/** Lifecycle epoch; bumped on destroy so an in-flight load from a PREVIOUS init can't settle
* against a new lifecycle's globals (which destroy cleared). Captured by _ensureModel. */
flightState._modelEpoch = 0;
flightState._modelEpoch = 0;
/** DEFAULT-ON in PROXIMITY (owner directive 2026-08-22). A fresh boot never runs
/** DEFAULT-ON in PROXIMITY (owner directive 2026-08-22). A fresh boot never runs
* layer-state restoration, so this initializer — not the codec — is what the app
* actually starts with; it must stay in lockstep with the `models3d` default in
* `layerState.js` and `this._models3dEnabled` in ui.js, or the DISPLAY rail would
* light a button the layer has not armed. */
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)
flightState._lastModelCapWarnMs = 0;
flightState._lastModelCapWarnMs = 0;
// throttle the "more planes in view than the cap" console notice
// The tracked entity's billboard goes transparent (rather than hidden) once the model takes
// over, so it keeps supplying a bounding sphere for follow-camera framing. We only drop its
// alpha after the GLB is preloaded so the model is ready to render the instant the billboard
// fades — no gap, no double-image. Preloaded once at init; the instance is retained (not
// destroyed) purely to keep Cesium's glTF cache warm for fast tracked-model instantiation.
// The tracked entity's billboard goes transparent (rather than hidden) once the model takes
// over, so it keeps supplying a bounding sphere for follow-camera framing. We only drop its
// alpha after the GLB is preloaded so the model is ready to render the instant the billboard
// fades — no gap, no double-image. Preloaded once at init; the instance is retained (not
// destroyed) purely to keep Cesium's glTF cache warm for fast tracked-model instantiation.
flightState._planeModelLoaded = false;
flightState._planeModelLoaded = false;
/** @type {Cesium.Model|null} retained preload that keeps the glTF cache warm */
/** @type {Cesium.Model|null} retained preload that keeps the glTF cache warm */
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(new Cesium.Cartesian3(), 1.0);
flightState._scratchModelBS = new Cesium.BoundingSphere(
new Cesium.Cartesian3(),
1.0,
);
// 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();
// keep last N positions per aircraft
// ---------------------------------------------------------------------------
// Module-level state: billboard collection and per-aircraft lookup maps
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Module-level state: billboard collection and per-aircraft lookup maps
// ---------------------------------------------------------------------------
/** @type {Cesium.BillboardCollection|null} */
/** @type {Cesium.BillboardCollection|null} */
flightState._billboardCollection = null;
flightState._billboardCollection = null;
/** @type {Map<string, Cesium.Billboard>} icao24 -> billboard primitive */
/** @type {Map<string, Cesium.Billboard>} icao24 -> billboard primitive */
flightState._billboards = new Map();
flightState._billboards = new Map();
/** Stable lightweight records reused by the detection overlay between polls. */
/** Stable lightweight records reused by the detection overlay between polls. */
flightState._detectionObjects = new Map();
flightState._detectionObjects = new Map();
/** @type {Map<string, {callsign:string, altitude:number, velocity:number, true_track:number}>} */
/** @type {Map<string, {callsign:string, altitude:number, velocity:number, true_track:number}>} */
flightState._flightData = new Map();
flightState._flightData = new Map();
/** DEV-only explicit-position contacts used by qa-focus-evidence.mjs. */
/** DEV-only explicit-position contacts used by qa-focus-evidence.mjs. */
flightState._focusEvidenceIds = new Set();
flightState._focusEvidenceIds = new Set();
/** @type {Map<string, Array<{time:Cesium.JulianDate, position:Cesium.Cartesian3}>>} */
/** @type {Map<string, Array<{time:Cesium.JulianDate, position:Cesium.Cartesian3}>>} */
flightState._positionHistory = new Map();
flightState._positionHistory = new Map();
/** @type {boolean} True once ensureGeoidReady() has resolved (awaited once at enable()) */
/** @type {boolean} True once ensureGeoidReady() has resolved (awaited once at enable()) */
flightState._geoidReady = false;
flightState._geoidReady = false;
/** @type {Map<string, number>} icao24 -> geoid undulation N (m), cached (negligible drift per-aircraft). */
/** @type {Map<string, number>} icao24 -> geoid undulation N (m), cached (negligible drift per-aircraft). */
flightState._geoidNCache = new Map();
flightState._geoidNCache = new Map();
/** @type {number} Current number of visible aircraft */
/** @type {number} Current number of visible aircraft */
flightState._count = 0;
flightState._count = 0;
/** @type {number|null} Epoch ms of last successful API update */
/** @type {number|null} Epoch ms of last successful API update */
flightState._lastUpdate = null;
flightState._lastUpdate = null;
/** @type {boolean} True while in a backoff/cooldown window */
/** @type {boolean} True while in a backoff/cooldown window */
flightState._backoff = false;
flightState._backoff = false;
/** @type {number} Epoch ms — earliest time the next fetch is allowed */
/** @type {number} Epoch ms — earliest time the next fetch is allowed */
flightState._retryAt = 0;
flightState._retryAt = 0;
/** @type {string|null} Human-readable error string shown in stats chip */
/** @type {string|null} Human-readable error string shown in stats chip */
flightState._lastError = null;
flightState._lastError = null;
flightState._activeUpdateControllers = new Set();
flightState._activeUpdateControllers = new Set();
/** @type {number|null} HTTP status of the most recent API response */
/** @type {number|null} HTTP status of the most recent API response */
flightState._lastStatus = null;
flightState._lastStatus = null;
/** @type {string} Source used by the latest successful snapshot. */
/** @type {string} Source used by the latest successful snapshot. */
flightState._lastSource = source?.label || 'Aircraft';
flightState._lastSource = source?.label || 'Aircraft';
/** @type {string} Completeness boundary for the latest successful snapshot. */
/** @type {string} Completeness boundary for the latest successful snapshot. */
flightState._lastCoverage = 'worldwide upstream snapshot';
flightState._lastCoverage = 'worldwide upstream snapshot';
// ---------------------------------------------------------------------------
// Click-to-track state
// ---------------------------------------------------------------------------
/** @type {string|null} ICAO24 of the currently tracked aircraft */
// ---------------------------------------------------------------------------
// Click-to-track state
// ---------------------------------------------------------------------------
flightState._trackedIcao = null;
/** @type {string|null} ICAO24 of the currently tracked aircraft */
flightState._pendingTrackingRestore = null;
flightState._trackedIcao = null;
flightState._trackingIntentGeneration = 0;
flightState._trackingRefreshEpoch = 0;
flightState._pendingTrackingRestore = null;
flightState._trackingIntentGeneration = 0;
flightState._trackingRefreshEpoch = 0;
flightState._lastTrackingRefreshOutcome = {
flightState._lastTrackingRefreshOutcome = {
epoch: 0,
status: 'unavailable',
ids: new Set(),
source: flightState._lastSource,
coverage: null,
};
};
/** @type {Cesium.Entity|null} Entity used for camera tracking */
/** @type {Cesium.Entity|null} Entity used for camera tracking */
flightState._trackedEntity = null;
flightState._trackedEntity = null;
/** Disposes the single active tracked-camera framing owner. */
/** Disposes the single active tracked-camera framing owner. */
flightState._trackedCameraFrameStop = null;
flightState._trackedCameraFrameStop = null;
/** @type {Cesium.Model|null} Standalone 3D model for the tracked aircraft. Deliberately NOT a
/** @type {Cesium.Model|null} Standalone 3D model for the tracked aircraft. Deliberately NOT a
* graphic on _trackedEntity: viewer.trackedEntity derives the follow-camera from the entity's
* bounding sphere, and a model graphic reports PENDING until its glTF loads — which stalls (or, on
* 3D-toggle, freezes) the centering. A pure-billboard entity always supplies a ready sphere; the
* model rides in _modelCollection and is driven per-frame, fully decoupled from the camera. */
flightState._trackedModel = null;
flightState._trackedModel = null;
/** Bumped on untrack/teardown so an in-flight tracked-model load resolves into a no-op. */
/** Bumped on untrack/teardown so an in-flight tracked-model load resolves into a no-op. */
flightState._trackedModelGen = 0;
flightState._trackedModelGen = 0;
flightState._trackedModelLoading = false;
flightState._trackedModelLoading = false;
/** @type {Cesium.ScreenSpaceEventHandler|null} Click handler on the scene canvas */
/** @type {Cesium.ScreenSpaceEventHandler|null} Click handler on the scene canvas */
flightState._clickHandler = null;
flightState._clickHandler = null;
/** @type {Cesium.Viewer|null} Cached viewer reference */
/** @type {Cesium.Viewer|null} Cached viewer reference */
flightState._viewer = null;
flightState._viewer = null;
/** Cockpit presentation switches ambient AIR contacts between near aircraft and far dots. */
/** Cockpit presentation switches ambient AIR contacts between near aircraft and far dots. */
flightState._cockpitContactMode = false;
flightState._cockpitContactMode = false;
/** AIR contacts inside the selected Display range; independent from model admission/load/cap. */
/** AIR contacts inside the selected Display range; independent from model admission/load/cap. */
flightState._cockpitNearContacts = new Set();
flightState._cockpitNearContacts = new Set();
/** Normalized ICAO24 of the active Cockpit subject, omitted from detection candidates. */
/** Normalized ICAO24 of the active Cockpit subject, omitted from detection candidates. */
flightState._cockpitSubjectId = null;
flightState._cockpitSubjectId = null;
/** @type {((event: CustomEvent) => void)|null} */
/** @type {((event: CustomEvent) => void)|null} */
flightState._cockpitModeListener = null;
flightState._cockpitModeListener = null;
/** @type {{setPositions: Function, clear: Function, destroy: Function}|null} Shared fading-trail renderer */
/** @type {{setPositions: Function, clear: Function, destroy: Function}|null} Shared fading-trail renderer */
flightState._trail = null;
flightState._trail = null;
/** @type {Cesium.Entity|null} Cheap 2-point head segment bridging the last fix to the LIVE
/** @type {Cesium.Entity|null} Cheap 2-point head segment bridging the last fix to the LIVE
* dead-reckoned icon, updated per frame via a CallbackProperty — so the trail head stays
* glued to the 12 Hz icon without rebuilding the 400-point trail primitive every frame. */
flightState._trailHeadEntity = null;
flightState._trailHeadEntity = null;
/** @type {number} Uniquifier for head-segment entity ids (Cesium requires unique ids). */
/** @type {number} Uniquifier for head-segment entity ids (Cesium requires unique ids). */
flightState._trailHeadSeq = 0;
flightState._trailHeadSeq = 0;
/** @type {Cesium.Cartesian3[]} Chronological tracked-aircraft fixes (oldest first) */
/** @type {Cesium.Cartesian3[]} Chronological tracked-aircraft fixes (oldest first) */
flightState._trailPositions = [];
flightState._trailPositions = [];
/** @type {number} Monotonic token — invalidates in-flight backfill responses */
/** @type {number} Monotonic token — invalidates in-flight backfill responses */
flightState._trailBackfillToken = 0;
flightState._trailBackfillToken = 0;
/** @type {Map<string, number>} icao24 -> consecutive missed polls */
/** @type {Map<string, number>} icao24 -> consecutive missed polls */
flightState._missingPolls = new Map();
flightState._missingPolls = new Map();
/** @type {number} Epoch ms of the last fleet dead-reckoning pass */
/** @type {number} Epoch ms of the last fleet dead-reckoning pass */
flightState._lastFleetTickMs = 0;
flightState._lastFleetTickMs = 0;
/** @type {string} Camera pose signature at the last rotation pass */
/** @type {string} Camera pose signature at the last rotation pass */
flightState._lastCamPoseSig = '';
flightState._lastCamPoseSig = '';
/** @type {number} Epoch ms of the last full rotation pass */
/** @type {number} Epoch ms of the last full rotation pass */
flightState._lastRotPassMs = 0;
flightState._lastRotPassMs = 0;
/** @type {number} Last computed rotation for the tracked entity (radians) */
/** @type {number} Last computed rotation for the tracked entity (radians) */
flightState._lastTrackedRotation = 0;
flightState._lastTrackedRotation = 0;
/** @type {Cesium.Event.RemoveCallback|null} preRender listener disposer */
/** @type {Cesium.Event.RemoveCallback|null} preRender listener disposer */
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) */
flightState._moveEndRemove = null;
flightState._moveEndRemove = null;
/** @type {Cesium.Event.RemoveCallback|null} trackedEntityChanged listener disposer (cross-layer untrack) */
/** @type {Cesium.Event.RemoveCallback|null} trackedEntityChanged listener disposer (cross-layer untrack) */
flightState._trackedEntityChangedRemove = null;
flightState._trackedEntityChangedRemove = null;
/** @type {(() => void)|null} militaryRegistry active-transition unsubscribe (M2 handoff sweep) */
/** @type {(() => void)|null} militaryRegistry active-transition unsubscribe (M2 handoff sweep) */
flightState._milActiveChangeUnsub = null;
flightState._milActiveChangeUnsub = null;
// ---------------------------------------------------------------------------
// Scratch (reusable) variables — avoid per-frame heap allocation
// ---------------------------------------------------------------------------
flightState._scratchOffset = new Cesium.Cartesian3();
// ---------------------------------------------------------------------------
// Scratch (reusable) variables — avoid per-frame heap allocation
// ---------------------------------------------------------------------------
flightState._scratchCarto = new Cesium.Cartographic();
flightState._scratchEnu = new Cesium.Matrix4();
flightState._scratchOffset = new Cesium.Cartesian3();
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
flightState._scratchRenderTime = new Cesium.JulianDate();
flightState._scratchCarto = new Cesium.Cartographic();
flightState._scratchFleetPos = new Cesium.Cartesian3();
flightState._scratchDrRaw = new Cesium.Cartesian3();
flightState._scratchEnu = new Cesium.Matrix4();
flightState._scratchWarmupTime = new Cesium.JulianDate();
flightState._trackedPosHolder = new Cesium.Cartesian3();
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
// ---------------------------------------------------------------------------
// Per-frame cache for the tracked entity's dead-reckoned position.
// The position, alignedAxis, and rotation CallbackProperties all fire each
// render frame; caching avoids running _deadReckon three times.
// ---------------------------------------------------------------------------
/** @type {Cesium.Cartesian3|null} */
flightState._scratchRenderTime = new Cesium.JulianDate();
flightState._cachedDRPosition = null;
/** @type {number} Frame number for which _cachedDRPosition is valid */
flightState._scratchFleetPos = new Cesium.Cartesian3();
flightState._cachedDRFrame = -1;
flightState._scratchDrRaw = new Cesium.Cartesian3();
flightState._scratchWarmupTime = new Cesium.JulianDate();
flightState._trackedPosHolder = new Cesium.Cartesian3();
// ---------------------------------------------------------------------------
// Per-frame cache for the tracked entity's dead-reckoned position.
// The position, alignedAxis, and rotation CallbackProperties all fire each
// render frame; caching avoids running _deadReckon three times.
// ---------------------------------------------------------------------------
/** @type {Cesium.Cartesian3|null} */
flightState._cachedDRPosition = null;
/** @type {number} Frame number for which _cachedDRPosition is valid */
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
* synchronous flow; module-scratch idiom, like the Cartesian scratches). */
flightState._drCourseDeg = null;
flightState._drCourseDeg = null;
/** Sibling scratches of _drCourseDeg: the displayed ground speed of the motion
/** Sibling scratches of _drCourseDeg: the displayed ground speed of the motion
* `_deadReckon` just returned, and whether that motion is too slow for ANY
* course source to be trusted (hover/GPS drift — consumers HOLD their
* previous display course instead of chasing noise). */
flightState._drSpeedMps = null;
flightState._drSpeedMps = null;
flightState._drCourseHold = false;
flightState._drCourseHold = false;
/** Sibling scratch: whether the position `_deadReckon` just returned came from
/** Sibling scratch: whether the position `_deadReckon` just returned came from
* EXTRAPOLATION (coasting past the newest fix, or the pre-history warm-up)
* rather than interpolation between two known fixes. The display-floor
* corridor needs it — an interpolating contact is walking TOWARD its newest
* fix, a coasting one is walking AWAY from it along its course, and warming
* the wrong end leaves a coaster permanently ahead of its floor data. */
flightState._drExtrapolating = false;
flightState._drExtrapolating = false;
/** Frame-cached course for the tracked aircraft (sibling of _cachedDRPosition). */
/** Frame-cached course for the tracked aircraft (sibling of _cachedDRPosition). */
flightState._cachedDRCourse = null;
flightState._cachedDRCourse = null;
/** Frame-cached siblings of _cachedDRCourse (same discipline). */
/** Frame-cached siblings of _cachedDRCourse (same discipline). */
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 —
* the course VALUE lives in the shared per-icao _displayCourse map below). */
flightState._trackedCourseMs = 0;
flightState._trackedCourseMs = 0;
/** Per-aircraft smoothed display course — the SINGLE source of truth for the
/** Per-aircraft smoothed display course — the SINGLE source of truth for the
* nose direction an aircraft displays. The fleet pass reads/writes it at
* tick cadence for untracked planes; _trackedDisplayCourse reads/writes the
* SAME entry per frame for the tracked plane (the fleet pass skips the
@@ -400,95 +373,76 @@ flightState._trackedCourseMs = 0;
* entry while tracked, so clicking / un-clicking a 65 kt helicopter FLIPPED
* its nose — "looks like it's going in reverse"). */
flightState._displayCourse = new Map();
flightState._displayCourse = new Map();
flightState._enrichActive = 0;
flightState._enrichActive = 0;
flightState._enrichLastDispatchMs = 0;
/** @type {ReturnType<typeof setTimeout>|null} pending drip wake-up */
flightState._enrichLastDispatchMs = 0;
flightState._enrichDripTimer = null;
/** @type {ReturnType<typeof setTimeout>|null} pending drip wake-up */
flightState._enrichQueue = [];
flightState._enrichDripTimer = null;
flightState._enrichSeen = new Set();
flightState._enrichAmbientBudget = ENRICH_AMBIENT_BUDGET_CEIL;
flightState._enrichQueue = [];
/** Epoch ms the bucket last accounted a refill window from (0 = unset). */
flightState._enrichAmbientRefillAnchorMs = 0;
flightState._enrichSeen = new Set();
flightState._drCorrection = new Cesium.Cartesian3(0, 0, 0);
flightState._drCorrectionStartMs = 0;
flightState._enrichAmbientBudget = ENRICH_AMBIENT_BUDGET_CEIL;
flightState._drPrevRaw = new Cesium.Cartesian3();
/** Epoch ms the bucket last accounted a refill window from (0 = unset). */
flightState._drPrevDisplay = new Cesium.Cartesian3();
flightState._enrichAmbientRefillAnchorMs = 0;
flightState._drPrevMs = 0;
flightState._drReconcileValid = false;
flightState._drCorrection = new Cesium.Cartesian3(0, 0, 0);
/** @type {string|null} icao the reconciliation state currently belongs to */
flightState._drReconcileIcao = null;
flightState._drCorrectionStartMs = 0;
/** @type {Cesium.Cartesian3} Scratch for the tracked model's rendered translation. */
flightState._trackedVisualPos = new Cesium.Cartesian3();
flightState._drPrevRaw = new Cesium.Cartesian3();
flightState._trackedTrailPos = new Cesium.Cartesian3();
/** Scratch for the tracked model's world-space origin (the envelope centre). */
flightState._drPrevDisplay = new Cesium.Cartesian3();
flightState._scratchTrailClip = new Cesium.Cartesian3();
flightState._drPrevMs = 0;
flightState._drReconcileValid = false;
/** @type {string|null} icao the reconciliation state currently belongs to */
flightState._drReconcileIcao = null;
/** @type {Cesium.Cartesian3} Scratch for the tracked model's rendered translation. */
flightState._trackedVisualPos = new Cesium.Cartesian3();
flightState._trackedTrailPos = new Cesium.Cartesian3();
/** Scratch for the tracked model's world-space origin (the envelope centre). */
flightState._scratchTrailClip = new Cesium.Cartesian3();
/** Scratch for the shortened trail-head start. Owned by the head-segment
/** Scratch for the shortened trail-head start. Owned by the head-segment
* callback alone, so nothing else can overwrite it mid-frame. */
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
* 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._trackedModelFailCount = 0;
flightState._trackedModelFailIcao = null;
flightState._trackedModelRetryAtMs = 0;
/** @type {Cesium.Cartographic} Scratch for the grounded display-floor read. */
flightState._trackedModelFailCount = 0;
flightState._scratchDisplayCarto = new Cesium.Cartographic();
flightState._trackedModelRetryAtMs = 0;
/** @type {Cesium.Cartographic} Scratch for the grounded display-floor read. */
flightState._scratchDisplayCarto = new Cesium.Cartographic();
/** @type {Map<string, {cell: {lat: number, lon: number}, effectiveM: number|null,
/** @type {Map<string, {cell: {lat: number, lon: number}, effectiveM: number|null,
* in: Cesium.Cartesian3, out: Cesium.Cartesian3|null, heldM: number|null,
* heldCell: {lat: number, lon: number}|null,
* heldTier: 'own'|'neighbor'|null, heldActive: boolean, seeded: boolean,
@@ -503,33 +457,31 @@ flightState._scratchDisplayCarto = new Cesium.Cartographic();
* displaying while it approaches a lower floor.
* Dropped with the contact, and whenever it stops being a grounded billboard. */
flightState._displayFloorState = new Map();
flightState._displayFloorState = new Map();
/** @type {number} Poll counter handed to the corridor allocator: it rotates
/** @type {number} Poll counter handed to the corridor allocator: it rotates
* runs of EQUALLY needy contacts so a tie larger than the budget cycles across
* polls instead of the same prefix winning forever. */
flightState._corridorEpoch = 0;
flightState._corridorEpoch = 0;
/** @type {Cesium.Cartographic} Scratch for the corridor's display-end read. */
/** @type {Cesium.Cartographic} Scratch for the corridor's display-end read. */
flightState._scratchCorridorCarto = new Cesium.Cartographic();
flightState._scratchCorridorCarto = new Cesium.Cartographic();
/** @type {Cesium.Cartesian3} Scratch for the corridor's dead-reckon probe. */
/** @type {Cesium.Cartesian3} Scratch for the corridor's dead-reckon probe. */
flightState._scratchCorridorPos = new Cesium.Cartesian3();
flightState._scratchCorridorPos = new Cesium.Cartesian3();
/** IR hot-target mode (owner playtest 2026-08-16): the NVG/FLIR post-styles
/** IR hot-target mode (owner playtest 2026-08-16): the NVG/FLIR post-styles
* map LUMINANCE, so mid-gray textured models read cold and vanish into
* terrain. While a boost style is active every model renders flat white
* (hottest); per-spec color/tint restores on style exit. Driven by ui.js
* setStyle via the `irBoost` layer param. */
flightState._irBoost = false;
flightState._irBoost = false;
/** Boosted models render UNLIT (owner cockpit-FLIR field rounds, 2026-08-16):
/** Boosted models render UNLIT (owner cockpit-FLIR field rounds, 2026-08-16):
* the white tint alone is applied to the MATERIAL, so Cesium still
* sun-shades it — near-horizon viewing shows a plane's SIDE, ~90° to a high
* sun, so it rendered near-BLACK in FLIR/NVG while sun-lit neighbors glowed.
@@ -540,9 +492,10 @@ flightState._irBoost = false;
* (see setParams), so every boosted model gets the shader AT CREATION.
* One shared shader instance — stateless, safe across models. */
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;
return flightState;
flightState._irReloadQueue = null;
return flightState;
}
+94 -70
View File
@@ -1,30 +1,42 @@
import * as Cesium from 'cesium';
import { CYAN_TRANSPARENT } from './policy.js';
export function createTesting({ flightState, services, parts, layer, resolveAsset }) {
/** Test seam for the display-floor clamp (unit-tested against real Cesium math
export function createTesting({
flightState,
services,
parts,
layer,
resolveAsset,
}) {
/** Test seam for the display-floor clamp (unit-tested against real Cesium math
* with seeded mesh cells — the drift mechanism is otherwise only reachable
* through a live poll + render loop). */
function _floorGroundedDisplayPositionForTest(
info, pos, modelOwnsVisual, icao24 = '__test__', nowMs = Date.now(),
) {
return parts.motion._floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs);
}
function _floorGroundedDisplayPositionForTest(
info,
pos,
modelOwnsVisual,
icao24 = '__test__',
nowMs = Date.now(),
) {
return parts.motion._floorGroundedDisplayPosition(
icao24,
info,
pos,
modelOwnsVisual,
nowMs,
);
}
/** Test hook: drops the per-contact display-floor state (hysteresis + rebuild
/** Test hook: drops the per-contact display-floor state (hysteresis + rebuild
* cache) so each case starts clean. There is no cross-contact state to reset —
* the clamp is per-contact all the way down. */
function _clearDisplayFloorStateForTest() {
function _clearDisplayFloorStateForTest() {
flightState._displayFloorState.clear();
}
}
/**
/**
* Seed only the mutable state needed to exercise tracked-card refreshes through
* the production poll reconciler. Tests still call `flightsLayer.update()`;
* this seam avoids constructing the browser-only Cesium layer lifecycle.
@@ -41,7 +53,7 @@ function _clearDisplayFloorStateForTest() {
* by icao24, for the billboard-hidden/model-shown handoff state.
*/
function _setTrackedFlightRefreshStateForTest({
function _setTrackedFlightRefreshStateForTest({
icao24,
entity,
meta,
@@ -52,7 +64,7 @@ function _setTrackedFlightRefreshStateForTest({
tracked = true,
models = [],
modelCollection = null,
}) {
}) {
flightState._viewer = viewer;
flightState._modelCollection = modelCollection;
flightState._billboardCollection = billboardCollection;
@@ -76,17 +88,16 @@ function _setTrackedFlightRefreshStateForTest({
// seam is used to test.
flightState._backoff = false;
flightState._retryAt = 0;
}
}
/** Seed the authoritative snapshot outcome used by share-Follow tests. */
/** Seed the authoritative snapshot outcome used by share-Follow tests. */
function _setFlightTrackingRefreshOutcomeForTest({
function _setFlightTrackingRefreshOutcomeForTest({
status = 'accepted',
ids = [],
source = 'OpenSky Network',
coverage = 'test',
} = {}) {
} = {}) {
const epoch = ++flightState._trackingRefreshEpoch;
flightState._lastTrackingRefreshOutcome = {
epoch,
@@ -95,82 +106,80 @@ function _setFlightTrackingRefreshOutcomeForTest({
source,
coverage,
};
}
}
/** 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 _addFlightTrackingCandidateForTest({ icao24, meta, billboard, history = [] }) {
function _addFlightTrackingCandidateForTest({
icao24,
meta,
billboard,
history = [],
}) {
flightState._billboards.set(icao24, billboard);
flightState._flightData.set(icao24, meta);
flightState._positionHistory.set(icao24, history);
}
}
/** Expose the military-suppression decision for the civil duplicate. */
/** Expose the military-suppression decision for the civil duplicate. */
function _militaryLayerSuppressesForTest(icao24) {
function _militaryLayerSuppressesForTest(icao24) {
return parts.tracking._militaryLayerSuppresses(icao24);
}
}
/** Arm the deferred restore latch directly, without a full setParams turn. */
/** Arm the deferred restore latch directly, without a full setParams turn. */
function _armFlightTrackingRestoreForTest(id, origin = 'share-restore') {
flightState._pendingTrackingRestore = id === null
function _armFlightTrackingRestoreForTest(id, origin = 'share-restore') {
flightState._pendingTrackingRestore =
id === null
? null
: { id, generation: flightState._trackingIntentGeneration, origin };
}
}
/** Return the deferred restore target held by the production tracker. */
/** Return the deferred restore target held by the production tracker. */
function _pendingFlightTrackingRestoreForTest() {
function _pendingFlightTrackingRestoreForTest() {
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 _applyPendingFlightTrackingRestoreForTest() {
function _applyPendingFlightTrackingRestoreForTest() {
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 });
}
}
/** 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)
* and otherwise only observable through a live scene, so tests drive it here. */
function _trackedModelRegimeActiveForTest() {
function _trackedModelRegimeActiveForTest() {
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. */
function _updateTrackedModelForTest() {
function _updateTrackedModelForTest() {
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() {
return parts.rendering._modelOwnsVisual(flightState._trackedIcao)
? CYAN_TRANSPARENT
: Cesium.Color.CYAN;
}
function _trackedBillboardColorForTest() {
return parts.rendering._modelOwnsVisual(flightState._trackedIcao) ? CYAN_TRANSPARENT : Cesium.Color.CYAN;
}
/** 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 }) {
return parts.rendering._driveFleetModelHandoff(
icao24,
flightState._models.get(icao24),
@@ -178,14 +187,29 @@ function _driveFleetModelHandoffForTest({ icao24, position, course = 0 }) {
position,
course,
);
}
}
/** 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);
return flightState._models.get(icao24) || null;
}
return { _floorGroundedDisplayPositionForTest, _clearDisplayFloorStateForTest, _setTrackedFlightRefreshStateForTest, _setFlightTrackingRefreshOutcomeForTest, _addFlightTrackingCandidateForTest, _militaryLayerSuppressesForTest, _armFlightTrackingRestoreForTest, _pendingFlightTrackingRestoreForTest, _applyPendingFlightTrackingRestoreForTest, _setCockpitDetectionSubjectForTest, _trackedModelRegimeActiveForTest, _updateTrackedModelForTest, _trackedBillboardColorForTest, _driveFleetModelHandoffForTest, _ensureFleetModelForTest };
}
return {
_floorGroundedDisplayPositionForTest,
_clearDisplayFloorStateForTest,
_setTrackedFlightRefreshStateForTest,
_setFlightTrackingRefreshOutcomeForTest,
_addFlightTrackingCandidateForTest,
_militaryLayerSuppressesForTest,
_armFlightTrackingRestoreForTest,
_pendingFlightTrackingRestoreForTest,
_applyPendingFlightTrackingRestoreForTest,
_setCockpitDetectionSubjectForTest,
_trackedModelRegimeActiveForTest,
_updateTrackedModelForTest,
_trackedBillboardColorForTest,
_driveFleetModelHandoffForTest,
_ensureFleetModelForTest,
};
}
File diff suppressed because it is too large. Load diff
+5 -2
View File
@@ -7,6 +7,9 @@ export function readLayerSource(file) {
const path = file instanceof URL ? fileURLToPath(file) : file;
if (basename(path) !== 'flights.js') return readFileSync(path, 'utf8');
const directory = join(dirname(path), '../layers/flights');
return readdirSync(directory).filter((name) => name.endsWith('.js'))
.sort().map((name) => readFileSync(join(directory, name), 'utf8')).join('\n');
return readdirSync(directory)
.filter((name) => name.endsWith('.js'))
.sort()
.map((name) => readFileSync(join(directory, name), 'utf8'))
.join('\n');
}