mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Format civil-flight components
This commit is contained in:
1 parent
7c7e83c9e3
commit
e18aeb8565
16 files changed
+6435
-5086
No files matched your search
+53
-16
@@ -17,22 +17,59 @@ import * as render from '../renderGovernor.js';
|
|||||||
import * as groundSnap from './groundSnap.js';
|
import * as groundSnap from './groundSnap.js';
|
||||||
import * as recession from './aircraftRecession.js';
|
import * as recession from './aircraftRecession.js';
|
||||||
|
|
||||||
const 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 { TRACKED_MODEL_MAX_PX } from '../layers/flights/policy.js';
|
||||||
export const _floorGroundedDisplayPositionForTest = flightsLayer.testing._floorGroundedDisplayPositionForTest;
|
export const _floorGroundedDisplayPositionForTest =
|
||||||
export const _clearDisplayFloorStateForTest = flightsLayer.testing._clearDisplayFloorStateForTest;
|
flightsLayer.testing._floorGroundedDisplayPositionForTest;
|
||||||
export const _setTrackedFlightRefreshStateForTest = flightsLayer.testing._setTrackedFlightRefreshStateForTest;
|
export const _clearDisplayFloorStateForTest =
|
||||||
export const _setFlightTrackingRefreshOutcomeForTest = flightsLayer.testing._setFlightTrackingRefreshOutcomeForTest;
|
flightsLayer.testing._clearDisplayFloorStateForTest;
|
||||||
export const _addFlightTrackingCandidateForTest = flightsLayer.testing._addFlightTrackingCandidateForTest;
|
export const _setTrackedFlightRefreshStateForTest =
|
||||||
export const _militaryLayerSuppressesForTest = flightsLayer.testing._militaryLayerSuppressesForTest;
|
flightsLayer.testing._setTrackedFlightRefreshStateForTest;
|
||||||
export const _armFlightTrackingRestoreForTest = flightsLayer.testing._armFlightTrackingRestoreForTest;
|
export const _setFlightTrackingRefreshOutcomeForTest =
|
||||||
export const _pendingFlightTrackingRestoreForTest = flightsLayer.testing._pendingFlightTrackingRestoreForTest;
|
flightsLayer.testing._setFlightTrackingRefreshOutcomeForTest;
|
||||||
export const _applyPendingFlightTrackingRestoreForTest = flightsLayer.testing._applyPendingFlightTrackingRestoreForTest;
|
export const _addFlightTrackingCandidateForTest =
|
||||||
export const _setCockpitDetectionSubjectForTest = flightsLayer.testing._setCockpitDetectionSubjectForTest;
|
flightsLayer.testing._addFlightTrackingCandidateForTest;
|
||||||
export const _trackedModelRegimeActiveForTest = flightsLayer.testing._trackedModelRegimeActiveForTest;
|
export const _militaryLayerSuppressesForTest =
|
||||||
export const _updateTrackedModelForTest = flightsLayer.testing._updateTrackedModelForTest;
|
flightsLayer.testing._militaryLayerSuppressesForTest;
|
||||||
export const _trackedBillboardColorForTest = flightsLayer.testing._trackedBillboardColorForTest;
|
export const _armFlightTrackingRestoreForTest =
|
||||||
export const _driveFleetModelHandoffForTest = flightsLayer.testing._driveFleetModelHandoffForTest;
|
flightsLayer.testing._armFlightTrackingRestoreForTest;
|
||||||
export const _ensureFleetModelForTest = flightsLayer.testing._ensureFleetModelForTest;
|
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 const mapAnalystRecord = flightsLayer.mapAnalystRecord;
|
||||||
export default flightsLayer;
|
export default flightsLayer;
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
|
||||||
export function createController({ flightState, services, parts, layer, resolveAsset }) {
|
export function createController({
|
||||||
|
flightState,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
layer,
|
||||||
|
resolveAsset,
|
||||||
|
}) {
|
||||||
|
function _abortActiveUpdates() {
|
||||||
|
for (const controller of flightState._activeUpdateControllers)
|
||||||
|
controller.abort();
|
||||||
|
flightState._activeUpdateControllers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _flightQuery(viewer) {
|
||||||
|
const cartographic = viewer?.camera?.positionCartographic;
|
||||||
function _abortActiveUpdates() {
|
return cartographic
|
||||||
for (const controller of flightState._activeUpdateControllers) controller.abort();
|
? {
|
||||||
flightState._activeUpdateControllers.clear();
|
latitude: Cesium.Math.toDegrees(cartographic.latitude),
|
||||||
}
|
longitude: Cesium.Math.toDegrees(cartographic.longitude),
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
}
|
||||||
function _flightQuery(viewer) {
|
return { _abortActiveUpdates, _flightQuery };
|
||||||
const cartographic = viewer?.camera?.positionCartographic;
|
|
||||||
return cartographic ? {
|
|
||||||
latitude: Cesium.Math.toDegrees(cartographic.latitude),
|
|
||||||
longitude: Cesium.Math.toDegrees(cartographic.longitude),
|
|
||||||
} : {};
|
|
||||||
}
|
|
||||||
return { _abortActiveUpdates, _flightQuery };
|
|
||||||
}
|
}
|
||||||
+225
-134
@@ -1,149 +1,240 @@
|
|||||||
import { classifyAircraft } from '../../data/aircraftClass.js';
|
import { classifyAircraft } from '../../data/aircraftClass.js';
|
||||||
import { horizonOccluder } from '../../data/iconOrientation.js';
|
import { horizonOccluder } from '../../data/iconOrientation.js';
|
||||||
import * as Cesium from 'cesium';
|
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 }) {
|
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);
|
||||||
|
_drainEnrich();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _drainEnrich() {
|
||||||
|
if (flightState.lifetime.signal.aborted) return;
|
||||||
function _enqueueEnrich(key, query, onData, priority = false) {
|
while (
|
||||||
if (flightState.lifetime.signal.aborted) return;
|
flightState._enrichActive < ENRICH_MAX_INFLIGHT &&
|
||||||
if (flightState._enrichSeen.has(key)) return;
|
flightState._enrichQueue.length
|
||||||
flightState._enrichSeen.add(key);
|
) {
|
||||||
const job = { query, onData };
|
// Drip: at most one dispatch per ENRICH_DISPATCH_GAP_MS. When the gap
|
||||||
// Priority (tracked / model-eligible) goes to the FRONT so a deep ambient
|
// hasn't elapsed yet, park a single wake-up timer and stop — completions
|
||||||
// backlog can never delay the plane the user just clicked or zoomed into.
|
// and enqueues in the meantime re-enter here harmlessly.
|
||||||
if (priority) flightState._enrichQueue.unshift(job); else flightState._enrichQueue.push(job);
|
const wait =
|
||||||
_drainEnrich();
|
ENRICH_DISPATCH_GAP_MS -
|
||||||
}
|
(Date.now() - flightState._enrichLastDispatchMs);
|
||||||
|
if (wait > 0) {
|
||||||
|
if (!flightState._enrichDripTimer) {
|
||||||
|
flightState._enrichDripTimer = setTimeout(() => {
|
||||||
function _drainEnrich() {
|
flightState._enrichDripTimer = null;
|
||||||
if (flightState.lifetime.signal.aborted) return;
|
_drainEnrich();
|
||||||
while (flightState._enrichActive < ENRICH_MAX_INFLIGHT && flightState._enrichQueue.length) {
|
}, wait);
|
||||||
// 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
|
return;
|
||||||
// and enqueues in the meantime re-enter here harmlessly.
|
|
||||||
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._enrichLastDispatchMs = Date.now();
|
||||||
|
const job = flightState._enrichQueue.shift();
|
||||||
|
flightState._enrichActive += 1;
|
||||||
|
const lifetime = flightState.lifetime;
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
lifetime.signal.throwIfAborted();
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _requestTypeEnrichment(icao24, priority = false) {
|
||||||
|
if (!/^[0-9a-f]{6}$/i.test(icao24)) return;
|
||||||
|
_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,
|
||||||
|
});
|
||||||
|
if (klass !== meta.klass) {
|
||||||
|
meta.klass = klass;
|
||||||
|
const bb = flightState._billboards.get(icao24);
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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) {
|
||||||
|
const { ceil, refillTokens, windowMs } = _ambientBudgetKnobs();
|
||||||
|
if (!flightState._enrichAmbientRefillAnchorMs) {
|
||||||
|
flightState._enrichAmbientRefillAnchorMs = nowMs;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
flightState._enrichLastDispatchMs = Date.now();
|
const windows = Math.floor(
|
||||||
const job = flightState._enrichQueue.shift();
|
(nowMs - flightState._enrichAmbientRefillAnchorMs) / windowMs,
|
||||||
flightState._enrichActive += 1;
|
);
|
||||||
const lifetime = flightState.lifetime;
|
if (windows <= 0) return;
|
||||||
Promise.resolve().then(() => {
|
flightState._enrichAmbientBudget = Math.min(
|
||||||
lifetime.signal.throwIfAborted();
|
ceil,
|
||||||
return flightState._source.getEnrichment?.(job.query, { signal: lifetime.signal });
|
flightState._enrichAmbientBudget + windows * refillTokens,
|
||||||
})
|
);
|
||||||
.then((data) => { if (!lifetime.signal.aborted && data && data.found) job.onData(data); })
|
flightState._enrichAmbientRefillAnchorMs += windows * windowMs;
|
||||||
.catch(() => { /* enrichment never surfaces errors */ })
|
|
||||||
.finally(() => { if (lifetime.signal.aborted) return; flightState._enrichActive -= 1; _drainEnrich(); });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
function _sweepAmbientEnrichment() {
|
||||||
|
_refillAmbientBudget(Date.now());
|
||||||
function _requestTypeEnrichment(icao24, priority = false) {
|
if (
|
||||||
if (!/^[0-9a-f]{6}$/i.test(icao24)) return;
|
flightState._enrichAmbientBudget <= 0 ||
|
||||||
_enqueueEnrich(`t:${icao24}`, { kind: 'type', id: icao24.toLowerCase() }, (data) => {
|
!flightState._viewer ||
|
||||||
const meta = flightState._flightData.get(icao24);
|
!flightState._billboardCollection ||
|
||||||
if (!meta) return; // evicted while the lookup was in flight
|
!flightState._billboardCollection.show
|
||||||
meta.typeCode = data.typeCode || meta.typeCode;
|
)
|
||||||
meta.typeName = data.typeName || meta.typeName;
|
return;
|
||||||
meta.registration = data.registration || meta.registration;
|
try {
|
||||||
if (meta.typeCode) {
|
const camera = flightState._viewer.camera;
|
||||||
const klass = classifyAircraft({ typeCode: meta.typeCode, category: meta.category });
|
const camPos = camera.positionWC;
|
||||||
if (klass !== meta.klass) {
|
const occluder = horizonOccluder(camera);
|
||||||
meta.klass = klass;
|
const cull = camera.frustum.computeCullingVolume(
|
||||||
const bb = flightState._billboards.get(icao24);
|
camPos,
|
||||||
if (bb) parts.rendering._applyFleetBillboardPresentation(icao24, bb);
|
camera.directionWC,
|
||||||
// Hangar fleet: the class's GLB/scale may have changed — resync the
|
camera.upWC,
|
||||||
// live model, any in-flight load, and the tracked standalone model.
|
);
|
||||||
parts.rendering._syncModelToClass(icao24);
|
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),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
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 */
|
||||||
}
|
}
|
||||||
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) parts.tracking._updateTrackedLabelModel(icao24);
|
}
|
||||||
}, priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** 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;
|
|
||||||
return {
|
return {
|
||||||
ceil: Number.isFinite(o?.ceil) && o.ceil > 0 ? o.ceil : ENRICH_AMBIENT_BUDGET_CEIL,
|
_enqueueEnrich,
|
||||||
refillTokens: Number.isFinite(o?.refillTokens) && o.refillTokens > 0 ? o.refillTokens : ENRICH_AMBIENT_REFILL_TOKENS,
|
_drainEnrich,
|
||||||
windowMs: Number.isFinite(o?.windowMs) && o.windowMs > 0 ? o.windowMs : ENRICH_AMBIENT_REFILL_WINDOW_MS,
|
_requestTypeEnrichment,
|
||||||
|
_requestRouteEnrichment,
|
||||||
|
_ambientBudgetKnobs,
|
||||||
|
_refillAmbientBudget,
|
||||||
|
_sweepAmbientEnrichment,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** 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) {
|
|
||||||
const { ceil, refillTokens, windowMs } = _ambientBudgetKnobs();
|
|
||||||
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._enrichAmbientRefillAnchorMs += windows * windowMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _sweepAmbientEnrichment() {
|
|
||||||
_refillAmbientBudget(Date.now());
|
|
||||||
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 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)]);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
+159
-125
@@ -2,135 +2,169 @@ import * as Cesium from 'cesium';
|
|||||||
import { aircraftIcon } from '../../data/aircraftIcons.js';
|
import { aircraftIcon } from '../../data/aircraftIcons.js';
|
||||||
import { FOCUS_EVIDENCE_DEV } from './policy.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. */
|
||||||
|
|
||||||
|
function _focusEvidencePosition(record) {
|
||||||
/** Resolve a JSON-safe evidence position into ECEF. DEV-only caller. */
|
const cartesian = record?.cartesian;
|
||||||
|
if (
|
||||||
function _focusEvidencePosition(record) {
|
Array.isArray(cartesian) &&
|
||||||
const cartesian = record?.cartesian;
|
cartesian.length >= 3 &&
|
||||||
if (Array.isArray(cartesian) && cartesian.length >= 3
|
cartesian.slice(0, 3).every(Number.isFinite)
|
||||||
&& cartesian.slice(0, 3).every(Number.isFinite)) {
|
) {
|
||||||
return Cesium.Cartesian3.fromElements(cartesian[0], cartesian[1], cartesian[2]);
|
return Cesium.Cartesian3.fromElements(
|
||||||
}
|
cartesian[0],
|
||||||
if (!Number.isFinite(record?.longitude) || !Number.isFinite(record?.latitude)) return null;
|
cartesian[1],
|
||||||
return Cesium.Cartesian3.fromDegrees(
|
cartesian[2],
|
||||||
record.longitude,
|
);
|
||||||
record.latitude,
|
|
||||||
Number.isFinite(record.altitudeM) ? record.altitudeM : 3_000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** 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 };
|
|
||||||
if (flightState._trackedIcao) parts.tracking._clearTracking();
|
|
||||||
parts.rendering._releaseModels();
|
|
||||||
for (const bb of flightState._billboards.values()) flightState._billboardCollection.remove(bb);
|
|
||||||
flightState._billboards.clear();
|
|
||||||
flightState._flightData.clear();
|
|
||||||
flightState._positionHistory.clear();
|
|
||||||
flightState._displayCourse.clear();
|
|
||||||
flightState._missingPolls.clear();
|
|
||||||
flightState._focusEvidenceIds.clear();
|
|
||||||
|
|
||||||
for (const record of Array.isArray(records) ? records : []) {
|
|
||||||
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);
|
|
||||||
const meta = {
|
|
||||||
callsign: String(record.callsign || id).toUpperCase(),
|
|
||||||
altitude: altitudeM,
|
|
||||||
renderAltitudeM: altitudeM,
|
|
||||||
velocity: Number.isFinite(record.velocityMps) ? record.velocityMps : 0,
|
|
||||||
true_track: Number.isFinite(record.trackDeg) ? record.trackDeg : 90,
|
|
||||||
klass,
|
|
||||||
onGround: false,
|
|
||||||
wasAirborne: true,
|
|
||||||
turnRateDps: 0,
|
|
||||||
lastContactEpochMs: Date.now(),
|
|
||||||
rawLat: record.latitude ?? null,
|
|
||||||
rawLon: record.longitude ?? null,
|
|
||||||
cullPosition: null,
|
|
||||||
};
|
|
||||||
flightState._flightData.set(id, meta);
|
|
||||||
flightState._focusEvidenceIds.add(id);
|
|
||||||
const bb = flightState._billboardCollection.add({
|
|
||||||
position,
|
|
||||||
image: aircraftIcon(parts.rendering._iconKind(id, klass)),
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
scale: parts.rendering._fleetBillboardScale(id, klass),
|
|
||||||
rotation: 0,
|
|
||||||
alignedAxis: Cesium.Cartesian3.ZERO,
|
|
||||||
color: parts.rendering._fleetBillboardColor(id),
|
|
||||||
sizeInMeters: false,
|
|
||||||
scaleByDistance: parts.rendering._normalBillboardScaleByDistance(),
|
|
||||||
disableDepthTestDistance: parts.rendering._groundDepthDistance(),
|
|
||||||
id,
|
|
||||||
show: true,
|
|
||||||
});
|
|
||||||
flightState._billboards.set(id, bb);
|
|
||||||
}
|
|
||||||
flightState._count = flightState._billboards.size;
|
|
||||||
flightState._lastFleetTickMs = 0;
|
|
||||||
flightState._viewer.scene.requestRender?.();
|
|
||||||
return { ok: true, count: flightState._count };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Update explicit evidence positions without rebuilding billboards. */
|
|
||||||
|
|
||||||
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();
|
|
||||||
if (!flightState._focusEvidenceIds.has(id)) continue;
|
|
||||||
const position = _focusEvidencePosition(record);
|
|
||||||
const bb = flightState._billboards.get(id);
|
|
||||||
if (!position || !bb) continue;
|
|
||||||
bb.position = position;
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
moved += 1;
|
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
flightState._lastFleetTickMs = 0;
|
|
||||||
flightState._viewer?.scene?.requestRender?.();
|
|
||||||
return { ok: true, moved };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/** Replace the real fleet with deterministic explicit-position contacts. */
|
||||||
|
|
||||||
/** JSON-safe visual snapshot for the evidence report. */
|
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);
|
||||||
|
flightState._billboards.clear();
|
||||||
|
flightState._flightData.clear();
|
||||||
|
flightState._positionHistory.clear();
|
||||||
|
flightState._displayCourse.clear();
|
||||||
|
flightState._missingPolls.clear();
|
||||||
|
flightState._focusEvidenceIds.clear();
|
||||||
|
|
||||||
function _focusEvidenceSnapshot() {
|
for (const record of Array.isArray(records) ? records : []) {
|
||||||
if (!FOCUS_EVIDENCE_DEV || !flightState._viewer) return [];
|
const id = String(record?.id || '')
|
||||||
return [...flightState._focusEvidenceIds].map((id) => {
|
.trim()
|
||||||
const bb = flightState._billboards.get(id);
|
.toLowerCase();
|
||||||
const screen = bb?.position
|
const position = _focusEvidencePosition(record);
|
||||||
? Cesium.SceneTransforms.worldToWindowCoordinates(flightState._viewer.scene, bb.position)
|
if (!id || !position) continue;
|
||||||
: null;
|
const klass = record.klass || 'airliner';
|
||||||
return {
|
const altitudeM = Number.isFinite(record.altitudeM)
|
||||||
id,
|
? record.altitudeM
|
||||||
show: bb?.show === true,
|
: Cesium.Cartographic.fromCartesian(position)?.height || 3_000;
|
||||||
scale: bb?.scale ?? null,
|
const meta = {
|
||||||
alpha: bb?.color?.alpha ?? null,
|
callsign: String(record.callsign || id).toUpperCase(),
|
||||||
x: screen?.x ?? null,
|
altitude: altitudeM,
|
||||||
y: screen?.y ?? null,
|
renderAltitudeM: altitudeM,
|
||||||
cameraDistanceM: bb?.position
|
velocity: Number.isFinite(record.velocityMps) ? record.velocityMps : 0,
|
||||||
? Cesium.Cartesian3.distance(flightState._viewer.camera.positionWC, bb.position)
|
true_track: Number.isFinite(record.trackDeg) ? record.trackDeg : 90,
|
||||||
: null,
|
klass,
|
||||||
};
|
onGround: false,
|
||||||
});
|
wasAirborne: true,
|
||||||
}
|
turnRateDps: 0,
|
||||||
return { _focusEvidencePosition, _setFocusEvidenceAircraft, _moveFocusEvidenceAircraft, _focusEvidenceSnapshot };
|
lastContactEpochMs: Date.now(),
|
||||||
|
rawLat: record.latitude ?? null,
|
||||||
|
rawLon: record.longitude ?? null,
|
||||||
|
cullPosition: null,
|
||||||
|
};
|
||||||
|
flightState._flightData.set(id, meta);
|
||||||
|
flightState._focusEvidenceIds.add(id);
|
||||||
|
const bb = flightState._billboardCollection.add({
|
||||||
|
position,
|
||||||
|
image: aircraftIcon(parts.rendering._iconKind(id, klass)),
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
scale: parts.rendering._fleetBillboardScale(id, klass),
|
||||||
|
rotation: 0,
|
||||||
|
alignedAxis: Cesium.Cartesian3.ZERO,
|
||||||
|
color: parts.rendering._fleetBillboardColor(id),
|
||||||
|
sizeInMeters: false,
|
||||||
|
scaleByDistance: parts.rendering._normalBillboardScaleByDistance(),
|
||||||
|
disableDepthTestDistance: parts.rendering._groundDepthDistance(),
|
||||||
|
id,
|
||||||
|
show: true,
|
||||||
|
});
|
||||||
|
flightState._billboards.set(id, bb);
|
||||||
|
}
|
||||||
|
flightState._count = flightState._billboards.size;
|
||||||
|
flightState._lastFleetTickMs = 0;
|
||||||
|
flightState._viewer.scene.requestRender?.();
|
||||||
|
return { ok: true, count: flightState._count };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update explicit evidence positions without rebuilding billboards. */
|
||||||
|
|
||||||
|
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();
|
||||||
|
if (!flightState._focusEvidenceIds.has(id)) continue;
|
||||||
|
const position = _focusEvidencePosition(record);
|
||||||
|
const bb = flightState._billboards.get(id);
|
||||||
|
if (!position || !bb) continue;
|
||||||
|
bb.position = position;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
moved += 1;
|
||||||
|
}
|
||||||
|
flightState._lastFleetTickMs = 0;
|
||||||
|
flightState._viewer?.scene?.requestRender?.();
|
||||||
|
return { ok: true, moved };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JSON-safe visual snapshot for the evidence report. */
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
show: bb?.show === true,
|
||||||
|
scale: bb?.scale ?? null,
|
||||||
|
alpha: bb?.color?.alpha ?? null,
|
||||||
|
x: screen?.x ?? null,
|
||||||
|
y: screen?.y ?? null,
|
||||||
|
cameraDistanceM: bb?.position
|
||||||
|
? Cesium.Cartesian3.distance(
|
||||||
|
flightState._viewer.camera.positionWC,
|
||||||
|
bb.position,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
_focusEvidencePosition,
|
||||||
|
_setFocusEvidenceAircraft,
|
||||||
|
_moveFocusEvidenceAircraft,
|
||||||
|
_focusEvidenceSnapshot,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
+27
-18
@@ -10,23 +10,32 @@ import { createEvidence } from './evidence.js';
|
|||||||
import { createTesting } from './testing.js';
|
import { createTesting } from './testing.js';
|
||||||
import { createQueries } from './queries.js';
|
import { createQueries } from './queries.js';
|
||||||
/** Compose one civil-flight layer with application-owned scene services. */
|
/** Compose one civil-flight layer with application-owned scene services. */
|
||||||
export function createCivilFlightLayer({ source, services, resolveAsset = (url) => url } = {}) {
|
export function createCivilFlightLayer({
|
||||||
const flightState = createFlightState({ source, services });
|
source,
|
||||||
const parts = {};
|
services,
|
||||||
const layer = {};
|
resolveAsset = (url) => url,
|
||||||
const context = { flightState, services, parts, layer, resolveAsset };
|
} = {}) {
|
||||||
parts.rendering = createRendering(context);
|
const flightState = createFlightState({ source, services });
|
||||||
parts.motion = createMotion(context);
|
const parts = {};
|
||||||
parts.tracking = createTracking(context);
|
const layer = {};
|
||||||
parts.controller = createController(context);
|
const context = { flightState, services, parts, layer, resolveAsset };
|
||||||
parts.enrichment = createEnrichment(context);
|
parts.rendering = createRendering(context);
|
||||||
parts.ingestion = createIngestion(context);
|
parts.motion = createMotion(context);
|
||||||
parts.lifecycle = createLifecycle(context);
|
parts.tracking = createTracking(context);
|
||||||
parts.evidence = createEvidence(context);
|
parts.controller = createController(context);
|
||||||
parts.testing = createTesting(context);
|
parts.enrichment = createEnrichment(context);
|
||||||
parts.queries = createQueries(context);
|
parts.ingestion = createIngestion(context);
|
||||||
Object.assign(layer, parts.queries.methods, parts.lifecycle.methods, parts.ingestion.methods);
|
parts.lifecycle = createLifecycle(context);
|
||||||
Object.defineProperty(layer, 'testing', { value: parts.testing });
|
parts.evidence = createEvidence(context);
|
||||||
return layer;
|
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';
|
export { TRACKED_MODEL_MAX_PX } from './policy.js';
|
||||||
+647
-525
File diff suppressed because it is too large.
Load diff
+321
-259
@@ -1,274 +1,336 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { PLANE_MODEL_URL } from './policy.js';
|
import { PLANE_MODEL_URL } from './policy.js';
|
||||||
|
|
||||||
export function createLifecycle({ flightState, services, parts, layer, resolveAsset }) {
|
export function createLifecycle({
|
||||||
const { clearFocusTarget } = services.focus;
|
flightState,
|
||||||
const { registerSpriteCollection, restoreSpriteOrder, restoreSpriteOrderOnEnable } = services.sprites;
|
services,
|
||||||
const { onMilitaryLayerActiveChange } = services.militaryRegistry;
|
parts,
|
||||||
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
layer,
|
||||||
const { ensureGeoidReady } = services.geoid;
|
resolveAsset,
|
||||||
const { registerPickOwner, unregisterPickOwner } = services.picking;
|
}) {
|
||||||
|
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 = {
|
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');
|
||||||
|
flightState._source = source;
|
||||||
|
flightState._lastSource = source.label || flightState._lastSource;
|
||||||
|
this.source = flightState._lastSource;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
/** Configure the source before initialization; an active layer keeps its owner. */
|
* Initialize the flights layer.
|
||||||
setSource(source) {
|
* Creates the BillboardCollection, resets all state, and installs the
|
||||||
if (flightState._viewer) throw new Error('Configure the source before layer initialization');
|
* click-to-track handler on the scene canvas.
|
||||||
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
|
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
|
||||||
flightState._source = source;
|
*/
|
||||||
flightState._lastSource = source.label || flightState._lastSource;
|
init(viewer) {
|
||||||
this.source = flightState._lastSource;
|
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)
|
||||||
* Initialize the flights layer.
|
flightState.lifetime = new AbortController();
|
||||||
* Creates the BillboardCollection, resets all state, and installs the
|
clearFocusTarget('flights');
|
||||||
* click-to-track handler on the scene canvas.
|
flightState._focusEvidenceIds.clear();
|
||||||
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
|
flightState._viewer = viewer;
|
||||||
*/
|
flightState._billboardCollection = new Cesium.BillboardCollection();
|
||||||
init(viewer) {
|
viewer.scene.primitives.add(flightState._billboardCollection);
|
||||||
if (flightState._viewer) throw new Error('Flight layer is already initialized');
|
registerSpriteCollection('flights', flightState._billboardCollection);
|
||||||
if (typeof flightState._source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
|
flightState._modelCollection = new Cesium.PrimitiveCollection();
|
||||||
if (flightState.lifetime.signal.aborted) flightState.lifetime = new AbortController();
|
viewer.scene.primitives.add(flightState._modelCollection);
|
||||||
clearFocusTarget('flights');
|
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
|
||||||
flightState._focusEvidenceIds.clear();
|
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
|
||||||
flightState._viewer = viewer;
|
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
|
||||||
flightState._billboardCollection = new Cesium.BillboardCollection();
|
if (!flightState._preloadModel) {
|
||||||
viewer.scene.primitives.add(flightState._billboardCollection);
|
const epoch = flightState._modelEpoch;
|
||||||
registerSpriteCollection('flights', flightState._billboardCollection);
|
Cesium.Model.fromGltfAsync({
|
||||||
flightState._modelCollection = new Cesium.PrimitiveCollection();
|
url: resolveAsset(PLANE_MODEL_URL),
|
||||||
viewer.scene.primitives.add(flightState._modelCollection);
|
asynchronous: false,
|
||||||
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
|
|
||||||
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
|
|
||||||
// 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 */ } }
|
|
||||||
})
|
})
|
||||||
.catch(() => { /* tracked plane just stays a billboard a beat longer */ });
|
.then((m) => {
|
||||||
}
|
if (epoch === flightState._modelEpoch) {
|
||||||
flightState._billboards = new Map();
|
flightState._preloadModel = m;
|
||||||
flightState._detectionObjects = new Map();
|
flightState._planeModelLoaded = true;
|
||||||
flightState._flightData = new Map();
|
} else {
|
||||||
flightState._positionHistory = new Map();
|
try {
|
||||||
flightState._displayCourse.clear();
|
m.destroy();
|
||||||
flightState._groundSnap.clear();
|
} catch {
|
||||||
flightState._count = 0;
|
/* gone */
|
||||||
flightState._lastUpdate = null;
|
}
|
||||||
flightState._backoff = false;
|
}
|
||||||
flightState._retryAt = 0;
|
})
|
||||||
flightState._lastError = null;
|
.catch(() => {
|
||||||
flightState._lastStatus = null;
|
/* tracked plane just stays a billboard a beat longer */
|
||||||
flightState._lastSource = flightState._source.label || 'Aircraft';
|
});
|
||||||
flightState._lastCoverage = 'worldwide upstream snapshot';
|
}
|
||||||
flightState._trackedIcao = null;
|
flightState._billboards = new Map();
|
||||||
parts.tracking._resetTrackedSelectionState();
|
flightState._detectionObjects = new Map();
|
||||||
flightState._trackedEntity = null;
|
flightState._flightData = new Map();
|
||||||
flightState._cockpitSubjectId = null;
|
flightState._positionHistory = new Map();
|
||||||
flightState._cockpitContactMode = document.body.classList.contains('cockpit-mode');
|
flightState._displayCourse.clear();
|
||||||
flightState._cockpitNearContacts = new Set();
|
flightState._groundSnap.clear();
|
||||||
if (!flightState._cockpitModeListener) {
|
flightState._count = 0;
|
||||||
flightState._cockpitModeListener = (event) => parts.tracking._applyCockpitState(event?.detail);
|
flightState._lastUpdate = null;
|
||||||
window.addEventListener('gev:cockpit-mode-changed', flightState._cockpitModeListener);
|
flightState._backoff = false;
|
||||||
}
|
flightState._retryAt = 0;
|
||||||
// Fresh session — full bucket, anchor re-seeded on the first sweep.
|
flightState._lastError = null;
|
||||||
flightState._enrichAmbientBudget = parts.enrichment._ambientBudgetKnobs().ceil;
|
flightState._lastStatus = null;
|
||||||
flightState._enrichAmbientRefillAnchorMs = 0;
|
flightState._lastSource = flightState._source.label || 'Aircraft';
|
||||||
|
flightState._lastCoverage = 'worldwide upstream snapshot';
|
||||||
|
flightState._trackedIcao = null;
|
||||||
|
parts.tracking._resetTrackedSelectionState();
|
||||||
|
flightState._trackedEntity = null;
|
||||||
|
flightState._cockpitSubjectId = null;
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Fresh session — full bucket, anchor re-seeded on the first sweep.
|
||||||
|
flightState._enrichAmbientBudget =
|
||||||
|
parts.enrichment._ambientBudgetKnobs().ceil;
|
||||||
|
flightState._enrichAmbientRefillAnchorMs = 0;
|
||||||
|
|
||||||
parts.tracking._installClickHandler(viewer);
|
parts.tracking._installClickHandler(viewer);
|
||||||
|
|
||||||
// React to Military-layer toggles IMMEDIATELY (suppress/restore sweep)
|
// React to Military-layer toggles IMMEDIATELY (suppress/restore sweep)
|
||||||
// instead of waiting out the 30 s poll (M2).
|
// instead of waiting out the 30 s poll (M2).
|
||||||
if (!flightState._milActiveChangeUnsub) {
|
if (!flightState._milActiveChangeUnsub) {
|
||||||
flightState._milActiveChangeUnsub = onMilitaryLayerActiveChange(parts.tracking._onMilitaryActiveChange);
|
flightState._milActiveChangeUnsub = onMilitaryLayerActiveChange(
|
||||||
}
|
parts.tracking._onMilitaryActiveChange,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
restoreSpriteOrder(viewer);
|
restoreSpriteOrder(viewer);
|
||||||
|
|
||||||
console.log('[Data:Flights] Initialized with billboard icons');
|
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;
|
||||||
|
holdContinuousRender('flights'); // per-frame animator (perf wave 2)
|
||||||
|
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 */
|
||||||
|
});
|
||||||
|
}
|
||||||
|
parts.tracking._installClickHandler(viewer);
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!flightState._trackedModelPreUpdateRemove && viewer?.scene) {
|
||||||
|
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
|
||||||
|
// come to them"): when a camera move SETTLES (voice fly-to, fast pan
|
||||||
|
// release), force a full rotation pass on the very next frame. The
|
||||||
|
// pose-signature gate alone can eat the settle — the final easing frames
|
||||||
|
// of a flight land inside one quantization bucket (10 m / 0.06°), leaving
|
||||||
|
// every icon wearing its last mid-flight rotation for up to
|
||||||
|
// 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._lastCamPoseSig = '';
|
||||||
|
flightState._lastFleetTickMs = 0;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
restoreSpriteOrderOnEnable('flights', viewer);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the billboard collection and re-install the click handler.
|
* Hide all flight billboards and tear down click/keyboard handlers.
|
||||||
* @param {Cesium.Viewer} viewer
|
* Also clears any active flight tracking so the camera is released.
|
||||||
*/
|
* @param {Cesium.Viewer} viewer
|
||||||
enable(viewer) {
|
*/
|
||||||
if (flightState._billboardCollection) flightState._billboardCollection.show = true;
|
disable(viewer) {
|
||||||
holdContinuousRender('flights'); // per-frame animator (perf wave 2)
|
parts.controller._abortActiveUpdates();
|
||||||
if (flightState._modelCollection) flightState._modelCollection.show = true;
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
parts.tracking._setCockpitContactMode(document.body.classList.contains('cockpit-mode'));
|
if (flightState._billboardCollection)
|
||||||
// Height-datum fix: warm the geoid grid once per layer-enable. The poll loop
|
flightState._billboardCollection.show = false;
|
||||||
// only reads geoidHeight() synchronously after this resolves (guarded by
|
releaseContinuousRender('flights');
|
||||||
// _geoidReady) — never awaited per-aircraft, never blocking a poll tick.
|
parts.rendering._releaseModels();
|
||||||
if (!flightState._geoidReady) {
|
if (flightState._modelCollection)
|
||||||
const lifetime = flightState.lifetime;
|
flightState._modelCollection.show = false;
|
||||||
ensureGeoidReady()
|
parts.tracking._clearTracking();
|
||||||
.then(() => { if (!lifetime.signal.aborted) flightState._geoidReady = true; })
|
parts.tracking._destroyTrail();
|
||||||
.catch(() => { /* geoid grid failed to load — baro path stays un-geoid-corrected until retried */ });
|
// Remove click handler + keydown listener while disabled to avoid
|
||||||
}
|
// intercepting input when the layer is off
|
||||||
parts.tracking._installClickHandler(viewer);
|
if (flightState._clickHandler) {
|
||||||
registerPickOwner('flights', (pickedId) => flightState._billboards.has(pickedId));
|
flightState._clickHandler.destroy();
|
||||||
// Force a fresh rotation pass on the first tick after re-enable
|
flightState._clickHandler = null;
|
||||||
flightState._lastCamPoseSig = '';
|
}
|
||||||
if (!flightState._preRenderRemove && viewer?.scene) {
|
if (flightState._trackedEntityChangedRemove) {
|
||||||
flightState._preRenderRemove = viewer.scene.preRender.addEventListener(parts.rendering._fleetTick);
|
flightState._trackedEntityChangedRemove();
|
||||||
}
|
flightState._trackedEntityChangedRemove = null;
|
||||||
if (!flightState._trackedModelPreUpdateRemove && viewer?.scene) {
|
}
|
||||||
flightState._trackedModelPreUpdateRemove = viewer.scene.preUpdate.addEventListener(parts.rendering._updateTrackedModel);
|
document.removeEventListener('keydown', parts.tracking._onKeyDown);
|
||||||
}
|
unregisterPickOwner('flights');
|
||||||
if (!flightState._moveEndRemove && viewer?.camera) {
|
if (flightState._preRenderRemove) {
|
||||||
// Arrival polish (field test 2026-07-03: "planes look weird when you first
|
flightState._preRenderRemove();
|
||||||
// come to them"): when a camera move SETTLES (voice fly-to, fast pan
|
flightState._preRenderRemove = null;
|
||||||
// release), force a full rotation pass on the very next frame. The
|
}
|
||||||
// pose-signature gate alone can eat the settle — the final easing frames
|
if (flightState._trackedModelPreUpdateRemove) {
|
||||||
// of a flight land inside one quantization bucket (10 m / 0.06°), leaving
|
flightState._trackedModelPreUpdateRemove();
|
||||||
// every icon wearing its last mid-flight rotation for up to
|
flightState._trackedModelPreUpdateRemove = null;
|
||||||
// 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
|
if (flightState._moveEndRemove) {
|
||||||
// extra rotation pass per completed camera gesture — nothing per-frame.
|
flightState._moveEndRemove();
|
||||||
flightState._moveEndRemove = viewer.camera.moveEnd.addEventListener(() => {
|
flightState._moveEndRemove = null;
|
||||||
flightState._lastCamPoseSig = '';
|
}
|
||||||
flightState._lastFleetTickMs = 0;
|
},
|
||||||
});
|
|
||||||
}
|
|
||||||
restoreSpriteOrderOnEnable('flights', viewer);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully tear down the flights layer — remove primitives, handlers,
|
||||||
|
* tracked entities, and clear all internal state maps.
|
||||||
|
* @param {Cesium.Viewer} viewer
|
||||||
|
*/
|
||||||
|
destroy(viewer) {
|
||||||
|
flightState.lifetime.abort();
|
||||||
|
flightState._enrichActive = 0;
|
||||||
|
flightState._enrichLastDispatchMs = 0;
|
||||||
|
parts.controller._abortActiveUpdates();
|
||||||
|
releaseContinuousRender('flights'); // direct-destroy path (perf wave 2 fix)
|
||||||
|
parts.tracking._clearTracking();
|
||||||
|
parts.tracking._destroyTrail();
|
||||||
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
|
if (flightState._clickHandler) {
|
||||||
|
flightState._clickHandler.destroy();
|
||||||
|
flightState._clickHandler = null;
|
||||||
|
}
|
||||||
|
if (flightState._trackedEntityChangedRemove) {
|
||||||
|
flightState._trackedEntityChangedRemove();
|
||||||
|
flightState._trackedEntityChangedRemove = null;
|
||||||
|
}
|
||||||
|
if (flightState._milActiveChangeUnsub) {
|
||||||
|
flightState._milActiveChangeUnsub();
|
||||||
|
flightState._milActiveChangeUnsub = null;
|
||||||
|
}
|
||||||
|
document.removeEventListener('keydown', parts.tracking._onKeyDown);
|
||||||
|
if (flightState._cockpitModeListener) {
|
||||||
|
window.removeEventListener(
|
||||||
|
'gev:cockpit-mode-changed',
|
||||||
|
flightState._cockpitModeListener,
|
||||||
|
);
|
||||||
|
flightState._cockpitModeListener = null;
|
||||||
|
}
|
||||||
|
unregisterPickOwner('flights');
|
||||||
|
if (flightState._preRenderRemove) {
|
||||||
|
flightState._preRenderRemove();
|
||||||
|
flightState._preRenderRemove = null;
|
||||||
|
}
|
||||||
|
if (flightState._trackedModelPreUpdateRemove) {
|
||||||
|
flightState._trackedModelPreUpdateRemove();
|
||||||
|
flightState._trackedModelPreUpdateRemove = null;
|
||||||
|
}
|
||||||
|
if (flightState._moveEndRemove) {
|
||||||
|
flightState._moveEndRemove();
|
||||||
|
flightState._moveEndRemove = null;
|
||||||
|
}
|
||||||
|
parts.rendering._releaseModels();
|
||||||
|
if (flightState._billboardCollection) {
|
||||||
|
viewer.scene.primitives.remove(flightState._billboardCollection);
|
||||||
|
flightState._billboardCollection = null;
|
||||||
|
}
|
||||||
|
if (flightState._modelCollection) {
|
||||||
|
viewer.scene.primitives.remove(flightState._modelCollection); // removing destroys it + its models
|
||||||
|
flightState._modelCollection = null;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
flightState._planeModelLoaded = false;
|
||||||
|
flightState._billboards.clear();
|
||||||
|
flightState._detectionObjects.clear();
|
||||||
|
flightState._flightData.clear();
|
||||||
|
flightState._positionHistory.clear();
|
||||||
|
flightState._displayCourse.clear();
|
||||||
|
flightState._groundSnap.clear();
|
||||||
|
flightState._displayFloorState.clear();
|
||||||
|
flightState._enrichQueue.length = 0;
|
||||||
|
flightState._enrichSeen.clear();
|
||||||
|
if (flightState._enrichDripTimer) {
|
||||||
|
clearTimeout(flightState._enrichDripTimer);
|
||||||
|
flightState._enrichDripTimer = null;
|
||||||
|
}
|
||||||
|
flightState._missingPolls.clear();
|
||||||
|
flightState._focusEvidenceIds.clear();
|
||||||
|
flightState._count = 0;
|
||||||
|
flightState._lastUpdate = null;
|
||||||
|
flightState._cockpitContactMode = false;
|
||||||
|
flightState._cockpitNearContacts = new Set();
|
||||||
|
flightState._cockpitSubjectId = null;
|
||||||
|
flightState._trackingRefreshEpoch += 1;
|
||||||
|
flightState._lastTrackingRefreshOutcome = {
|
||||||
|
epoch: flightState._trackingRefreshEpoch,
|
||||||
|
status: 'destroyed',
|
||||||
|
ids: new Set(),
|
||||||
|
source: flightState._lastSource,
|
||||||
|
coverage: flightState._lastCoverage,
|
||||||
|
};
|
||||||
|
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
|
||||||
|
flightState._viewer = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
return { methods };
|
||||||
* Hide all flight billboards and tear down click/keyboard handlers.
|
|
||||||
* Also clears any active flight tracking so the camera is released.
|
|
||||||
* @param {Cesium.Viewer} viewer
|
|
||||||
*/
|
|
||||||
disable(viewer) {
|
|
||||||
parts.controller._abortActiveUpdates();
|
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
|
||||||
if (flightState._billboardCollection) flightState._billboardCollection.show = false;
|
|
||||||
releaseContinuousRender('flights');
|
|
||||||
parts.rendering._releaseModels();
|
|
||||||
if (flightState._modelCollection) flightState._modelCollection.show = false;
|
|
||||||
parts.tracking._clearTracking();
|
|
||||||
parts.tracking._destroyTrail();
|
|
||||||
// Remove click handler + keydown listener while disabled to avoid
|
|
||||||
// intercepting input when the layer is off
|
|
||||||
if (flightState._clickHandler) {
|
|
||||||
flightState._clickHandler.destroy();
|
|
||||||
flightState._clickHandler = null;
|
|
||||||
}
|
|
||||||
if (flightState._trackedEntityChangedRemove) {
|
|
||||||
flightState._trackedEntityChangedRemove();
|
|
||||||
flightState._trackedEntityChangedRemove = null;
|
|
||||||
}
|
|
||||||
document.removeEventListener('keydown', parts.tracking._onKeyDown);
|
|
||||||
unregisterPickOwner('flights');
|
|
||||||
if (flightState._preRenderRemove) {
|
|
||||||
flightState._preRenderRemove();
|
|
||||||
flightState._preRenderRemove = null;
|
|
||||||
}
|
|
||||||
if (flightState._trackedModelPreUpdateRemove) {
|
|
||||||
flightState._trackedModelPreUpdateRemove();
|
|
||||||
flightState._trackedModelPreUpdateRemove = null;
|
|
||||||
}
|
|
||||||
if (flightState._moveEndRemove) {
|
|
||||||
flightState._moveEndRemove();
|
|
||||||
flightState._moveEndRemove = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fully tear down the flights layer — remove primitives, handlers,
|
|
||||||
* tracked entities, and clear all internal state maps.
|
|
||||||
* @param {Cesium.Viewer} viewer
|
|
||||||
*/
|
|
||||||
destroy(viewer) {
|
|
||||||
flightState.lifetime.abort();
|
|
||||||
flightState._enrichActive = 0;
|
|
||||||
flightState._enrichLastDispatchMs = 0;
|
|
||||||
parts.controller._abortActiveUpdates();
|
|
||||||
releaseContinuousRender('flights'); // direct-destroy path (perf wave 2 fix)
|
|
||||||
parts.tracking._clearTracking();
|
|
||||||
parts.tracking._destroyTrail();
|
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
|
||||||
if (flightState._clickHandler) {
|
|
||||||
flightState._clickHandler.destroy();
|
|
||||||
flightState._clickHandler = null;
|
|
||||||
}
|
|
||||||
if (flightState._trackedEntityChangedRemove) {
|
|
||||||
flightState._trackedEntityChangedRemove();
|
|
||||||
flightState._trackedEntityChangedRemove = null;
|
|
||||||
}
|
|
||||||
if (flightState._milActiveChangeUnsub) {
|
|
||||||
flightState._milActiveChangeUnsub();
|
|
||||||
flightState._milActiveChangeUnsub = null;
|
|
||||||
}
|
|
||||||
document.removeEventListener('keydown', parts.tracking._onKeyDown);
|
|
||||||
if (flightState._cockpitModeListener) {
|
|
||||||
window.removeEventListener('gev:cockpit-mode-changed', flightState._cockpitModeListener);
|
|
||||||
flightState._cockpitModeListener = null;
|
|
||||||
}
|
|
||||||
unregisterPickOwner('flights');
|
|
||||||
if (flightState._preRenderRemove) {
|
|
||||||
flightState._preRenderRemove();
|
|
||||||
flightState._preRenderRemove = null;
|
|
||||||
}
|
|
||||||
if (flightState._trackedModelPreUpdateRemove) {
|
|
||||||
flightState._trackedModelPreUpdateRemove();
|
|
||||||
flightState._trackedModelPreUpdateRemove = null;
|
|
||||||
}
|
|
||||||
if (flightState._moveEndRemove) {
|
|
||||||
flightState._moveEndRemove();
|
|
||||||
flightState._moveEndRemove = null;
|
|
||||||
}
|
|
||||||
parts.rendering._releaseModels();
|
|
||||||
if (flightState._billboardCollection) {
|
|
||||||
viewer.scene.primitives.remove(flightState._billboardCollection);
|
|
||||||
flightState._billboardCollection = null;
|
|
||||||
}
|
|
||||||
if (flightState._modelCollection) {
|
|
||||||
viewer.scene.primitives.remove(flightState._modelCollection); // removing destroys it + its models
|
|
||||||
flightState._modelCollection = null;
|
|
||||||
}
|
|
||||||
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; }
|
|
||||||
flightState._planeModelLoaded = false;
|
|
||||||
flightState._billboards.clear();
|
|
||||||
flightState._detectionObjects.clear();
|
|
||||||
flightState._flightData.clear();
|
|
||||||
flightState._positionHistory.clear();
|
|
||||||
flightState._displayCourse.clear();
|
|
||||||
flightState._groundSnap.clear();
|
|
||||||
flightState._displayFloorState.clear();
|
|
||||||
flightState._enrichQueue.length = 0;
|
|
||||||
flightState._enrichSeen.clear();
|
|
||||||
if (flightState._enrichDripTimer) { clearTimeout(flightState._enrichDripTimer); flightState._enrichDripTimer = null; }
|
|
||||||
flightState._missingPolls.clear();
|
|
||||||
flightState._focusEvidenceIds.clear();
|
|
||||||
flightState._count = 0;
|
|
||||||
flightState._lastUpdate = null;
|
|
||||||
flightState._cockpitContactMode = false;
|
|
||||||
flightState._cockpitNearContacts = new Set();
|
|
||||||
flightState._cockpitSubjectId = null;
|
|
||||||
flightState._trackingRefreshEpoch += 1;
|
|
||||||
flightState._lastTrackingRefreshOutcome = {
|
|
||||||
epoch: flightState._trackingRefreshEpoch,
|
|
||||||
status: 'destroyed',
|
|
||||||
ids: new Set(),
|
|
||||||
source: flightState._lastSource,
|
|
||||||
coverage: flightState._lastCoverage,
|
|
||||||
};
|
|
||||||
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
|
|
||||||
flightState._viewer = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { methods };
|
|
||||||
}
|
}
|
||||||
+1068
-812
File diff suppressed because it is too large.
Load diff
@@ -5,16 +5,37 @@ import { createFlightState } from './state.js';
|
|||||||
import { createEnrichment } from './enrichment.js';
|
import { createEnrichment } from './enrichment.js';
|
||||||
|
|
||||||
function services() {
|
function services() {
|
||||||
const names = ['picking', 'sprites', 'trails', 'aircraftPresentation', 'camera',
|
const names = [
|
||||||
'militaryRegistry', 'labels', 'groundFloor', 'meshFloor', 'geoid', 'focus',
|
'picking',
|
||||||
'readout', 'context', 'render', 'recession'];
|
'sprites',
|
||||||
return { ...Object.fromEntries(names.map((name) => [name, {}])),
|
'trails',
|
||||||
groundSnap: { createGroundSnap: () => ({ clear() {} }) } };
|
'aircraftPresentation',
|
||||||
|
'camera',
|
||||||
|
'militaryRegistry',
|
||||||
|
'labels',
|
||||||
|
'groundFloor',
|
||||||
|
'meshFloor',
|
||||||
|
'geoid',
|
||||||
|
'focus',
|
||||||
|
'readout',
|
||||||
|
'context',
|
||||||
|
'render',
|
||||||
|
'recession',
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
...Object.fromEntries(names.map((name) => [name, {}])),
|
||||||
|
groundSnap: { createGroundSnap: () => ({ clear() {} }) },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('constructing independent flight layers performs no source request and isolates selection', () => {
|
test('constructing independent flight layers performs no source request and isolates selection', () => {
|
||||||
let requests = 0;
|
let requests = 0;
|
||||||
const source = { label: 'Fixture aircraft', getSnapshot() { requests++; } };
|
const source = {
|
||||||
|
label: 'Fixture aircraft',
|
||||||
|
getSnapshot() {
|
||||||
|
requests++;
|
||||||
|
},
|
||||||
|
};
|
||||||
const first = createCivilFlightLayer({ source, services: services() });
|
const first = createCivilFlightLayer({ source, services: services() });
|
||||||
const second = createCivilFlightLayer({ source, services: services() });
|
const second = createCivilFlightLayer({ source, services: services() });
|
||||||
first.testing._armFlightTrackingRestoreForTest('abc123');
|
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', () => {
|
test('flight state owns distinct mutable records, scratch objects and floor samplers', () => {
|
||||||
const first = createFlightState({ services: services() });
|
const first = createFlightState({ services: services() });
|
||||||
const second = createFlightState({ services: services() });
|
const second = createFlightState({ services: services() });
|
||||||
for (const key of ['_flightData', '_billboards', '_positionHistory', '_displayFloorState',
|
for (const key of [
|
||||||
'_groundSnap', '_scratchCarto', '_models', '_activeUpdateControllers', 'lifetime']) {
|
'_flightData',
|
||||||
|
'_billboards',
|
||||||
|
'_positionHistory',
|
||||||
|
'_displayFloorState',
|
||||||
|
'_groundSnap',
|
||||||
|
'_scratchCarto',
|
||||||
|
'_models',
|
||||||
|
'_activeUpdateControllers',
|
||||||
|
'lifetime',
|
||||||
|
]) {
|
||||||
assert.notEqual(first[key], second[key], key);
|
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 () => {
|
test('aborted enrichment cannot write into a later lifecycle or decrement its active count', async () => {
|
||||||
let resolve;
|
let resolve;
|
||||||
let signal;
|
let signal;
|
||||||
const source = { getEnrichment(query, options) {
|
const source = {
|
||||||
signal = options.signal;
|
getEnrichment(query, options) {
|
||||||
return new Promise((done) => { resolve = done; });
|
signal = options.signal;
|
||||||
} };
|
return new Promise((done) => {
|
||||||
|
resolve = done;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
const flightState = createFlightState({ source, services: services() });
|
const flightState = createFlightState({ source, services: services() });
|
||||||
const enrichment = createEnrichment({ flightState, services: {}, parts: {} });
|
const enrichment = createEnrichment({ flightState, services: {}, parts: {} });
|
||||||
let writes = 0;
|
let writes = 0;
|
||||||
enrichment._enqueueEnrich('test', { kind: 'type', id: 'abc123' }, () => writes++);
|
enrichment._enqueueEnrich(
|
||||||
|
'test',
|
||||||
|
{ kind: 'type', id: 'abc123' },
|
||||||
|
() => writes++,
|
||||||
|
);
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
assert.equal(flightState._enrichActive, 1);
|
assert.equal(flightState._enrichActive, 1);
|
||||||
flightState.lifetime.abort();
|
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 () => {
|
test('a synchronous enrichment failure stays bounded and releases its slot', async () => {
|
||||||
const flightState = createFlightState({ services: services(), source: {
|
const flightState = createFlightState({
|
||||||
getEnrichment() { throw new Error('fixture failure'); },
|
services: services(),
|
||||||
} });
|
source: {
|
||||||
|
getEnrichment() {
|
||||||
|
throw new Error('fixture failure');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
const enrichment = createEnrichment({ flightState, services: {}, parts: {} });
|
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));
|
await new Promise((done) => setImmediate(done));
|
||||||
assert.equal(flightState._enrichActive, 0);
|
assert.equal(flightState._enrichActive, 0);
|
||||||
});
|
});
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
|
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). */
|
/** 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');
|
export const MIL_TINT = Cesium.Color.fromCssColorString('#FFB800');
|
||||||
|
|
||||||
|
|
||||||
// --- Ground traffic (owner reversal 2026-07-03: "absolutely we should see planes
|
// --- Ground traffic (owner reversal 2026-07-03: "absolutely we should see planes
|
||||||
// taxiing and landing") -----------------------------------------------------------
|
// taxiing and landing") -----------------------------------------------------------
|
||||||
// Present-but-grounded planes are RENDERED instead of being skipped: same class
|
// 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;
|
export const GROUND_SCALE = 0.8;
|
||||||
|
|
||||||
|
|
||||||
// --- 3D model rendering (B3) ---------------------------------------------------------
|
// --- 3D model rendering (B3) ---------------------------------------------------------
|
||||||
// When enabled, aircraft render as 3D glTF models once the camera is below MODEL_ALT_CEIL_M
|
// 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
|
// (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 PLANE_MODEL_URL = '/models/airplane.glb';
|
||||||
|
|
||||||
|
|
||||||
export const MODEL_ALT_CEIL_M = 800000;
|
export const MODEL_ALT_CEIL_M = 800000;
|
||||||
// m: below this camera altitude, draw 3D models (raised so it's easy to trigger)
|
// m: below this camera altitude, draw 3D models (raised so it's easy to trigger)
|
||||||
|
|
||||||
export const MODEL_MIN_PX = 24;
|
export const MODEL_MIN_PX = 24;
|
||||||
// floor so distant models stay visible WITHOUT ballooning into a giant
|
// floor so distant models stay visible WITHOUT ballooning into a giant
|
||||||
// min-pixel blob (was 54 — far planes at the All radius became white
|
// 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
|
// star-bursts); ~matches the 2D icon size so the model↔billboard read is consistent
|
||||||
|
|
||||||
export const TRACKED_MODEL_MIN_PX = 40;
|
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;
|
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_NATIVE_RADIUS_M = 34.41;
|
||||||
|
|
||||||
|
|
||||||
export const MODEL_SCALE = 1;
|
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.
|
// Per-mode caps. Each model is its own draw call (no instancing yet), so these bound the frame cost.
|
||||||
|
|
||||||
export const MODEL_MAX = 150;
|
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;
|
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
|
// 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
|
// 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
|
// rendered the same). 'proximity' = a tight ring; 'all' = roughly to the horizon (state-scale). Each
|
||||||
@@ -75,24 +68,23 @@ export const MODEL_MAX_ALL = 350;
|
|||||||
// giant floating blob (the old 422 km airport-cluster bug), so far planes stay 2D dots; the cap +
|
// giant floating blob (the old 422 km airport-cluster bug), so far planes stay 2D dots; the cap +
|
||||||
// on-screen priority then spend the model slots on planes you can actually see.
|
// on-screen priority then spend the model slots on planes you can actually see.
|
||||||
|
|
||||||
export const MODEL_PROX_ADD_M = 150000;
|
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;
|
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;
|
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;
|
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;
|
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;
|
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,
|
// Owner launch-polish direction: models should read as clean light silhouettes,
|
||||||
// with only a weak diffuse contribution from the existing approved textures.
|
// 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 MODEL_BELLY_OFFSET_NATIVE = 6.719;
|
||||||
|
|
||||||
|
|
||||||
export const CYAN_TRANSPARENT = Cesium.Color.CYAN.withAlpha(0);
|
export const CYAN_TRANSPARENT = Cesium.Color.CYAN.withAlpha(0);
|
||||||
|
|
||||||
|
|
||||||
/** @constant {number} ERROR_BACKOFF_INTERVAL - Cooldown (ms) after transient errors */
|
/** @constant {number} ERROR_BACKOFF_INTERVAL - Cooldown (ms) after transient errors */
|
||||||
|
|
||||||
export const ERROR_BACKOFF_INTERVAL = 20000;
|
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 */
|
/** @constant {number} POSITION_HISTORY_LIMIT - Max position samples kept per aircraft for dead reckoning */
|
||||||
|
|
||||||
export const POSITION_HISTORY_LIMIT = 5;
|
export const POSITION_HISTORY_LIMIT = 5;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const COCKPIT_CONTACT_SIZE_PX = 6;
|
export const COCKPIT_CONTACT_SIZE_PX = 6;
|
||||||
|
|
||||||
|
export const COCKPIT_CIVILIAN_COLOR =
|
||||||
export const COCKPIT_CIVILIAN_COLOR = Cesium.Color.fromCssColorString('#DCEEFF');
|
Cesium.Color.fromCssColorString('#DCEEFF');
|
||||||
|
|
||||||
|
|
||||||
export const TRACKED_BILLBOARD_SCALE_BY_DISTANCE = new Cesium.NearFarScalar(
|
export const TRACKED_BILLBOARD_SCALE_BY_DISTANCE = new Cesium.NearFarScalar(
|
||||||
1000, 3.0, 8000000, 0.5,
|
1000,
|
||||||
|
3.0,
|
||||||
|
8000000,
|
||||||
|
0.5,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Track-history trail state (PRD WS-F F1/F4): a fading polyline behind the
|
// Track-history trail state (PRD WS-F F1/F4): a fading polyline behind the
|
||||||
// tracked aircraft. The accumulation array is intentionally SEPARATE from
|
// tracked aircraft. The accumulation array is intentionally SEPARATE from
|
||||||
@@ -147,7 +136,6 @@ export const TRAIL_COLOR = '#00d4ff';
|
|||||||
|
|
||||||
export const TRAIL_MAX_POINTS = 400;
|
export const TRAIL_MAX_POINTS = 400;
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Render-behind smoothing (PRD WS-C C2 — approved product decision):
|
// Render-behind smoothing (PRD WS-C C2 — approved product decision):
|
||||||
// the fleet renders at now - RENDER_DELAY_SEC so positions interpolate
|
// 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;
|
export const GROUND_FLOOR_CLAMP_RADIUS_KM = 150;
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Icon orientation (2026-06-10 playtest fix): rotation is computed by
|
// Icon orientation (2026-06-10 playtest fix): rotation is computed by
|
||||||
// projecting each aircraft's course vector into WINDOW coordinates
|
// 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;
|
export const COURSE_SLEW_DT_MAX_SEC = 0.25;
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// adsbdb enrichment (best-effort, fail-silent). Bounded fan-out: max 4
|
// adsbdb enrichment (best-effort, fail-silent). Bounded fan-out: max 4
|
||||||
// concurrent requests, dispatches dripped ≥ENRICH_DISPATCH_GAP_MS apart
|
// 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;
|
export const ENRICH_DISPATCH_GAP_MS = 200;
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Ambient fleet type enrichment (2026-07-02 field data: OpenSky's live
|
// 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
|
// 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;
|
export const ENRICH_AMBIENT_PER_SWEEP = 150;
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tracked-display reconciliation. _deadReckon gives the RAW position from real
|
// Tracked-display reconciliation. _deadReckon gives the RAW position from real
|
||||||
// fixes; at the warm-up→interpolation handoff (and on feed glitches / backfill
|
// 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).
|
// (the correction stays ~0 whenever motion is already continuous).
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
export const DR_CORRECTION_MS = 900;
|
export const DR_CORRECTION_MS = 900;
|
||||||
|
|
||||||
|
|
||||||
/** Bounded on-demand loading for the tracked model. The tracked regime is
|
/** 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
|
* 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
|
* 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_MAX_LOAD_FAILS = 3;
|
||||||
|
|
||||||
|
|
||||||
export const TRACKED_MODEL_RETRY_BACKOFF_MS = 1500;
|
export const TRACKED_MODEL_RETRY_BACKOFF_MS = 1500;
|
||||||
|
|
||||||
/** @constant {number} Cap on NEW cells the display corridors may add to one
|
/** @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;
|
export const DISPLAY_CORRIDOR_RADIUS_KM = 25;
|
||||||
|
|
||||||
|
|
||||||
/** @constant {number} How far a contact may travel from the cell that supplied
|
/** @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.
|
* 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;
|
export const FLOOR_EASE_EPSILON_M = 0.02;
|
||||||
|
|
||||||
|
|
||||||
/** @constant {number} How long a retired hold stays usable as a rehydration
|
/** @constant {number} How long a retired hold stays usable as a rehydration
|
||||||
* seed — three poll intervals.
|
* seed — three poll intervals.
|
||||||
*
|
*
|
||||||
|
|||||||
+929
-749
File diff suppressed because it is too large.
Load diff
+1180
-838
File diff suppressed because it is too large.
Load diff
+301
-348
@@ -2,547 +2,500 @@ import * as Cesium from 'cesium';
|
|||||||
import { ENRICH_AMBIENT_BUDGET_CEIL } from './policy.js';
|
import { ENRICH_AMBIENT_BUDGET_CEIL } from './policy.js';
|
||||||
|
|
||||||
export function createFlightState({ source, services }) {
|
export function createFlightState({ source, services }) {
|
||||||
const { createGroundSnap } = services.groundSnap;
|
const { createGroundSnap } = services.groundSnap;
|
||||||
const flightState = { lifetime: new AbortController() };
|
const flightState = { lifetime: new AbortController() };
|
||||||
|
|
||||||
|
flightState._source = source;
|
||||||
|
|
||||||
|
/** 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
|
||||||
|
* (MODEL_SCALE × CLASS_SCALE_3D). nativeRadiusM is PER SCALE UNIT — pixel-cap
|
||||||
|
* 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
|
||||||
|
* 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._source = source;
|
flightState._specCache = new Map();
|
||||||
|
|
||||||
/** Per-class model spec. Hangar-fleet classes (CLASS_MODEL_REAL) ship GLBs
|
/** One-shot cached tile-skin heights for MODELED grounded planes (see groundSnap.js). */
|
||||||
* 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
|
|
||||||
* (MODEL_SCALE × CLASS_SCALE_3D). nativeRadiusM is PER SCALE UNIT — pixel-cap
|
|
||||||
* 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
|
|
||||||
* 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._groundSnap = createGroundSnap();
|
||||||
|
|
||||||
/** One-shot cached tile-skin heights for MODELED grounded planes (see groundSnap.js). */
|
flightState._scratchGroundCarto = new Cesium.Cartographic();
|
||||||
|
|
||||||
flightState._groundSnap = createGroundSnap();
|
flightState._scratchGroundPos = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
/** @type {Cesium.PrimitiveCollection|null} */
|
||||||
|
|
||||||
flightState._scratchGroundCarto = new Cesium.Cartographic();
|
flightState._modelCollection = null;
|
||||||
|
|
||||||
|
/** @type {Map<string, Cesium.Model>} icao24 → model */
|
||||||
|
|
||||||
flightState._scratchGroundPos = new Cesium.Cartesian3();
|
flightState._models = new Map();
|
||||||
|
|
||||||
/** @type {Cesium.PrimitiveCollection|null} */
|
/** @type {Set<string>} icao24 currently loading (async) */
|
||||||
|
|
||||||
flightState._modelCollection = null;
|
flightState._modelPending = new Set();
|
||||||
|
|
||||||
/** @type {Map<string, Cesium.Model>} icao24 → model */
|
/** @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._models = new Map();
|
flightState._modelGen = new Map();
|
||||||
|
|
||||||
/** @type {Set<string>} icao24 currently loading (async) */
|
/** 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._modelPending = new Set();
|
flightState._modelEpoch = 0;
|
||||||
|
|
||||||
/** @type {Map<string, number>} icao24 → load generation; bumped on release to invalidate
|
/** DEFAULT-ON in PROXIMITY (owner directive 2026-08-22). A fresh boot never runs
|
||||||
* an in-flight load (so a track/untrack/remove during fromGltfAsync can't add a stale model). */
|
* 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._modelGen = new Map();
|
flightState._models3dEnabled = true;
|
||||||
|
|
||||||
/** Lifecycle epoch; bumped on destroy so an in-flight load from a PREVIOUS init can't settle
|
flightState._models3dMode = 'proximity';
|
||||||
* against a new lifecycle's globals (which destroy cleared). Captured by _ensureModel. */
|
// 'proximity' = nearest MODEL_MAX in view; 'all' = every in-view plane (≤ MODEL_MAX_ALL)
|
||||||
|
|
||||||
flightState._modelEpoch = 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.
|
||||||
|
|
||||||
/** DEFAULT-ON in PROXIMITY (owner directive 2026-08-22). A fresh boot never runs
|
flightState._planeModelLoaded = false;
|
||||||
* 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;
|
/** @type {Cesium.Model|null} retained preload that keeps the glTF cache warm */
|
||||||
|
|
||||||
|
flightState._preloadModel = null;
|
||||||
|
|
||||||
flightState._models3dMode = 'proximity';
|
flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0);
|
||||||
// 'proximity' = nearest MODEL_MAX in view; 'all' = every in-view plane (≤ MODEL_MAX_ALL)
|
|
||||||
|
|
||||||
flightState._lastModelCapWarnMs = 0;
|
flightState._scratchModelMtx = new Cesium.Matrix4();
|
||||||
// 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.
|
|
||||||
|
|
||||||
flightState._planeModelLoaded = false;
|
flightState._scratchModelBS = new Cesium.BoundingSphere(
|
||||||
|
new Cesium.Cartesian3(),
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
// frustum-visibility test
|
||||||
|
/** Last limb taper per billboard, retained across class/ground/cockpit repaints. */
|
||||||
|
|
||||||
/** @type {Cesium.Model|null} retained preload that keeps the glTF cache warm */
|
flightState._billboardLimbScale = new WeakMap();
|
||||||
|
// keep last N positions per aircraft
|
||||||
|
|
||||||
flightState._preloadModel = null;
|
// ---------------------------------------------------------------------------
|
||||||
|
// Module-level state: billboard collection and per-aircraft lookup maps
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** @type {Cesium.BillboardCollection|null} */
|
||||||
|
|
||||||
flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0);
|
flightState._billboardCollection = null;
|
||||||
|
|
||||||
|
/** @type {Map<string, Cesium.Billboard>} icao24 -> billboard primitive */
|
||||||
|
|
||||||
flightState._scratchModelMtx = new Cesium.Matrix4();
|
flightState._billboards = new Map();
|
||||||
|
|
||||||
|
/** Stable lightweight records reused by the detection overlay between polls. */
|
||||||
|
|
||||||
flightState._scratchModelBS = new Cesium.BoundingSphere(new Cesium.Cartesian3(), 1.0);
|
flightState._detectionObjects = new Map();
|
||||||
// frustum-visibility test
|
|
||||||
/** Last limb taper per billboard, retained across class/ground/cockpit repaints. */
|
|
||||||
|
|
||||||
flightState._billboardLimbScale = new WeakMap();
|
/** @type {Map<string, {callsign:string, altitude:number, velocity:number, true_track:number}>} */
|
||||||
// keep last N positions per aircraft
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
flightState._flightData = new Map();
|
||||||
// Module-level state: billboard collection and per-aircraft lookup maps
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** @type {Cesium.BillboardCollection|null} */
|
/** DEV-only explicit-position contacts used by qa-focus-evidence.mjs. */
|
||||||
|
|
||||||
flightState._billboardCollection = null;
|
flightState._focusEvidenceIds = new Set();
|
||||||
|
|
||||||
/** @type {Map<string, Cesium.Billboard>} icao24 -> billboard primitive */
|
/** @type {Map<string, Array<{time:Cesium.JulianDate, position:Cesium.Cartesian3}>>} */
|
||||||
|
|
||||||
flightState._billboards = new Map();
|
flightState._positionHistory = new Map();
|
||||||
|
|
||||||
/** Stable lightweight records reused by the detection overlay between polls. */
|
/** @type {boolean} True once ensureGeoidReady() has resolved (awaited once at enable()) */
|
||||||
|
|
||||||
flightState._detectionObjects = new Map();
|
flightState._geoidReady = false;
|
||||||
|
|
||||||
/** @type {Map<string, {callsign:string, altitude:number, velocity:number, true_track:number}>} */
|
/** @type {Map<string, number>} icao24 -> geoid undulation N (m), cached (negligible drift per-aircraft). */
|
||||||
|
|
||||||
flightState._flightData = new Map();
|
flightState._geoidNCache = new Map();
|
||||||
|
|
||||||
/** DEV-only explicit-position contacts used by qa-focus-evidence.mjs. */
|
/** @type {number} Current number of visible aircraft */
|
||||||
|
|
||||||
flightState._focusEvidenceIds = new Set();
|
flightState._count = 0;
|
||||||
|
|
||||||
/** @type {Map<string, Array<{time:Cesium.JulianDate, position:Cesium.Cartesian3}>>} */
|
/** @type {number|null} Epoch ms of last successful API update */
|
||||||
|
|
||||||
flightState._positionHistory = new Map();
|
flightState._lastUpdate = null;
|
||||||
|
|
||||||
/** @type {boolean} True once ensureGeoidReady() has resolved (awaited once at enable()) */
|
/** @type {boolean} True while in a backoff/cooldown window */
|
||||||
|
|
||||||
flightState._geoidReady = false;
|
flightState._backoff = false;
|
||||||
|
|
||||||
/** @type {Map<string, number>} icao24 -> geoid undulation N (m), cached (negligible drift per-aircraft). */
|
/** @type {number} Epoch ms — earliest time the next fetch is allowed */
|
||||||
|
|
||||||
flightState._geoidNCache = new Map();
|
flightState._retryAt = 0;
|
||||||
|
|
||||||
/** @type {number} Current number of visible aircraft */
|
/** @type {string|null} Human-readable error string shown in stats chip */
|
||||||
|
|
||||||
flightState._count = 0;
|
flightState._lastError = null;
|
||||||
|
|
||||||
/** @type {number|null} Epoch ms of last successful API update */
|
flightState._activeUpdateControllers = new Set();
|
||||||
|
|
||||||
flightState._lastUpdate = null;
|
/** @type {number|null} HTTP status of the most recent API response */
|
||||||
|
|
||||||
/** @type {boolean} True while in a backoff/cooldown window */
|
flightState._lastStatus = null;
|
||||||
|
|
||||||
flightState._backoff = false;
|
/** @type {string} Source used by the latest successful snapshot. */
|
||||||
|
|
||||||
/** @type {number} Epoch ms — earliest time the next fetch is allowed */
|
flightState._lastSource = source?.label || 'Aircraft';
|
||||||
|
|
||||||
flightState._retryAt = 0;
|
/** @type {string} Completeness boundary for the latest successful snapshot. */
|
||||||
|
|
||||||
/** @type {string|null} Human-readable error string shown in stats chip */
|
flightState._lastCoverage = 'worldwide upstream snapshot';
|
||||||
|
|
||||||
flightState._lastError = null;
|
// ---------------------------------------------------------------------------
|
||||||
|
// Click-to-track state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** @type {string|null} ICAO24 of the currently tracked aircraft */
|
||||||
|
|
||||||
flightState._activeUpdateControllers = new Set();
|
flightState._trackedIcao = null;
|
||||||
|
|
||||||
/** @type {number|null} HTTP status of the most recent API response */
|
flightState._pendingTrackingRestore = null;
|
||||||
|
|
||||||
flightState._lastStatus = null;
|
flightState._trackingIntentGeneration = 0;
|
||||||
|
|
||||||
/** @type {string} Source used by the latest successful snapshot. */
|
flightState._trackingRefreshEpoch = 0;
|
||||||
|
|
||||||
flightState._lastSource = source?.label || 'Aircraft';
|
flightState._lastTrackingRefreshOutcome = {
|
||||||
|
epoch: 0,
|
||||||
|
status: 'unavailable',
|
||||||
|
ids: new Set(),
|
||||||
|
source: flightState._lastSource,
|
||||||
|
coverage: null,
|
||||||
|
};
|
||||||
|
|
||||||
/** @type {string} Completeness boundary for the latest successful snapshot. */
|
/** @type {Cesium.Entity|null} Entity used for camera tracking */
|
||||||
|
|
||||||
flightState._lastCoverage = 'worldwide upstream snapshot';
|
flightState._trackedEntity = null;
|
||||||
|
|
||||||
|
/** Disposes the single active tracked-camera framing owner. */
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
flightState._trackedCameraFrameStop = null;
|
||||||
// Click-to-track state
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** @type {string|null} ICAO24 of the currently tracked aircraft */
|
/** @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._trackedIcao = null;
|
flightState._trackedModel = null;
|
||||||
|
|
||||||
|
/** Bumped on untrack/teardown so an in-flight tracked-model load resolves into a no-op. */
|
||||||
|
|
||||||
flightState._pendingTrackingRestore = null;
|
flightState._trackedModelGen = 0;
|
||||||
|
|
||||||
|
flightState._trackedModelLoading = false;
|
||||||
|
|
||||||
flightState._trackingIntentGeneration = 0;
|
/** @type {Cesium.ScreenSpaceEventHandler|null} Click handler on the scene canvas */
|
||||||
|
|
||||||
|
flightState._clickHandler = null;
|
||||||
|
|
||||||
flightState._trackingRefreshEpoch = 0;
|
/** @type {Cesium.Viewer|null} Cached viewer reference */
|
||||||
|
|
||||||
|
flightState._viewer = null;
|
||||||
|
|
||||||
flightState._lastTrackingRefreshOutcome = {
|
/** Cockpit presentation switches ambient AIR contacts between near aircraft and far dots. */
|
||||||
epoch: 0,
|
|
||||||
status: 'unavailable',
|
|
||||||
ids: new Set(),
|
|
||||||
source: flightState._lastSource,
|
|
||||||
coverage: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {Cesium.Entity|null} Entity used for camera tracking */
|
flightState._cockpitContactMode = false;
|
||||||
|
|
||||||
flightState._trackedEntity = null;
|
/** AIR contacts inside the selected Display range; independent from model admission/load/cap. */
|
||||||
|
|
||||||
/** Disposes the single active tracked-camera framing owner. */
|
flightState._cockpitNearContacts = new Set();
|
||||||
|
|
||||||
flightState._trackedCameraFrameStop = null;
|
/** Normalized ICAO24 of the active Cockpit subject, omitted from detection candidates. */
|
||||||
|
|
||||||
/** @type {Cesium.Model|null} Standalone 3D model for the tracked aircraft. Deliberately NOT a
|
flightState._cockpitSubjectId = null;
|
||||||
* 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;
|
/** @type {((event: CustomEvent) => void)|null} */
|
||||||
|
|
||||||
/** Bumped on untrack/teardown so an in-flight tracked-model load resolves into a no-op. */
|
flightState._cockpitModeListener = null;
|
||||||
|
|
||||||
flightState._trackedModelGen = 0;
|
/** @type {{setPositions: Function, clear: Function, destroy: Function}|null} Shared fading-trail renderer */
|
||||||
|
|
||||||
|
flightState._trail = null;
|
||||||
|
|
||||||
flightState._trackedModelLoading = false;
|
/** @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. */
|
||||||
|
|
||||||
/** @type {Cesium.ScreenSpaceEventHandler|null} Click handler on the scene canvas */
|
flightState._trailHeadEntity = null;
|
||||||
|
|
||||||
flightState._clickHandler = null;
|
/** @type {number} Uniquifier for head-segment entity ids (Cesium requires unique ids). */
|
||||||
|
|
||||||
/** @type {Cesium.Viewer|null} Cached viewer reference */
|
flightState._trailHeadSeq = 0;
|
||||||
|
|
||||||
flightState._viewer = null;
|
/** @type {Cesium.Cartesian3[]} Chronological tracked-aircraft fixes (oldest first) */
|
||||||
|
|
||||||
/** Cockpit presentation switches ambient AIR contacts between near aircraft and far dots. */
|
flightState._trailPositions = [];
|
||||||
|
|
||||||
flightState._cockpitContactMode = false;
|
/** @type {number} Monotonic token — invalidates in-flight backfill responses */
|
||||||
|
|
||||||
/** AIR contacts inside the selected Display range; independent from model admission/load/cap. */
|
flightState._trailBackfillToken = 0;
|
||||||
|
|
||||||
flightState._cockpitNearContacts = new Set();
|
/** @type {Map<string, number>} icao24 -> consecutive missed polls */
|
||||||
|
|
||||||
/** Normalized ICAO24 of the active Cockpit subject, omitted from detection candidates. */
|
flightState._missingPolls = new Map();
|
||||||
|
|
||||||
flightState._cockpitSubjectId = null;
|
/** @type {number} Epoch ms of the last fleet dead-reckoning pass */
|
||||||
|
|
||||||
/** @type {((event: CustomEvent) => void)|null} */
|
flightState._lastFleetTickMs = 0;
|
||||||
|
|
||||||
flightState._cockpitModeListener = null;
|
/** @type {string} Camera pose signature at the last rotation pass */
|
||||||
|
|
||||||
/** @type {{setPositions: Function, clear: Function, destroy: Function}|null} Shared fading-trail renderer */
|
flightState._lastCamPoseSig = '';
|
||||||
|
|
||||||
flightState._trail = null;
|
/** @type {number} Epoch ms of the last full rotation pass */
|
||||||
|
|
||||||
/** @type {Cesium.Entity|null} Cheap 2-point head segment bridging the last fix to the LIVE
|
flightState._lastRotPassMs = 0;
|
||||||
* 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;
|
/** @type {number} Last computed rotation for the tracked entity (radians) */
|
||||||
|
|
||||||
/** @type {number} Uniquifier for head-segment entity ids (Cesium requires unique ids). */
|
flightState._lastTrackedRotation = 0;
|
||||||
|
|
||||||
flightState._trailHeadSeq = 0;
|
/** @type {Cesium.Event.RemoveCallback|null} preRender listener disposer */
|
||||||
|
|
||||||
/** @type {Cesium.Cartesian3[]} Chronological tracked-aircraft fixes (oldest first) */
|
flightState._preRenderRemove = null;
|
||||||
|
|
||||||
flightState._trailPositions = [];
|
flightState._trackedModelPreUpdateRemove = null;
|
||||||
|
|
||||||
/** @type {number} Monotonic token — invalidates in-flight backfill responses */
|
/** @type {Cesium.Event.RemoveCallback|null} camera.moveEnd listener disposer (arrival rotation pass) */
|
||||||
|
|
||||||
flightState._trailBackfillToken = 0;
|
flightState._moveEndRemove = null;
|
||||||
|
|
||||||
/** @type {Map<string, number>} icao24 -> consecutive missed polls */
|
/** @type {Cesium.Event.RemoveCallback|null} trackedEntityChanged listener disposer (cross-layer untrack) */
|
||||||
|
|
||||||
flightState._missingPolls = new Map();
|
flightState._trackedEntityChangedRemove = null;
|
||||||
|
|
||||||
/** @type {number} Epoch ms of the last fleet dead-reckoning pass */
|
/** @type {(() => void)|null} militaryRegistry active-transition unsubscribe (M2 handoff sweep) */
|
||||||
|
|
||||||
flightState._lastFleetTickMs = 0;
|
flightState._milActiveChangeUnsub = null;
|
||||||
|
|
||||||
/** @type {string} Camera pose signature at the last rotation pass */
|
// ---------------------------------------------------------------------------
|
||||||
|
// Scratch (reusable) variables — avoid per-frame heap allocation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
flightState._lastCamPoseSig = '';
|
flightState._scratchOffset = new Cesium.Cartesian3();
|
||||||
|
|
||||||
/** @type {number} Epoch ms of the last full rotation pass */
|
flightState._scratchCarto = new Cesium.Cartographic();
|
||||||
|
|
||||||
flightState._lastRotPassMs = 0;
|
flightState._scratchEnu = new Cesium.Matrix4();
|
||||||
|
|
||||||
/** @type {number} Last computed rotation for the tracked entity (radians) */
|
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
||||||
|
|
||||||
flightState._lastTrackedRotation = 0;
|
flightState._scratchRenderTime = new Cesium.JulianDate();
|
||||||
|
|
||||||
/** @type {Cesium.Event.RemoveCallback|null} preRender listener disposer */
|
flightState._scratchFleetPos = new Cesium.Cartesian3();
|
||||||
|
|
||||||
flightState._preRenderRemove = null;
|
flightState._scratchDrRaw = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
flightState._scratchWarmupTime = new Cesium.JulianDate();
|
||||||
|
|
||||||
flightState._trackedModelPreUpdateRemove = null;
|
flightState._trackedPosHolder = new Cesium.Cartesian3();
|
||||||
|
|
||||||
/** @type {Cesium.Event.RemoveCallback|null} camera.moveEnd listener disposer (arrival rotation pass) */
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
flightState._moveEndRemove = null;
|
/** @type {Cesium.Cartesian3|null} */
|
||||||
|
|
||||||
/** @type {Cesium.Event.RemoveCallback|null} trackedEntityChanged listener disposer (cross-layer untrack) */
|
flightState._cachedDRPosition = null;
|
||||||
|
|
||||||
flightState._trackedEntityChangedRemove = null;
|
/** @type {number} Frame number for which _cachedDRPosition is valid */
|
||||||
|
|
||||||
/** @type {(() => void)|null} militaryRegistry active-transition unsubscribe (M2 handoff sweep) */
|
flightState._cachedDRFrame = -1;
|
||||||
|
|
||||||
flightState._milActiveChangeUnsub = null;
|
/** 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;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
/** Sibling scratches of _drCourseDeg: the displayed ground speed of the motion
|
||||||
// Scratch (reusable) variables — avoid per-frame heap allocation
|
* `_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._scratchOffset = new Cesium.Cartesian3();
|
flightState._drCourseHold = false;
|
||||||
|
|
||||||
|
/** 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._scratchCarto = new Cesium.Cartographic();
|
flightState._drExtrapolating = false;
|
||||||
|
|
||||||
|
/** Frame-cached course for the tracked aircraft (sibling of _cachedDRPosition). */
|
||||||
|
|
||||||
flightState._scratchEnu = new Cesium.Matrix4();
|
flightState._cachedDRCourse = null;
|
||||||
|
|
||||||
|
/** Frame-cached siblings of _cachedDRCourse (same discipline). */
|
||||||
|
|
||||||
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
flightState._cachedDRSpeedMps = null;
|
||||||
|
|
||||||
|
flightState._cachedDRHold = false;
|
||||||
|
|
||||||
flightState._scratchRenderTime = new Cesium.JulianDate();
|
/** 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._scratchFleetPos = new Cesium.Cartesian3();
|
/** 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
|
||||||
|
* tracked icao, so exactly one writer owns an entry at a time). Sharing the
|
||||||
|
* entry — including its slew state — is what makes the tracked↔fleet
|
||||||
|
* handoff seamless (2026-07-03 field fix: separate states froze the fleet
|
||||||
|
* 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._scratchDrRaw = new Cesium.Cartesian3();
|
flightState._enrichActive = 0;
|
||||||
|
|
||||||
|
flightState._enrichLastDispatchMs = 0;
|
||||||
|
|
||||||
flightState._scratchWarmupTime = new Cesium.JulianDate();
|
/** @type {ReturnType<typeof setTimeout>|null} pending drip wake-up */
|
||||||
|
|
||||||
|
flightState._enrichDripTimer = null;
|
||||||
|
|
||||||
flightState._trackedPosHolder = new Cesium.Cartesian3();
|
flightState._enrichQueue = [];
|
||||||
|
|
||||||
|
flightState._enrichSeen = new Set();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
flightState._enrichAmbientBudget = ENRICH_AMBIENT_BUDGET_CEIL;
|
||||||
// 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} */
|
/** Epoch ms the bucket last accounted a refill window from (0 = unset). */
|
||||||
|
|
||||||
flightState._cachedDRPosition = null;
|
flightState._enrichAmbientRefillAnchorMs = 0;
|
||||||
|
|
||||||
/** @type {number} Frame number for which _cachedDRPosition is valid */
|
flightState._drCorrection = new Cesium.Cartesian3(0, 0, 0);
|
||||||
|
|
||||||
flightState._cachedDRFrame = -1;
|
flightState._drCorrectionStartMs = 0;
|
||||||
|
|
||||||
|
flightState._drPrevRaw = new Cesium.Cartesian3();
|
||||||
|
|
||||||
/** Course (deg) of the position `_deadReckon` most recently returned — set on
|
flightState._drPrevDisplay = new Cesium.Cartesian3();
|
||||||
* every branch of `_deadReckon`, read IMMEDIATELY by the caller (same
|
|
||||||
* synchronous flow; module-scratch idiom, like the Cartesian scratches). */
|
|
||||||
|
|
||||||
flightState._drCourseDeg = null;
|
flightState._drPrevMs = 0;
|
||||||
|
|
||||||
/** Sibling scratches of _drCourseDeg: the displayed ground speed of the motion
|
flightState._drReconcileValid = false;
|
||||||
* `_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;
|
/** @type {string|null} icao the reconciliation state currently belongs to */
|
||||||
|
|
||||||
|
flightState._drReconcileIcao = null;
|
||||||
|
|
||||||
flightState._drCourseHold = false;
|
/** @type {Cesium.Cartesian3} Scratch for the tracked model's rendered translation. */
|
||||||
|
|
||||||
/** Sibling scratch: whether the position `_deadReckon` just returned came from
|
flightState._trackedVisualPos = new Cesium.Cartesian3();
|
||||||
* 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._trackedTrailPos = new Cesium.Cartesian3();
|
||||||
|
|
||||||
/** Frame-cached course for the tracked aircraft (sibling of _cachedDRPosition). */
|
/** Scratch for the tracked model's world-space origin (the envelope centre). */
|
||||||
|
|
||||||
flightState._cachedDRCourse = null;
|
flightState._scratchTrailClip = new Cesium.Cartesian3();
|
||||||
|
|
||||||
/** Frame-cached siblings of _cachedDRCourse (same discipline). */
|
/** Scratch for the shortened trail-head start. Owned by the head-segment
|
||||||
|
* callback alone, so nothing else can overwrite it mid-frame. */
|
||||||
|
|
||||||
flightState._cachedDRSpeedMps = null;
|
flightState._scratchTrailHead = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
/** 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._cachedDRHold = false;
|
flightState._trackedZoomLatched = false;
|
||||||
|
|
||||||
/** Wall-clock of the tracked course limiter's last advance (dt source only —
|
flightState._trackedZoomLatchIcao = null;
|
||||||
* the course VALUE lives in the shared per-icao _displayCourse map below). */
|
|
||||||
|
|
||||||
flightState._trackedCourseMs = 0;
|
flightState._trackedModelFailIcao = null;
|
||||||
|
|
||||||
/** Per-aircraft smoothed display course — the SINGLE source of truth for the
|
flightState._trackedModelFailCount = 0;
|
||||||
* 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
|
|
||||||
* tracked icao, so exactly one writer owns an entry at a time). Sharing the
|
|
||||||
* entry — including its slew state — is what makes the tracked↔fleet
|
|
||||||
* handoff seamless (2026-07-03 field fix: separate states froze the fleet
|
|
||||||
* 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._trackedModelRetryAtMs = 0;
|
||||||
|
|
||||||
|
/** @type {Cesium.Cartographic} Scratch for the grounded display-floor read. */
|
||||||
|
|
||||||
flightState._enrichActive = 0;
|
flightState._scratchDisplayCarto = new Cesium.Cartographic();
|
||||||
|
|
||||||
|
/** @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,
|
||||||
|
* probeMs: number|null, retiredMs: number|null,
|
||||||
|
* easedM: number|null, easeMs: number|null}>} Per-grounded-contact
|
||||||
|
* display-floor state: the cell it is currently reading (boundary hysteresis),
|
||||||
|
* the last input position and the effective floor that produced the cached
|
||||||
|
* output (rebuild skip), the last floor that actually RESOLVED for it plus the
|
||||||
|
* tier it came from (the hold, below), whether that floor is a REHYDRATED SEED
|
||||||
|
* rather than something measured while the contact stood here (`seeded` — it
|
||||||
|
* ranks below live evidence), and the value the downward ease is currently
|
||||||
|
* displaying while it approaches a lower floor.
|
||||||
|
* Dropped with the contact, and whenever it stops being a grounded billboard. */
|
||||||
|
|
||||||
flightState._enrichLastDispatchMs = 0;
|
flightState._displayFloorState = new Map();
|
||||||
|
|
||||||
/** @type {ReturnType<typeof setTimeout>|null} pending drip wake-up */
|
/** @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._enrichDripTimer = null;
|
flightState._corridorEpoch = 0;
|
||||||
|
|
||||||
|
/** @type {Cesium.Cartographic} Scratch for the corridor's display-end read. */
|
||||||
|
|
||||||
flightState._enrichQueue = [];
|
flightState._scratchCorridorCarto = new Cesium.Cartographic();
|
||||||
|
|
||||||
|
/** @type {Cesium.Cartesian3} Scratch for the corridor's dead-reckon probe. */
|
||||||
|
|
||||||
flightState._enrichSeen = new Set();
|
flightState._scratchCorridorPos = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
/** 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._enrichAmbientBudget = ENRICH_AMBIENT_BUDGET_CEIL;
|
flightState._irBoost = false;
|
||||||
|
|
||||||
/** Epoch ms the bucket last accounted a refill window from (0 = unset). */
|
/** 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.
|
||||||
|
* LightingModel.UNLIT emits the flat white directly, orientation be damned.
|
||||||
|
* CRITICAL (field-verified via scene.pick): assigning customShader to an
|
||||||
|
* already-READY model is a silent no-op — the property sets but the shader
|
||||||
|
* program never rebuilds. The boost therefore flips by RELEASE-AND-RELOAD
|
||||||
|
* (see setParams), so every boosted model gets the shader AT CREATION.
|
||||||
|
* One shared shader instance — stateless, safe across models. */
|
||||||
|
|
||||||
flightState._enrichAmbientRefillAnchorMs = 0;
|
flightState._IR_UNLIT_SHADER = new Cesium.CustomShader({
|
||||||
|
lightingModel: Cesium.LightingModel.UNLIT,
|
||||||
|
});
|
||||||
|
|
||||||
|
flightState._irReloadQueue = null;
|
||||||
flightState._drCorrection = new Cesium.Cartesian3(0, 0, 0);
|
return flightState;
|
||||||
|
|
||||||
|
|
||||||
flightState._drCorrectionStartMs = 0;
|
|
||||||
|
|
||||||
|
|
||||||
flightState._drPrevRaw = new Cesium.Cartesian3();
|
|
||||||
|
|
||||||
|
|
||||||
flightState._drPrevDisplay = 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
|
|
||||||
* callback alone, so nothing else can overwrite it mid-frame. */
|
|
||||||
|
|
||||||
flightState._scratchTrailHead = new Cesium.Cartesian3();
|
|
||||||
|
|
||||||
|
|
||||||
/** 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._trackedZoomLatchIcao = null;
|
|
||||||
|
|
||||||
|
|
||||||
flightState._trackedModelFailIcao = null;
|
|
||||||
|
|
||||||
|
|
||||||
flightState._trackedModelFailCount = 0;
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
* in: Cesium.Cartesian3, out: Cesium.Cartesian3|null, heldM: number|null,
|
|
||||||
* heldCell: {lat: number, lon: number}|null,
|
|
||||||
* heldTier: 'own'|'neighbor'|null, heldActive: boolean, seeded: boolean,
|
|
||||||
* probeMs: number|null, retiredMs: number|null,
|
|
||||||
* easedM: number|null, easeMs: number|null}>} Per-grounded-contact
|
|
||||||
* display-floor state: the cell it is currently reading (boundary hysteresis),
|
|
||||||
* the last input position and the effective floor that produced the cached
|
|
||||||
* output (rebuild skip), the last floor that actually RESOLVED for it plus the
|
|
||||||
* tier it came from (the hold, below), whether that floor is a REHYDRATED SEED
|
|
||||||
* rather than something measured while the contact stood here (`seeded` — it
|
|
||||||
* ranks below live evidence), and the value the downward ease is currently
|
|
||||||
* displaying while it approaches a lower floor.
|
|
||||||
* Dropped with the contact, and whenever it stops being a grounded billboard. */
|
|
||||||
|
|
||||||
flightState._displayFloorState = new Map();
|
|
||||||
|
|
||||||
/** @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;
|
|
||||||
|
|
||||||
|
|
||||||
/** @type {Cesium.Cartographic} Scratch for the corridor's display-end read. */
|
|
||||||
|
|
||||||
flightState._scratchCorridorCarto = new Cesium.Cartographic();
|
|
||||||
|
|
||||||
/** @type {Cesium.Cartesian3} Scratch for the corridor's dead-reckon probe. */
|
|
||||||
|
|
||||||
flightState._scratchCorridorPos = new Cesium.Cartesian3();
|
|
||||||
|
|
||||||
|
|
||||||
/** 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;
|
|
||||||
|
|
||||||
/** 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.
|
|
||||||
* LightingModel.UNLIT emits the flat white directly, orientation be damned.
|
|
||||||
* CRITICAL (field-verified via scene.pick): assigning customShader to an
|
|
||||||
* already-READY model is a silent no-op — the property sets but the shader
|
|
||||||
* program never rebuilds. The boost therefore flips by RELEASE-AND-RELOAD
|
|
||||||
* (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._irReloadQueue = null;
|
|
||||||
return flightState;
|
|
||||||
}
|
}
|
||||||
+206
-182
@@ -1,191 +1,215 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { CYAN_TRANSPARENT } from './policy.js';
|
import { CYAN_TRANSPARENT } from './policy.js';
|
||||||
|
|
||||||
export function createTesting({ flightState, services, parts, layer, resolveAsset }) {
|
export function createTesting({
|
||||||
|
flightState,
|
||||||
|
services,
|
||||||
/** Test seam for the display-floor clamp (unit-tested against real Cesium math
|
parts,
|
||||||
* with seeded mesh cells — the drift mechanism is otherwise only reachable
|
layer,
|
||||||
* through a live poll + render loop). */
|
resolveAsset,
|
||||||
|
|
||||||
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
|
|
||||||
* 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() {
|
|
||||||
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.
|
|
||||||
* @param {object} state
|
|
||||||
* @param {string} state.icao24
|
|
||||||
* @param {object} state.entity
|
|
||||||
* @param {object} state.meta
|
|
||||||
* @param {object} state.billboard
|
|
||||||
* @param {object} state.billboardCollection
|
|
||||||
* @param {object} state.viewer
|
|
||||||
* @param {Array<Object>} [state.history=[]]
|
|
||||||
* @param {boolean} [state.tracked=true]
|
|
||||||
* @param {Iterable<[string, object]>} [state.models=[]] - Fleet 3D models keyed
|
|
||||||
* by icao24, for the billboard-hidden/model-shown handoff state.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function _setTrackedFlightRefreshStateForTest({
|
|
||||||
icao24,
|
|
||||||
entity,
|
|
||||||
meta,
|
|
||||||
billboard,
|
|
||||||
billboardCollection,
|
|
||||||
viewer,
|
|
||||||
history = [],
|
|
||||||
tracked = true,
|
|
||||||
models = [],
|
|
||||||
modelCollection = null,
|
|
||||||
}) {
|
}) {
|
||||||
flightState._viewer = viewer;
|
/** Test seam for the display-floor clamp (unit-tested against real Cesium math
|
||||||
flightState._modelCollection = modelCollection;
|
* with seeded mesh cells — the drift mechanism is otherwise only reachable
|
||||||
flightState._billboardCollection = billboardCollection;
|
* through a live poll + render loop). */
|
||||||
flightState._billboards = new Map([[icao24, billboard]]);
|
|
||||||
flightState._models.clear();
|
|
||||||
for (const [key, model] of models) flightState._models.set(key, model);
|
|
||||||
flightState._detectionObjects = new Map();
|
|
||||||
flightState._flightData = new Map([[icao24, meta]]);
|
|
||||||
flightState._positionHistory = new Map([[icao24, history]]);
|
|
||||||
flightState._missingPolls = new Map();
|
|
||||||
flightState._displayCourse.clear();
|
|
||||||
flightState._geoidNCache.clear();
|
|
||||||
flightState._trackedIcao = tracked ? icao24 : null;
|
|
||||||
flightState._trackedEntity = tracked ? entity : null;
|
|
||||||
flightState._trackedModel = null;
|
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
|
||||||
flightState._trackedModelLoading = false;
|
|
||||||
// NOTE: deliberately does NOT reset the per-selection latches. They are
|
|
||||||
// production state owned by the tracking lifecycle (_resetTrackedSelectionState),
|
|
||||||
// and clearing them here would mask exactly the deselect→re-track hole this
|
|
||||||
// seam is used to test.
|
|
||||||
flightState._backoff = false;
|
|
||||||
flightState._retryAt = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
function _floorGroundedDisplayPositionForTest(
|
||||||
|
info,
|
||||||
|
pos,
|
||||||
|
modelOwnsVisual,
|
||||||
|
icao24 = '__test__',
|
||||||
|
nowMs = Date.now(),
|
||||||
|
) {
|
||||||
|
return parts.motion._floorGroundedDisplayPosition(
|
||||||
|
icao24,
|
||||||
|
info,
|
||||||
|
pos,
|
||||||
|
modelOwnsVisual,
|
||||||
|
nowMs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Seed the authoritative snapshot outcome used by share-Follow tests. */
|
/** 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 _setFlightTrackingRefreshOutcomeForTest({
|
function _clearDisplayFloorStateForTest() {
|
||||||
status = 'accepted',
|
flightState._displayFloorState.clear();
|
||||||
ids = [],
|
}
|
||||||
source = 'OpenSky Network',
|
|
||||||
coverage = 'test',
|
/**
|
||||||
} = {}) {
|
* Seed only the mutable state needed to exercise tracked-card refreshes through
|
||||||
const epoch = ++flightState._trackingRefreshEpoch;
|
* the production poll reconciler. Tests still call `flightsLayer.update()`;
|
||||||
flightState._lastTrackingRefreshOutcome = {
|
* this seam avoids constructing the browser-only Cesium layer lifecycle.
|
||||||
epoch,
|
* @param {object} state
|
||||||
status,
|
* @param {string} state.icao24
|
||||||
ids: new Set(ids.map((id) => String(id).trim().toLowerCase())),
|
* @param {object} state.entity
|
||||||
source,
|
* @param {object} state.meta
|
||||||
coverage,
|
* @param {object} state.billboard
|
||||||
|
* @param {object} state.billboardCollection
|
||||||
|
* @param {object} state.viewer
|
||||||
|
* @param {Array<Object>} [state.history=[]]
|
||||||
|
* @param {boolean} [state.tracked=true]
|
||||||
|
* @param {Iterable<[string, object]>} [state.models=[]] - Fleet 3D models keyed
|
||||||
|
* by icao24, for the billboard-hidden/model-shown handoff state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function _setTrackedFlightRefreshStateForTest({
|
||||||
|
icao24,
|
||||||
|
entity,
|
||||||
|
meta,
|
||||||
|
billboard,
|
||||||
|
billboardCollection,
|
||||||
|
viewer,
|
||||||
|
history = [],
|
||||||
|
tracked = true,
|
||||||
|
models = [],
|
||||||
|
modelCollection = null,
|
||||||
|
}) {
|
||||||
|
flightState._viewer = viewer;
|
||||||
|
flightState._modelCollection = modelCollection;
|
||||||
|
flightState._billboardCollection = billboardCollection;
|
||||||
|
flightState._billboards = new Map([[icao24, billboard]]);
|
||||||
|
flightState._models.clear();
|
||||||
|
for (const [key, model] of models) flightState._models.set(key, model);
|
||||||
|
flightState._detectionObjects = new Map();
|
||||||
|
flightState._flightData = new Map([[icao24, meta]]);
|
||||||
|
flightState._positionHistory = new Map([[icao24, history]]);
|
||||||
|
flightState._missingPolls = new Map();
|
||||||
|
flightState._displayCourse.clear();
|
||||||
|
flightState._geoidNCache.clear();
|
||||||
|
flightState._trackedIcao = tracked ? icao24 : null;
|
||||||
|
flightState._trackedEntity = tracked ? entity : null;
|
||||||
|
flightState._trackedModel = null;
|
||||||
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
|
flightState._trackedModelLoading = false;
|
||||||
|
// NOTE: deliberately does NOT reset the per-selection latches. They are
|
||||||
|
// production state owned by the tracking lifecycle (_resetTrackedSelectionState),
|
||||||
|
// and clearing them here would mask exactly the deselect→re-track hole this
|
||||||
|
// seam is used to test.
|
||||||
|
flightState._backoff = false;
|
||||||
|
flightState._retryAt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Seed the authoritative snapshot outcome used by share-Follow tests. */
|
||||||
|
|
||||||
|
function _setFlightTrackingRefreshOutcomeForTest({
|
||||||
|
status = 'accepted',
|
||||||
|
ids = [],
|
||||||
|
source = 'OpenSky Network',
|
||||||
|
coverage = 'test',
|
||||||
|
} = {}) {
|
||||||
|
const epoch = ++flightState._trackingRefreshEpoch;
|
||||||
|
flightState._lastTrackingRefreshOutcome = {
|
||||||
|
epoch,
|
||||||
|
status,
|
||||||
|
ids: new Set(ids.map((id) => String(id).trim().toLowerCase())),
|
||||||
|
source,
|
||||||
|
coverage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a cached contact so tests can model a target arriving on a later feed. */
|
||||||
|
|
||||||
|
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. */
|
||||||
|
|
||||||
|
function _militaryLayerSuppressesForTest(icao24) {
|
||||||
|
return parts.tracking._militaryLayerSuppresses(icao24);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Arm the deferred restore latch directly, without a full setParams turn. */
|
||||||
|
|
||||||
|
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. */
|
||||||
|
|
||||||
|
function _pendingFlightTrackingRestoreForTest() {
|
||||||
|
return flightState._pendingTrackingRestore?.id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exercise the production deferred-restore retry after a simulated feed refresh. */
|
||||||
|
|
||||||
|
function _applyPendingFlightTrackingRestoreForTest() {
|
||||||
|
return parts.tracking._applyPendingTrackingRestore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the exact Cockpit subject through the production state transition for focused tests. */
|
||||||
|
|
||||||
|
function _setCockpitDetectionSubjectForTest(active, subjectId = null) {
|
||||||
|
parts.tracking._applyCockpitState({ active, subjectId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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() {
|
||||||
|
return parts.tracking._trackedModelRegimeActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run one frame of the production tracked-model driver (normally a
|
||||||
|
* `scene.preUpdate` listener) so tests can pin its bounded load retries. */
|
||||||
|
|
||||||
|
function _updateTrackedModelForTest() {
|
||||||
|
return parts.rendering._updateTrackedModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Evaluate the production tracked-billboard handoff colour for focused tests. */
|
||||||
|
|
||||||
|
function _trackedBillboardColorForTest() {
|
||||||
|
return parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
|
? CYAN_TRANSPARENT
|
||||||
|
: Cesium.Color.CYAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drive the exact fleet billboard-to-model handoff used by `_fleetTick`. */
|
||||||
|
|
||||||
|
function _driveFleetModelHandoffForTest({ icao24, position, course = 0 }) {
|
||||||
|
return parts.rendering._driveFleetModelHandoff(
|
||||||
|
icao24,
|
||||||
|
flightState._models.get(icao24),
|
||||||
|
flightState._billboards.get(icao24),
|
||||||
|
position,
|
||||||
|
course,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exercise the exact asynchronous fleet loader and return its admitted model. */
|
||||||
|
|
||||||
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Add a cached contact so tests can model a target arriving on a later feed. */
|
|
||||||
|
|
||||||
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. */
|
|
||||||
|
|
||||||
function _militaryLayerSuppressesForTest(icao24) {
|
|
||||||
return parts.tracking._militaryLayerSuppresses(icao24);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Arm the deferred restore latch directly, without a full setParams turn. */
|
|
||||||
|
|
||||||
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. */
|
|
||||||
|
|
||||||
function _pendingFlightTrackingRestoreForTest() {
|
|
||||||
return flightState._pendingTrackingRestore?.id ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Exercise the production deferred-restore retry after a simulated feed refresh. */
|
|
||||||
|
|
||||||
function _applyPendingFlightTrackingRestoreForTest() {
|
|
||||||
return parts.tracking._applyPendingTrackingRestore();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Set the exact Cockpit subject through the production state transition for focused tests. */
|
|
||||||
|
|
||||||
function _setCockpitDetectionSubjectForTest(active, subjectId = null) {
|
|
||||||
parts.tracking._applyCockpitState({ active, subjectId });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** 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() {
|
|
||||||
return parts.tracking._trackedModelRegimeActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Run one frame of the production tracked-model driver (normally a
|
|
||||||
* `scene.preUpdate` listener) so tests can pin its bounded load retries. */
|
|
||||||
|
|
||||||
function _updateTrackedModelForTest() {
|
|
||||||
return parts.rendering._updateTrackedModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Evaluate the production tracked-billboard handoff colour for focused tests. */
|
|
||||||
|
|
||||||
function _trackedBillboardColorForTest() {
|
|
||||||
return parts.rendering._modelOwnsVisual(flightState._trackedIcao) ? CYAN_TRANSPARENT : Cesium.Color.CYAN;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Drive the exact fleet billboard-to-model handoff used by `_fleetTick`. */
|
|
||||||
|
|
||||||
function _driveFleetModelHandoffForTest({ icao24, position, course = 0 }) {
|
|
||||||
return parts.rendering._driveFleetModelHandoff(
|
|
||||||
icao24,
|
|
||||||
flightState._models.get(icao24),
|
|
||||||
flightState._billboards.get(icao24),
|
|
||||||
position,
|
|
||||||
course,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Exercise the exact asynchronous fleet loader and return its admitted model. */
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
+1205
-997
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,9 @@ export function readLayerSource(file) {
|
|||||||
const path = file instanceof URL ? fileURLToPath(file) : file;
|
const path = file instanceof URL ? fileURLToPath(file) : file;
|
||||||
if (basename(path) !== 'flights.js') return readFileSync(path, 'utf8');
|
if (basename(path) !== 'flights.js') return readFileSync(path, 'utf8');
|
||||||
const directory = join(dirname(path), '../layers/flights');
|
const directory = join(dirname(path), '../layers/flights');
|
||||||
return readdirSync(directory).filter((name) => name.endsWith('.js'))
|
return readdirSync(directory)
|
||||||
.sort().map((name) => readFileSync(join(directory, name), 'utf8')).join('\n');
|
.filter((name) => name.endsWith('.js'))
|
||||||
|
.sort()
|
||||||
|
.map((name) => readFileSync(join(directory, name), 'utf8'))
|
||||||
|
.join('\n');
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user