mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Format civil-flight components
This commit is contained in:
1 parent
7c7e83c9e3
commit
e18aeb8565
16 files changed
+2136
-787
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() {
|
function _abortActiveUpdates() {
|
||||||
for (const controller of flightState._activeUpdateControllers) controller.abort();
|
for (const controller of flightState._activeUpdateControllers)
|
||||||
|
controller.abort();
|
||||||
flightState._activeUpdateControllers.clear();
|
flightState._activeUpdateControllers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _flightQuery(viewer) {
|
function _flightQuery(viewer) {
|
||||||
const cartographic = viewer?.camera?.positionCartographic;
|
const cartographic = viewer?.camera?.positionCartographic;
|
||||||
return cartographic ? {
|
return cartographic
|
||||||
|
? {
|
||||||
latitude: Cesium.Math.toDegrees(cartographic.latitude),
|
latitude: Cesium.Math.toDegrees(cartographic.latitude),
|
||||||
longitude: Cesium.Math.toDegrees(cartographic.longitude),
|
longitude: Cesium.Math.toDegrees(cartographic.longitude),
|
||||||
} : {};
|
}
|
||||||
|
: {};
|
||||||
}
|
}
|
||||||
return { _abortActiveUpdates, _flightQuery };
|
return { _abortActiveUpdates, _flightQuery };
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
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,
|
||||||
export function createEnrichment({ flightState, services, parts, layer, resolveAsset }) {
|
ENRICH_DISPATCH_GAP_MS,
|
||||||
|
ENRICH_AMBIENT_BUDGET_CEIL,
|
||||||
|
ENRICH_AMBIENT_REFILL_TOKENS,
|
||||||
|
ENRICH_AMBIENT_REFILL_WINDOW_MS,
|
||||||
|
ENRICH_AMBIENT_PER_SWEEP,
|
||||||
|
} from './policy.js';
|
||||||
|
|
||||||
|
export function createEnrichment({
|
||||||
|
flightState,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
layer,
|
||||||
|
resolveAsset,
|
||||||
|
}) {
|
||||||
function _enqueueEnrich(key, query, onData, priority = false) {
|
function _enqueueEnrich(key, query, onData, priority = false) {
|
||||||
if (flightState.lifetime.signal.aborted) return;
|
if (flightState.lifetime.signal.aborted) return;
|
||||||
if (flightState._enrichSeen.has(key)) return;
|
if (flightState._enrichSeen.has(key)) return;
|
||||||
@@ -14,22 +24,29 @@ function _enqueueEnrich(key, query, onData, priority = false) {
|
|||||||
const job = { query, onData };
|
const job = { query, onData };
|
||||||
// Priority (tracked / model-eligible) goes to the FRONT so a deep ambient
|
// 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.
|
// backlog can never delay the plane the user just clicked or zoomed into.
|
||||||
if (priority) flightState._enrichQueue.unshift(job); else flightState._enrichQueue.push(job);
|
if (priority) flightState._enrichQueue.unshift(job);
|
||||||
|
else flightState._enrichQueue.push(job);
|
||||||
_drainEnrich();
|
_drainEnrich();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _drainEnrich() {
|
function _drainEnrich() {
|
||||||
if (flightState.lifetime.signal.aborted) return;
|
if (flightState.lifetime.signal.aborted) return;
|
||||||
while (flightState._enrichActive < ENRICH_MAX_INFLIGHT && flightState._enrichQueue.length) {
|
while (
|
||||||
|
flightState._enrichActive < ENRICH_MAX_INFLIGHT &&
|
||||||
|
flightState._enrichQueue.length
|
||||||
|
) {
|
||||||
// Drip: at most one dispatch per ENRICH_DISPATCH_GAP_MS. When the gap
|
// 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
|
// hasn't elapsed yet, park a single wake-up timer and stop — completions
|
||||||
// and enqueues in the meantime re-enter here harmlessly.
|
// and enqueues in the meantime re-enter here harmlessly.
|
||||||
const wait = ENRICH_DISPATCH_GAP_MS - (Date.now() - flightState._enrichLastDispatchMs);
|
const wait =
|
||||||
|
ENRICH_DISPATCH_GAP_MS -
|
||||||
|
(Date.now() - flightState._enrichLastDispatchMs);
|
||||||
if (wait > 0) {
|
if (wait > 0) {
|
||||||
if (!flightState._enrichDripTimer) {
|
if (!flightState._enrichDripTimer) {
|
||||||
flightState._enrichDripTimer = setTimeout(() => { flightState._enrichDripTimer = null; _drainEnrich(); }, wait);
|
flightState._enrichDripTimer = setTimeout(() => {
|
||||||
|
flightState._enrichDripTimer = null;
|
||||||
|
_drainEnrich();
|
||||||
|
}, wait);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -37,56 +54,81 @@ function _drainEnrich() {
|
|||||||
const job = flightState._enrichQueue.shift();
|
const job = flightState._enrichQueue.shift();
|
||||||
flightState._enrichActive += 1;
|
flightState._enrichActive += 1;
|
||||||
const lifetime = flightState.lifetime;
|
const lifetime = flightState.lifetime;
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
lifetime.signal.throwIfAborted();
|
lifetime.signal.throwIfAborted();
|
||||||
return flightState._source.getEnrichment?.(job.query, { signal: lifetime.signal });
|
return flightState._source.getEnrichment?.(job.query, {
|
||||||
|
signal: lifetime.signal,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then((data) => { if (!lifetime.signal.aborted && data && data.found) job.onData(data); })
|
.then((data) => {
|
||||||
.catch(() => { /* enrichment never surfaces errors */ })
|
if (!lifetime.signal.aborted && data && data.found) job.onData(data);
|
||||||
.finally(() => { if (lifetime.signal.aborted) return; flightState._enrichActive -= 1; _drainEnrich(); });
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* enrichment never surfaces errors */
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (lifetime.signal.aborted) return;
|
||||||
|
flightState._enrichActive -= 1;
|
||||||
|
_drainEnrich();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _requestTypeEnrichment(icao24, priority = false) {
|
function _requestTypeEnrichment(icao24, priority = false) {
|
||||||
if (!/^[0-9a-f]{6}$/i.test(icao24)) return;
|
if (!/^[0-9a-f]{6}$/i.test(icao24)) return;
|
||||||
_enqueueEnrich(`t:${icao24}`, { kind: 'type', id: icao24.toLowerCase() }, (data) => {
|
_enqueueEnrich(
|
||||||
|
`t:${icao24}`,
|
||||||
|
{ kind: 'type', id: icao24.toLowerCase() },
|
||||||
|
(data) => {
|
||||||
const meta = flightState._flightData.get(icao24);
|
const meta = flightState._flightData.get(icao24);
|
||||||
if (!meta) return; // evicted while the lookup was in flight
|
if (!meta) return; // evicted while the lookup was in flight
|
||||||
meta.typeCode = data.typeCode || meta.typeCode;
|
meta.typeCode = data.typeCode || meta.typeCode;
|
||||||
meta.typeName = data.typeName || meta.typeName;
|
meta.typeName = data.typeName || meta.typeName;
|
||||||
meta.registration = data.registration || meta.registration;
|
meta.registration = data.registration || meta.registration;
|
||||||
if (meta.typeCode) {
|
if (meta.typeCode) {
|
||||||
const klass = classifyAircraft({ typeCode: meta.typeCode, category: meta.category });
|
const klass = classifyAircraft({
|
||||||
|
typeCode: meta.typeCode,
|
||||||
|
category: meta.category,
|
||||||
|
});
|
||||||
if (klass !== meta.klass) {
|
if (klass !== meta.klass) {
|
||||||
meta.klass = klass;
|
meta.klass = klass;
|
||||||
const bb = flightState._billboards.get(icao24);
|
const bb = flightState._billboards.get(icao24);
|
||||||
if (bb) parts.rendering._applyFleetBillboardPresentation(icao24, bb);
|
if (bb)
|
||||||
|
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
|
||||||
// Hangar fleet: the class's GLB/scale may have changed — resync the
|
// Hangar fleet: the class's GLB/scale may have changed — resync the
|
||||||
// live model, any in-flight load, and the tracked standalone model.
|
// live model, any in-flight load, and the tracked standalone model.
|
||||||
parts.rendering._syncModelToClass(icao24);
|
parts.rendering._syncModelToClass(icao24);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) parts.tracking._updateTrackedLabelModel(icao24);
|
if (icao24 === flightState._trackedIcao && flightState._trackedEntity)
|
||||||
}, priority);
|
parts.tracking._updateTrackedLabelModel(icao24);
|
||||||
|
},
|
||||||
|
priority,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _requestRouteEnrichment(icao24) {
|
function _requestRouteEnrichment(icao24) {
|
||||||
const cs = String(flightState._flightData.get(icao24)?.callsign || '').trim().toUpperCase();
|
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
|
if (!/^[A-Z]{3}\d/.test(cs)) return; // airline-style callsigns only (LLL + digit); GA tails won't resolve
|
||||||
_enqueueEnrich(`r:${cs}`, { kind: 'route', id: cs }, (data) => {
|
_enqueueEnrich(
|
||||||
|
`r:${cs}`,
|
||||||
|
{ kind: 'route', id: cs },
|
||||||
|
(data) => {
|
||||||
const meta = flightState._flightData.get(icao24);
|
const meta = flightState._flightData.get(icao24);
|
||||||
if (!meta) return;
|
if (!meta) return;
|
||||||
meta.airline = data.airline || meta.airline;
|
meta.airline = data.airline || meta.airline;
|
||||||
if (data.origin && data.destination) meta.route = { origin: data.origin, destination: data.destination };
|
if (data.origin && data.destination)
|
||||||
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) parts.tracking._updateTrackedLabelModel(icao24);
|
meta.route = { origin: data.origin, destination: data.destination };
|
||||||
}, true); // route lookups only fire for the TRACKED plane — front of the queue
|
if (icao24 === flightState._trackedIcao && flightState._trackedEntity)
|
||||||
|
parts.tracking._updateTrackedLabelModel(icao24);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
); // route lookups only fire for the TRACKED plane — front of the queue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** QA seam: headless harnesses (scripts/qa-enrich-ambient.mjs) shrink the
|
/** QA seam: headless harnesses (scripts/qa-enrich-ambient.mjs) shrink the
|
||||||
* bucket knobs via window.__GEV_ENRICH_AMBIENT_QA = {ceil, refillTokens,
|
* bucket knobs via window.__GEV_ENRICH_AMBIENT_QA = {ceil, refillTokens,
|
||||||
* windowMs} — they cannot wait out a real 5-minute window. Read lazily each
|
* windowMs} — they cannot wait out a real 5-minute window. Read lazily each
|
||||||
@@ -94,15 +136,24 @@ function _requestRouteEnrichment(icao24) {
|
|||||||
* Production never sets this; the constants above are the defaults. */
|
* Production never sets this; the constants above are the defaults. */
|
||||||
|
|
||||||
function _ambientBudgetKnobs() {
|
function _ambientBudgetKnobs() {
|
||||||
const o = (typeof window !== 'undefined' && window.__GEV_ENRICH_AMBIENT_QA) || null;
|
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,
|
ceil:
|
||||||
refillTokens: Number.isFinite(o?.refillTokens) && o.refillTokens > 0 ? o.refillTokens : ENRICH_AMBIENT_REFILL_TOKENS,
|
Number.isFinite(o?.ceil) && o.ceil > 0
|
||||||
windowMs: Number.isFinite(o?.windowMs) && o.windowMs > 0 ? o.windowMs : ENRICH_AMBIENT_REFILL_WINDOW_MS,
|
? o.ceil
|
||||||
|
: ENRICH_AMBIENT_BUDGET_CEIL,
|
||||||
|
refillTokens:
|
||||||
|
Number.isFinite(o?.refillTokens) && o.refillTokens > 0
|
||||||
|
? o.refillTokens
|
||||||
|
: ENRICH_AMBIENT_REFILL_TOKENS,
|
||||||
|
windowMs:
|
||||||
|
Number.isFinite(o?.windowMs) && o.windowMs > 0
|
||||||
|
? o.windowMs
|
||||||
|
: ENRICH_AMBIENT_REFILL_WINDOW_MS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Advance the token bucket: add refillTokens per FULLY elapsed window since
|
/** Advance the token bucket: add refillTokens per FULLY elapsed window since
|
||||||
* the anchor, clamp at the ceiling, and move the anchor forward by the whole
|
* 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
|
* windows consumed (while the bucket sits full this still advances, so idle
|
||||||
@@ -110,40 +161,80 @@ function _ambientBudgetKnobs() {
|
|||||||
|
|
||||||
function _refillAmbientBudget(nowMs) {
|
function _refillAmbientBudget(nowMs) {
|
||||||
const { ceil, refillTokens, windowMs } = _ambientBudgetKnobs();
|
const { ceil, refillTokens, windowMs } = _ambientBudgetKnobs();
|
||||||
if (!flightState._enrichAmbientRefillAnchorMs) { flightState._enrichAmbientRefillAnchorMs = nowMs; return; }
|
if (!flightState._enrichAmbientRefillAnchorMs) {
|
||||||
const windows = Math.floor((nowMs - flightState._enrichAmbientRefillAnchorMs) / windowMs);
|
flightState._enrichAmbientRefillAnchorMs = nowMs;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const windows = Math.floor(
|
||||||
|
(nowMs - flightState._enrichAmbientRefillAnchorMs) / windowMs,
|
||||||
|
);
|
||||||
if (windows <= 0) return;
|
if (windows <= 0) return;
|
||||||
flightState._enrichAmbientBudget = Math.min(ceil, flightState._enrichAmbientBudget + windows * refillTokens);
|
flightState._enrichAmbientBudget = Math.min(
|
||||||
|
ceil,
|
||||||
|
flightState._enrichAmbientBudget + windows * refillTokens,
|
||||||
|
);
|
||||||
flightState._enrichAmbientRefillAnchorMs += windows * windowMs;
|
flightState._enrichAmbientRefillAnchorMs += windows * windowMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _sweepAmbientEnrichment() {
|
function _sweepAmbientEnrichment() {
|
||||||
_refillAmbientBudget(Date.now());
|
_refillAmbientBudget(Date.now());
|
||||||
if (flightState._enrichAmbientBudget <= 0 || !flightState._viewer || !flightState._billboardCollection || !flightState._billboardCollection.show) return;
|
if (
|
||||||
|
flightState._enrichAmbientBudget <= 0 ||
|
||||||
|
!flightState._viewer ||
|
||||||
|
!flightState._billboardCollection ||
|
||||||
|
!flightState._billboardCollection.show
|
||||||
|
)
|
||||||
|
return;
|
||||||
try {
|
try {
|
||||||
const camera = flightState._viewer.camera;
|
const camera = flightState._viewer.camera;
|
||||||
const camPos = camera.positionWC;
|
const camPos = camera.positionWC;
|
||||||
const occluder = horizonOccluder(camera);
|
const occluder = horizonOccluder(camera);
|
||||||
const cull = camera.frustum.computeCullingVolume(camPos, camera.directionWC, camera.upWC);
|
const cull = camera.frustum.computeCullingVolume(
|
||||||
|
camPos,
|
||||||
|
camera.directionWC,
|
||||||
|
camera.upWC,
|
||||||
|
);
|
||||||
const cand = [];
|
const cand = [];
|
||||||
for (const [icao24, bb] of flightState._billboards) {
|
for (const [icao24, bb] of flightState._billboards) {
|
||||||
if (flightState._enrichSeen.has(`t:${icao24}`)) continue; // answered / queued / negative this session
|
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 (!/^[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 (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
|
if (!bb.position || !occluder.isPointVisible(bb.position)) continue; // beyond the limb
|
||||||
Cesium.Cartesian3.clone(bb.position, flightState._scratchModelBS.center);
|
Cesium.Cartesian3.clone(
|
||||||
if (cull.computeVisibility(flightState._scratchModelBS) === Cesium.Intersect.OUTSIDE) continue; // off-screen
|
bb.position,
|
||||||
cand.push([icao24, Cesium.Cartesian3.distanceSquared(camPos, 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
|
cand.sort((a, b) => a[1] - b[1]); // nearest first — what the user is looking at resolves first
|
||||||
const n = Math.min(cand.length, ENRICH_AMBIENT_PER_SWEEP, flightState._enrichAmbientBudget);
|
const n = Math.min(
|
||||||
|
cand.length,
|
||||||
|
ENRICH_AMBIENT_PER_SWEEP,
|
||||||
|
flightState._enrichAmbientBudget,
|
||||||
|
);
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
flightState._enrichAmbientBudget -= 1;
|
flightState._enrichAmbientBudget -= 1;
|
||||||
_requestTypeEnrichment(cand[i][0]); // non-priority: fills the back of the queue
|
_requestTypeEnrichment(cand[i][0]); // non-priority: fills the back of the queue
|
||||||
}
|
}
|
||||||
} catch { /* ambient enrichment is best-effort — never disturb the poll loop */ }
|
} catch {
|
||||||
|
/* ambient enrichment is best-effort — never disturb the poll loop */
|
||||||
}
|
}
|
||||||
return { _enqueueEnrich, _drainEnrich, _requestTypeEnrichment, _requestRouteEnrichment, _ambientBudgetKnobs, _refillAmbientBudget, _sweepAmbientEnrichment };
|
}
|
||||||
|
return {
|
||||||
|
_enqueueEnrich,
|
||||||
|
_drainEnrich,
|
||||||
|
_requestTypeEnrichment,
|
||||||
|
_requestRouteEnrichment,
|
||||||
|
_ambientBudgetKnobs,
|
||||||
|
_refillAmbientBudget,
|
||||||
|
_sweepAmbientEnrichment,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -2,18 +2,33 @@ 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. */
|
/** Resolve a JSON-safe evidence position into ECEF. DEV-only caller. */
|
||||||
|
|
||||||
function _focusEvidencePosition(record) {
|
function _focusEvidencePosition(record) {
|
||||||
const cartesian = record?.cartesian;
|
const cartesian = record?.cartesian;
|
||||||
if (Array.isArray(cartesian) && cartesian.length >= 3
|
if (
|
||||||
&& cartesian.slice(0, 3).every(Number.isFinite)) {
|
Array.isArray(cartesian) &&
|
||||||
return Cesium.Cartesian3.fromElements(cartesian[0], cartesian[1], cartesian[2]);
|
cartesian.length >= 3 &&
|
||||||
|
cartesian.slice(0, 3).every(Number.isFinite)
|
||||||
|
) {
|
||||||
|
return Cesium.Cartesian3.fromElements(
|
||||||
|
cartesian[0],
|
||||||
|
cartesian[1],
|
||||||
|
cartesian[2],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!Number.isFinite(record?.longitude) || !Number.isFinite(record?.latitude)) return null;
|
if (
|
||||||
|
!Number.isFinite(record?.longitude) ||
|
||||||
|
!Number.isFinite(record?.latitude)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
return Cesium.Cartesian3.fromDegrees(
|
return Cesium.Cartesian3.fromDegrees(
|
||||||
record.longitude,
|
record.longitude,
|
||||||
record.latitude,
|
record.latitude,
|
||||||
@@ -21,14 +36,19 @@ function _focusEvidencePosition(record) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Replace the real fleet with deterministic explicit-position contacts. */
|
/** Replace the real fleet with deterministic explicit-position contacts. */
|
||||||
|
|
||||||
function _setFocusEvidenceAircraft(records = []) {
|
function _setFocusEvidenceAircraft(records = []) {
|
||||||
if (!FOCUS_EVIDENCE_DEV || !flightState._billboardCollection || !flightState._viewer) return { ok: false, count: 0 };
|
if (
|
||||||
|
!FOCUS_EVIDENCE_DEV ||
|
||||||
|
!flightState._billboardCollection ||
|
||||||
|
!flightState._viewer
|
||||||
|
)
|
||||||
|
return { ok: false, count: 0 };
|
||||||
if (flightState._trackedIcao) parts.tracking._clearTracking();
|
if (flightState._trackedIcao) parts.tracking._clearTracking();
|
||||||
parts.rendering._releaseModels();
|
parts.rendering._releaseModels();
|
||||||
for (const bb of flightState._billboards.values()) flightState._billboardCollection.remove(bb);
|
for (const bb of flightState._billboards.values())
|
||||||
|
flightState._billboardCollection.remove(bb);
|
||||||
flightState._billboards.clear();
|
flightState._billboards.clear();
|
||||||
flightState._flightData.clear();
|
flightState._flightData.clear();
|
||||||
flightState._positionHistory.clear();
|
flightState._positionHistory.clear();
|
||||||
@@ -37,13 +57,15 @@ function _setFocusEvidenceAircraft(records = []) {
|
|||||||
flightState._focusEvidenceIds.clear();
|
flightState._focusEvidenceIds.clear();
|
||||||
|
|
||||||
for (const record of Array.isArray(records) ? records : []) {
|
for (const record of Array.isArray(records) ? records : []) {
|
||||||
const id = String(record?.id || '').trim().toLowerCase();
|
const id = String(record?.id || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
const position = _focusEvidencePosition(record);
|
const position = _focusEvidencePosition(record);
|
||||||
if (!id || !position) continue;
|
if (!id || !position) continue;
|
||||||
const klass = record.klass || 'airliner';
|
const klass = record.klass || 'airliner';
|
||||||
const altitudeM = Number.isFinite(record.altitudeM)
|
const altitudeM = Number.isFinite(record.altitudeM)
|
||||||
? record.altitudeM
|
? record.altitudeM
|
||||||
: (Cesium.Cartographic.fromCartesian(position)?.height || 3_000);
|
: Cesium.Cartographic.fromCartesian(position)?.height || 3_000;
|
||||||
const meta = {
|
const meta = {
|
||||||
callsign: String(record.callsign || id).toUpperCase(),
|
callsign: String(record.callsign || id).toUpperCase(),
|
||||||
altitude: altitudeM,
|
altitude: altitudeM,
|
||||||
@@ -84,14 +106,15 @@ function _setFocusEvidenceAircraft(records = []) {
|
|||||||
return { ok: true, count: flightState._count };
|
return { ok: true, count: flightState._count };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Update explicit evidence positions without rebuilding billboards. */
|
/** Update explicit evidence positions without rebuilding billboards. */
|
||||||
|
|
||||||
function _moveFocusEvidenceAircraft(records = []) {
|
function _moveFocusEvidenceAircraft(records = []) {
|
||||||
if (!FOCUS_EVIDENCE_DEV) return { ok: false, moved: 0 };
|
if (!FOCUS_EVIDENCE_DEV) return { ok: false, moved: 0 };
|
||||||
let moved = 0;
|
let moved = 0;
|
||||||
for (const record of Array.isArray(records) ? records : []) {
|
for (const record of Array.isArray(records) ? records : []) {
|
||||||
const id = String(record?.id || '').trim().toLowerCase();
|
const id = String(record?.id || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
if (!flightState._focusEvidenceIds.has(id)) continue;
|
if (!flightState._focusEvidenceIds.has(id)) continue;
|
||||||
const position = _focusEvidencePosition(record);
|
const position = _focusEvidencePosition(record);
|
||||||
const bb = flightState._billboards.get(id);
|
const bb = flightState._billboards.get(id);
|
||||||
@@ -100,7 +123,8 @@ function _moveFocusEvidenceAircraft(records = []) {
|
|||||||
const meta = flightState._flightData.get(id);
|
const meta = flightState._flightData.get(id);
|
||||||
if (meta) {
|
if (meta) {
|
||||||
if (Number.isFinite(record.trackDeg)) meta.true_track = record.trackDeg;
|
if (Number.isFinite(record.trackDeg)) meta.true_track = record.trackDeg;
|
||||||
if (Number.isFinite(record.velocityMps)) meta.velocity = record.velocityMps;
|
if (Number.isFinite(record.velocityMps))
|
||||||
|
meta.velocity = record.velocityMps;
|
||||||
}
|
}
|
||||||
moved += 1;
|
moved += 1;
|
||||||
}
|
}
|
||||||
@@ -109,7 +133,6 @@ function _moveFocusEvidenceAircraft(records = []) {
|
|||||||
return { ok: true, moved };
|
return { ok: true, moved };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** JSON-safe visual snapshot for the evidence report. */
|
/** JSON-safe visual snapshot for the evidence report. */
|
||||||
|
|
||||||
function _focusEvidenceSnapshot() {
|
function _focusEvidenceSnapshot() {
|
||||||
@@ -117,7 +140,10 @@ function _focusEvidenceSnapshot() {
|
|||||||
return [...flightState._focusEvidenceIds].map((id) => {
|
return [...flightState._focusEvidenceIds].map((id) => {
|
||||||
const bb = flightState._billboards.get(id);
|
const bb = flightState._billboards.get(id);
|
||||||
const screen = bb?.position
|
const screen = bb?.position
|
||||||
? Cesium.SceneTransforms.worldToWindowCoordinates(flightState._viewer.scene, bb.position)
|
? Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||||
|
flightState._viewer.scene,
|
||||||
|
bb.position,
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -127,10 +153,18 @@ function _focusEvidenceSnapshot() {
|
|||||||
x: screen?.x ?? null,
|
x: screen?.x ?? null,
|
||||||
y: screen?.y ?? null,
|
y: screen?.y ?? null,
|
||||||
cameraDistanceM: bb?.position
|
cameraDistanceM: bb?.position
|
||||||
? Cesium.Cartesian3.distance(flightState._viewer.camera.positionWC, bb.position)
|
? Cesium.Cartesian3.distance(
|
||||||
|
flightState._viewer.camera.positionWC,
|
||||||
|
bb.position,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { _focusEvidencePosition, _setFocusEvidenceAircraft, _moveFocusEvidenceAircraft, _focusEvidenceSnapshot };
|
return {
|
||||||
|
_focusEvidencePosition,
|
||||||
|
_setFocusEvidenceAircraft,
|
||||||
|
_moveFocusEvidenceAircraft,
|
||||||
|
_focusEvidenceSnapshot,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,11 @@ 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({
|
||||||
|
source,
|
||||||
|
services,
|
||||||
|
resolveAsset = (url) => url,
|
||||||
|
} = {}) {
|
||||||
const flightState = createFlightState({ source, services });
|
const flightState = createFlightState({ source, services });
|
||||||
const parts = {};
|
const parts = {};
|
||||||
const layer = {};
|
const layer = {};
|
||||||
@@ -25,7 +29,12 @@ parts.lifecycle = createLifecycle(context);
|
|||||||
parts.evidence = createEvidence(context);
|
parts.evidence = createEvidence(context);
|
||||||
parts.testing = createTesting(context);
|
parts.testing = createTesting(context);
|
||||||
parts.queries = createQueries(context);
|
parts.queries = createQueries(context);
|
||||||
Object.assign(layer, parts.queries.methods, parts.lifecycle.methods, parts.ingestion.methods);
|
Object.assign(
|
||||||
|
layer,
|
||||||
|
parts.queries.methods,
|
||||||
|
parts.lifecycle.methods,
|
||||||
|
parts.ingestion.methods,
|
||||||
|
);
|
||||||
Object.defineProperty(layer, 'testing', { value: parts.testing });
|
Object.defineProperty(layer, 'testing', { value: parts.testing });
|
||||||
return layer;
|
return layer;
|
||||||
}
|
}
|
||||||
|
|||||||
+174
-52
@@ -1,21 +1,41 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { stickyNumber, stickyText } from '../../data/aircraftMeta.js';
|
import { stickyNumber, stickyText } from '../../data/aircraftMeta.js';
|
||||||
import { geoidSurfaceLastResortM, pickRenderAltitudeM } from '../../data/renderAltitude.js';
|
import {
|
||||||
|
geoidSurfaceLastResortM,
|
||||||
|
pickRenderAltitudeM,
|
||||||
|
} from '../../data/renderAltitude.js';
|
||||||
import { classifyAircraft } from '../../data/aircraftClass.js';
|
import { classifyAircraft } from '../../data/aircraftClass.js';
|
||||||
import { turnRateFromFixHistory, liftRepeatedGroundFix, synthesizeForwardKinematicsFix } from '../../data/motionModel.js';
|
import {
|
||||||
|
turnRateFromFixHistory,
|
||||||
|
liftRepeatedGroundFix,
|
||||||
|
synthesizeForwardKinematicsFix,
|
||||||
|
} from '../../data/motionModel.js';
|
||||||
import { aircraftIcon } from '../../data/aircraftIcons.js';
|
import { aircraftIcon } from '../../data/aircraftIcons.js';
|
||||||
import { GROUND_FLOOR_CLAMP_RADIUS_KM, GROUND_FLOOR_WARM_MAX_ALT_M, POSITION_HISTORY_LIMIT, LANDED_MISSING_POLL_LIMIT, MISSING_POLL_LIMIT, ERROR_BACKOFF_INTERVAL } from './policy.js';
|
import {
|
||||||
|
GROUND_FLOOR_CLAMP_RADIUS_KM,
|
||||||
|
GROUND_FLOOR_WARM_MAX_ALT_M,
|
||||||
|
POSITION_HISTORY_LIMIT,
|
||||||
|
LANDED_MISSING_POLL_LIMIT,
|
||||||
|
MISSING_POLL_LIMIT,
|
||||||
|
ERROR_BACKOFF_INTERVAL,
|
||||||
|
} from './policy.js';
|
||||||
|
|
||||||
export function createIngestion({ flightState, services, parts, layer, resolveAsset }) {
|
export function createIngestion({
|
||||||
const { refreshMilitaryRegistryIfStale, isMilitaryIcao } = services.militaryRegistry;
|
flightState,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
layer,
|
||||||
|
resolveAsset,
|
||||||
|
}) {
|
||||||
|
const { refreshMilitaryRegistryIfStale, isMilitaryIcao } =
|
||||||
|
services.militaryRegistry;
|
||||||
const { geoidHeight } = services.geoid;
|
const { geoidHeight } = services.geoid;
|
||||||
const { cachedGroundFloor, floorAltitudeM, warmGroundFloor } = services.groundFloor;
|
const { cachedGroundFloor, floorAltitudeM, warmGroundFloor } =
|
||||||
|
services.groundFloor;
|
||||||
const { sampleMeshFloorCells } = services.meshFloor;
|
const { sampleMeshFloorCells } = services.meshFloor;
|
||||||
const flightsLayer = layer;
|
const flightsLayer = layer;
|
||||||
|
|
||||||
const methods = {
|
const methods = {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the latest aircraft state vectors from the OpenSky proxy and
|
* Fetch the latest aircraft state vectors from the OpenSky proxy and
|
||||||
* reconcile them with the billboard collection.
|
* reconcile them with the billboard collection.
|
||||||
@@ -50,7 +70,10 @@ const methods = {
|
|||||||
: resourceController.signal;
|
: resourceController.signal;
|
||||||
try {
|
try {
|
||||||
updateSignal.throwIfAborted();
|
updateSignal.throwIfAborted();
|
||||||
const snapshot = await flightState._source.getSnapshot(parts.controller._flightQuery(viewer || flightState._viewer), { signal: updateSignal });
|
const snapshot = await flightState._source.getSnapshot(
|
||||||
|
parts.controller._flightQuery(viewer || flightState._viewer),
|
||||||
|
{ signal: updateSignal },
|
||||||
|
);
|
||||||
updateSignal.throwIfAborted();
|
updateSignal.throwIfAborted();
|
||||||
flightState._lastStatus = snapshot.status ?? 200;
|
flightState._lastStatus = snapshot.status ?? 200;
|
||||||
const usableStates = snapshot.records;
|
const usableStates = snapshot.records;
|
||||||
@@ -60,7 +83,8 @@ const methods = {
|
|||||||
flightState._backoff = sourceStale;
|
flightState._backoff = sourceStale;
|
||||||
flightState._retryAt = 0;
|
flightState._retryAt = 0;
|
||||||
flightState._lastError = sourceStale
|
flightState._lastError = sourceStale
|
||||||
? sourceAgeMs == null ? 'Source snapshot time unavailable'
|
? sourceAgeMs == null
|
||||||
|
? 'Source snapshot time unavailable'
|
||||||
: `Source snapshot ${Math.max(2, Math.round(sourceAgeMs / 60_000))} min old`
|
: `Source snapshot ${Math.max(2, Math.round(sourceAgeMs / 60_000))} min old`
|
||||||
: null;
|
: null;
|
||||||
flightState._lastSource = snapshot.source;
|
flightState._lastSource = snapshot.source;
|
||||||
@@ -72,18 +96,33 @@ const methods = {
|
|||||||
// Field-test round 3 (2026-07-06, Austin fleet-underground): viewer
|
// Field-test round 3 (2026-07-06, Austin fleet-underground): viewer
|
||||||
// subpoint + collected floor cells for the viewer-proximate low-contact
|
// subpoint + collected floor cells for the viewer-proximate low-contact
|
||||||
// clamp below — one carto read per poll, one batch warm after the loop.
|
// clamp below — one carto read per poll, one batch warm after the loop.
|
||||||
const viewerCarto = (viewer || flightState._viewer)?.camera?.positionCartographic || null;
|
const viewerCarto =
|
||||||
const viewerLatDeg = viewerCarto ? Cesium.Math.toDegrees(viewerCarto.latitude) : null;
|
(viewer || flightState._viewer)?.camera?.positionCartographic || null;
|
||||||
const viewerLonDeg = viewerCarto ? Cesium.Math.toDegrees(viewerCarto.longitude) : null;
|
const viewerLatDeg = viewerCarto
|
||||||
|
? Cesium.Math.toDegrees(viewerCarto.latitude)
|
||||||
|
: null;
|
||||||
|
const viewerLonDeg = viewerCarto
|
||||||
|
? Cesium.Math.toDegrees(viewerCarto.longitude)
|
||||||
|
: null;
|
||||||
const floorWarmPoints = [];
|
const floorWarmPoints = [];
|
||||||
|
|
||||||
// Classification and display policy consume source-independent observations.
|
// Classification and display policy consume source-independent observations.
|
||||||
refreshMilitaryRegistryIfStale();
|
refreshMilitaryRegistryIfStale();
|
||||||
for (const observation of usableStates) {
|
for (const observation of usableStates) {
|
||||||
const { id: icao24, callsign, originCountry: origin_country,
|
const {
|
||||||
longitude: lon, latitude: lat, baroAltitudeM: baro_alt, onGround: on_ground,
|
id: icao24,
|
||||||
speedMps: velocity, courseDeg: true_track, ellipsoidAltitudeM: geo_alt,
|
callsign,
|
||||||
category, verticalRateMps: vertical_rate } = observation;
|
originCountry: origin_country,
|
||||||
|
longitude: lon,
|
||||||
|
latitude: lat,
|
||||||
|
baroAltitudeM: baro_alt,
|
||||||
|
onGround: on_ground,
|
||||||
|
speedMps: velocity,
|
||||||
|
courseDeg: true_track,
|
||||||
|
ellipsoidAltitudeM: geo_alt,
|
||||||
|
category,
|
||||||
|
verticalRateMps: vertical_rate,
|
||||||
|
} = observation;
|
||||||
acceptedSnapshotIcaos.add(icao24);
|
acceptedSnapshotIcaos.add(icao24);
|
||||||
const onGround = on_ground === true;
|
const onGround = on_ground === true;
|
||||||
|
|
||||||
@@ -126,7 +165,11 @@ const methods = {
|
|||||||
// ellipsoidal), else baro+geoid as a visual fallback, else ground
|
// ellipsoidal), else baro+geoid as a visual fallback, else ground
|
||||||
// surface when parked. `Cartesian3.fromDegrees` gets renderAltitudeM,
|
// surface when parked. `Cartesian3.fromDegrees` gets renderAltitudeM,
|
||||||
// never `alt` directly.
|
// never `alt` directly.
|
||||||
const alt = stickyNumber(baro_alt, prevMeta?.altitude, onGround ? 0 : 10000);
|
const alt = stickyNumber(
|
||||||
|
baro_alt,
|
||||||
|
prevMeta?.altitude,
|
||||||
|
onGround ? 0 : 10000,
|
||||||
|
);
|
||||||
|
|
||||||
// geoid undulation N: cached per-aircraft (negligible drift — see
|
// geoid undulation N: cached per-aircraft (negligible drift — see
|
||||||
// task brief) once the geoid grid has loaded; unavailable pre-load
|
// task brief) once the geoid grid has loaded; unavailable pre-load
|
||||||
@@ -154,14 +197,25 @@ const methods = {
|
|||||||
// (warmed by last poll's batch; aprons are flat across adjacent
|
// (warmed by last poll's batch; aprons are flat across adjacent
|
||||||
// 111 m cells). Round-5 verify caught taxiing contacts stuck at
|
// 111 m cells). Round-5 verify caught taxiing contacts stuck at
|
||||||
// the geoid without this (round 2's lesson, at cell granularity).
|
// the geoid without this (round 2's lesson, at cell granularity).
|
||||||
if (surfaceM == null && Number.isFinite(prevMeta?.rawLat) && Number.isFinite(prevMeta?.rawLon)) {
|
if (
|
||||||
|
surfaceM == null &&
|
||||||
|
Number.isFinite(prevMeta?.rawLat) &&
|
||||||
|
Number.isFinite(prevMeta?.rawLon)
|
||||||
|
) {
|
||||||
surfaceM = cachedGroundFloor(prevMeta.rawLat, prevMeta.rawLon);
|
surfaceM = cachedGroundFloor(prevMeta.rawLat, prevMeta.rawLon);
|
||||||
}
|
}
|
||||||
// Grounded contacts near the viewer feed the floor warm/sampler
|
// Grounded contacts near the viewer feed the floor warm/sampler
|
||||||
// (the only ones whose exact height is visible; far contacts are
|
// (the only ones whose exact height is visible; far contacts are
|
||||||
// subpixel and always-on-top anyway).
|
// subpixel and always-on-top anyway).
|
||||||
if (viewerLatDeg != null &&
|
if (
|
||||||
parts.queries._approxDistanceKm(viewerLatDeg, viewerLonDeg, lat, lon) <= GROUND_FLOOR_CLAMP_RADIUS_KM) {
|
viewerLatDeg != null &&
|
||||||
|
parts.queries._approxDistanceKm(
|
||||||
|
viewerLatDeg,
|
||||||
|
viewerLonDeg,
|
||||||
|
lat,
|
||||||
|
lon,
|
||||||
|
) <= GROUND_FLOOR_CLAMP_RADIUS_KM
|
||||||
|
) {
|
||||||
floorWarmPoints.push({ lat, lon });
|
floorWarmPoints.push({ lat, lon });
|
||||||
}
|
}
|
||||||
// Last synchronous resort for a BRAND-NEW grounded contact with NO
|
// Last synchronous resort for a BRAND-NEW grounded contact with NO
|
||||||
@@ -228,17 +282,33 @@ const methods = {
|
|||||||
// within GROUND_FLOOR_CLAMP_RADIUS_KM of the viewer — the only ones
|
// within GROUND_FLOOR_CLAMP_RADIUS_KM of the viewer — the only ones
|
||||||
// whose burial is visible. Cells warm in one batch after the loop.
|
// whose burial is visible. Cells warm in one batch after the loop.
|
||||||
// Airborne only (grounded planes keep the surface-cache path above).
|
// Airborne only (grounded planes keep the surface-cache path above).
|
||||||
if (!onGround && renderAltitudeM < GROUND_FLOOR_WARM_MAX_ALT_M &&
|
if (
|
||||||
|
!onGround &&
|
||||||
|
renderAltitudeM < GROUND_FLOOR_WARM_MAX_ALT_M &&
|
||||||
(icao24 === flightState._trackedIcao ||
|
(icao24 === flightState._trackedIcao ||
|
||||||
(viewerLatDeg != null &&
|
(viewerLatDeg != null &&
|
||||||
parts.queries._approxDistanceKm(viewerLatDeg, viewerLonDeg, lat, lon) <= GROUND_FLOOR_CLAMP_RADIUS_KM))) {
|
parts.queries._approxDistanceKm(
|
||||||
renderAltitudeM = floorAltitudeM(renderAltitudeM, cachedGroundFloor(lat, lon));
|
viewerLatDeg,
|
||||||
|
viewerLonDeg,
|
||||||
|
lat,
|
||||||
|
lon,
|
||||||
|
) <= GROUND_FLOOR_CLAMP_RADIUS_KM))
|
||||||
|
) {
|
||||||
|
renderAltitudeM = floorAltitudeM(
|
||||||
|
renderAltitudeM,
|
||||||
|
cachedGroundFloor(lat, lon),
|
||||||
|
);
|
||||||
floorWarmPoints.push({ lat, lon });
|
floorWarmPoints.push({ lat, lon });
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = Cesium.Cartesian3.fromDegrees(lon, lat, renderAltitudeM);
|
const position = Cesium.Cartesian3.fromDegrees(
|
||||||
|
lon,
|
||||||
|
lat,
|
||||||
|
renderAltitudeM,
|
||||||
|
);
|
||||||
// Landing/takeoff transition: the on_ground flip restyles IN PLACE.
|
// Landing/takeoff transition: the on_ground flip restyles IN PLACE.
|
||||||
const groundFlipped = !!prevMeta && (prevMeta.onGround === true) !== onGround;
|
const groundFlipped =
|
||||||
|
!!prevMeta && (prevMeta.onGround === true) !== onGround;
|
||||||
// Either flip direction retires the model's ground snap: a departing plane
|
// Either flip direction retires the model's ground snap: a departing plane
|
||||||
// flies free of it, a landing plane earns a fresh sample where it rolls out.
|
// flies free of it, a landing plane earns a fresh sample where it rolls out.
|
||||||
if (groundFlipped) flightState._groundSnap.forget(icao24);
|
if (groundFlipped) flightState._groundSnap.forget(icao24);
|
||||||
@@ -263,17 +333,28 @@ const methods = {
|
|||||||
// Round 6: lifted occlusion-test point for contacts rendering
|
// Round 6: lifted occlusion-test point for contacts rendering
|
||||||
// at/below the ellipsoid (fleet pass reads it — see the occluder
|
// at/below the ellipsoid (fleet pass reads it — see the occluder
|
||||||
// note there). Null for the overwhelmingly common airborne case.
|
// note there). Null for the overwhelmingly common airborne case.
|
||||||
cullPosition: renderAltitudeM < 10 ? Cesium.Cartesian3.fromDegrees(lon, lat, 12) : null,
|
cullPosition:
|
||||||
|
renderAltitudeM < 10
|
||||||
|
? Cesium.Cartesian3.fromDegrees(lon, lat, 12)
|
||||||
|
: null,
|
||||||
velocity: stickyNumber(velocity, prevMeta?.velocity, 0),
|
velocity: stickyNumber(velocity, prevMeta?.velocity, 0),
|
||||||
true_track: stickyNumber(true_track, prevMeta?.true_track, 0),
|
true_track: stickyNumber(true_track, prevMeta?.true_track, 0),
|
||||||
category: cat,
|
category: cat,
|
||||||
// An adsbdb-enriched type code outranks the coarse OpenSky category.
|
// An adsbdb-enriched type code outranks the coarse OpenSky category.
|
||||||
klass: classifyAircraft({ typeCode: prevMeta?.typeCode ?? null, category: cat }),
|
klass: classifyAircraft({
|
||||||
|
typeCode: prevMeta?.typeCode ?? null,
|
||||||
|
category: cat,
|
||||||
|
}),
|
||||||
turnRateDps: prevMeta?.turnRateDps || 0,
|
turnRateDps: prevMeta?.turnRateDps || 0,
|
||||||
verticalRate: stickyNumber(vertical_rate, prevMeta?.verticalRate, null),
|
verticalRate: stickyNumber(
|
||||||
|
vertical_rate,
|
||||||
|
prevMeta?.verticalRate,
|
||||||
|
null,
|
||||||
|
),
|
||||||
// Analyst seam: OpenSky origin_country (state[2]) — additive, sticky
|
// Analyst seam: OpenSky origin_country (state[2]) — additive, sticky
|
||||||
// like callsign so a transient blank row doesn't blank the field.
|
// like callsign so a transient blank row doesn't blank the field.
|
||||||
originCountry: stickyText(origin_country, prevMeta?.originCountry) || null,
|
originCountry:
|
||||||
|
stickyText(origin_country, prevMeta?.originCountry) || null,
|
||||||
// OpenSky distinguishes the last position epoch from the last
|
// OpenSky distinguishes the last position epoch from the last
|
||||||
// transponder message. The fleet coast horizon uses this actual
|
// transponder message. The fleet coast horizon uses this actual
|
||||||
// contact time so a temporarily old position does not hard-freeze
|
// contact time so a temporarily old position does not hard-freeze
|
||||||
@@ -305,7 +386,9 @@ const methods = {
|
|||||||
// 5-15s stale and receipt-time stamping is what caused the
|
// 5-15s stale and receipt-time stamping is what caused the
|
||||||
// back/forward oscillation. Only append when the fix actually
|
// back/forward oscillation. Only append when the fix actually
|
||||||
// advances, so stale repeats don't create zero-dt segments.
|
// advances, so stale repeats don't create zero-dt segments.
|
||||||
const fixEpochMs = Number.isFinite(observation.positionTimeMs) && observation.positionTimeMs > 0
|
const fixEpochMs =
|
||||||
|
Number.isFinite(observation.positionTimeMs) &&
|
||||||
|
observation.positionTimeMs > 0
|
||||||
? observation.positionTimeMs
|
? observation.positionTimeMs
|
||||||
: Date.now();
|
: Date.now();
|
||||||
const fixTime = Cesium.JulianDate.fromDate(new Date(fixEpochMs));
|
const fixTime = Cesium.JulianDate.fromDate(new Date(fixEpochMs));
|
||||||
@@ -339,15 +422,17 @@ const methods = {
|
|||||||
// surface-clamped via the surfaceM chain, so the ground leg drapes).
|
// surface-clamped via the surfaceM chain, so the ground leg drapes).
|
||||||
if (isTracked) parts.tracking._appendTrailFix(position.clone());
|
if (isTracked) parts.tracking._appendTrailFix(position.clone());
|
||||||
} else {
|
} else {
|
||||||
const modelOwnsGroundVisual = parts.rendering._modelOwnsVisual(icao24);
|
const modelOwnsGroundVisual =
|
||||||
|
parts.rendering._modelOwnsVisual(icao24);
|
||||||
if (!modelOwnsGroundVisual) {
|
if (!modelOwnsGroundVisual) {
|
||||||
liftRepeatedGroundFix(newest, position, meta.onGround);
|
liftRepeatedGroundFix(newest, position, meta.onGround);
|
||||||
}
|
}
|
||||||
// Apply fresh kinematics only from a forward synthetic fix. Mutating
|
// Apply fresh kinematics only from a forward synthetic fix. Mutating
|
||||||
// the historical fix reprojects the entire stale interval and snaps
|
// the historical fix reprojects the entire stale interval and snaps
|
||||||
// the rendered aircraft when course or speed changes late.
|
// the rendered aircraft when course or speed changes late.
|
||||||
const kinematicsChanged = newest.velocity !== meta.velocity
|
const kinematicsChanged =
|
||||||
|| newest.track !== meta.true_track;
|
newest.velocity !== meta.velocity ||
|
||||||
|
newest.track !== meta.true_track;
|
||||||
if (kinematicsChanged) {
|
if (kinematicsChanged) {
|
||||||
const synthetic = synthesizeForwardKinematicsFix(newest, {
|
const synthetic = synthesizeForwardKinematicsFix(newest, {
|
||||||
epochMs: Date.now(),
|
epochMs: Date.now(),
|
||||||
@@ -359,7 +444,8 @@ const methods = {
|
|||||||
history.push(synthetic);
|
history.push(synthetic);
|
||||||
if (history.length > POSITION_HISTORY_LIMIT) history.shift();
|
if (history.length > POSITION_HISTORY_LIMIT) history.shift();
|
||||||
meta.turnRateDps = turnRateFromFixHistory(history);
|
meta.turnRateDps = turnRateFromFixHistory(history);
|
||||||
if (isTracked) parts.tracking._appendTrailFix(synthetic.position.clone());
|
if (isTracked)
|
||||||
|
parts.tracking._appendTrailFix(synthetic.position.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,27 +462,39 @@ const methods = {
|
|||||||
if (!isTracked) {
|
if (!isTracked) {
|
||||||
// Refresh affiliation hue without clobbering the tick-owned
|
// Refresh affiliation hue without clobbering the tick-owned
|
||||||
// freshness × focus × horizon alpha composition.
|
// freshness × focus × horizon alpha composition.
|
||||||
bb.color = parts.rendering._fleetBillboardColor(icao24).withAlpha(bb.color?.alpha ?? 1);
|
bb.color = parts.rendering
|
||||||
|
._fleetBillboardColor(icao24)
|
||||||
|
.withAlpha(bb.color?.alpha ?? 1);
|
||||||
}
|
}
|
||||||
if (prevMeta?.klass !== meta.klass || groundFlipped || flightState._cockpitContactMode) {
|
if (
|
||||||
|
prevMeta?.klass !== meta.klass ||
|
||||||
|
groundFlipped ||
|
||||||
|
flightState._cockpitContactMode
|
||||||
|
) {
|
||||||
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
|
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
|
||||||
}
|
}
|
||||||
// Poll-path class change (category updates): same model resync rule
|
// Poll-path class change (category updates): same model resync rule
|
||||||
// as the enrichment path — the class's GLB/scale may have changed.
|
// as the enrichment path — the class's GLB/scale may have changed.
|
||||||
if (prevMeta?.klass !== meta.klass) parts.rendering._syncModelToClass(icao24);
|
if (prevMeta?.klass !== meta.klass)
|
||||||
|
parts.rendering._syncModelToClass(icao24);
|
||||||
} else {
|
} else {
|
||||||
const bb = flightState._billboardCollection.add({
|
const bb = flightState._billboardCollection.add({
|
||||||
position,
|
position,
|
||||||
image: aircraftIcon(parts.rendering._iconKind(icao24, meta.klass)),
|
image: aircraftIcon(
|
||||||
|
parts.rendering._iconKind(icao24, meta.klass),
|
||||||
|
),
|
||||||
width: isTracked ? 24 : 20,
|
width: isTracked ? 24 : 20,
|
||||||
height: isTracked ? 24 : 20,
|
height: isTracked ? 24 : 20,
|
||||||
scale: parts.rendering._fleetBillboardScale(icao24, meta.klass),
|
scale: parts.rendering._fleetBillboardScale(icao24, meta.klass),
|
||||||
// Screen-projected rotation lands on the next fleet tick.
|
// Screen-projected rotation lands on the next fleet tick.
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
alignedAxis: Cesium.Cartesian3.ZERO,
|
alignedAxis: Cesium.Cartesian3.ZERO,
|
||||||
color: isTracked ? Cesium.Color.CYAN : parts.rendering._fleetBillboardColor(icao24),
|
color: isTracked
|
||||||
|
? Cesium.Color.CYAN
|
||||||
|
: parts.rendering._fleetBillboardColor(icao24),
|
||||||
sizeInMeters: false,
|
sizeInMeters: false,
|
||||||
scaleByDistance: parts.rendering._normalBillboardScaleByDistance(),
|
scaleByDistance:
|
||||||
|
parts.rendering._normalBillboardScaleByDistance(),
|
||||||
// Grounded/near-surface planes sit at/below the photoreal tile
|
// Grounded/near-surface planes sit at/below the photoreal tile
|
||||||
// surface — render them depth-test-free so they never vanish up
|
// surface — render them depth-test-free so they never vanish up
|
||||||
// close (_groundDepthDistance).
|
// close (_groundDepthDistance).
|
||||||
@@ -411,7 +509,8 @@ const methods = {
|
|||||||
// Takeoff while TRACKED: ground traffic drew no trail, so start one
|
// Takeoff while TRACKED: ground traffic drew no trail, so start one
|
||||||
// from the fresh airborne history (touchdown needs no action — the
|
// from the fresh airborne history (touchdown needs no action — the
|
||||||
// append gate above simply freezes the existing trail).
|
// append gate above simply freezes the existing trail).
|
||||||
if (isTracked && groundFlipped && !meta.onGround) parts.tracking._startTrail(icao24);
|
if (isTracked && groundFlipped && !meta.onGround)
|
||||||
|
parts.tracking._startTrail(icao24);
|
||||||
|
|
||||||
// If this is the tracked aircraft, update label text
|
// If this is the tracked aircraft, update label text
|
||||||
// (position updates automatically via dead-reckoning CallbackProperty)
|
// (position updates automatically via dead-reckoning CallbackProperty)
|
||||||
@@ -429,12 +528,23 @@ const methods = {
|
|||||||
for (const [icao24, bb] of flightState._billboards) {
|
for (const [icao24, bb] of flightState._billboards) {
|
||||||
if (currentIcaos.has(icao24)) continue;
|
if (currentIcaos.has(icao24)) continue;
|
||||||
// Partial admissions do not prove absence, but stale retention is bounded.
|
// Partial admissions do not prove absence, but stale retention is bounded.
|
||||||
if (!snapshot.complete && Date.now() - (flightState._flightData.get(icao24)?.observedReceiptMs ?? 0) < 300000) continue;
|
if (
|
||||||
|
!snapshot.complete &&
|
||||||
|
Date.now() -
|
||||||
|
(flightState._flightData.get(icao24)?.observedReceiptMs ?? 0) <
|
||||||
|
300000
|
||||||
|
)
|
||||||
|
continue;
|
||||||
const misses = (flightState._missingPolls.get(icao24) || 0) + 1;
|
const misses = (flightState._missingPolls.get(icao24) || 0) + 1;
|
||||||
const limit = parts.queries._likelyLanded(icao24) ? LANDED_MISSING_POLL_LIMIT : MISSING_POLL_LIMIT;
|
const limit = parts.queries._likelyLanded(icao24)
|
||||||
|
? LANDED_MISSING_POLL_LIMIT
|
||||||
|
: MISSING_POLL_LIMIT;
|
||||||
if (misses < limit) {
|
if (misses < limit) {
|
||||||
flightState._missingPolls.set(icao24, misses);
|
flightState._missingPolls.set(icao24, misses);
|
||||||
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) {
|
if (
|
||||||
|
icao24 === flightState._trackedIcao &&
|
||||||
|
flightState._trackedEntity
|
||||||
|
) {
|
||||||
// Honest readout: the tracked plane has no faded billboard (its
|
// Honest readout: the tracked plane has no faded billboard (its
|
||||||
// entity owns the visual), so refresh the label — with icao24 now
|
// entity owns the visual), so refresh the label — with icao24 now
|
||||||
// in _missingPolls, _trackedLabelText appends the STALE cue so
|
// in _missingPolls, _trackedLabelText appends the STALE cue so
|
||||||
@@ -477,7 +587,11 @@ const methods = {
|
|||||||
// contact renders across every cell its dead-reckoned position drifts
|
// contact renders across every cell its dead-reckoned position drifts
|
||||||
// through. Add those too, so the display clamp has data where the contact
|
// through. Add those too, so the display clamp has data where the contact
|
||||||
// actually is instead of silently passing.
|
// actually is instead of silently passing.
|
||||||
parts.motion._collectDisplayCorridorCells(floorWarmPoints, viewerLatDeg, viewerLonDeg);
|
parts.motion._collectDisplayCorridorCells(
|
||||||
|
floorWarmPoints,
|
||||||
|
viewerLatDeg,
|
||||||
|
viewerLonDeg,
|
||||||
|
);
|
||||||
|
|
||||||
// Field-test round 3: one batch warm of the viewer-proximate low-contact
|
// Field-test round 3: one batch warm of the viewer-proximate low-contact
|
||||||
// floor cells collected in the loop (fire-and-forget, single-flight;
|
// floor cells collected in the loop (fire-and-forget, single-flight;
|
||||||
@@ -488,7 +602,11 @@ const methods = {
|
|||||||
// billboards/models are excluded so a vertical probe can't land on an
|
// billboards/models are excluded so a vertical probe can't land on an
|
||||||
// aircraft instead of the pavement.
|
// aircraft instead of the pavement.
|
||||||
sampleMeshFloorCells(flightState._viewer?.scene, floorWarmPoints, {
|
sampleMeshFloorCells(flightState._viewer?.scene, floorWarmPoints, {
|
||||||
excludeObjects: [...flightState._billboards.values(), ...flightState._models.values(), flightState._trackedModel].filter(Boolean),
|
excludeObjects: [
|
||||||
|
...flightState._billboards.values(),
|
||||||
|
...flightState._models.values(),
|
||||||
|
flightState._trackedModel,
|
||||||
|
].filter(Boolean),
|
||||||
viewerLat: viewerLatDeg,
|
viewerLat: viewerLatDeg,
|
||||||
viewerLon: viewerLonDeg,
|
viewerLon: viewerLonDeg,
|
||||||
});
|
});
|
||||||
@@ -518,21 +636,25 @@ const methods = {
|
|||||||
};
|
};
|
||||||
console.log(`[Data:Flights] Updated: ${flightState._count} aircraft`);
|
console.log(`[Data:Flights] Updated: ${flightState._count} aircraft`);
|
||||||
parts.tracking._applyPendingTrackingRestore();
|
parts.tracking._applyPendingTrackingRestore();
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (updateSignal.aborted || e?.name === 'AbortError') {
|
if (updateSignal.aborted || e?.name === 'AbortError') {
|
||||||
throw new DOMException('Flights update aborted', 'AbortError');
|
throw new DOMException('Flights update aborted', 'AbortError');
|
||||||
}
|
}
|
||||||
console.warn('[Data:Flights] Fetch error:', e);
|
console.warn('[Data:Flights] Fetch error:', e);
|
||||||
flightState._backoff = true;
|
flightState._backoff = true;
|
||||||
flightState._retryAt = Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
|
flightState._retryAt =
|
||||||
|
Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
|
||||||
flightState._lastStatus = e?.status ?? null;
|
flightState._lastStatus = e?.status ?? null;
|
||||||
if (e?.source) { flightState._lastSource = e.source; this.source = flightState._lastSource; }
|
if (e?.source) {
|
||||||
flightState._lastError = e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
|
flightState._lastSource = e.source;
|
||||||
|
this.source = flightState._lastSource;
|
||||||
|
}
|
||||||
|
flightState._lastError =
|
||||||
|
e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
|
||||||
} finally {
|
} finally {
|
||||||
flightState._activeUpdateControllers.delete(resourceController);
|
flightState._activeUpdateControllers.delete(resourceController);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return { methods };
|
return { methods };
|
||||||
|
|||||||
+101
-39
@@ -1,27 +1,36 @@
|
|||||||
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({
|
||||||
|
flightState,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
layer,
|
||||||
|
resolveAsset,
|
||||||
|
}) {
|
||||||
const { clearFocusTarget } = services.focus;
|
const { clearFocusTarget } = services.focus;
|
||||||
const { registerSpriteCollection, restoreSpriteOrder, restoreSpriteOrderOnEnable } = services.sprites;
|
const {
|
||||||
|
registerSpriteCollection,
|
||||||
|
restoreSpriteOrder,
|
||||||
|
restoreSpriteOrderOnEnable,
|
||||||
|
} = services.sprites;
|
||||||
const { onMilitaryLayerActiveChange } = services.militaryRegistry;
|
const { onMilitaryLayerActiveChange } = services.militaryRegistry;
|
||||||
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
||||||
const { ensureGeoidReady } = services.geoid;
|
const { ensureGeoidReady } = services.geoid;
|
||||||
const { registerPickOwner, unregisterPickOwner } = services.picking;
|
const { registerPickOwner, unregisterPickOwner } = services.picking;
|
||||||
|
|
||||||
const methods = {
|
const methods = {
|
||||||
|
|
||||||
|
|
||||||
/** Configure the source before initialization; an active layer keeps its owner. */
|
/** Configure the source before initialization; an active layer keeps its owner. */
|
||||||
setSource(source) {
|
setSource(source) {
|
||||||
if (flightState._viewer) throw new Error('Configure the source before layer initialization');
|
if (flightState._viewer)
|
||||||
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
|
throw new Error('Configure the source before layer initialization');
|
||||||
|
if (typeof source?.getSnapshot !== 'function')
|
||||||
|
throw new TypeError('A snapshot source is required');
|
||||||
flightState._source = source;
|
flightState._source = source;
|
||||||
flightState._lastSource = source.label || flightState._lastSource;
|
flightState._lastSource = source.label || flightState._lastSource;
|
||||||
this.source = flightState._lastSource;
|
this.source = flightState._lastSource;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the flights layer.
|
* Initialize the flights layer.
|
||||||
* Creates the BillboardCollection, resets all state, and installs the
|
* Creates the BillboardCollection, resets all state, and installs the
|
||||||
@@ -29,9 +38,12 @@ const methods = {
|
|||||||
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
|
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
|
||||||
*/
|
*/
|
||||||
init(viewer) {
|
init(viewer) {
|
||||||
if (flightState._viewer) throw new Error('Flight layer is already initialized');
|
if (flightState._viewer)
|
||||||
if (typeof flightState._source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
|
throw new Error('Flight layer is already initialized');
|
||||||
if (flightState.lifetime.signal.aborted) flightState.lifetime = new AbortController();
|
if (typeof flightState._source?.getSnapshot !== 'function')
|
||||||
|
throw new TypeError('A snapshot source is required');
|
||||||
|
if (flightState.lifetime.signal.aborted)
|
||||||
|
flightState.lifetime = new AbortController();
|
||||||
clearFocusTarget('flights');
|
clearFocusTarget('flights');
|
||||||
flightState._focusEvidenceIds.clear();
|
flightState._focusEvidenceIds.clear();
|
||||||
flightState._viewer = viewer;
|
flightState._viewer = viewer;
|
||||||
@@ -45,12 +57,25 @@ const methods = {
|
|||||||
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
|
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
|
||||||
if (!flightState._preloadModel) {
|
if (!flightState._preloadModel) {
|
||||||
const epoch = flightState._modelEpoch;
|
const epoch = flightState._modelEpoch;
|
||||||
Cesium.Model.fromGltfAsync({ url: resolveAsset(PLANE_MODEL_URL), asynchronous: false })
|
Cesium.Model.fromGltfAsync({
|
||||||
.then((m) => {
|
url: resolveAsset(PLANE_MODEL_URL),
|
||||||
if (epoch === flightState._modelEpoch) { flightState._preloadModel = m; flightState._planeModelLoaded = true; }
|
asynchronous: false,
|
||||||
else { try { m.destroy(); } catch { /* gone */ } }
|
|
||||||
})
|
})
|
||||||
.catch(() => { /* tracked plane just stays a billboard a beat longer */ });
|
.then((m) => {
|
||||||
|
if (epoch === flightState._modelEpoch) {
|
||||||
|
flightState._preloadModel = m;
|
||||||
|
flightState._planeModelLoaded = true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
m.destroy();
|
||||||
|
} catch {
|
||||||
|
/* gone */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* tracked plane just stays a billboard a beat longer */
|
||||||
|
});
|
||||||
}
|
}
|
||||||
flightState._billboards = new Map();
|
flightState._billboards = new Map();
|
||||||
flightState._detectionObjects = new Map();
|
flightState._detectionObjects = new Map();
|
||||||
@@ -70,14 +95,20 @@ const methods = {
|
|||||||
parts.tracking._resetTrackedSelectionState();
|
parts.tracking._resetTrackedSelectionState();
|
||||||
flightState._trackedEntity = null;
|
flightState._trackedEntity = null;
|
||||||
flightState._cockpitSubjectId = null;
|
flightState._cockpitSubjectId = null;
|
||||||
flightState._cockpitContactMode = document.body.classList.contains('cockpit-mode');
|
flightState._cockpitContactMode =
|
||||||
|
document.body.classList.contains('cockpit-mode');
|
||||||
flightState._cockpitNearContacts = new Set();
|
flightState._cockpitNearContacts = new Set();
|
||||||
if (!flightState._cockpitModeListener) {
|
if (!flightState._cockpitModeListener) {
|
||||||
flightState._cockpitModeListener = (event) => parts.tracking._applyCockpitState(event?.detail);
|
flightState._cockpitModeListener = (event) =>
|
||||||
window.addEventListener('gev:cockpit-mode-changed', flightState._cockpitModeListener);
|
parts.tracking._applyCockpitState(event?.detail);
|
||||||
|
window.addEventListener(
|
||||||
|
'gev:cockpit-mode-changed',
|
||||||
|
flightState._cockpitModeListener,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Fresh session — full bucket, anchor re-seeded on the first sweep.
|
// Fresh session — full bucket, anchor re-seeded on the first sweep.
|
||||||
flightState._enrichAmbientBudget = parts.enrichment._ambientBudgetKnobs().ceil;
|
flightState._enrichAmbientBudget =
|
||||||
|
parts.enrichment._ambientBudgetKnobs().ceil;
|
||||||
flightState._enrichAmbientRefillAnchorMs = 0;
|
flightState._enrichAmbientRefillAnchorMs = 0;
|
||||||
|
|
||||||
parts.tracking._installClickHandler(viewer);
|
parts.tracking._installClickHandler(viewer);
|
||||||
@@ -85,7 +116,9 @@ const methods = {
|
|||||||
// 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);
|
||||||
@@ -93,34 +126,48 @@ const methods = {
|
|||||||
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.
|
* Show the billboard collection and re-install the click handler.
|
||||||
* @param {Cesium.Viewer} viewer
|
* @param {Cesium.Viewer} viewer
|
||||||
*/
|
*/
|
||||||
enable(viewer) {
|
enable(viewer) {
|
||||||
if (flightState._billboardCollection) flightState._billboardCollection.show = true;
|
if (flightState._billboardCollection)
|
||||||
|
flightState._billboardCollection.show = true;
|
||||||
holdContinuousRender('flights'); // per-frame animator (perf wave 2)
|
holdContinuousRender('flights'); // per-frame animator (perf wave 2)
|
||||||
if (flightState._modelCollection) flightState._modelCollection.show = true;
|
if (flightState._modelCollection)
|
||||||
parts.tracking._setCockpitContactMode(document.body.classList.contains('cockpit-mode'));
|
flightState._modelCollection.show = true;
|
||||||
|
parts.tracking._setCockpitContactMode(
|
||||||
|
document.body.classList.contains('cockpit-mode'),
|
||||||
|
);
|
||||||
// Height-datum fix: warm the geoid grid once per layer-enable. The poll loop
|
// Height-datum fix: warm the geoid grid once per layer-enable. The poll loop
|
||||||
// only reads geoidHeight() synchronously after this resolves (guarded by
|
// only reads geoidHeight() synchronously after this resolves (guarded by
|
||||||
// _geoidReady) — never awaited per-aircraft, never blocking a poll tick.
|
// _geoidReady) — never awaited per-aircraft, never blocking a poll tick.
|
||||||
if (!flightState._geoidReady) {
|
if (!flightState._geoidReady) {
|
||||||
const lifetime = flightState.lifetime;
|
const lifetime = flightState.lifetime;
|
||||||
ensureGeoidReady()
|
ensureGeoidReady()
|
||||||
.then(() => { if (!lifetime.signal.aborted) flightState._geoidReady = true; })
|
.then(() => {
|
||||||
.catch(() => { /* geoid grid failed to load — baro path stays un-geoid-corrected until retried */ });
|
if (!lifetime.signal.aborted) flightState._geoidReady = true;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* geoid grid failed to load — baro path stays un-geoid-corrected until retried */
|
||||||
|
});
|
||||||
}
|
}
|
||||||
parts.tracking._installClickHandler(viewer);
|
parts.tracking._installClickHandler(viewer);
|
||||||
registerPickOwner('flights', (pickedId) => flightState._billboards.has(pickedId));
|
registerPickOwner('flights', (pickedId) =>
|
||||||
|
flightState._billboards.has(pickedId),
|
||||||
|
);
|
||||||
// Force a fresh rotation pass on the first tick after re-enable
|
// Force a fresh rotation pass on the first tick after re-enable
|
||||||
flightState._lastCamPoseSig = '';
|
flightState._lastCamPoseSig = '';
|
||||||
if (!flightState._preRenderRemove && viewer?.scene) {
|
if (!flightState._preRenderRemove && viewer?.scene) {
|
||||||
flightState._preRenderRemove = viewer.scene.preRender.addEventListener(parts.rendering._fleetTick);
|
flightState._preRenderRemove = viewer.scene.preRender.addEventListener(
|
||||||
|
parts.rendering._fleetTick,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!flightState._trackedModelPreUpdateRemove && viewer?.scene) {
|
if (!flightState._trackedModelPreUpdateRemove && viewer?.scene) {
|
||||||
flightState._trackedModelPreUpdateRemove = viewer.scene.preUpdate.addEventListener(parts.rendering._updateTrackedModel);
|
flightState._trackedModelPreUpdateRemove =
|
||||||
|
viewer.scene.preUpdate.addEventListener(
|
||||||
|
parts.rendering._updateTrackedModel,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!flightState._moveEndRemove && viewer?.camera) {
|
if (!flightState._moveEndRemove && viewer?.camera) {
|
||||||
// Arrival polish (field test 2026-07-03: "planes look weird when you first
|
// Arrival polish (field test 2026-07-03: "planes look weird when you first
|
||||||
@@ -132,15 +179,16 @@ const methods = {
|
|||||||
// ROTATION_REFRESH_MS. Zeroing the tick throttle too means the pass runs
|
// 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
|
// on the next preRender, not up to FLEET_DR_INTERVAL_MS later. Cost: one
|
||||||
// extra rotation pass per completed camera gesture — nothing per-frame.
|
// extra rotation pass per completed camera gesture — nothing per-frame.
|
||||||
flightState._moveEndRemove = viewer.camera.moveEnd.addEventListener(() => {
|
flightState._moveEndRemove = viewer.camera.moveEnd.addEventListener(
|
||||||
|
() => {
|
||||||
flightState._lastCamPoseSig = '';
|
flightState._lastCamPoseSig = '';
|
||||||
flightState._lastFleetTickMs = 0;
|
flightState._lastFleetTickMs = 0;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
restoreSpriteOrderOnEnable('flights', viewer);
|
restoreSpriteOrderOnEnable('flights', viewer);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide all flight billboards and tear down click/keyboard handlers.
|
* Hide all flight billboards and tear down click/keyboard handlers.
|
||||||
* Also clears any active flight tracking so the camera is released.
|
* Also clears any active flight tracking so the camera is released.
|
||||||
@@ -149,10 +197,12 @@ const methods = {
|
|||||||
disable(viewer) {
|
disable(viewer) {
|
||||||
parts.controller._abortActiveUpdates();
|
parts.controller._abortActiveUpdates();
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
if (flightState._billboardCollection) flightState._billboardCollection.show = false;
|
if (flightState._billboardCollection)
|
||||||
|
flightState._billboardCollection.show = false;
|
||||||
releaseContinuousRender('flights');
|
releaseContinuousRender('flights');
|
||||||
parts.rendering._releaseModels();
|
parts.rendering._releaseModels();
|
||||||
if (flightState._modelCollection) flightState._modelCollection.show = false;
|
if (flightState._modelCollection)
|
||||||
|
flightState._modelCollection.show = false;
|
||||||
parts.tracking._clearTracking();
|
parts.tracking._clearTracking();
|
||||||
parts.tracking._destroyTrail();
|
parts.tracking._destroyTrail();
|
||||||
// Remove click handler + keydown listener while disabled to avoid
|
// Remove click handler + keydown listener while disabled to avoid
|
||||||
@@ -181,7 +231,6 @@ const methods = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fully tear down the flights layer — remove primitives, handlers,
|
* Fully tear down the flights layer — remove primitives, handlers,
|
||||||
* tracked entities, and clear all internal state maps.
|
* tracked entities, and clear all internal state maps.
|
||||||
@@ -210,7 +259,10 @@ const methods = {
|
|||||||
}
|
}
|
||||||
document.removeEventListener('keydown', parts.tracking._onKeyDown);
|
document.removeEventListener('keydown', parts.tracking._onKeyDown);
|
||||||
if (flightState._cockpitModeListener) {
|
if (flightState._cockpitModeListener) {
|
||||||
window.removeEventListener('gev:cockpit-mode-changed', flightState._cockpitModeListener);
|
window.removeEventListener(
|
||||||
|
'gev:cockpit-mode-changed',
|
||||||
|
flightState._cockpitModeListener,
|
||||||
|
);
|
||||||
flightState._cockpitModeListener = null;
|
flightState._cockpitModeListener = null;
|
||||||
}
|
}
|
||||||
unregisterPickOwner('flights');
|
unregisterPickOwner('flights');
|
||||||
@@ -238,7 +290,14 @@ const methods = {
|
|||||||
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
|
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
|
||||||
flightState._modelPending.clear();
|
flightState._modelPending.clear();
|
||||||
flightState._modelGen.clear();
|
flightState._modelGen.clear();
|
||||||
if (flightState._preloadModel) { try { flightState._preloadModel.destroy(); } catch { /* gone */ } flightState._preloadModel = null; }
|
if (flightState._preloadModel) {
|
||||||
|
try {
|
||||||
|
flightState._preloadModel.destroy();
|
||||||
|
} catch {
|
||||||
|
/* gone */
|
||||||
|
}
|
||||||
|
flightState._preloadModel = null;
|
||||||
|
}
|
||||||
flightState._planeModelLoaded = false;
|
flightState._planeModelLoaded = false;
|
||||||
flightState._billboards.clear();
|
flightState._billboards.clear();
|
||||||
flightState._detectionObjects.clear();
|
flightState._detectionObjects.clear();
|
||||||
@@ -249,7 +308,10 @@ const methods = {
|
|||||||
flightState._displayFloorState.clear();
|
flightState._displayFloorState.clear();
|
||||||
flightState._enrichQueue.length = 0;
|
flightState._enrichQueue.length = 0;
|
||||||
flightState._enrichSeen.clear();
|
flightState._enrichSeen.clear();
|
||||||
if (flightState._enrichDripTimer) { clearTimeout(flightState._enrichDripTimer); flightState._enrichDripTimer = null; }
|
if (flightState._enrichDripTimer) {
|
||||||
|
clearTimeout(flightState._enrichDripTimer);
|
||||||
|
flightState._enrichDripTimer = null;
|
||||||
|
}
|
||||||
flightState._missingPolls.clear();
|
flightState._missingPolls.clear();
|
||||||
flightState._focusEvidenceIds.clear();
|
flightState._focusEvidenceIds.clear();
|
||||||
flightState._count = 0;
|
flightState._count = 0;
|
||||||
@@ -267,7 +329,7 @@ const methods = {
|
|||||||
};
|
};
|
||||||
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
|
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
|
||||||
flightState._viewer = null;
|
flightState._viewer = null;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return { methods };
|
return { methods };
|
||||||
|
|||||||
+358
-102
@@ -1,14 +1,64 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { COURSE_HOLD_SPEED_MPS, lerpAngleDeg, speedRamp, courseBetweenCartesians, staleCoastLimitSeconds, arcOffsetEnu, courseSlewCapDps, limitCourseStep, corridorPathLatLon } from '../../data/motionModel.js';
|
import {
|
||||||
|
COURSE_HOLD_SPEED_MPS,
|
||||||
|
lerpAngleDeg,
|
||||||
|
speedRamp,
|
||||||
|
courseBetweenCartesians,
|
||||||
|
staleCoastLimitSeconds,
|
||||||
|
arcOffsetEnu,
|
||||||
|
courseSlewCapDps,
|
||||||
|
limitCourseStep,
|
||||||
|
corridorPathLatLon,
|
||||||
|
} from '../../data/motionModel.js';
|
||||||
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
||||||
import { modelVisualAnchor, modelAnchorWorld } from '../../data/modelVisualAnchor.js';
|
import {
|
||||||
import { FOCUS_EVIDENCE_DEV, RENDER_DELAY_SEC, TRACKED_MODEL_MAX_PX, TRACKED_MODEL_MIN_PX, TRACKED_BILLBOARD_SCALE_BY_DISTANCE, DR_CORRECTION_MS, COURSE_SLEW_DT_MAX_SEC, COURSE_MAX_DPS, HELD_FLOOR_MAX_DRIFT_KM, NEIGHBOR_FLOOR_PROBE_MS, FLOOR_SEED_GRACE_MS, FLOOR_EASE_MAX_STEP, FLOOR_EASE_TAU_MS, FLOOR_EASE_EPSILON_M, DISPLAY_CORRIDOR_RADIUS_KM, DISPLAY_CORRIDOR_LOOKAHEAD_SEC, DISPLAY_CORRIDOR_CELL_BUDGET, DISPLAY_CORRIDOR_FAIR_SHARE } from './policy.js';
|
modelVisualAnchor,
|
||||||
|
modelAnchorWorld,
|
||||||
|
} from '../../data/modelVisualAnchor.js';
|
||||||
|
import {
|
||||||
|
FOCUS_EVIDENCE_DEV,
|
||||||
|
RENDER_DELAY_SEC,
|
||||||
|
TRACKED_MODEL_MAX_PX,
|
||||||
|
TRACKED_MODEL_MIN_PX,
|
||||||
|
TRACKED_BILLBOARD_SCALE_BY_DISTANCE,
|
||||||
|
DR_CORRECTION_MS,
|
||||||
|
COURSE_SLEW_DT_MAX_SEC,
|
||||||
|
COURSE_MAX_DPS,
|
||||||
|
HELD_FLOOR_MAX_DRIFT_KM,
|
||||||
|
NEIGHBOR_FLOOR_PROBE_MS,
|
||||||
|
FLOOR_SEED_GRACE_MS,
|
||||||
|
FLOOR_EASE_MAX_STEP,
|
||||||
|
FLOOR_EASE_TAU_MS,
|
||||||
|
FLOOR_EASE_EPSILON_M,
|
||||||
|
DISPLAY_CORRIDOR_RADIUS_KM,
|
||||||
|
DISPLAY_CORRIDOR_LOOKAHEAD_SEC,
|
||||||
|
DISPLAY_CORRIDOR_CELL_BUDGET,
|
||||||
|
DISPLAY_CORRIDOR_FAIR_SHARE,
|
||||||
|
} from './policy.js';
|
||||||
|
|
||||||
export function createMotion({ flightState, services, parts, layer, resolveAsset }) {
|
export function createMotion({
|
||||||
const { cachedGroundFloor, GROUND_FLOOR_LIFT_M, neighborFloorM, stickyFloorCell, displayFloorHeightM, coarseFloorCoord, corridorFloorCells, allocateCorridorCells } = services.groundFloor;
|
flightState,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
layer,
|
||||||
|
resolveAsset,
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
cachedGroundFloor,
|
||||||
|
GROUND_FLOOR_LIFT_M,
|
||||||
|
neighborFloorM,
|
||||||
|
stickyFloorCell,
|
||||||
|
displayFloorHeightM,
|
||||||
|
coarseFloorCoord,
|
||||||
|
corridorFloorCells,
|
||||||
|
allocateCorridorCells,
|
||||||
|
} = services.groundFloor;
|
||||||
const { trackedModelScaleForPixelCap } = services.camera;
|
const { trackedModelScaleForPixelCap } = services.camera;
|
||||||
const { nearFarScalarValueAtDistance, clearFocusTarget, publishFocusTargetFromCachedPosition } = services.focus;
|
const {
|
||||||
|
nearFarScalarValueAtDistance,
|
||||||
|
clearFocusTarget,
|
||||||
|
publishFocusTargetFromCachedPosition,
|
||||||
|
} = services.focus;
|
||||||
|
|
||||||
// Round 5: the old `_warmGroundedAircraftSurfaceCache` (global exact-5-decimal
|
// Round 5: the old `_warmGroundedAircraftSurfaceCache` (global exact-5-decimal
|
||||||
// warm for every grounded contact on Earth) is GONE. Parked-aircraft GPS
|
// warm for every grounded contact on Earth) is GONE. Parked-aircraft GPS
|
||||||
@@ -33,23 +83,32 @@ const { nearFarScalarValueAtDistance, clearFocusTarget, publishFocusTargetFromCa
|
|||||||
function _refloorStaleGroundedContacts(currentIcaos) {
|
function _refloorStaleGroundedContacts(currentIcaos) {
|
||||||
for (const [icao24, info] of flightState._flightData) {
|
for (const [icao24, info] of flightState._flightData) {
|
||||||
if (!info?.onGround || currentIcaos.has(icao24)) continue;
|
if (!info?.onGround || currentIcaos.has(icao24)) continue;
|
||||||
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon)) continue;
|
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon))
|
||||||
|
continue;
|
||||||
const floor = cachedGroundFloor(info.rawLat, info.rawLon);
|
const floor = cachedGroundFloor(info.rawLat, info.rawLon);
|
||||||
if (!Number.isFinite(floor)) continue;
|
if (!Number.isFinite(floor)) continue;
|
||||||
const lifted = floor + GROUND_FLOOR_LIFT_M;
|
const lifted = floor + GROUND_FLOOR_LIFT_M;
|
||||||
if (Number.isFinite(info.renderAltitudeM) && info.renderAltitudeM >= floor - 1) continue;
|
if (
|
||||||
|
Number.isFinite(info.renderAltitudeM) &&
|
||||||
|
info.renderAltitudeM >= floor - 1
|
||||||
|
)
|
||||||
|
continue;
|
||||||
info.renderAltitudeM = lifted;
|
info.renderAltitudeM = lifted;
|
||||||
info.cullPosition = null; // above the ellipsoid now (or floors say otherwise next poll)
|
info.cullPosition = null; // above the ellipsoid now (or floors say otherwise next poll)
|
||||||
const position = Cesium.Cartesian3.fromDegrees(info.rawLon, info.rawLat, lifted);
|
const position = Cesium.Cartesian3.fromDegrees(
|
||||||
|
info.rawLon,
|
||||||
|
info.rawLat,
|
||||||
|
lifted,
|
||||||
|
);
|
||||||
const history = flightState._positionHistory.get(icao24);
|
const history = flightState._positionHistory.get(icao24);
|
||||||
const newest = history?.[history.length - 1];
|
const newest = history?.[history.length - 1];
|
||||||
if (newest) newest.position = Cesium.Cartesian3.clone(position, newest.position);
|
if (newest)
|
||||||
|
newest.position = Cesium.Cartesian3.clone(position, newest.position);
|
||||||
const bb = flightState._billboards.get(icao24);
|
const bb = flightState._billboards.get(icao24);
|
||||||
if (bb) bb.position = position;
|
if (bb) bb.position = position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dead-reckon an aircraft's current position using ENU (East-North-Up) frame math.
|
* Dead-reckon an aircraft's current position using ENU (East-North-Up) frame math.
|
||||||
*
|
*
|
||||||
@@ -71,13 +130,19 @@ function _deadReckon(icao24, result) {
|
|||||||
const position = flightState._billboards.get(icao24)?.position;
|
const position = flightState._billboards.get(icao24)?.position;
|
||||||
flightState._drCourseDeg = info?.true_track || 0;
|
flightState._drCourseDeg = info?.true_track || 0;
|
||||||
flightState._drSpeedMps = info?.velocity || 0;
|
flightState._drSpeedMps = info?.velocity || 0;
|
||||||
flightState._drCourseHold = flightState._drSpeedMps < COURSE_HOLD_SPEED_MPS;
|
flightState._drCourseHold =
|
||||||
|
flightState._drSpeedMps < COURSE_HOLD_SPEED_MPS;
|
||||||
flightState._drExtrapolating = false;
|
flightState._drExtrapolating = false;
|
||||||
return position ? Cesium.Cartesian3.clone(position, result || new Cesium.Cartesian3()) : null;
|
return position
|
||||||
|
? Cesium.Cartesian3.clone(position, result || new Cesium.Cartesian3())
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
const history = flightState._positionHistory.get(icao24);
|
const history = flightState._positionHistory.get(icao24);
|
||||||
if (!history || history.length === 0) {
|
if (!history || history.length === 0) {
|
||||||
flightState._drCourseDeg = null; flightState._drSpeedMps = null; flightState._drCourseHold = false; flightState._drExtrapolating = false;
|
flightState._drCourseDeg = null;
|
||||||
|
flightState._drSpeedMps = null;
|
||||||
|
flightState._drCourseHold = false;
|
||||||
|
flightState._drExtrapolating = false;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +150,9 @@ function _deadReckon(icao24, result) {
|
|||||||
// Render one poll interval behind real time so we interpolate between
|
// Render one poll interval behind real time so we interpolate between
|
||||||
// two KNOWN fixes whenever possible (see RENDER_DELAY_SEC rationale).
|
// two KNOWN fixes whenever possible (see RENDER_DELAY_SEC rationale).
|
||||||
const renderTime = Cesium.JulianDate.addSeconds(
|
const renderTime = Cesium.JulianDate.addSeconds(
|
||||||
Cesium.JulianDate.now(), -RENDER_DELAY_SEC, flightState._scratchRenderTime
|
Cesium.JulianDate.now(),
|
||||||
|
-RENDER_DELAY_SEC,
|
||||||
|
flightState._scratchRenderTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Bracketing pair: interpolate — no extrapolation error, no snap-back.
|
// Bracketing pair: interpolate — no extrapolation error, no snap-back.
|
||||||
@@ -97,7 +164,8 @@ function _deadReckon(icao24, result) {
|
|||||||
Cesium.JulianDate.lessThanOrEquals(renderTime, b.time)
|
Cesium.JulianDate.lessThanOrEquals(renderTime, b.time)
|
||||||
) {
|
) {
|
||||||
const span = Cesium.JulianDate.secondsDifference(b.time, a.time);
|
const span = Cesium.JulianDate.secondsDifference(b.time, a.time);
|
||||||
const t = span > 0
|
const t =
|
||||||
|
span > 0
|
||||||
? Cesium.JulianDate.secondsDifference(renderTime, a.time) / span
|
? Cesium.JulianDate.secondsDifference(renderTime, a.time) / span
|
||||||
: 1.0;
|
: 1.0;
|
||||||
// Course of the DISPLAYED motion. The chord is only trustworthy when the
|
// Course of the DISPLAYED motion. The chord is only trustworthy when the
|
||||||
@@ -109,14 +177,19 @@ function _deadReckon(icao24, result) {
|
|||||||
// of snapping once per poll. Helicopters always use the reported track
|
// of snapping once per poll. Helicopters always use the reported track
|
||||||
// (rotorcraft chords are noise-dominated at their typical speeds).
|
// (rotorcraft chords are noise-dominated at their typical speeds).
|
||||||
const chordLenM = Cesium.Cartesian3.distance(a.position, b.position);
|
const chordLenM = Cesium.Cartesian3.distance(a.position, b.position);
|
||||||
const segSpeed = span > 0 ? chordLenM / span : ((info && info.velocity) || 0);
|
const segSpeed =
|
||||||
|
span > 0 ? chordLenM / span : (info && info.velocity) || 0;
|
||||||
const fallbackTrack = (info && info.true_track) || 0;
|
const fallbackTrack = (info && info.true_track) || 0;
|
||||||
const trackFrom = Number.isFinite(a.track) ? a.track : fallbackTrack;
|
const trackFrom = Number.isFinite(a.track) ? a.track : fallbackTrack;
|
||||||
const trackTo = Number.isFinite(b.track) ? b.track : trackFrom;
|
const trackTo = Number.isFinite(b.track) ? b.track : trackFrom;
|
||||||
const trackCourse = lerpAngleDeg(trackFrom, trackTo, t);
|
const trackCourse = lerpAngleDeg(trackFrom, trackTo, t);
|
||||||
const w = (info && info.klass === 'helicopter') ? 0 : speedRamp(segSpeed);
|
const w = info && info.klass === 'helicopter' ? 0 : speedRamp(segSpeed);
|
||||||
const chordCourse = w > 0 ? courseBetweenCartesians(a.position, b.position) : null;
|
const chordCourse =
|
||||||
flightState._drCourseDeg = chordCourse != null ? lerpAngleDeg(trackCourse, chordCourse, w) : trackCourse;
|
w > 0 ? courseBetweenCartesians(a.position, b.position) : null;
|
||||||
|
flightState._drCourseDeg =
|
||||||
|
chordCourse != null
|
||||||
|
? lerpAngleDeg(trackCourse, chordCourse, w)
|
||||||
|
: trackCourse;
|
||||||
flightState._drSpeedMps = segSpeed;
|
flightState._drSpeedMps = segSpeed;
|
||||||
flightState._drCourseHold = segSpeed < COURSE_HOLD_SPEED_MPS;
|
flightState._drCourseHold = segSpeed < COURSE_HOLD_SPEED_MPS;
|
||||||
flightState._drExtrapolating = false;
|
flightState._drExtrapolating = false;
|
||||||
@@ -125,7 +198,10 @@ function _deadReckon(icao24, result) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newest = history[history.length - 1];
|
const newest = history[history.length - 1];
|
||||||
const elapsedSec = Cesium.JulianDate.secondsDifference(renderTime, newest.time);
|
const elapsedSec = Cesium.JulianDate.secondsDifference(
|
||||||
|
renderTime,
|
||||||
|
newest.time,
|
||||||
|
);
|
||||||
if (elapsedSec <= 0) {
|
if (elapsedSec <= 0) {
|
||||||
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
|
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
|
||||||
// aircraft, before RENDER_DELAY_SEC of history has accumulated, so no bracketing
|
// aircraft, before RENDER_DELAY_SEC of history has accumulated, so no bracketing
|
||||||
@@ -136,8 +212,17 @@ function _deadReckon(icao24, result) {
|
|||||||
// the oldest fix froze the icon; extrapolating the NEWEST fix to wall-clock now made
|
// the oldest fix froze the icon; extrapolating the NEWEST fix to wall-clock now made
|
||||||
// the icon jump back ~one poll interval the instant interpolation took over.)
|
// the icon jump back ~one poll interval the instant interpolation took over.)
|
||||||
const oldest = history[0];
|
const oldest = history[0];
|
||||||
const lookbackSec = Cesium.JulianDate.secondsDifference(oldest.time, renderTime); // ≥ 0
|
const lookbackSec = Cesium.JulianDate.secondsDifference(
|
||||||
return _extrapolateFix(oldest, info, -Math.min(lookbackSec, 60), out, (info && info.turnRateDps) || 0);
|
oldest.time,
|
||||||
|
renderTime,
|
||||||
|
); // ≥ 0
|
||||||
|
return _extrapolateFix(
|
||||||
|
oldest,
|
||||||
|
info,
|
||||||
|
-Math.min(lookbackSec, 60),
|
||||||
|
out,
|
||||||
|
(info && info.turnRateDps) || 0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Newest POSITION is older than renderTime. OpenSky can still be receiving
|
// Newest POSITION is older than renderTime. OpenSky can still be receiving
|
||||||
@@ -166,7 +251,6 @@ function _deadReckon(icao24, result) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dead-reckon a fix along its own velocity/track by `dt` seconds, integrating a
|
* Dead-reckon a fix along its own velocity/track by `dt` seconds, integrating a
|
||||||
* constant-rate-turn arc when `turnRateDps` is significant (straight line
|
* constant-rate-turn arc when `turnRateDps` is significant (straight line
|
||||||
@@ -179,8 +263,12 @@ function _deadReckon(icao24, result) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) {
|
function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) {
|
||||||
const speed = Number.isFinite(fix.velocity) ? fix.velocity : ((info && info.velocity) || 0);
|
const speed = Number.isFinite(fix.velocity)
|
||||||
const heading = Number.isFinite(fix.track) ? fix.track : ((info && info.true_track) || 0);
|
? fix.velocity
|
||||||
|
: (info && info.velocity) || 0;
|
||||||
|
const heading = Number.isFinite(fix.track)
|
||||||
|
? fix.track
|
||||||
|
: (info && info.true_track) || 0;
|
||||||
flightState._drSpeedMps = speed;
|
flightState._drSpeedMps = speed;
|
||||||
flightState._drCourseHold = speed < COURSE_HOLD_SPEED_MPS;
|
flightState._drCourseHold = speed < COURSE_HOLD_SPEED_MPS;
|
||||||
flightState._drExtrapolating = true;
|
flightState._drExtrapolating = true;
|
||||||
@@ -192,12 +280,20 @@ function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) {
|
|||||||
// standard-rate turn is ~90° of arc wrong per 30 s if extrapolated straight.
|
// standard-rate turn is ~90° of arc wrong per 30 s if extrapolated straight.
|
||||||
arcOffsetEnu(speed, heading, turnRateDps, dt, flightState._scratchArc);
|
arcOffsetEnu(speed, heading, turnRateDps, dt, flightState._scratchArc);
|
||||||
flightState._drCourseDeg = flightState._scratchArc.endCourseDeg;
|
flightState._drCourseDeg = flightState._scratchArc.endCourseDeg;
|
||||||
Cesium.Cartesian3.fromElements(flightState._scratchArc.east, flightState._scratchArc.north, 0, flightState._scratchOffset);
|
Cesium.Cartesian3.fromElements(
|
||||||
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(fix.position, Cesium.Ellipsoid.WGS84, flightState._scratchEnu);
|
flightState._scratchArc.east,
|
||||||
|
flightState._scratchArc.north,
|
||||||
|
0,
|
||||||
|
flightState._scratchOffset,
|
||||||
|
);
|
||||||
|
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(
|
||||||
|
fix.position,
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
flightState._scratchEnu,
|
||||||
|
);
|
||||||
return Cesium.Matrix4.multiplyByPoint(enu, flightState._scratchOffset, out);
|
return Cesium.Matrix4.multiplyByPoint(enu, flightState._scratchOffset, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True while the tracked aircraft's delayed display time (now − RENDER_DELAY_SEC)
|
* True while the tracked aircraft's delayed display time (now − RENDER_DELAY_SEC)
|
||||||
* predates its oldest real fix — i.e. _deadReckon is extrapolating backward, with no
|
* predates its oldest real fix — i.e. _deadReckon is extrapolating backward, with no
|
||||||
@@ -211,12 +307,13 @@ function _isTrackWarmingUp() {
|
|||||||
const history = flightState._positionHistory.get(flightState._trackedIcao);
|
const history = flightState._positionHistory.get(flightState._trackedIcao);
|
||||||
if (!history || history.length === 0) return true;
|
if (!history || history.length === 0) return true;
|
||||||
const renderTime = Cesium.JulianDate.addSeconds(
|
const renderTime = Cesium.JulianDate.addSeconds(
|
||||||
Cesium.JulianDate.now(), -RENDER_DELAY_SEC, flightState._scratchWarmupTime
|
Cesium.JulianDate.now(),
|
||||||
|
-RENDER_DELAY_SEC,
|
||||||
|
flightState._scratchWarmupTime,
|
||||||
);
|
);
|
||||||
return Cesium.JulianDate.lessThan(renderTime, history[0].time);
|
return Cesium.JulianDate.lessThan(renderTime, history[0].time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Resolve the selected aircraft's actual rendered square extent this frame. */
|
/** Resolve the selected aircraft's actual rendered square extent this frame. */
|
||||||
|
|
||||||
function _trackedFocusSizePx(icao24, position) {
|
function _trackedFocusSizePx(icao24, position) {
|
||||||
@@ -225,7 +322,9 @@ function _trackedFocusSizePx(icao24, position) {
|
|||||||
if (!camera?.positionWC || !position || !scene) return 28;
|
if (!camera?.positionWC || !position || !scene) return 28;
|
||||||
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
|
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
|
||||||
if (parts.rendering._modelOwnsVisual(icao24)) {
|
if (parts.rendering._modelOwnsVisual(icao24)) {
|
||||||
const spec = parts.rendering._modelSpec(flightState._flightData.get(icao24)?.klass);
|
const spec = parts.rendering._modelSpec(
|
||||||
|
flightState._flightData.get(icao24)?.klass,
|
||||||
|
);
|
||||||
const scale = trackedModelScaleForPixelCap({
|
const scale = trackedModelScaleForPixelCap({
|
||||||
baseScale: spec.scale,
|
baseScale: spec.scale,
|
||||||
nativeRadiusM: spec.nativeRadiusM,
|
nativeRadiusM: spec.nativeRadiusM,
|
||||||
@@ -234,24 +333,30 @@ function _trackedFocusSizePx(icao24, position) {
|
|||||||
fovyRad: camera.frustum.fovy,
|
fovyRad: camera.frustum.fovy,
|
||||||
maximumPixelSize: TRACKED_MODEL_MAX_PX,
|
maximumPixelSize: TRACKED_MODEL_MAX_PX,
|
||||||
});
|
});
|
||||||
const focalLengthPx = scene.canvas.clientHeight / (2 * Math.tan(camera.frustum.fovy / 2));
|
const focalLengthPx =
|
||||||
const projectedDiameterPx = (2 * spec.nativeRadiusM * scale * focalLengthPx) / rangeM;
|
scene.canvas.clientHeight / (2 * Math.tan(camera.frustum.fovy / 2));
|
||||||
return Math.max(TRACKED_MODEL_MIN_PX, Math.min(TRACKED_MODEL_MAX_PX, projectedDiameterPx));
|
const projectedDiameterPx =
|
||||||
|
(2 * spec.nativeRadiusM * scale * focalLengthPx) / rangeM;
|
||||||
|
return Math.max(
|
||||||
|
TRACKED_MODEL_MIN_PX,
|
||||||
|
Math.min(TRACKED_MODEL_MAX_PX, projectedDiameterPx),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const billboard = flightState._trackedEntity?.billboard;
|
const billboard = flightState._trackedEntity?.billboard;
|
||||||
const time = flightState._viewer.clock.currentTime;
|
const time = flightState._viewer.clock.currentTime;
|
||||||
const width = billboard?.width?.getValue(time) ?? 28;
|
const width = billboard?.width?.getValue(time) ?? 28;
|
||||||
const height = billboard?.height?.getValue(time) ?? 28;
|
const height = billboard?.height?.getValue(time) ?? 28;
|
||||||
const scale = billboard?.scale?.getValue(time)
|
const scale =
|
||||||
?? (CLASS_SCALE_2D[flightState._flightData.get(icao24)?.klass] || 1);
|
billboard?.scale?.getValue(time) ??
|
||||||
const scaleByDistance = billboard?.scaleByDistance?.getValue(time)
|
(CLASS_SCALE_2D[flightState._flightData.get(icao24)?.klass] || 1);
|
||||||
?? TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
|
const scaleByDistance =
|
||||||
|
billboard?.scaleByDistance?.getValue(time) ??
|
||||||
|
TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
|
||||||
const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM);
|
const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM);
|
||||||
return Math.max(width, height) * scale * distanceScale;
|
return Math.max(width, height) * scale * distanceScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-frame-cached, discontinuity-smoothed tracked DISPLAY position. Cached by Cesium
|
* Per-frame-cached, discontinuity-smoothed tracked DISPLAY position. Cached by Cesium
|
||||||
* frame number so the position / rotation / trail-head callbacks share ONE computation
|
* frame number so the position / rotation / trail-head callbacks share ONE computation
|
||||||
@@ -263,11 +368,16 @@ function _trackedFocusSizePx(icao24, position) {
|
|||||||
|
|
||||||
function _trackedDisplayPosition(icao24) {
|
function _trackedDisplayPosition(icao24) {
|
||||||
const frame = flightState._viewer?.scene?.frameState?.frameNumber ?? -1;
|
const frame = flightState._viewer?.scene?.frameState?.frameNumber ?? -1;
|
||||||
if (frame === flightState._cachedDRFrame && icao24 === flightState._drReconcileIcao) return flightState._cachedDRPosition;
|
if (
|
||||||
|
frame === flightState._cachedDRFrame &&
|
||||||
|
icao24 === flightState._drReconcileIcao
|
||||||
|
)
|
||||||
|
return flightState._cachedDRPosition;
|
||||||
|
|
||||||
// Does the reconciliation state belong to THIS aircraft? (Capture before overwriting
|
// Does the reconciliation state belong to THIS aircraft? (Capture before overwriting
|
||||||
// _drReconcileIcao, so a track switch doesn't inherit the old plane's _drPrevRaw.)
|
// _drReconcileIcao, so a track switch doesn't inherit the old plane's _drPrevRaw.)
|
||||||
const sameTrack = flightState._drReconcileValid && flightState._drReconcileIcao === icao24;
|
const sameTrack =
|
||||||
|
flightState._drReconcileValid && flightState._drReconcileIcao === icao24;
|
||||||
const raw = _deadReckon(icao24, flightState._scratchDrRaw);
|
const raw = _deadReckon(icao24, flightState._scratchDrRaw);
|
||||||
flightState._cachedDRCourse = flightState._drCourseDeg;
|
flightState._cachedDRCourse = flightState._drCourseDeg;
|
||||||
flightState._cachedDRSpeedMps = flightState._drSpeedMps;
|
flightState._cachedDRSpeedMps = flightState._drSpeedMps;
|
||||||
@@ -290,7 +400,11 @@ function _trackedDisplayPosition(icao24) {
|
|||||||
const plausible = speed * dtSec * 4 + 25;
|
const plausible = speed * dtSec * 4 + 25;
|
||||||
if (Cesium.Cartesian3.distance(raw, flightState._drPrevRaw) > plausible) {
|
if (Cesium.Cartesian3.distance(raw, flightState._drPrevRaw) > plausible) {
|
||||||
// Discontinuity — re-anchor so the DISPLAYED position stays continuous, then decay.
|
// Discontinuity — re-anchor so the DISPLAYED position stays continuous, then decay.
|
||||||
Cesium.Cartesian3.subtract(flightState._drPrevDisplay, raw, flightState._drCorrection);
|
Cesium.Cartesian3.subtract(
|
||||||
|
flightState._drPrevDisplay,
|
||||||
|
raw,
|
||||||
|
flightState._drCorrection,
|
||||||
|
);
|
||||||
flightState._drCorrectionStartMs = nowMs;
|
flightState._drCorrectionStartMs = nowMs;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -299,8 +413,13 @@ function _trackedDisplayPosition(icao24) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const elapsed = nowMs - flightState._drCorrectionStartMs;
|
const elapsed = nowMs - flightState._drCorrectionStartMs;
|
||||||
const factor = elapsed >= DR_CORRECTION_MS ? 0 : 1 - elapsed / DR_CORRECTION_MS;
|
const factor =
|
||||||
let display = Cesium.Cartesian3.multiplyByScalar(flightState._drCorrection, factor, flightState._trackedPosHolder);
|
elapsed >= DR_CORRECTION_MS ? 0 : 1 - elapsed / DR_CORRECTION_MS;
|
||||||
|
let display = Cesium.Cartesian3.multiplyByScalar(
|
||||||
|
flightState._drCorrection,
|
||||||
|
factor,
|
||||||
|
flightState._trackedPosHolder,
|
||||||
|
);
|
||||||
Cesium.Cartesian3.add(raw, display, display);
|
Cesium.Cartesian3.add(raw, display, display);
|
||||||
// Same display floor the fleet pass applies — otherwise selecting a correctly
|
// Same display floor the fleet pass applies — otherwise selecting a correctly
|
||||||
// floored grounded billboard swapped it for an unfloored tracked entity and
|
// floored grounded billboard swapped it for an unfloored tracked entity and
|
||||||
@@ -312,14 +431,23 @@ function _trackedDisplayPosition(icao24) {
|
|||||||
// reports sensor truth — see the note there. Skipped while a tracked 3D model
|
// reports sensor truth — see the note there. Skipped while a tracked 3D model
|
||||||
// owns the visual: it rides groundSnap's one-shot sample and moving its input
|
// owns the visual: it rides groundSnap's one-shot sample and moving its input
|
||||||
// would force a re-sample (T7).
|
// would force a re-sample (T7).
|
||||||
display = _floorGroundedDisplayPosition(icao24, info, display, parts.rendering._modelOwnsVisual(icao24), nowMs);
|
display = _floorGroundedDisplayPosition(
|
||||||
|
icao24,
|
||||||
|
info,
|
||||||
|
display,
|
||||||
|
parts.rendering._modelOwnsVisual(icao24),
|
||||||
|
nowMs,
|
||||||
|
);
|
||||||
|
|
||||||
Cesium.Cartesian3.clone(raw, flightState._drPrevRaw);
|
Cesium.Cartesian3.clone(raw, flightState._drPrevRaw);
|
||||||
Cesium.Cartesian3.clone(display, flightState._drPrevDisplay);
|
Cesium.Cartesian3.clone(display, flightState._drPrevDisplay);
|
||||||
flightState._drPrevMs = nowMs;
|
flightState._drPrevMs = nowMs;
|
||||||
flightState._drReconcileValid = true;
|
flightState._drReconcileValid = true;
|
||||||
flightState._cachedDRPosition = display;
|
flightState._cachedDRPosition = display;
|
||||||
const focusSizePx = _trackedFocusSizePx(icao24, flightState._cachedDRPosition);
|
const focusSizePx = _trackedFocusSizePx(
|
||||||
|
icao24,
|
||||||
|
flightState._cachedDRPosition,
|
||||||
|
);
|
||||||
// Publish only the exact per-frame display cache the tracked entity/camera
|
// Publish only the exact per-frame display cache the tracked entity/camera
|
||||||
// consumes. Re-running DR from a later frame phase recreates the historical
|
// consumes. Re-running DR from a later frame phase recreates the historical
|
||||||
// target-vs-camera jitter bug.
|
// target-vs-camera jitter bug.
|
||||||
@@ -335,7 +463,6 @@ function _trackedDisplayPosition(icao24) {
|
|||||||
return display;
|
return display;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** The tracked plane's current display position WITHOUT recomputing — the exact value the
|
/** The tracked plane's current display position WITHOUT recomputing — the exact value the
|
||||||
* follow-camera already settled on this frame (in the Viewer _onTick). getDetectableObjects + the
|
* follow-camera already settled on this frame (in the Viewer _onTick). getDetectableObjects + the
|
||||||
* readout run in postRender at a LATER frameNumber, so calling _trackedDisplayPosition there would
|
* readout run in postRender at a LATER frameNumber, so calling _trackedDisplayPosition there would
|
||||||
@@ -344,10 +471,12 @@ function _trackedDisplayPosition(icao24) {
|
|||||||
* there's no valid fix for the tracked aircraft, so callers fall back to the billboard position. */
|
* there's no valid fix for the tracked aircraft, so callers fall back to the billboard position. */
|
||||||
|
|
||||||
function _trackedDisplayCached() {
|
function _trackedDisplayCached() {
|
||||||
return (flightState._drReconcileValid && flightState._drReconcileIcao === flightState._trackedIcao) ? flightState._cachedDRPosition : null;
|
return flightState._drReconcileValid &&
|
||||||
|
flightState._drReconcileIcao === flightState._trackedIcao
|
||||||
|
? flightState._cachedDRPosition
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The position the tracked aircraft is VISUALLY at this frame — the translation its
|
* The position the tracked aircraft is VISUALLY at this frame — the translation its
|
||||||
* 3D model is actually rendering with when the model owns the visual, otherwise the
|
* 3D model is actually rendering with when the model owns the visual, otherwise the
|
||||||
@@ -365,19 +494,25 @@ function _trackedDisplayCached() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function _trackedVisualCached() {
|
function _trackedVisualCached() {
|
||||||
if (flightState._trackedIcao && parts.rendering._modelOwnsVisual(flightState._trackedIcao)) {
|
if (
|
||||||
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass);
|
flightState._trackedIcao &&
|
||||||
|
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
|
) {
|
||||||
|
const spec = parts.rendering._modelSpec(
|
||||||
|
flightState._flightData.get(flightState._trackedIcao)?.klass,
|
||||||
|
);
|
||||||
return modelVisualAnchor(
|
return modelVisualAnchor(
|
||||||
flightState._trackedModel.modelMatrix,
|
flightState._trackedModel.modelMatrix,
|
||||||
spec.visualCenterNative,
|
spec.visualCenterNative,
|
||||||
Number.isFinite(flightState._trackedModel.computedScale) ? flightState._trackedModel.computedScale : spec.scale,
|
Number.isFinite(flightState._trackedModel.computedScale)
|
||||||
|
? flightState._trackedModel.computedScale
|
||||||
|
: spec.scale,
|
||||||
flightState._trackedVisualPos,
|
flightState._trackedVisualPos,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return _trackedDisplayCached();
|
return _trackedDisplayCached();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Trail endpoint for the rendered tracked owner. Brackets/readouts stay on
|
/** Trail endpoint for the rendered tracked owner. Brackets/readouts stay on
|
||||||
* the visual centre; only the trail moves to the model's lower-centre hardpoint. */
|
* the visual centre; only the trail moves to the model's lower-centre hardpoint. */
|
||||||
/** The tracked model's rendered bounding radius (m), or 0 when no model of this
|
/** The tracked model's rendered bounding radius (m), or 0 when no model of this
|
||||||
@@ -389,35 +524,54 @@ function _trackedVisualCached() {
|
|||||||
* measured from, so the trail clip and the envelope agree on one frame. */
|
* measured from, so the trail clip and the envelope agree on one frame. */
|
||||||
|
|
||||||
function _trackedModelCenterWorld() {
|
function _trackedModelCenterWorld() {
|
||||||
if (!flightState._trackedIcao || !flightState._trackedModel || !parts.rendering._modelOwnsVisual(flightState._trackedIcao)) return null;
|
if (
|
||||||
return Cesium.Matrix4.getTranslation(flightState._trackedModel.modelMatrix, flightState._scratchTrailClip);
|
!flightState._trackedIcao ||
|
||||||
|
!flightState._trackedModel ||
|
||||||
|
!parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
return Cesium.Matrix4.getTranslation(
|
||||||
|
flightState._trackedModel.modelMatrix,
|
||||||
|
flightState._scratchTrailClip,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _trackedModelEnvelopeM() {
|
function _trackedModelEnvelopeM() {
|
||||||
if (!flightState._trackedIcao || !flightState._trackedModel || !parts.rendering._modelOwnsVisual(flightState._trackedIcao)) return 0;
|
if (
|
||||||
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass);
|
!flightState._trackedIcao ||
|
||||||
|
!flightState._trackedModel ||
|
||||||
|
!parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
|
)
|
||||||
|
return 0;
|
||||||
|
const spec = parts.rendering._modelSpec(
|
||||||
|
flightState._flightData.get(flightState._trackedIcao)?.klass,
|
||||||
|
);
|
||||||
const scale = Number.isFinite(flightState._trackedModel.computedScale)
|
const scale = Number.isFinite(flightState._trackedModel.computedScale)
|
||||||
? flightState._trackedModel.computedScale
|
? flightState._trackedModel.computedScale
|
||||||
: spec.scale;
|
: spec.scale;
|
||||||
return spec.nativeRadiusM * scale;
|
return spec.nativeRadiusM * scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _trackedTrailCached() {
|
function _trackedTrailCached() {
|
||||||
if (flightState._trackedIcao && parts.rendering._modelOwnsVisual(flightState._trackedIcao)) {
|
if (
|
||||||
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass);
|
flightState._trackedIcao &&
|
||||||
|
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
|
) {
|
||||||
|
const spec = parts.rendering._modelSpec(
|
||||||
|
flightState._flightData.get(flightState._trackedIcao)?.klass,
|
||||||
|
);
|
||||||
// Through the model's OWN render chain (modelVisualAnchor's hand-rolled
|
// Through the model's OWN render chain (modelVisualAnchor's hand-rolled
|
||||||
// half-correction put this offset on the lateral axis — see
|
// half-correction put this offset on the lateral axis — see
|
||||||
// modelAnchorWorld).
|
// modelAnchorWorld).
|
||||||
return modelAnchorWorld(flightState._trackedModel, spec.trailAnchorNative, flightState._trackedTrailPos);
|
return modelAnchorWorld(
|
||||||
|
flightState._trackedModel,
|
||||||
|
spec.trailAnchorNative,
|
||||||
|
flightState._trackedTrailPos,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return _trackedDisplayCached();
|
return _trackedDisplayCached();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Smoothed world course for the tracked aircraft this frame. Reads the
|
/** Smoothed world course for the tracked aircraft this frame. Reads the
|
||||||
* frame-cached course (set by _trackedDisplayPosition — the follow-camera's
|
* frame-cached course (set by _trackedDisplayPosition — the follow-camera's
|
||||||
* own computation), NEVER re-runs _deadReckon. Safe to call more than once
|
* own computation), NEVER re-runs _deadReckon. Safe to call more than once
|
||||||
@@ -432,24 +586,34 @@ function _trackedTrailCached() {
|
|||||||
function _trackedDisplayCourse() {
|
function _trackedDisplayCourse() {
|
||||||
const info = flightState._flightData.get(flightState._trackedIcao);
|
const info = flightState._flightData.get(flightState._trackedIcao);
|
||||||
const fallback = (info && info.true_track) || 0;
|
const fallback = (info && info.true_track) || 0;
|
||||||
const cacheValid = flightState._drReconcileValid && flightState._drReconcileIcao === flightState._trackedIcao && flightState._cachedDRCourse != null;
|
const cacheValid =
|
||||||
|
flightState._drReconcileValid &&
|
||||||
|
flightState._drReconcileIcao === flightState._trackedIcao &&
|
||||||
|
flightState._cachedDRCourse != null;
|
||||||
const raw = cacheValid ? flightState._cachedDRCourse : fallback;
|
const raw = cacheValid ? flightState._cachedDRCourse : fallback;
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
const dt = flightState._trackedCourseMs
|
const dt = flightState._trackedCourseMs
|
||||||
? Math.min(COURSE_SLEW_DT_MAX_SEC, (nowMs - flightState._trackedCourseMs) / 1000)
|
? Math.min(
|
||||||
|
COURSE_SLEW_DT_MAX_SEC,
|
||||||
|
(nowMs - flightState._trackedCourseMs) / 1000,
|
||||||
|
)
|
||||||
: 0;
|
: 0;
|
||||||
flightState._trackedCourseMs = nowMs;
|
flightState._trackedCourseMs = nowMs;
|
||||||
const prev = flightState._displayCourse.get(flightState._trackedIcao);
|
const prev = flightState._displayCourse.get(flightState._trackedIcao);
|
||||||
// Hover hold: at near-zero displayed speed both the chord and the reported
|
// Hover hold: at near-zero displayed speed both the chord and the reported
|
||||||
// track are noise — keep the last stable nose direction instead of chasing.
|
// track are noise — keep the last stable nose direction instead of chasing.
|
||||||
if (cacheValid && flightState._cachedDRHold && prev != null) return prev;
|
if (cacheValid && flightState._cachedDRHold && prev != null) return prev;
|
||||||
const cap = courseSlewCapDps(cacheValid ? flightState._cachedDRSpeedMps : ((info && info.velocity) ?? NaN), COURSE_MAX_DPS);
|
const cap = courseSlewCapDps(
|
||||||
|
cacheValid
|
||||||
|
? flightState._cachedDRSpeedMps
|
||||||
|
: ((info && info.velocity) ?? NaN),
|
||||||
|
COURSE_MAX_DPS,
|
||||||
|
);
|
||||||
const course = limitCourseStep(prev, raw, cap, dt);
|
const course = limitCourseStep(prev, raw, cap, dt);
|
||||||
flightState._displayCourse.set(flightState._trackedIcao, course);
|
flightState._displayCourse.set(flightState._trackedIcao, course);
|
||||||
return course;
|
return course;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Reset reconciliation + per-frame cache when tracking stops or switches
|
/** Reset reconciliation + per-frame cache when tracking stops or switches
|
||||||
* target. Deliberately does NOT touch _displayCourse: the smoothed course
|
* target. Deliberately does NOT touch _displayCourse: the smoothed course
|
||||||
* entry belongs to the AIRCRAFT (not to the tracked session) and must
|
* entry belongs to the AIRCRAFT (not to the tracked session) and must
|
||||||
@@ -511,22 +675,35 @@ function _resetTrackedDisplay() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function _heldDisplayFloorM(state, cell, nowMs) {
|
function _heldDisplayFloorM(state, cell, nowMs) {
|
||||||
if (state.heldTier && !(Number.isFinite(state.heldM) && state.heldCell
|
if (
|
||||||
&& parts.queries._approxDistanceKm(cell.lat, cell.lon, state.heldCell.lat, state.heldCell.lon)
|
state.heldTier &&
|
||||||
<= HELD_FLOOR_MAX_DRIFT_KM)) {
|
!(
|
||||||
|
Number.isFinite(state.heldM) &&
|
||||||
|
state.heldCell &&
|
||||||
|
parts.queries._approxDistanceKm(
|
||||||
|
cell.lat,
|
||||||
|
cell.lon,
|
||||||
|
state.heldCell.lat,
|
||||||
|
state.heldCell.lon,
|
||||||
|
) <= HELD_FLOOR_MAX_DRIFT_KM
|
||||||
|
)
|
||||||
|
) {
|
||||||
// Out of range: the held value is no longer a measurement of anywhere this
|
// Out of range: the held value is no longer a measurement of anywhere this
|
||||||
// contact has been. Drop it rather than stretch it.
|
// contact has been. Drop it rather than stretch it.
|
||||||
_dropHeldFloor(state);
|
_dropHeldFloor(state);
|
||||||
}
|
}
|
||||||
if (state.heldTier === 'own' && !state.seeded) return state.heldM;
|
if (state.heldTier === 'own' && !state.seeded) return state.heldM;
|
||||||
if (state.probeMs != null && nowMs - state.probeMs < NEIGHBOR_FLOOR_PROBE_MS) return state.heldM;
|
if (
|
||||||
|
state.probeMs != null &&
|
||||||
|
nowMs - state.probeMs < NEIGHBOR_FLOOR_PROBE_MS
|
||||||
|
)
|
||||||
|
return state.heldM;
|
||||||
state.probeMs = nowMs;
|
state.probeMs = nowMs;
|
||||||
const near = neighborFloorM(cell);
|
const near = neighborFloorM(cell);
|
||||||
if (near != null) return _adoptHeldFloorM(state, near, cell, 'neighbor');
|
if (near != null) return _adoptHeldFloorM(state, near, cell, 'neighbor');
|
||||||
return state.heldM;
|
return state.heldM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Records a held floor and the tier it came from; returns it. Every caller
|
/** Records a held floor and the tier it came from; returns it. Every caller
|
||||||
* passes a floor measured for the cell the contact is reading NOW, so an
|
* passes a floor measured for the cell the contact is reading NOW, so an
|
||||||
* adoption always clears the seed flag: live evidence has arrived. */
|
* adoption always clears the seed flag: live evidence has arrived. */
|
||||||
@@ -539,7 +716,6 @@ function _adoptHeldFloorM(state, floorM, cell, tier) {
|
|||||||
return floorM;
|
return floorM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Forgets the held floor and everything that describes where it came from,
|
/** Forgets the held floor and everything that describes where it came from,
|
||||||
* leaving the rest of the contact's display state alone. */
|
* leaving the rest of the contact's display state alone. */
|
||||||
|
|
||||||
@@ -550,7 +726,6 @@ function _dropHeldFloor(state) {
|
|||||||
state.seeded = false;
|
state.seeded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Whether a parked seed has been away longer than the grace window.
|
/** Whether a parked seed has been away longer than the grace window.
|
||||||
*
|
*
|
||||||
* ONE judgement, asked from BOTH sides of the park, because neither side sees
|
* ONE judgement, asked from BOTH sides of the park, because neither side sees
|
||||||
@@ -565,10 +740,11 @@ function _dropHeldFloor(state) {
|
|||||||
* @param {object} state @param {number} nowMs - Tick clock. */
|
* @param {object} state @param {number} nowMs - Tick clock. */
|
||||||
|
|
||||||
function _seedExpired(state, nowMs) {
|
function _seedExpired(state, nowMs) {
|
||||||
return state.retiredMs != null && nowMs - state.retiredMs > FLOOR_SEED_GRACE_MS;
|
return (
|
||||||
|
state.retiredMs != null && nowMs - state.retiredMs > FLOOR_SEED_GRACE_MS
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Retires a contact's display-floor state. Called the moment it stops being a
|
/** Retires a contact's display-floor state. Called the moment it stops being a
|
||||||
* grounded billboard — airborne, model-owned, or gone.
|
* grounded billboard — airborne, model-owned, or gone.
|
||||||
*
|
*
|
||||||
@@ -597,7 +773,6 @@ function _retireDisplayFloorState(icao24, nowMs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Floors a GROUNDED contact's DISPLAYED position onto the local ground.
|
* Floors a GROUNDED contact's DISPLAYED position onto the local ground.
|
||||||
*
|
*
|
||||||
@@ -645,23 +820,45 @@ function _retireDisplayFloorState(icao24, nowMs) {
|
|||||||
* case — no allocation, no rebuild), otherwise the lifted position.
|
* case — no allocation, no rebuild), otherwise the lifted position.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs = Date.now()) {
|
function _floorGroundedDisplayPosition(
|
||||||
|
icao24,
|
||||||
|
info,
|
||||||
|
pos,
|
||||||
|
modelOwnsVisual,
|
||||||
|
nowMs = Date.now(),
|
||||||
|
) {
|
||||||
if (!pos || !info?.onGround || modelOwnsVisual) {
|
if (!pos || !info?.onGround || modelOwnsVisual) {
|
||||||
_retireDisplayFloorState(icao24, nowMs);
|
_retireDisplayFloorState(icao24, nowMs);
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
const state = flightState._displayFloorState.get(icao24);
|
const state = flightState._displayFloorState.get(icao24);
|
||||||
const carto = Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84, flightState._scratchDisplayCarto);
|
const carto = Cesium.Cartographic.fromCartesian(
|
||||||
|
pos,
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
flightState._scratchDisplayCarto,
|
||||||
|
);
|
||||||
// Boundary hysteresis: a position jittering across a cell edge would flip
|
// Boundary hysteresis: a position jittering across a cell edge would flip
|
||||||
// floors at fleet-tick rate (see stickyFloorCell).
|
// floors at fleet-tick rate (see stickyFloorCell).
|
||||||
const cell = stickyFloorCell(
|
const cell = stickyFloorCell(
|
||||||
Cesium.Math.toDegrees(carto.latitude), Cesium.Math.toDegrees(carto.longitude), state?.cell,
|
Cesium.Math.toDegrees(carto.latitude),
|
||||||
|
Cesium.Math.toDegrees(carto.longitude),
|
||||||
|
state?.cell,
|
||||||
);
|
);
|
||||||
const floor = cachedGroundFloor(cell.lat, cell.lon);
|
const floor = cachedGroundFloor(cell.lat, cell.lon);
|
||||||
const next = state || {
|
const next = state || {
|
||||||
cell, in: new Cesium.Cartesian3(), out: null, effectiveM: null,
|
cell,
|
||||||
heldM: null, heldCell: null, heldTier: null, heldActive: false, seeded: false,
|
in: new Cesium.Cartesian3(),
|
||||||
probeMs: null, easedM: null, easeMs: null, retiredMs: null,
|
out: null,
|
||||||
|
effectiveM: null,
|
||||||
|
heldM: null,
|
||||||
|
heldCell: null,
|
||||||
|
heldTier: null,
|
||||||
|
heldActive: false,
|
||||||
|
seeded: false,
|
||||||
|
probeMs: null,
|
||||||
|
easedM: null,
|
||||||
|
easeMs: null,
|
||||||
|
retiredMs: null,
|
||||||
};
|
};
|
||||||
// Back on the ground. Judge the seed's AGE here, before `retiredMs` is
|
// Back on the ground. Judge the seed's AGE here, before `retiredMs` is
|
||||||
// cleared: a contact that made no calls while it was away never reached the
|
// cleared: a contact that made no calls while it was away never reached the
|
||||||
@@ -697,8 +894,13 @@ function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs
|
|||||||
// AGAIN while the first approach is still running.
|
// AGAIN while the first approach is still running.
|
||||||
// Scoped to a borrowed floor on purpose: an ordinary cell-to-cell change
|
// Scoped to a borrowed floor on purpose: an ordinary cell-to-cell change
|
||||||
// between two resolved floors is the existing path and keeps its timing.
|
// between two resolved floors is the existing path and keeps its timing.
|
||||||
if (next.easedM == null && wasHeld && Number.isFinite(stoodOnM)
|
if (
|
||||||
&& Number.isFinite(effective) && effective < stoodOnM) {
|
next.easedM == null &&
|
||||||
|
wasHeld &&
|
||||||
|
Number.isFinite(stoodOnM) &&
|
||||||
|
Number.isFinite(effective) &&
|
||||||
|
effective < stoodOnM
|
||||||
|
) {
|
||||||
next.easedM = stoodOnM; // start from where the contact is actually drawn
|
next.easedM = stoodOnM; // start from where the contact is actually drawn
|
||||||
next.easeMs = nowMs;
|
next.easeMs = nowMs;
|
||||||
}
|
}
|
||||||
@@ -713,7 +915,10 @@ function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs
|
|||||||
// where it is, so there is no seam to jump across.
|
// where it is, so there is no seam to jump across.
|
||||||
const dtMs = Math.max(0, nowMs - next.easeMs);
|
const dtMs = Math.max(0, nowMs - next.easeMs);
|
||||||
next.easeMs = nowMs;
|
next.easeMs = nowMs;
|
||||||
const closed = Math.min(FLOOR_EASE_MAX_STEP, 1 - Math.exp(-dtMs / FLOOR_EASE_TAU_MS));
|
const closed = Math.min(
|
||||||
|
FLOOR_EASE_MAX_STEP,
|
||||||
|
1 - Math.exp(-dtMs / FLOOR_EASE_TAU_MS),
|
||||||
|
);
|
||||||
let value = next.easedM + (effective - next.easedM) * closed;
|
let value = next.easedM + (effective - next.easedM) * closed;
|
||||||
if (Math.abs(value - effective) <= FLOOR_EASE_EPSILON_M) {
|
if (Math.abs(value - effective) <= FLOOR_EASE_EPSILON_M) {
|
||||||
value = effective; // arrive exactly, so a parked contact stops rebuilding
|
value = effective; // arrive exactly, so a parked contact stops rebuilding
|
||||||
@@ -735,7 +940,11 @@ function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs
|
|||||||
// never adopted. One owned entry per grounded
|
// never adopted. One owned entry per grounded
|
||||||
// contact — O(grounded), dropped on eviction, on destroy, and the moment the
|
// contact — O(grounded), dropped on eviction, on destroy, and the moment the
|
||||||
// contact stops being a grounded billboard.
|
// contact stops being a grounded billboard.
|
||||||
if (state && state.effectiveM === effective && Cesium.Cartesian3.equals(pos, state.in)) {
|
if (
|
||||||
|
state &&
|
||||||
|
state.effectiveM === effective &&
|
||||||
|
Cesium.Cartesian3.equals(pos, state.in)
|
||||||
|
) {
|
||||||
return state.out || pos;
|
return state.out || pos;
|
||||||
}
|
}
|
||||||
const lifted = displayFloorHeightM(carto.height, effective);
|
const lifted = displayFloorHeightM(carto.height, effective);
|
||||||
@@ -748,7 +957,10 @@ function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs
|
|||||||
// The cache OWNS its output: returning a shared scratch would let the next
|
// The cache OWNS its output: returning a shared scratch would let the next
|
||||||
// contact in the fleet loop overwrite a position already handed out.
|
// contact in the fleet loop overwrite a position already handed out.
|
||||||
next.out = Cesium.Cartesian3.fromRadians(
|
next.out = Cesium.Cartesian3.fromRadians(
|
||||||
carto.longitude, carto.latitude, lifted, Cesium.Ellipsoid.WGS84,
|
carto.longitude,
|
||||||
|
carto.latitude,
|
||||||
|
lifted,
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
next.out || new Cesium.Cartesian3(),
|
next.out || new Cesium.Cartesian3(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -756,7 +968,6 @@ function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs
|
|||||||
return next.out || pos;
|
return next.out || pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the cells each grounded contact's DISPLAY is about to render over to
|
* Adds the cells each grounded contact's DISPLAY is about to render over to
|
||||||
* this poll's floor warm/sample batch.
|
* this poll's floor warm/sample batch.
|
||||||
@@ -811,18 +1022,37 @@ function _collectDisplayCorridorCells(out, viewerLat, viewerLon) {
|
|||||||
if (!info?.onGround) continue;
|
if (!info?.onGround) continue;
|
||||||
// T7: a contact whose 3D model is the visual never reads a display floor.
|
// T7: a contact whose 3D model is the visual never reads a display floor.
|
||||||
if (parts.rendering._modelOwnsVisual(icao24)) continue;
|
if (parts.rendering._modelOwnsVisual(icao24)) continue;
|
||||||
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon)) continue;
|
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon))
|
||||||
if (parts.queries._approxDistanceKm(viewerLat, viewerLon, info.rawLat, info.rawLon) > DISPLAY_CORRIDOR_RADIUS_KM) continue;
|
continue;
|
||||||
|
if (
|
||||||
|
parts.queries._approxDistanceKm(
|
||||||
|
viewerLat,
|
||||||
|
viewerLon,
|
||||||
|
info.rawLat,
|
||||||
|
info.rawLon,
|
||||||
|
) > DISPLAY_CORRIDOR_RADIUS_KM
|
||||||
|
)
|
||||||
|
continue;
|
||||||
const dr = _deadReckon(icao24, flightState._scratchCorridorPos);
|
const dr = _deadReckon(icao24, flightState._scratchCorridorPos);
|
||||||
if (!dr) continue;
|
if (!dr) continue;
|
||||||
// Read the sibling scratches IMMEDIATELY, before any other _deadReckon call.
|
// Read the sibling scratches IMMEDIATELY, before any other _deadReckon call.
|
||||||
const extrapolating = flightState._drExtrapolating;
|
const extrapolating = flightState._drExtrapolating;
|
||||||
const speedMps = Number.isFinite(flightState._drSpeedMps) ? flightState._drSpeedMps : (info.velocity || 0);
|
const speedMps = Number.isFinite(flightState._drSpeedMps)
|
||||||
const courseDeg = flightState._drCourseDeg != null ? flightState._drCourseDeg : (info.true_track || 0);
|
? flightState._drSpeedMps
|
||||||
const c = Cesium.Cartographic.fromCartesian(dr, Cesium.Ellipsoid.WGS84, flightState._scratchCorridorCarto);
|
: info.velocity || 0;
|
||||||
|
const courseDeg =
|
||||||
|
flightState._drCourseDeg != null
|
||||||
|
? flightState._drCourseDeg
|
||||||
|
: info.true_track || 0;
|
||||||
|
const c = Cesium.Cartographic.fromCartesian(
|
||||||
|
dr,
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
flightState._scratchCorridorCarto,
|
||||||
|
);
|
||||||
const lat = Cesium.Math.toDegrees(c.latitude);
|
const lat = Cesium.Math.toDegrees(c.latitude);
|
||||||
const lon = Cesium.Math.toDegrees(c.longitude);
|
const lon = Cesium.Math.toDegrees(c.longitude);
|
||||||
const cells = corridorFloorCells(corridorPathLatLon({
|
const cells = corridorFloorCells(
|
||||||
|
corridorPathLatLon({
|
||||||
extrapolating,
|
extrapolating,
|
||||||
displayLat: lat,
|
displayLat: lat,
|
||||||
displayLon: lon,
|
displayLon: lon,
|
||||||
@@ -834,7 +1064,8 @@ function _collectDisplayCorridorCells(out, viewerLat, viewerLon) {
|
|||||||
fixLat: info.rawLat,
|
fixLat: info.rawLat,
|
||||||
fixLon: info.rawLon,
|
fixLon: info.rawLon,
|
||||||
lookaheadSec: DISPLAY_CORRIDOR_LOOKAHEAD_SEC,
|
lookaheadSec: DISPLAY_CORRIDOR_LOOKAHEAD_SEC,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
let cold = 0;
|
let cold = 0;
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
if (cachedGroundFloor(cell.lat, cell.lon) == null) cold += 1;
|
if (cachedGroundFloor(cell.lat, cell.lon) == null) cold += 1;
|
||||||
@@ -843,10 +1074,35 @@ function _collectDisplayCorridorCells(out, viewerLat, viewerLon) {
|
|||||||
}
|
}
|
||||||
flightState._corridorEpoch += 1;
|
flightState._corridorEpoch += 1;
|
||||||
for (const cell of allocateCorridorCells(
|
for (const cell of allocateCorridorCells(
|
||||||
candidates, seen, DISPLAY_CORRIDOR_CELL_BUDGET, DISPLAY_CORRIDOR_FAIR_SHARE, flightState._corridorEpoch,
|
candidates,
|
||||||
|
seen,
|
||||||
|
DISPLAY_CORRIDOR_CELL_BUDGET,
|
||||||
|
DISPLAY_CORRIDOR_FAIR_SHARE,
|
||||||
|
flightState._corridorEpoch,
|
||||||
)) {
|
)) {
|
||||||
out.push(cell);
|
out.push(cell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { _refloorStaleGroundedContacts, _deadReckon, _extrapolateFix, _isTrackWarmingUp, _trackedFocusSizePx, _trackedDisplayPosition, _trackedDisplayCached, _trackedVisualCached, _trackedModelCenterWorld, _trackedModelEnvelopeM, _trackedTrailCached, _trackedDisplayCourse, _resetTrackedDisplay, _heldDisplayFloorM, _adoptHeldFloorM, _dropHeldFloor, _seedExpired, _retireDisplayFloorState, _floorGroundedDisplayPosition, _collectDisplayCorridorCells };
|
return {
|
||||||
|
_refloorStaleGroundedContacts,
|
||||||
|
_deadReckon,
|
||||||
|
_extrapolateFix,
|
||||||
|
_isTrackWarmingUp,
|
||||||
|
_trackedFocusSizePx,
|
||||||
|
_trackedDisplayPosition,
|
||||||
|
_trackedDisplayCached,
|
||||||
|
_trackedVisualCached,
|
||||||
|
_trackedModelCenterWorld,
|
||||||
|
_trackedModelEnvelopeM,
|
||||||
|
_trackedTrailCached,
|
||||||
|
_trackedDisplayCourse,
|
||||||
|
_resetTrackedDisplay,
|
||||||
|
_heldDisplayFloorM,
|
||||||
|
_adoptHeldFloorM,
|
||||||
|
_dropHeldFloor,
|
||||||
|
_seedExpired,
|
||||||
|
_retireDisplayFloorState,
|
||||||
|
_floorGroundedDisplayPosition,
|
||||||
|
_collectDisplayCorridorCells,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -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 = {
|
||||||
|
getEnrichment(query, options) {
|
||||||
signal = options.signal;
|
signal = options.signal;
|
||||||
return new Promise((done) => { resolve = done; });
|
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,7 +35,6 @@ 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)
|
||||||
|
|
||||||
@@ -57,7 +51,6 @@ export const TRACKED_MODEL_MAX_PX = 200;
|
|||||||
|
|
||||||
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.
|
||||||
@@ -81,7 +74,6 @@ export const MODEL_PROX_ADD_M = 150000;
|
|||||||
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)
|
||||||
|
|
||||||
@@ -107,10 +99,8 @@ 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;
|
||||||
@@ -119,19 +109,18 @@ export const ERROR_BACKOFF_INTERVAL = 20000;
|
|||||||
|
|
||||||
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.
|
||||||
*
|
*
|
||||||
|
|||||||
+294
-114
@@ -2,18 +2,38 @@ import { displayedKinematics } from '../../data/motionModel.js';
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
|
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
|
||||||
import { modelVisualAnchor } from '../../data/modelVisualAnchor.js';
|
import { modelVisualAnchor } from '../../data/modelVisualAnchor.js';
|
||||||
import { rankContactMatch, contactMatchWins, CONTACT_MATCH_TIER } from '../../data/contactMatch.js';
|
import {
|
||||||
|
rankContactMatch,
|
||||||
|
contactMatchWins,
|
||||||
|
CONTACT_MATCH_TIER,
|
||||||
|
} from '../../data/contactMatch.js';
|
||||||
import { aircraftIncludedInNearby } from '../../data/aircraftNearbyPolicy.js';
|
import { aircraftIncludedInNearby } from '../../data/aircraftNearbyPolicy.js';
|
||||||
import { LANDED_ALT_MAX_M, LANDED_SPEED_MAX_MPS, FOCUS_EVIDENCE_DEV, FLEET_DR_INTERVAL_MS } from './policy.js';
|
import {
|
||||||
|
LANDED_ALT_MAX_M,
|
||||||
|
LANDED_SPEED_MAX_MPS,
|
||||||
|
FOCUS_EVIDENCE_DEV,
|
||||||
|
FLEET_DR_INTERVAL_MS,
|
||||||
|
} from './policy.js';
|
||||||
|
|
||||||
export function createQueries({ flightState, services, parts, layer, resolveAsset }) {
|
export function createQueries({
|
||||||
|
flightState,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
layer,
|
||||||
|
resolveAsset,
|
||||||
|
}) {
|
||||||
const { tr3bTypeLabel, tr3bAircraftClass } = services.aircraftPresentation;
|
const { tr3bTypeLabel, tr3bAircraftClass } = services.aircraftPresentation;
|
||||||
const { formatFlightLevel } = services.labels;
|
const { formatFlightLevel } = services.labels;
|
||||||
const { isMilitaryIcao } = services.militaryRegistry;
|
const { isMilitaryIcao } = services.militaryRegistry;
|
||||||
const { applyTrackedCameraFrame } = services.camera;
|
const { applyTrackedCameraFrame } = services.camera;
|
||||||
const { setFocusDeemphasisParams, getFocusDeemphasisParams, setFocusEvidenceNowMs, advanceFocusEvidenceNowMs } = services.focus;
|
const {
|
||||||
const { setAircraftRecessionParams, getAircraftRecessionParams } = services.recession;
|
setFocusDeemphasisParams,
|
||||||
|
getFocusDeemphasisParams,
|
||||||
|
setFocusEvidenceNowMs,
|
||||||
|
advanceFocusEvidenceNowMs,
|
||||||
|
} = services.focus;
|
||||||
|
const { setAircraftRecessionParams, getAircraftRecessionParams } =
|
||||||
|
services.recession;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cheap equirectangular distance (km) — plenty accurate for the ~150 km
|
* Cheap equirectangular distance (km) — plenty accurate for the ~150 km
|
||||||
@@ -25,11 +45,11 @@ const { setAircraftRecessionParams, getAircraftRecessionParams } = services.rece
|
|||||||
|
|
||||||
function _approxDistanceKm(lat1, lon1, lat2, lon2) {
|
function _approxDistanceKm(lat1, lon1, lat2, lon2) {
|
||||||
const dLat = (lat2 - lat1) * 111.32;
|
const dLat = (lat2 - lat1) * 111.32;
|
||||||
const dLon = (lon2 - lon1) * 111.32 * Math.cos(((lat1 + lat2) / 2) * Math.PI / 180);
|
const dLon =
|
||||||
|
(lon2 - lon1) * 111.32 * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180);
|
||||||
return Math.hypot(dLat, dLon);
|
return Math.hypot(dLat, dLon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when the aircraft's latest metadata reads "on or about the runway"
|
* True when the aircraft's latest metadata reads "on or about the runway"
|
||||||
* (low + slow) — see the landed fast-cull rationale above. Both gates must
|
* (low + slow) — see the landed fast-cull rationale above. Both gates must
|
||||||
@@ -53,11 +73,14 @@ function _likelyLanded(icao24) {
|
|||||||
if (info.wasAirborne !== true) return false;
|
if (info.wasAirborne !== true) return false;
|
||||||
// Was airborne, then grounded, then VANISHED from the poll — landed ghost.
|
// Was airborne, then grounded, then VANISHED from the poll — landed ghost.
|
||||||
if (info.onGround) return true;
|
if (info.onGround) return true;
|
||||||
return Number.isFinite(info.altitude) && info.altitude < LANDED_ALT_MAX_M
|
return (
|
||||||
&& Number.isFinite(info.velocity) && info.velocity < LANDED_SPEED_MAX_MPS;
|
Number.isFinite(info.altitude) &&
|
||||||
|
info.altitude < LANDED_ALT_MAX_M &&
|
||||||
|
Number.isFinite(info.velocity) &&
|
||||||
|
info.velocity < LANDED_SPEED_MAX_MPS
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize a value to a trimmed string. A whitespace-only field (" ") is
|
* Normalize a value to a trimmed string. A whitespace-only field (" ") is
|
||||||
* truthy, so every label chain must trim FIRST and then fall through.
|
* truthy, so every label chain must trim FIRST and then fall through.
|
||||||
@@ -69,7 +92,6 @@ function _toCleanText(value) {
|
|||||||
return String(value || '').trim();
|
return String(value || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The layer's ONE label convention: spoken callsign → tail registration → raw
|
* The layer's ONE label convention: spoken callsign → tail registration → raw
|
||||||
* ICAO hex. Mirrors militaryFlights.js so the same aircraft reads identically
|
* ICAO hex. Mirrors militaryFlights.js so the same aircraft reads identically
|
||||||
@@ -88,10 +110,11 @@ function _toCleanText(value) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function _contactLabel(icao24, info) {
|
function _contactLabel(icao24, info) {
|
||||||
return _toCleanText(info?.callsign) || _toCleanText(info?.registration) || icao24;
|
return (
|
||||||
|
_toCleanText(info?.callsign) || _toCleanText(info?.registration) || icao24
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a public descriptor for one aircraft using its best current position
|
* Build a public descriptor for one aircraft using its best current position
|
||||||
* (dead-reckoned when history exists, billboard position otherwise).
|
* (dead-reckoned when history exists, billboard position otherwise).
|
||||||
@@ -115,7 +138,8 @@ function _describeFlight(icao24) {
|
|||||||
// `_trackedDisplayPosition`). If that ever changes, floor this path too.
|
// `_trackedDisplayPosition`). If that ever changes, floor this path too.
|
||||||
const info = flightState._flightData.get(icao24);
|
const info = flightState._flightData.get(icao24);
|
||||||
const bb = flightState._billboards.get(icao24);
|
const bb = flightState._billboards.get(icao24);
|
||||||
const basePos = parts.motion._deadReckon(icao24) || (bb ? bb.position : null);
|
const basePos =
|
||||||
|
parts.motion._deadReckon(icao24) || (bb ? bb.position : null);
|
||||||
if (!basePos) return null;
|
if (!basePos) return null;
|
||||||
const displayed = displayedKinematics({
|
const displayed = displayedKinematics({
|
||||||
derivedSpeedMps: flightState._drSpeedMps,
|
derivedSpeedMps: flightState._drSpeedMps,
|
||||||
@@ -123,7 +147,11 @@ function _describeFlight(icao24) {
|
|||||||
reportedSpeedMps: info?.velocity,
|
reportedSpeedMps: info?.velocity,
|
||||||
reportedTrackDeg: info?.true_track,
|
reportedTrackDeg: info?.true_track,
|
||||||
});
|
});
|
||||||
const carto = Cesium.Cartographic.fromCartesian(basePos, Cesium.Ellipsoid.WGS84, flightState._scratchCarto);
|
const carto = Cesium.Cartographic.fromCartesian(
|
||||||
|
basePos,
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
flightState._scratchCarto,
|
||||||
|
);
|
||||||
if (!carto) return null;
|
if (!carto) return null;
|
||||||
return {
|
return {
|
||||||
icao24,
|
icao24,
|
||||||
@@ -135,11 +163,15 @@ function _describeFlight(icao24) {
|
|||||||
// ellipsoid height of the camera/ground-clamped render position. The
|
// ellipsoid height of the camera/ground-clamped render position. The
|
||||||
// latter can be slightly negative over terrain near the surface.
|
// latter can be slightly negative over terrain near the surface.
|
||||||
altitudeM: Number.isFinite(info?.altitude) ? info.altitude : carto.height,
|
altitudeM: Number.isFinite(info?.altitude) ? info.altitude : carto.height,
|
||||||
renderAltitudeM: Number.isFinite(info?.renderAltitudeM) ? info.renderAltitudeM : carto.height,
|
renderAltitudeM: Number.isFinite(info?.renderAltitudeM)
|
||||||
|
? info.renderAltitudeM
|
||||||
|
: carto.height,
|
||||||
onGround: info?.onGround === true,
|
onGround: info?.onGround === true,
|
||||||
velocityMps: displayed.speedMps,
|
velocityMps: displayed.speedMps,
|
||||||
track: displayed.trackDeg,
|
track: displayed.trackDeg,
|
||||||
stale: Boolean(flightState._missingPolls.get(icao24) || flightState._backoff),
|
stale: Boolean(
|
||||||
|
flightState._missingPolls.get(icao24) || flightState._backoff,
|
||||||
|
),
|
||||||
airline: info?.airline ?? null,
|
airline: info?.airline ?? null,
|
||||||
// CLASS label follows the TR-3B conversion so every downstream card
|
// CLASS label follows the TR-3B conversion so every downstream card
|
||||||
// (cockpit, Contacts, analyst) agrees with the triangle on screen.
|
// (cockpit, Contacts, analyst) agrees with the triangle on screen.
|
||||||
@@ -152,23 +184,31 @@ function _describeFlight(icao24) {
|
|||||||
// narration, getTrackedSubject) can use it as a label link without
|
// narration, getTrackedSubject) can use it as a label link without
|
||||||
// re-guarding a whitespace-only enrichment value.
|
// re-guarding a whitespace-only enrichment value.
|
||||||
registration: _toCleanText(info?.registration) || null,
|
registration: _toCleanText(info?.registration) || null,
|
||||||
origin: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.origin.code : null,
|
origin:
|
||||||
destination: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.destination.code : null,
|
info?.route && parts.tracking._routeIsPlausible(icao24, info.route)
|
||||||
route: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? {
|
? info.route.origin.code
|
||||||
|
: null,
|
||||||
|
destination:
|
||||||
|
info?.route && parts.tracking._routeIsPlausible(icao24, info.route)
|
||||||
|
? info.route.destination.code
|
||||||
|
: null,
|
||||||
|
route:
|
||||||
|
info?.route && parts.tracking._routeIsPlausible(icao24, info.route)
|
||||||
|
? {
|
||||||
origin: { ...info.route.origin },
|
origin: { ...info.route.origin },
|
||||||
destination: { ...info.route.destination },
|
destination: { ...info.route.destination },
|
||||||
} : null,
|
}
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _normalizeTrackedIcao(candidate) {
|
function _normalizeTrackedIcao(candidate) {
|
||||||
const normalized = String(candidate ?? '').trim().toLowerCase();
|
const normalized = String(candidate ?? '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
return normalized || null;
|
return normalized || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map one aircraft's internal poll record to a plain JSON-safe analyst
|
* Map one aircraft's internal poll record to a plain JSON-safe analyst
|
||||||
* record (analyst query engine seam). Pure — no Cesium types, no fetches;
|
* record (analyst query engine seam). Pure — no Cesium types, no fetches;
|
||||||
@@ -187,9 +227,16 @@ function _normalizeTrackedIcao(candidate) {
|
|||||||
* operator: string|null, routeOrigin: string|null, routeDestination: string|null}}
|
* operator: string|null, routeOrigin: string|null, routeDestination: string|null}}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function mapAnalystRecord(icao24, info, { military = false, routeOk = false } = {}) {
|
function mapAnalystRecord(
|
||||||
|
icao24,
|
||||||
|
info,
|
||||||
|
{ military = false, routeOk = false } = {},
|
||||||
|
) {
|
||||||
const num = (v) => (Number.isFinite(v) ? v : null);
|
const num = (v) => (Number.isFinite(v) ? v : null);
|
||||||
const text = (v) => { const t = String(v ?? '').trim(); return t || null; };
|
const text = (v) => {
|
||||||
|
const t = String(v ?? '').trim();
|
||||||
|
return t || null;
|
||||||
|
};
|
||||||
const callsign = text(info?.callsign);
|
const callsign = text(info?.callsign);
|
||||||
return {
|
return {
|
||||||
// Display identity for the narration layer. `id` is NOT a queryable field
|
// Display identity for the narration layer. `id` is NOT a queryable field
|
||||||
@@ -215,7 +262,8 @@ function mapAnalystRecord(icao24, info, { military = false, routeOk = false } =
|
|||||||
routeDestination: routeOk ? text(info?.route?.destination?.code) : null,
|
routeDestination: routeOk ? text(info?.route?.destination?.code) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const methods = { mapAnalystRecord,
|
const methods = {
|
||||||
|
mapAnalystRecord,
|
||||||
|
|
||||||
id: 'flights',
|
id: 'flights',
|
||||||
|
|
||||||
@@ -227,12 +275,12 @@ const methods = { mapAnalystRecord,
|
|||||||
|
|
||||||
// Browser-harness seam: isolates synthetic display-floor scenarios without
|
// Browser-harness seam: isolates synthetic display-floor scenarios without
|
||||||
// changing any production lifecycle or cache policy.
|
// changing any production lifecycle or cache policy.
|
||||||
_clearDisplayFloorStateForTest: parts.testing._clearDisplayFloorStateForTest,
|
_clearDisplayFloorStateForTest:
|
||||||
|
parts.testing._clearDisplayFloorStateForTest,
|
||||||
|
|
||||||
/** @type {number} Polling interval (ms) between update() calls */
|
/** @type {number} Polling interval (ms) between update() calls */
|
||||||
updateInterval: 30000,
|
updateInterval: 30000,
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Live layer params.
|
* Live layer params.
|
||||||
* `models3d` toggles 3D glTF model rendering for the FLEET (altitude-gated): when on,
|
* `models3d` toggles 3D glTF model rendering for the FLEET (altitude-gated): when on,
|
||||||
@@ -243,11 +291,16 @@ const methods = { mapAnalystRecord,
|
|||||||
* @param {{models3d?: boolean, models3dMode?: 'proximity'|'all', selectedFlightsTrackingId?: string|null}} params
|
* @param {{models3d?: boolean, models3dMode?: 'proximity'|'all', selectedFlightsTrackingId?: string|null}} params
|
||||||
*/
|
*/
|
||||||
setParams(params = {}, { origin = 'programmatic' } = {}) {
|
setParams(params = {}, { origin = 'programmatic' } = {}) {
|
||||||
if (isExplicitLayerStateOrigin(origin)
|
if (
|
||||||
&& !Object.hasOwn(params, 'selectedFlightsTrackingId')) {
|
isExplicitLayerStateOrigin(origin) &&
|
||||||
|
!Object.hasOwn(params, 'selectedFlightsTrackingId')
|
||||||
|
) {
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
}
|
}
|
||||||
if (typeof params.models3d === 'boolean' && params.models3d !== flightState._models3dEnabled) {
|
if (
|
||||||
|
typeof params.models3d === 'boolean' &&
|
||||||
|
params.models3d !== flightState._models3dEnabled
|
||||||
|
) {
|
||||||
flightState._models3dEnabled = params.models3d;
|
flightState._models3dEnabled = params.models3d;
|
||||||
if (!flightState._models3dEnabled) {
|
if (!flightState._models3dEnabled) {
|
||||||
parts.rendering._releaseModels();
|
parts.rendering._releaseModels();
|
||||||
@@ -255,10 +308,17 @@ const methods = { mapAnalystRecord,
|
|||||||
// Restore fleet billboards (the horizon-cull pass re-asserts next tick), but NEVER the
|
// Restore fleet billboards (the horizon-cull pass re-asserts next tick), but NEVER the
|
||||||
// tracked plane's own fleet billboard — its tracked entity is the visual, so re-showing it
|
// tracked plane's own fleet billboard — its tracked entity is the visual, so re-showing it
|
||||||
// here would double-image the tracked plane when 3D is turned off mid-track.
|
// here would double-image the tracked plane when 3D is turned off mid-track.
|
||||||
if (flightState._billboardCollection) for (const [icao, bb] of flightState._billboards) { if (icao !== flightState._trackedIcao) bb.show = true; }
|
if (flightState._billboardCollection)
|
||||||
|
for (const [icao, bb] of flightState._billboards) {
|
||||||
|
if (icao !== flightState._trackedIcao) bb.show = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((params.models3dMode === 'proximity' || params.models3dMode === 'all') && params.models3dMode !== flightState._models3dMode) {
|
}
|
||||||
|
if (
|
||||||
|
(params.models3dMode === 'proximity' ||
|
||||||
|
params.models3dMode === 'all') &&
|
||||||
|
params.models3dMode !== flightState._models3dMode
|
||||||
|
) {
|
||||||
// The next fleet tick re-derives the eligible set under the new cap and releases the overflow.
|
// The next fleet tick re-derives the eligible set under the new cap and releases the overflow.
|
||||||
flightState._models3dMode = params.models3dMode;
|
flightState._models3dMode = params.models3dMode;
|
||||||
if (flightState._cockpitContactMode) {
|
if (flightState._cockpitContactMode) {
|
||||||
@@ -266,7 +326,10 @@ const methods = { mapAnalystRecord,
|
|||||||
flightState._lastFleetTickMs = 0;
|
flightState._lastFleetTickMs = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof params.irBoost === 'boolean' && params.irBoost !== flightState._irBoost) {
|
if (
|
||||||
|
typeof params.irBoost === 'boolean' &&
|
||||||
|
params.irBoost !== flightState._irBoost
|
||||||
|
) {
|
||||||
flightState._irBoost = params.irBoost;
|
flightState._irBoost = params.irBoost;
|
||||||
parts.rendering._reloadModelsForIrBoost();
|
parts.rendering._reloadModelsForIrBoost();
|
||||||
// Sprites don't reload with the models — swap the TR-3B glyph between its
|
// Sprites don't reload with the models — swap the TR-3B glyph between its
|
||||||
@@ -275,16 +338,24 @@ const methods = { mapAnalystRecord,
|
|||||||
parts.tracking._refreshTr3bForStyle();
|
parts.tracking._refreshTr3bForStyle();
|
||||||
}
|
}
|
||||||
if (Object.hasOwn(params, 'selectedFlightsTrackingId')) {
|
if (Object.hasOwn(params, 'selectedFlightsTrackingId')) {
|
||||||
const requested = _normalizeTrackedIcao(params.selectedFlightsTrackingId);
|
const requested = _normalizeTrackedIcao(
|
||||||
|
params.selectedFlightsTrackingId,
|
||||||
|
);
|
||||||
if (requested === flightState._trackedIcao) {
|
if (requested === flightState._trackedIcao) {
|
||||||
flightState._pendingTrackingRestore = null;
|
flightState._pendingTrackingRestore = null;
|
||||||
} else if (requested === null) {
|
} else if (requested === null) {
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
if (flightState._trackedIcao) parts.tracking._clearTracking(false, { origin });
|
if (flightState._trackedIcao)
|
||||||
|
parts.tracking._clearTracking(false, { origin });
|
||||||
} else {
|
} else {
|
||||||
const generation = ++flightState._trackingIntentGeneration;
|
const generation = ++flightState._trackingIntentGeneration;
|
||||||
flightState._pendingTrackingRestore = { id: requested, generation, origin };
|
flightState._pendingTrackingRestore = {
|
||||||
if (flightState._trackedIcao) parts.tracking._clearTracking(false, { origin });
|
id: requested,
|
||||||
|
generation,
|
||||||
|
origin,
|
||||||
|
};
|
||||||
|
if (flightState._trackedIcao)
|
||||||
|
parts.tracking._clearTracking(false, { origin });
|
||||||
parts.tracking._applyPendingTrackingRestore();
|
parts.tracking._applyPendingTrackingRestore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,15 +371,15 @@ const methods = { mapAnalystRecord,
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-render a contact whose TR-3B conversion just flipped (Easter egg).
|
* Re-render a contact whose TR-3B conversion just flipped (Easter egg).
|
||||||
* Callers own the registry write; this only re-derives what renders.
|
* Callers own the registry write; this only re-derives what renders.
|
||||||
* @param {string} icao24 - ICAO 24-bit address.
|
* @param {string} icao24 - ICAO 24-bit address.
|
||||||
* @returns {boolean} True when this layer owns the contact.
|
* @returns {boolean} True when this layer owns the contact.
|
||||||
*/
|
*/
|
||||||
refreshTr3b(icao24) { return parts.tracking._refreshTr3bContact(icao24); },
|
refreshTr3b(icao24) {
|
||||||
|
return parts.tracking._refreshTr3bContact(icao24);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a subsample of currently visible aircraft for detection overlay
|
* Return a subsample of currently visible aircraft for detection overlay
|
||||||
@@ -323,7 +394,11 @@ const methods = { mapAnalystRecord,
|
|||||||
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
|
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
|
||||||
*/
|
*/
|
||||||
getDetectableObjects(options = {}) {
|
getDetectableObjects(options = {}) {
|
||||||
if (!flightState._billboardCollection || !flightState._billboardCollection.show) return [];
|
if (
|
||||||
|
!flightState._billboardCollection ||
|
||||||
|
!flightState._billboardCollection.show
|
||||||
|
)
|
||||||
|
return [];
|
||||||
// Compute a stride that evenly samples the billboard map.
|
// Compute a stride that evenly samples the billboard map.
|
||||||
// seed shifts the starting offset so successive calls can sample
|
// seed shifts the starting offset so successive calls can sample
|
||||||
// different aircraft without shuffling the underlying Map order.
|
// different aircraft without shuffling the underlying Map order.
|
||||||
@@ -331,16 +406,23 @@ const methods = { mapAnalystRecord,
|
|||||||
? Math.max(1, Math.floor(options.maxCount))
|
? Math.max(1, Math.floor(options.maxCount))
|
||||||
: flightState._billboards.size;
|
: flightState._billboards.size;
|
||||||
const seed = Number.isFinite(options.seed) ? Math.floor(options.seed) : 0;
|
const seed = Number.isFinite(options.seed) ? Math.floor(options.seed) : 0;
|
||||||
const stride = Math.max(1, Math.ceil(flightState._billboards.size / maxCount));
|
const stride = Math.max(
|
||||||
|
1,
|
||||||
|
Math.ceil(flightState._billboards.size / maxCount),
|
||||||
|
);
|
||||||
const start = seed % stride;
|
const start = seed % stride;
|
||||||
|
|
||||||
const result = [];
|
const result = [];
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
for (const [icao24, bb] of flightState._billboards) {
|
for (const [icao24, bb] of flightState._billboards) {
|
||||||
const shouldTake = ((idx - start) % stride) === 0;
|
const shouldTake = (idx - start) % stride === 0;
|
||||||
idx++;
|
idx++;
|
||||||
if (!shouldTake) continue;
|
if (!shouldTake) continue;
|
||||||
if (flightState._cockpitContactMode && icao24.toLowerCase() === flightState._cockpitSubjectId) continue;
|
if (
|
||||||
|
flightState._cockpitContactMode &&
|
||||||
|
icao24.toLowerCase() === flightState._cockpitSubjectId
|
||||||
|
)
|
||||||
|
continue;
|
||||||
const isTracked = icao24 === flightState._trackedIcao;
|
const isTracked = icao24 === flightState._trackedIcao;
|
||||||
// Keep planes rendered as a 3D model (billboard hidden) so the detection box
|
// Keep planes rendered as a 3D model (billboard hidden) so the detection box
|
||||||
// doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden.
|
// doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden.
|
||||||
@@ -350,7 +432,11 @@ const methods = { mapAnalystRecord,
|
|||||||
const info = flightState._flightData.get(icao24);
|
const info = flightState._flightData.get(icao24);
|
||||||
let object = flightState._detectionObjects.get(icao24);
|
let object = flightState._detectionObjects.get(icao24);
|
||||||
if (!object) {
|
if (!object) {
|
||||||
object = { sourceId: icao24, type: 'AIR', _weldPos: new Cesium.Cartesian3() };
|
object = {
|
||||||
|
sourceId: icao24,
|
||||||
|
type: 'AIR',
|
||||||
|
_weldPos: new Cesium.Cartesian3(),
|
||||||
|
};
|
||||||
flightState._detectionObjects.set(icao24, object);
|
flightState._detectionObjects.set(icao24, object);
|
||||||
}
|
}
|
||||||
// WELD: anchor to whatever actually owns the visual. A model-owned contact is
|
// WELD: anchor to whatever actually owns the visual. A model-owned contact is
|
||||||
@@ -360,17 +446,21 @@ const methods = { mapAnalystRecord,
|
|||||||
// and rises only as the coarse ground-floor cell warms. Zero extra sampling and
|
// and rises only as the coarse ground-floor cell warms. Zero extra sampling and
|
||||||
// no `_modelDisplayPosition` call from postRender. Sprite-owned contacts keep
|
// no `_modelDisplayPosition` call from postRender. Sprite-owned contacts keep
|
||||||
// `bb.position` — sprite and bracket are co-located there, so association holds.
|
// `bb.position` — sprite and bracket are co-located there, so association holds.
|
||||||
const spec = modelOwnsVisual ? parts.rendering._modelSpec(info?.klass) : null;
|
const spec = modelOwnsVisual
|
||||||
|
? parts.rendering._modelSpec(info?.klass)
|
||||||
|
: null;
|
||||||
const pos = isTracked
|
const pos = isTracked
|
||||||
? (parts.motion._trackedVisualCached() || bb.position)
|
? parts.motion._trackedVisualCached() || bb.position
|
||||||
: (modelOwnsVisual
|
: modelOwnsVisual
|
||||||
? modelVisualAnchor(
|
? modelVisualAnchor(
|
||||||
model.modelMatrix,
|
model.modelMatrix,
|
||||||
spec.visualCenterNative,
|
spec.visualCenterNative,
|
||||||
Number.isFinite(model.computedScale) ? model.computedScale : spec.scale,
|
Number.isFinite(model.computedScale)
|
||||||
|
? model.computedScale
|
||||||
|
: spec.scale,
|
||||||
object._weldPos || (object._weldPos = new Cesium.Cartesian3()),
|
object._weldPos || (object._weldPos = new Cesium.Cartesian3()),
|
||||||
)
|
)
|
||||||
: bb.position);
|
: bb.position;
|
||||||
if (!pos) continue;
|
if (!pos) continue;
|
||||||
object.position = pos;
|
object.position = pos;
|
||||||
object.skipLabel = isTracked;
|
object.skipLabel = isTracked;
|
||||||
@@ -385,15 +475,18 @@ const methods = { mapAnalystRecord,
|
|||||||
result.push(object);
|
result.push(object);
|
||||||
if (result.length >= maxCount) break;
|
if (result.length >= maxCount) break;
|
||||||
}
|
}
|
||||||
if (flightState._detectionObjects.size > flightState._billboards.size + 512) {
|
if (
|
||||||
|
flightState._detectionObjects.size >
|
||||||
|
flightState._billboards.size + 512
|
||||||
|
) {
|
||||||
for (const icao24 of flightState._detectionObjects.keys()) {
|
for (const icao24 of flightState._detectionObjects.keys()) {
|
||||||
if (!flightState._billboards.has(icao24)) flightState._detectionObjects.delete(icao24);
|
if (!flightState._billboards.has(icao24))
|
||||||
|
flightState._detectionObjects.delete(icao24);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a single aircraft by free-text query.
|
* Find a single aircraft by free-text query.
|
||||||
* Match priority: exact icao24 hex, exact callsign, callsign prefix,
|
* Match priority: exact icao24 hex, exact callsign, callsign prefix,
|
||||||
@@ -403,8 +496,11 @@ const methods = { mapAnalystRecord,
|
|||||||
* Best match with a cloned, dead-reckoned position, or null if none.
|
* Best match with a cloned, dead-reckoned position, or null if none.
|
||||||
*/
|
*/
|
||||||
findByQuery(query) {
|
findByQuery(query) {
|
||||||
if (!flightState._flightData || flightState._flightData.size === 0) return null;
|
if (!flightState._flightData || flightState._flightData.size === 0)
|
||||||
const q = String(query || '').trim().toLowerCase();
|
return null;
|
||||||
|
const q = String(query || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
if (!q) return null;
|
if (!q) return null;
|
||||||
|
|
||||||
// Registration is searched alongside callsign because it is what the
|
// Registration is searched alongside callsign because it is what the
|
||||||
@@ -434,7 +530,6 @@ const methods = { mapAnalystRecord,
|
|||||||
return best ? _describeFlight(best.id) : null;
|
return best ? _describeFlight(best.id) : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find aircraft near a given ECEF position, sorted ascending by distance.
|
* Find aircraft near a given ECEF position, sorted ascending by distance.
|
||||||
* Return shape mirrors militaryFlightsLayer.getNearby (id/icao24/position/distance).
|
* Return shape mirrors militaryFlightsLayer.getNearby (id/icao24/position/distance).
|
||||||
@@ -446,12 +541,18 @@ const methods = { mapAnalystRecord,
|
|||||||
* @returns {Array<{id: string, icao24: string, callsign: string|null, position: Cesium.Cartesian3, distance: number, aircraftClass: string|null, altitudeM: number|null, velocityMps: number|null, track: number|null}>}
|
* @returns {Array<{id: string, icao24: string, callsign: string|null, position: Cesium.Cartesian3, distance: number, aircraftClass: string|null, altitudeM: number|null, velocityMps: number|null, track: number|null}>}
|
||||||
*/
|
*/
|
||||||
getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) {
|
getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) {
|
||||||
if (!center || !flightState._billboardCollection || !flightState._billboardCollection.show) return [];
|
if (
|
||||||
|
!center ||
|
||||||
|
!flightState._billboardCollection ||
|
||||||
|
!flightState._billboardCollection.show
|
||||||
|
)
|
||||||
|
return [];
|
||||||
|
|
||||||
const limit = Number.isFinite(maxCount)
|
const limit = Number.isFinite(maxCount)
|
||||||
? Math.max(1, Math.floor(maxCount))
|
? Math.max(1, Math.floor(maxCount))
|
||||||
: 50;
|
: 50;
|
||||||
const maxRange = Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY;
|
const maxRange =
|
||||||
|
Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
const now = Cesium.JulianDate.now();
|
const now = Cesium.JulianDate.now();
|
||||||
const nearby = [];
|
const nearby = [];
|
||||||
@@ -460,14 +561,19 @@ const methods = { mapAnalystRecord,
|
|||||||
const isTracked = icao24 === flightState._trackedIcao;
|
const isTracked = icao24 === flightState._trackedIcao;
|
||||||
// Keep planes rendered as a 3D model (billboard hidden) so proximity counts
|
// Keep planes rendered as a 3D model (billboard hidden) so proximity counts
|
||||||
// don't drop to zero on the 2D→3D handoff; bb.position stays current while hidden.
|
// don't drop to zero on the 2D→3D handoff; bb.position stays current while hidden.
|
||||||
if (!aircraftIncludedInNearby({
|
if (
|
||||||
|
!aircraftIncludedInNearby({
|
||||||
isTracked,
|
isTracked,
|
||||||
billboardShown: bb.show,
|
billboardShown: bb.show,
|
||||||
modelRendering: parts.rendering._modelOwnsVisual(icao24),
|
modelRendering: parts.rendering._modelOwnsVisual(icao24),
|
||||||
includeHidden,
|
includeHidden,
|
||||||
})) continue;
|
})
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
const trackedPos = isTracked ? parts.motion._trackedDisplayCached() : null; // cached, no recompute (anti-jitter)
|
const trackedPos = isTracked
|
||||||
|
? parts.motion._trackedDisplayCached()
|
||||||
|
: null; // cached, no recompute (anti-jitter)
|
||||||
const pos = trackedPos || bb.position;
|
const pos = trackedPos || bb.position;
|
||||||
if (!pos) continue;
|
if (!pos) continue;
|
||||||
|
|
||||||
@@ -486,7 +592,12 @@ const methods = { mapAnalystRecord,
|
|||||||
// Filter surface: the cockpit next/previous path matches on THIS field
|
// Filter surface: the cockpit next/previous path matches on THIS field
|
||||||
// (militaryAwareness.aircraftClassMatchesFilter), so a converted contact
|
// (militaryAwareness.aircraftClassMatchesFilter), so a converted contact
|
||||||
// has to report the class it renders as or a `tr3b` filter skips it.
|
// has to report the class it renders as or a `tr3b` filter skips it.
|
||||||
aircraftClass: tr3bAircraftClass(icao24, String(info?.klass || '').trim().toLowerCase() || null),
|
aircraftClass: tr3bAircraftClass(
|
||||||
|
icao24,
|
||||||
|
String(info?.klass || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase() || null,
|
||||||
|
),
|
||||||
altitudeM: info?.altitude ?? null,
|
altitudeM: info?.altitude ?? null,
|
||||||
velocityMps: info?.velocity ?? null,
|
velocityMps: info?.velocity ?? null,
|
||||||
track: info?.true_track ?? null,
|
track: info?.true_track ?? null,
|
||||||
@@ -497,7 +608,6 @@ const methods = { mapAnalystRecord,
|
|||||||
return nearby.slice(0, limit);
|
return nearby.slice(0, limit);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return id/label/position for up to maxCount currently rendered aircraft.
|
* Return id/label/position for up to maxCount currently rendered aircraft.
|
||||||
* Cheap snapshot for voice-tool framing — billboard positions only, no
|
* Cheap snapshot for voice-tool framing — billboard positions only, no
|
||||||
@@ -520,22 +630,39 @@ const methods = { mapAnalystRecord,
|
|||||||
* holds no data and therefore cannot answer.
|
* holds no data and therefore cannot answer.
|
||||||
*/
|
*/
|
||||||
hasContact(icao24) {
|
hasContact(icao24) {
|
||||||
if (!flightState._billboardCollection || !flightState._billboardCollection.show || flightState._billboards.size === 0) return null;
|
if (
|
||||||
|
!flightState._billboardCollection ||
|
||||||
|
!flightState._billboardCollection.show ||
|
||||||
|
flightState._billboards.size === 0
|
||||||
|
)
|
||||||
|
return null;
|
||||||
if (!icao24) return false;
|
if (!icao24) return false;
|
||||||
const id = String(icao24).trim();
|
const id = String(icao24).trim();
|
||||||
return flightState._billboards.has(id) || flightState._billboards.has(id.toLowerCase());
|
return (
|
||||||
|
flightState._billboards.has(id) ||
|
||||||
|
flightState._billboards.has(id.toLowerCase())
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
getAllPositions(maxCount = 500) {
|
getAllPositions(maxCount = 500) {
|
||||||
if (!flightState._billboardCollection || flightState._billboards.size === 0) return [];
|
if (
|
||||||
const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 500;
|
!flightState._billboardCollection ||
|
||||||
|
flightState._billboards.size === 0
|
||||||
|
)
|
||||||
|
return [];
|
||||||
|
const limit = Number.isFinite(maxCount)
|
||||||
|
? Math.max(1, Math.floor(maxCount))
|
||||||
|
: 500;
|
||||||
|
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const [icao24, bb] of flightState._billboards) {
|
for (const [icao24, bb] of flightState._billboards) {
|
||||||
const pos = bb.position;
|
const pos = bb.position;
|
||||||
if (!pos) continue;
|
if (!pos) continue;
|
||||||
const carto = Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84, flightState._scratchCarto);
|
const carto = Cesium.Cartographic.fromCartesian(
|
||||||
|
pos,
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
flightState._scratchCarto,
|
||||||
|
);
|
||||||
if (!carto) continue;
|
if (!carto) continue;
|
||||||
const info = flightState._flightData.get(icao24);
|
const info = flightState._flightData.get(icao24);
|
||||||
result.push({
|
result.push({
|
||||||
@@ -549,15 +676,20 @@ const methods = { mapAnalystRecord,
|
|||||||
typeName: info?.typeName ?? null,
|
typeName: info?.typeName ?? null,
|
||||||
typeCode: info?.typeCode ?? null,
|
typeCode: info?.typeCode ?? null,
|
||||||
registration: info?.registration ?? null,
|
registration: info?.registration ?? null,
|
||||||
origin: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.origin.code : null,
|
origin:
|
||||||
destination: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.destination.code : null,
|
info?.route && parts.tracking._routeIsPlausible(icao24, info.route)
|
||||||
|
? info.route.origin.code
|
||||||
|
: null,
|
||||||
|
destination:
|
||||||
|
info?.route && parts.tracking._routeIsPlausible(icao24, info.route)
|
||||||
|
? info.route.destination.code
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
if (result.length >= limit) break;
|
if (result.length >= limit) break;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Snapshot the layer's in-memory records as plain JSON-safe objects for
|
* Snapshot the layer's in-memory records as plain JSON-safe objects for
|
||||||
* the analyst query engine. On-demand only (called at most once per
|
* the analyst query engine. On-demand only (called at most once per
|
||||||
@@ -568,18 +700,30 @@ const methods = { mapAnalystRecord,
|
|||||||
* @returns {Array<Object>} See mapAnalystRecord for the record shape.
|
* @returns {Array<Object>} See mapAnalystRecord for the record shape.
|
||||||
*/
|
*/
|
||||||
getAnalystRecords(maxCount = 2000) {
|
getAnalystRecords(maxCount = 2000) {
|
||||||
if (!flightState._billboardCollection || !flightState._billboardCollection.show || flightState._flightData.size === 0) return [];
|
if (
|
||||||
const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 2000;
|
!flightState._billboardCollection ||
|
||||||
|
!flightState._billboardCollection.show ||
|
||||||
|
flightState._flightData.size === 0
|
||||||
|
)
|
||||||
|
return [];
|
||||||
|
const limit = Number.isFinite(maxCount)
|
||||||
|
? Math.max(1, Math.floor(maxCount))
|
||||||
|
: 2000;
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const [icao24, info] of flightState._flightData) {
|
for (const [icao24, info] of flightState._flightData) {
|
||||||
const routeOk = !!info?.route && parts.tracking._routeIsPlausible(icao24, info.route);
|
const routeOk =
|
||||||
result.push(mapAnalystRecord(icao24, info, { military: isMilitaryIcao(icao24), routeOk }));
|
!!info?.route && parts.tracking._routeIsPlausible(icao24, info.route);
|
||||||
|
result.push(
|
||||||
|
mapAnalystRecord(icao24, info, {
|
||||||
|
military: isMilitaryIcao(icao24),
|
||||||
|
routeOk,
|
||||||
|
}),
|
||||||
|
);
|
||||||
if (result.length >= limit) break;
|
if (result.length >= limit) break;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start camera-tracking an aircraft by ICAO24 address.
|
* Start camera-tracking an aircraft by ICAO24 address.
|
||||||
* @param {string} icao24 - ICAO 24-bit transponder address.
|
* @param {string} icao24 - ICAO 24-bit transponder address.
|
||||||
@@ -590,16 +734,24 @@ const methods = { mapAnalystRecord,
|
|||||||
let id = String(icao24).trim();
|
let id = String(icao24).trim();
|
||||||
if (!flightState._billboards.has(id)) id = id.toLowerCase();
|
if (!flightState._billboards.has(id)) id = id.toLowerCase();
|
||||||
if (!flightState._billboards.has(id)) return false;
|
if (!flightState._billboards.has(id)) return false;
|
||||||
if (parts.tracking._isExplicitTrackingOrigin(origin)) parts.tracking._cancelPendingTrackingRestore();
|
if (parts.tracking._isExplicitTrackingOrigin(origin))
|
||||||
if (flightState._trackedIcao === id) return parts.tracking._publishTrackedSelection(id, origin);
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
|
if (flightState._trackedIcao === id)
|
||||||
|
return parts.tracking._publishTrackedSelection(id, origin);
|
||||||
parts.tracking._trackFlight(id, { origin });
|
parts.tracking._trackFlight(id, { origin });
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/** Resolve a shared Follow target only against the latest accepted refresh. */
|
/** Resolve a shared Follow target only against the latest accepted refresh. */
|
||||||
async resolveTrackingRestoreTarget(icao24, { signal = null, origin = 'share-restore' } = {}) {
|
async resolveTrackingRestoreTarget(
|
||||||
if (signal?.aborted) return { status: 'cancelled', reason: String(signal.reason || 'aborted') };
|
icao24,
|
||||||
|
{ signal = null, origin = 'share-restore' } = {},
|
||||||
|
) {
|
||||||
|
if (signal?.aborted)
|
||||||
|
return {
|
||||||
|
status: 'cancelled',
|
||||||
|
reason: String(signal.reason || 'aborted'),
|
||||||
|
};
|
||||||
const id = _normalizeTrackedIcao(icao24);
|
const id = _normalizeTrackedIcao(icao24);
|
||||||
if (!id) return { status: 'missing', reason: 'invalid-target' };
|
if (!id) return { status: 'missing', reason: 'invalid-target' };
|
||||||
const outcome = flightState._lastTrackingRefreshOutcome;
|
const outcome = flightState._lastTrackingRefreshOutcome;
|
||||||
@@ -621,7 +773,11 @@ const methods = { mapAnalystRecord,
|
|||||||
coverage: outcome.coverage,
|
coverage: outcome.coverage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (signal?.aborted) return { status: 'cancelled', reason: String(signal.reason || 'aborted') };
|
if (signal?.aborted)
|
||||||
|
return {
|
||||||
|
status: 'cancelled',
|
||||||
|
reason: String(signal.reason || 'aborted'),
|
||||||
|
};
|
||||||
const followed = this.trackById(id, { origin });
|
const followed = this.trackById(id, { origin });
|
||||||
return followed
|
return followed
|
||||||
? {
|
? {
|
||||||
@@ -630,24 +786,35 @@ const methods = { mapAnalystRecord,
|
|||||||
source: outcome.source,
|
source: outcome.source,
|
||||||
coverage: outcome.coverage,
|
coverage: outcome.coverage,
|
||||||
}
|
}
|
||||||
: { status: 'source-unavailable', reason: 'target-not-renderable', refreshEpoch: outcome.epoch };
|
: {
|
||||||
|
status: 'source-unavailable',
|
||||||
|
reason: 'target-not-renderable',
|
||||||
|
refreshEpoch: outcome.epoch,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/** Reapply the canonical follow frame without recreating the selected flight. */
|
/** Reapply the canonical follow frame without recreating the selected flight. */
|
||||||
refocusTrackedById(icao24, { origin = 'programmatic' } = {}) {
|
refocusTrackedById(icao24, { origin = 'programmatic' } = {}) {
|
||||||
if (!icao24 || flightState._cockpitContactMode || !flightState._viewer || !flightState._trackedEntity) return false;
|
if (
|
||||||
|
!icao24 ||
|
||||||
|
flightState._cockpitContactMode ||
|
||||||
|
!flightState._viewer ||
|
||||||
|
!flightState._trackedEntity
|
||||||
|
)
|
||||||
|
return false;
|
||||||
let id = String(icao24).trim();
|
let id = String(icao24).trim();
|
||||||
if (!flightState._billboards.has(id)) id = id.toLowerCase();
|
if (!flightState._billboards.has(id)) id = id.toLowerCase();
|
||||||
if (
|
if (
|
||||||
id !== flightState._trackedIcao
|
id !== flightState._trackedIcao ||
|
||||||
|| !flightState._viewer.entities?.contains?.(flightState._trackedEntity)
|
!flightState._viewer.entities?.contains?.(flightState._trackedEntity) ||
|
||||||
|| flightState._viewer.trackedEntity !== flightState._trackedEntity
|
flightState._viewer.trackedEntity !== flightState._trackedEntity
|
||||||
) return false;
|
)
|
||||||
|
return false;
|
||||||
flightState._trackedCameraFrameStop?.();
|
flightState._trackedCameraFrameStop?.();
|
||||||
flightState._viewer.camera.cancelFlight();
|
flightState._viewer.camera.cancelFlight();
|
||||||
flightState._viewer.trackedEntity = flightState._trackedEntity;
|
flightState._viewer.trackedEntity = flightState._trackedEntity;
|
||||||
flightState._trackedCameraFrameStop = applyTrackedCameraFrame(
|
flightState._trackedCameraFrameStop =
|
||||||
|
applyTrackedCameraFrame(
|
||||||
flightState._viewer,
|
flightState._viewer,
|
||||||
flightState._trackedEntity,
|
flightState._trackedEntity,
|
||||||
flightState._trackedEntity.viewFrom,
|
flightState._trackedEntity.viewFrom,
|
||||||
@@ -656,7 +823,6 @@ const methods = { mapAnalystRecord,
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop tracking the currently followed aircraft (no-op if none).
|
* Stop tracking the currently followed aircraft (no-op if none).
|
||||||
* @returns {boolean} Always true.
|
* @returns {boolean} Always true.
|
||||||
@@ -667,12 +833,10 @@ const methods = { mapAnalystRecord,
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
cancelPendingTrackingRestore() {
|
cancelPendingTrackingRestore() {
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describe the currently tracked aircraft at its dead-reckoned position.
|
* Describe the currently tracked aircraft at its dead-reckoned position.
|
||||||
* @returns {{icao24: string, callsign: string|null, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
|
* @returns {{icao24: string, callsign: string|null, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
|
||||||
@@ -686,7 +850,6 @@ const methods = { mapAnalystRecord,
|
|||||||
return rest;
|
return rest;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the current aircraft as a Context subject without changing
|
* Return the current aircraft as a Context subject without changing
|
||||||
* tracking or camera ownership.
|
* tracking or camera ownership.
|
||||||
@@ -702,15 +865,16 @@ const methods = { mapAnalystRecord,
|
|||||||
id: described.icao24,
|
id: described.icao24,
|
||||||
// Same label chain as getNearby/getDetectableObjects: a callsign-less
|
// Same label chain as getNearby/getDetectableObjects: a callsign-less
|
||||||
// contact reads as its registration, never as the raw ICAO hex.
|
// contact reads as its registration, never as the raw ICAO hex.
|
||||||
label: described.callsign
|
label:
|
||||||
|| _toCleanText(described.registration)
|
described.callsign ||
|
||||||
|| described.icao24,
|
_toCleanText(described.registration) ||
|
||||||
|
described.icao24,
|
||||||
position: Cesium.Cartesian3.clone(described.position),
|
position: Cesium.Cartesian3.clone(described.position),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
...(FOCUS_EVIDENCE_DEV
|
||||||
...(FOCUS_EVIDENCE_DEV ? {
|
? {
|
||||||
__focusEvidence: Object.freeze({
|
__focusEvidence: Object.freeze({
|
||||||
setAircraft: parts.evidence._setFocusEvidenceAircraft,
|
setAircraft: parts.evidence._setFocusEvidenceAircraft,
|
||||||
moveAircraft: parts.evidence._moveFocusEvidenceAircraft,
|
moveAircraft: parts.evidence._moveFocusEvidenceAircraft,
|
||||||
@@ -728,35 +892,42 @@ const methods = { mapAnalystRecord,
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
takeFrameClock(startMs = 1_000_000_000) {
|
takeFrameClock(startMs = 1_000_000_000) {
|
||||||
if (!flightState._viewer || !Number.isFinite(startMs)) return { ok: false, nowMs: null };
|
if (!flightState._viewer || !Number.isFinite(startMs))
|
||||||
|
return { ok: false, nowMs: null };
|
||||||
flightState._viewer.useDefaultRenderLoop = false;
|
flightState._viewer.useDefaultRenderLoop = false;
|
||||||
setFocusEvidenceNowMs(startMs);
|
setFocusEvidenceNowMs(startMs);
|
||||||
flightState._lastFleetTickMs = startMs - FLEET_DR_INTERVAL_MS;
|
flightState._lastFleetTickMs = startMs - FLEET_DR_INTERVAL_MS;
|
||||||
// Cross a browser task boundary, then close Cesium's pending-loop
|
// Cross a browser task boundary, then close Cesium's pending-loop
|
||||||
// latch. Any already-queued callback still observes the false gate,
|
// latch. Any already-queued callback still observes the false gate,
|
||||||
// while manual evidence renders can begin without waiting on VSYNC.
|
// while manual evidence renders can begin without waiting on VSYNC.
|
||||||
return new Promise((resolve) => setTimeout(() => {
|
return new Promise((resolve) =>
|
||||||
if (flightState._viewer?._cesiumWidget) flightState._viewer._cesiumWidget._renderLoopRunning = false;
|
setTimeout(() => {
|
||||||
|
if (flightState._viewer?._cesiumWidget)
|
||||||
|
flightState._viewer._cesiumWidget._renderLoopRunning = false;
|
||||||
resolve({ ok: true, nowMs: startMs });
|
resolve({ ok: true, nowMs: startMs });
|
||||||
}, 0));
|
}, 0),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
advanceFrameClock(deltaMs = FLEET_DR_INTERVAL_MS) {
|
advanceFrameClock(deltaMs = FLEET_DR_INTERVAL_MS) {
|
||||||
return advanceFocusEvidenceNowMs(deltaMs);
|
return advanceFocusEvidenceNowMs(deltaMs);
|
||||||
},
|
},
|
||||||
releaseFrameClock() {
|
releaseFrameClock() {
|
||||||
setFocusEvidenceNowMs(null);
|
setFocusEvidenceNowMs(null);
|
||||||
if (flightState._viewer) flightState._viewer.useDefaultRenderLoop = true;
|
if (flightState._viewer)
|
||||||
|
flightState._viewer.useDefaultRenderLoop = true;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
} : {}),
|
}
|
||||||
|
: {}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return layer health/status for the HUD stats chip.
|
* Return layer health/status for the HUD stats chip.
|
||||||
* @returns {{count: number, lastUpdate: number|null, stale: boolean, error: string|null, status: number|null, retryInSec: number}}
|
* @returns {{count: number, lastUpdate: number|null, stale: boolean, error: string|null, status: number|null, retryInSec: number}}
|
||||||
*/
|
*/
|
||||||
getStats() {
|
getStats() {
|
||||||
const retryInSec = flightState._retryAt ? Math.max(0, Math.ceil((flightState._retryAt - Date.now()) / 1000)) : 0;
|
const retryInSec = flightState._retryAt
|
||||||
|
? Math.max(0, Math.ceil((flightState._retryAt - Date.now()) / 1000))
|
||||||
|
: 0;
|
||||||
return {
|
return {
|
||||||
count: flightState._count,
|
count: flightState._count,
|
||||||
lastUpdate: flightState._lastUpdate,
|
lastUpdate: flightState._lastUpdate,
|
||||||
@@ -767,8 +938,17 @@ const methods = { mapAnalystRecord,
|
|||||||
source: flightState._lastSource,
|
source: flightState._lastSource,
|
||||||
coverage: flightState._lastCoverage,
|
coverage: flightState._lastCoverage,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return { _approxDistanceKm, _likelyLanded, _toCleanText, _contactLabel, _describeFlight, _normalizeTrackedIcao, mapAnalystRecord, methods };
|
return {
|
||||||
|
_approxDistanceKm,
|
||||||
|
_likelyLanded,
|
||||||
|
_toCleanText,
|
||||||
|
_contactLabel,
|
||||||
|
_describeFlight,
|
||||||
|
_normalizeTrackedIcao,
|
||||||
|
mapAnalystRecord,
|
||||||
|
methods,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
+480
-138
File diff suppressed because it is too large.
Load diff
@@ -5,8 +5,6 @@ export function createFlightState({ source, services }) {
|
|||||||
const { createGroundSnap } = services.groundSnap;
|
const { createGroundSnap } = services.groundSnap;
|
||||||
const flightState = { lifetime: new AbortController() };
|
const flightState = { lifetime: new AbortController() };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
flightState._source = source;
|
flightState._source = source;
|
||||||
|
|
||||||
/** Per-class model spec. Hangar-fleet classes (CLASS_MODEL_REAL) ship GLBs
|
/** Per-class model spec. Hangar-fleet classes (CLASS_MODEL_REAL) ship GLBs
|
||||||
@@ -27,10 +25,8 @@ flightState._specCache = new Map();
|
|||||||
|
|
||||||
flightState._groundSnap = createGroundSnap();
|
flightState._groundSnap = createGroundSnap();
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchGroundCarto = new Cesium.Cartographic();
|
flightState._scratchGroundCarto = new Cesium.Cartographic();
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchGroundPos = new Cesium.Cartesian3();
|
flightState._scratchGroundPos = new Cesium.Cartesian3();
|
||||||
|
|
||||||
/** @type {Cesium.PrimitiveCollection|null} */
|
/** @type {Cesium.PrimitiveCollection|null} */
|
||||||
@@ -63,7 +59,6 @@ flightState._modelEpoch = 0;
|
|||||||
|
|
||||||
flightState._models3dEnabled = true;
|
flightState._models3dEnabled = true;
|
||||||
|
|
||||||
|
|
||||||
flightState._models3dMode = 'proximity';
|
flightState._models3dMode = 'proximity';
|
||||||
// 'proximity' = nearest MODEL_MAX in view; 'all' = every in-view plane (≤ MODEL_MAX_ALL)
|
// 'proximity' = nearest MODEL_MAX in view; 'all' = every in-view plane (≤ MODEL_MAX_ALL)
|
||||||
|
|
||||||
@@ -81,14 +76,14 @@ flightState._planeModelLoaded = false;
|
|||||||
|
|
||||||
flightState._preloadModel = null;
|
flightState._preloadModel = null;
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0);
|
flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0);
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchModelMtx = new Cesium.Matrix4();
|
flightState._scratchModelMtx = new Cesium.Matrix4();
|
||||||
|
|
||||||
|
flightState._scratchModelBS = new Cesium.BoundingSphere(
|
||||||
flightState._scratchModelBS = new Cesium.BoundingSphere(new Cesium.Cartesian3(), 1.0);
|
new Cesium.Cartesian3(),
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
// frustum-visibility test
|
// frustum-visibility test
|
||||||
/** Last limb taper per billboard, retained across class/ground/cockpit repaints. */
|
/** Last limb taper per billboard, retained across class/ground/cockpit repaints. */
|
||||||
|
|
||||||
@@ -151,7 +146,6 @@ flightState._retryAt = 0;
|
|||||||
|
|
||||||
flightState._lastError = null;
|
flightState._lastError = null;
|
||||||
|
|
||||||
|
|
||||||
flightState._activeUpdateControllers = new Set();
|
flightState._activeUpdateControllers = new Set();
|
||||||
|
|
||||||
/** @type {number|null} HTTP status of the most recent API response */
|
/** @type {number|null} HTTP status of the most recent API response */
|
||||||
@@ -166,7 +160,6 @@ flightState._lastSource = source?.label || 'Aircraft';
|
|||||||
|
|
||||||
flightState._lastCoverage = 'worldwide upstream snapshot';
|
flightState._lastCoverage = 'worldwide upstream snapshot';
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Click-to-track state
|
// Click-to-track state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -175,16 +168,12 @@ flightState._lastCoverage = 'worldwide upstream snapshot';
|
|||||||
|
|
||||||
flightState._trackedIcao = null;
|
flightState._trackedIcao = null;
|
||||||
|
|
||||||
|
|
||||||
flightState._pendingTrackingRestore = null;
|
flightState._pendingTrackingRestore = null;
|
||||||
|
|
||||||
|
|
||||||
flightState._trackingIntentGeneration = 0;
|
flightState._trackingIntentGeneration = 0;
|
||||||
|
|
||||||
|
|
||||||
flightState._trackingRefreshEpoch = 0;
|
flightState._trackingRefreshEpoch = 0;
|
||||||
|
|
||||||
|
|
||||||
flightState._lastTrackingRefreshOutcome = {
|
flightState._lastTrackingRefreshOutcome = {
|
||||||
epoch: 0,
|
epoch: 0,
|
||||||
status: 'unavailable',
|
status: 'unavailable',
|
||||||
@@ -213,7 +202,6 @@ flightState._trackedModel = null;
|
|||||||
|
|
||||||
flightState._trackedModelGen = 0;
|
flightState._trackedModelGen = 0;
|
||||||
|
|
||||||
|
|
||||||
flightState._trackedModelLoading = false;
|
flightState._trackedModelLoading = false;
|
||||||
|
|
||||||
/** @type {Cesium.ScreenSpaceEventHandler|null} Click handler on the scene canvas */
|
/** @type {Cesium.ScreenSpaceEventHandler|null} Click handler on the scene canvas */
|
||||||
@@ -286,7 +274,6 @@ flightState._lastTrackedRotation = 0;
|
|||||||
|
|
||||||
flightState._preRenderRemove = null;
|
flightState._preRenderRemove = null;
|
||||||
|
|
||||||
|
|
||||||
flightState._trackedModelPreUpdateRemove = null;
|
flightState._trackedModelPreUpdateRemove = null;
|
||||||
|
|
||||||
/** @type {Cesium.Event.RemoveCallback|null} camera.moveEnd listener disposer (arrival rotation pass) */
|
/** @type {Cesium.Event.RemoveCallback|null} camera.moveEnd listener disposer (arrival rotation pass) */
|
||||||
@@ -301,39 +288,28 @@ flightState._trackedEntityChangedRemove = null;
|
|||||||
|
|
||||||
flightState._milActiveChangeUnsub = null;
|
flightState._milActiveChangeUnsub = null;
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Scratch (reusable) variables — avoid per-frame heap allocation
|
// Scratch (reusable) variables — avoid per-frame heap allocation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchOffset = new Cesium.Cartesian3();
|
flightState._scratchOffset = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchCarto = new Cesium.Cartographic();
|
flightState._scratchCarto = new Cesium.Cartographic();
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchEnu = new Cesium.Matrix4();
|
flightState._scratchEnu = new Cesium.Matrix4();
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchRenderTime = new Cesium.JulianDate();
|
flightState._scratchRenderTime = new Cesium.JulianDate();
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchFleetPos = new Cesium.Cartesian3();
|
flightState._scratchFleetPos = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchDrRaw = new Cesium.Cartesian3();
|
flightState._scratchDrRaw = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
|
||||||
flightState._scratchWarmupTime = new Cesium.JulianDate();
|
flightState._scratchWarmupTime = new Cesium.JulianDate();
|
||||||
|
|
||||||
|
|
||||||
flightState._trackedPosHolder = new Cesium.Cartesian3();
|
flightState._trackedPosHolder = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Per-frame cache for the tracked entity's dead-reckoned position.
|
// Per-frame cache for the tracked entity's dead-reckoned position.
|
||||||
// The position, alignedAxis, and rotation CallbackProperties all fire each
|
// The position, alignedAxis, and rotation CallbackProperties all fire each
|
||||||
@@ -348,7 +324,6 @@ flightState._cachedDRPosition = null;
|
|||||||
|
|
||||||
flightState._cachedDRFrame = -1;
|
flightState._cachedDRFrame = -1;
|
||||||
|
|
||||||
|
|
||||||
/** Course (deg) of the position `_deadReckon` most recently returned — set on
|
/** Course (deg) of the position `_deadReckon` most recently returned — set on
|
||||||
* every branch of `_deadReckon`, read IMMEDIATELY by the caller (same
|
* every branch of `_deadReckon`, read IMMEDIATELY by the caller (same
|
||||||
* synchronous flow; module-scratch idiom, like the Cartesian scratches). */
|
* synchronous flow; module-scratch idiom, like the Cartesian scratches). */
|
||||||
@@ -362,7 +337,6 @@ flightState._drCourseDeg = null;
|
|||||||
|
|
||||||
flightState._drSpeedMps = null;
|
flightState._drSpeedMps = null;
|
||||||
|
|
||||||
|
|
||||||
flightState._drCourseHold = false;
|
flightState._drCourseHold = false;
|
||||||
|
|
||||||
/** Sibling scratch: whether the position `_deadReckon` just returned came from
|
/** Sibling scratch: whether the position `_deadReckon` just returned came from
|
||||||
@@ -382,7 +356,6 @@ flightState._cachedDRCourse = null;
|
|||||||
|
|
||||||
flightState._cachedDRSpeedMps = null;
|
flightState._cachedDRSpeedMps = null;
|
||||||
|
|
||||||
|
|
||||||
flightState._cachedDRHold = false;
|
flightState._cachedDRHold = false;
|
||||||
|
|
||||||
/** Wall-clock of the tracked course limiter's last advance (dt source only —
|
/** Wall-clock of the tracked course limiter's last advance (dt source only —
|
||||||
@@ -402,57 +375,44 @@ flightState._trackedCourseMs = 0;
|
|||||||
|
|
||||||
flightState._displayCourse = new Map();
|
flightState._displayCourse = new Map();
|
||||||
|
|
||||||
|
|
||||||
flightState._enrichActive = 0;
|
flightState._enrichActive = 0;
|
||||||
|
|
||||||
|
|
||||||
flightState._enrichLastDispatchMs = 0;
|
flightState._enrichLastDispatchMs = 0;
|
||||||
|
|
||||||
/** @type {ReturnType<typeof setTimeout>|null} pending drip wake-up */
|
/** @type {ReturnType<typeof setTimeout>|null} pending drip wake-up */
|
||||||
|
|
||||||
flightState._enrichDripTimer = null;
|
flightState._enrichDripTimer = null;
|
||||||
|
|
||||||
|
|
||||||
flightState._enrichQueue = [];
|
flightState._enrichQueue = [];
|
||||||
|
|
||||||
|
|
||||||
flightState._enrichSeen = new Set();
|
flightState._enrichSeen = new Set();
|
||||||
|
|
||||||
|
|
||||||
flightState._enrichAmbientBudget = ENRICH_AMBIENT_BUDGET_CEIL;
|
flightState._enrichAmbientBudget = ENRICH_AMBIENT_BUDGET_CEIL;
|
||||||
|
|
||||||
/** Epoch ms the bucket last accounted a refill window from (0 = unset). */
|
/** Epoch ms the bucket last accounted a refill window from (0 = unset). */
|
||||||
|
|
||||||
flightState._enrichAmbientRefillAnchorMs = 0;
|
flightState._enrichAmbientRefillAnchorMs = 0;
|
||||||
|
|
||||||
|
|
||||||
flightState._drCorrection = new Cesium.Cartesian3(0, 0, 0);
|
flightState._drCorrection = new Cesium.Cartesian3(0, 0, 0);
|
||||||
|
|
||||||
|
|
||||||
flightState._drCorrectionStartMs = 0;
|
flightState._drCorrectionStartMs = 0;
|
||||||
|
|
||||||
|
|
||||||
flightState._drPrevRaw = new Cesium.Cartesian3();
|
flightState._drPrevRaw = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
|
||||||
flightState._drPrevDisplay = new Cesium.Cartesian3();
|
flightState._drPrevDisplay = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
|
||||||
flightState._drPrevMs = 0;
|
flightState._drPrevMs = 0;
|
||||||
|
|
||||||
|
|
||||||
flightState._drReconcileValid = false;
|
flightState._drReconcileValid = false;
|
||||||
|
|
||||||
/** @type {string|null} icao the reconciliation state currently belongs to */
|
/** @type {string|null} icao the reconciliation state currently belongs to */
|
||||||
|
|
||||||
flightState._drReconcileIcao = null;
|
flightState._drReconcileIcao = null;
|
||||||
|
|
||||||
|
|
||||||
/** @type {Cesium.Cartesian3} Scratch for the tracked model's rendered translation. */
|
/** @type {Cesium.Cartesian3} Scratch for the tracked model's rendered translation. */
|
||||||
|
|
||||||
flightState._trackedVisualPos = new Cesium.Cartesian3();
|
flightState._trackedVisualPos = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
|
||||||
flightState._trackedTrailPos = new Cesium.Cartesian3();
|
flightState._trackedTrailPos = new Cesium.Cartesian3();
|
||||||
|
|
||||||
/** Scratch for the tracked model's world-space origin (the envelope centre). */
|
/** Scratch for the tracked model's world-space origin (the envelope centre). */
|
||||||
@@ -464,26 +424,20 @@ flightState._scratchTrailClip = new Cesium.Cartesian3();
|
|||||||
|
|
||||||
flightState._scratchTrailHead = new Cesium.Cartesian3();
|
flightState._scratchTrailHead = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
|
||||||
/** Hysteresis latch for the tracked contact's zoom regime, plus the selection it
|
/** Hysteresis latch for the tracked contact's zoom regime, plus the selection it
|
||||||
* belongs to. Scoped per selection so a NEW target re-evaluates against the ENTER
|
* belongs to. Scoped per selection so a NEW target re-evaluates against the ENTER
|
||||||
* ceiling instead of inheriting the previous target's looser EXIT band. */
|
* ceiling instead of inheriting the previous target's looser EXIT band. */
|
||||||
|
|
||||||
flightState._trackedZoomLatched = false;
|
flightState._trackedZoomLatched = false;
|
||||||
|
|
||||||
|
|
||||||
flightState._trackedZoomLatchIcao = null;
|
flightState._trackedZoomLatchIcao = null;
|
||||||
|
|
||||||
|
|
||||||
flightState._trackedModelFailIcao = null;
|
flightState._trackedModelFailIcao = null;
|
||||||
|
|
||||||
|
|
||||||
flightState._trackedModelFailCount = 0;
|
flightState._trackedModelFailCount = 0;
|
||||||
|
|
||||||
|
|
||||||
flightState._trackedModelRetryAtMs = 0;
|
flightState._trackedModelRetryAtMs = 0;
|
||||||
|
|
||||||
|
|
||||||
/** @type {Cesium.Cartographic} Scratch for the grounded display-floor read. */
|
/** @type {Cesium.Cartographic} Scratch for the grounded display-floor read. */
|
||||||
|
|
||||||
flightState._scratchDisplayCarto = new Cesium.Cartographic();
|
flightState._scratchDisplayCarto = new Cesium.Cartographic();
|
||||||
@@ -511,7 +465,6 @@ flightState._displayFloorState = new Map();
|
|||||||
|
|
||||||
flightState._corridorEpoch = 0;
|
flightState._corridorEpoch = 0;
|
||||||
|
|
||||||
|
|
||||||
/** @type {Cesium.Cartographic} Scratch for the corridor's display-end read. */
|
/** @type {Cesium.Cartographic} Scratch for the corridor's display-end read. */
|
||||||
|
|
||||||
flightState._scratchCorridorCarto = new Cesium.Cartographic();
|
flightState._scratchCorridorCarto = new Cesium.Cartographic();
|
||||||
@@ -520,7 +473,6 @@ flightState._scratchCorridorCarto = new Cesium.Cartographic();
|
|||||||
|
|
||||||
flightState._scratchCorridorPos = new Cesium.Cartesian3();
|
flightState._scratchCorridorPos = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
|
||||||
/** IR hot-target mode (owner playtest 2026-08-16): the NVG/FLIR post-styles
|
/** IR hot-target mode (owner playtest 2026-08-16): the NVG/FLIR post-styles
|
||||||
* map LUMINANCE, so mid-gray textured models read cold and vanish into
|
* map LUMINANCE, so mid-gray textured models read cold and vanish into
|
||||||
* terrain. While a boost style is active every model renders flat white
|
* terrain. While a boost style is active every model renders flat white
|
||||||
@@ -540,8 +492,9 @@ flightState._irBoost = false;
|
|||||||
* (see setParams), so every boosted model gets the shader AT CREATION.
|
* (see setParams), so every boosted model gets the shader AT CREATION.
|
||||||
* One shared shader instance — stateless, safe across models. */
|
* One shared shader instance — stateless, safe across models. */
|
||||||
|
|
||||||
flightState._IR_UNLIT_SHADER = new Cesium.CustomShader({ lightingModel: Cesium.LightingModel.UNLIT });
|
flightState._IR_UNLIT_SHADER = new Cesium.CustomShader({
|
||||||
|
lightingModel: Cesium.LightingModel.UNLIT,
|
||||||
|
});
|
||||||
|
|
||||||
flightState._irReloadQueue = null;
|
flightState._irReloadQueue = null;
|
||||||
return flightState;
|
return flightState;
|
||||||
|
|||||||
@@ -1,20 +1,33 @@
|
|||||||
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,
|
||||||
|
parts,
|
||||||
|
layer,
|
||||||
|
resolveAsset,
|
||||||
|
}) {
|
||||||
/** Test seam for the display-floor clamp (unit-tested against real Cesium math
|
/** Test seam for the display-floor clamp (unit-tested against real Cesium math
|
||||||
* with seeded mesh cells — the drift mechanism is otherwise only reachable
|
* with seeded mesh cells — the drift mechanism is otherwise only reachable
|
||||||
* through a live poll + render loop). */
|
* through a live poll + render loop). */
|
||||||
|
|
||||||
function _floorGroundedDisplayPositionForTest(
|
function _floorGroundedDisplayPositionForTest(
|
||||||
info, pos, modelOwnsVisual, icao24 = '__test__', nowMs = Date.now(),
|
info,
|
||||||
|
pos,
|
||||||
|
modelOwnsVisual,
|
||||||
|
icao24 = '__test__',
|
||||||
|
nowMs = Date.now(),
|
||||||
) {
|
) {
|
||||||
return parts.motion._floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs);
|
return parts.motion._floorGroundedDisplayPosition(
|
||||||
|
icao24,
|
||||||
|
info,
|
||||||
|
pos,
|
||||||
|
modelOwnsVisual,
|
||||||
|
nowMs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Test hook: drops the per-contact display-floor state (hysteresis + rebuild
|
/** Test hook: drops the per-contact display-floor state (hysteresis + rebuild
|
||||||
* cache) so each case starts clean. There is no cross-contact state to reset —
|
* cache) so each case starts clean. There is no cross-contact state to reset —
|
||||||
* the clamp is per-contact all the way down. */
|
* the clamp is per-contact all the way down. */
|
||||||
@@ -23,7 +36,6 @@ function _clearDisplayFloorStateForTest() {
|
|||||||
flightState._displayFloorState.clear();
|
flightState._displayFloorState.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seed only the mutable state needed to exercise tracked-card refreshes through
|
* Seed only the mutable state needed to exercise tracked-card refreshes through
|
||||||
* the production poll reconciler. Tests still call `flightsLayer.update()`;
|
* the production poll reconciler. Tests still call `flightsLayer.update()`;
|
||||||
@@ -78,7 +90,6 @@ function _setTrackedFlightRefreshStateForTest({
|
|||||||
flightState._retryAt = 0;
|
flightState._retryAt = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Seed the authoritative snapshot outcome used by share-Follow tests. */
|
/** Seed the authoritative snapshot outcome used by share-Follow tests. */
|
||||||
|
|
||||||
function _setFlightTrackingRefreshOutcomeForTest({
|
function _setFlightTrackingRefreshOutcomeForTest({
|
||||||
@@ -97,53 +108,52 @@ function _setFlightTrackingRefreshOutcomeForTest({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Add a cached contact so tests can model a target arriving on a later feed. */
|
/** Add a cached contact so tests can model a target arriving on a later feed. */
|
||||||
|
|
||||||
function _addFlightTrackingCandidateForTest({ icao24, meta, billboard, history = [] }) {
|
function _addFlightTrackingCandidateForTest({
|
||||||
|
icao24,
|
||||||
|
meta,
|
||||||
|
billboard,
|
||||||
|
history = [],
|
||||||
|
}) {
|
||||||
flightState._billboards.set(icao24, billboard);
|
flightState._billboards.set(icao24, billboard);
|
||||||
flightState._flightData.set(icao24, meta);
|
flightState._flightData.set(icao24, meta);
|
||||||
flightState._positionHistory.set(icao24, history);
|
flightState._positionHistory.set(icao24, history);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Expose the military-suppression decision for the civil duplicate. */
|
/** Expose the military-suppression decision for the civil duplicate. */
|
||||||
|
|
||||||
function _militaryLayerSuppressesForTest(icao24) {
|
function _militaryLayerSuppressesForTest(icao24) {
|
||||||
return parts.tracking._militaryLayerSuppresses(icao24);
|
return parts.tracking._militaryLayerSuppresses(icao24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Arm the deferred restore latch directly, without a full setParams turn. */
|
/** Arm the deferred restore latch directly, without a full setParams turn. */
|
||||||
|
|
||||||
function _armFlightTrackingRestoreForTest(id, origin = 'share-restore') {
|
function _armFlightTrackingRestoreForTest(id, origin = 'share-restore') {
|
||||||
flightState._pendingTrackingRestore = id === null
|
flightState._pendingTrackingRestore =
|
||||||
|
id === null
|
||||||
? null
|
? null
|
||||||
: { id, generation: flightState._trackingIntentGeneration, origin };
|
: { id, generation: flightState._trackingIntentGeneration, origin };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Return the deferred restore target held by the production tracker. */
|
/** Return the deferred restore target held by the production tracker. */
|
||||||
|
|
||||||
function _pendingFlightTrackingRestoreForTest() {
|
function _pendingFlightTrackingRestoreForTest() {
|
||||||
return flightState._pendingTrackingRestore?.id ?? null;
|
return flightState._pendingTrackingRestore?.id ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Exercise the production deferred-restore retry after a simulated feed refresh. */
|
/** Exercise the production deferred-restore retry after a simulated feed refresh. */
|
||||||
|
|
||||||
function _applyPendingFlightTrackingRestoreForTest() {
|
function _applyPendingFlightTrackingRestoreForTest() {
|
||||||
return parts.tracking._applyPendingTrackingRestore();
|
return parts.tracking._applyPendingTrackingRestore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Set the exact Cockpit subject through the production state transition for focused tests. */
|
/** Set the exact Cockpit subject through the production state transition for focused tests. */
|
||||||
|
|
||||||
function _setCockpitDetectionSubjectForTest(active, subjectId = null) {
|
function _setCockpitDetectionSubjectForTest(active, subjectId = null) {
|
||||||
parts.tracking._applyCockpitState({ active, subjectId });
|
parts.tracking._applyCockpitState({ active, subjectId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Evaluate the TRACKED contact's zoom regime through the production predicate.
|
/** Evaluate the TRACKED contact's zoom regime through the production predicate.
|
||||||
* The decision is latch-bearing (default-on, hysteretic, cockpit/TR-3B-suppressed)
|
* The decision is latch-bearing (default-on, hysteretic, cockpit/TR-3B-suppressed)
|
||||||
* and otherwise only observable through a live scene, so tests drive it here. */
|
* and otherwise only observable through a live scene, so tests drive it here. */
|
||||||
@@ -152,7 +162,6 @@ function _trackedModelRegimeActiveForTest() {
|
|||||||
return parts.tracking._trackedModelRegimeActive();
|
return parts.tracking._trackedModelRegimeActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Run one frame of the production tracked-model driver (normally a
|
/** Run one frame of the production tracked-model driver (normally a
|
||||||
* `scene.preUpdate` listener) so tests can pin its bounded load retries. */
|
* `scene.preUpdate` listener) so tests can pin its bounded load retries. */
|
||||||
|
|
||||||
@@ -160,14 +169,14 @@ function _updateTrackedModelForTest() {
|
|||||||
return parts.rendering._updateTrackedModel();
|
return parts.rendering._updateTrackedModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Evaluate the production tracked-billboard handoff colour for focused tests. */
|
/** Evaluate the production tracked-billboard handoff colour for focused tests. */
|
||||||
|
|
||||||
function _trackedBillboardColorForTest() {
|
function _trackedBillboardColorForTest() {
|
||||||
return parts.rendering._modelOwnsVisual(flightState._trackedIcao) ? CYAN_TRANSPARENT : Cesium.Color.CYAN;
|
return parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
|
? CYAN_TRANSPARENT
|
||||||
|
: Cesium.Color.CYAN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Drive the exact fleet billboard-to-model handoff used by `_fleetTick`. */
|
/** Drive the exact fleet billboard-to-model handoff used by `_fleetTick`. */
|
||||||
|
|
||||||
function _driveFleetModelHandoffForTest({ icao24, position, course = 0 }) {
|
function _driveFleetModelHandoffForTest({ icao24, position, course = 0 }) {
|
||||||
@@ -180,12 +189,27 @@ function _driveFleetModelHandoffForTest({ icao24, position, course = 0 }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Exercise the exact asynchronous fleet loader and return its admitted model. */
|
/** Exercise the exact asynchronous fleet loader and return its admitted model. */
|
||||||
|
|
||||||
async function _ensureFleetModelForTest(icao24) {
|
async function _ensureFleetModelForTest(icao24) {
|
||||||
await parts.rendering._ensureModel(icao24);
|
await parts.rendering._ensureModel(icao24);
|
||||||
return flightState._models.get(icao24) || null;
|
return flightState._models.get(icao24) || null;
|
||||||
}
|
}
|
||||||
return { _floorGroundedDisplayPositionForTest, _clearDisplayFloorStateForTest, _setTrackedFlightRefreshStateForTest, _setFlightTrackingRefreshOutcomeForTest, _addFlightTrackingCandidateForTest, _militaryLayerSuppressesForTest, _armFlightTrackingRestoreForTest, _pendingFlightTrackingRestoreForTest, _applyPendingFlightTrackingRestoreForTest, _setCockpitDetectionSubjectForTest, _trackedModelRegimeActiveForTest, _updateTrackedModelForTest, _trackedBillboardColorForTest, _driveFleetModelHandoffForTest, _ensureFleetModelForTest };
|
return {
|
||||||
|
_floorGroundedDisplayPositionForTest,
|
||||||
|
_clearDisplayFloorStateForTest,
|
||||||
|
_setTrackedFlightRefreshStateForTest,
|
||||||
|
_setFlightTrackingRefreshOutcomeForTest,
|
||||||
|
_addFlightTrackingCandidateForTest,
|
||||||
|
_militaryLayerSuppressesForTest,
|
||||||
|
_armFlightTrackingRestoreForTest,
|
||||||
|
_pendingFlightTrackingRestoreForTest,
|
||||||
|
_applyPendingFlightTrackingRestoreForTest,
|
||||||
|
_setCockpitDetectionSubjectForTest,
|
||||||
|
_trackedModelRegimeActiveForTest,
|
||||||
|
_updateTrackedModelForTest,
|
||||||
|
_trackedBillboardColorForTest,
|
||||||
|
_driveFleetModelHandoffForTest,
|
||||||
|
_ensureFleetModelForTest,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
+333
-125
@@ -1,20 +1,46 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js';
|
import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js';
|
||||||
import { trackedModelZoomActive } from '../../data/trackedModelRegime.js';
|
import { trackedModelZoomActive } from '../../data/trackedModelRegime.js';
|
||||||
import { screenProjectedRotation, stabilizeScreenRotation } from '../../data/iconOrientation.js';
|
import {
|
||||||
|
screenProjectedRotation,
|
||||||
|
stabilizeScreenRotation,
|
||||||
|
} from '../../data/iconOrientation.js';
|
||||||
import { trailHeadStart } from '../../data/modelVisualAnchor.js';
|
import { trailHeadStart } from '../../data/modelVisualAnchor.js';
|
||||||
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
|
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
|
||||||
import { routePlausible } from '../../data/routePlausible.js';
|
import { routePlausible } from '../../data/routePlausible.js';
|
||||||
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
||||||
import { bindTrackingClickGesture, isTrackingSelectionGesture, isTrackingClickGesture } from '../../data/trackingClickGesture.js';
|
import {
|
||||||
import { TRACKED_MODEL_MAX_LOAD_FAILS, TRACKED_MODEL_RETRY_BACKOFF_MS, TRAIL_MAX_POINTS, RENDER_DELAY_SEC, TRAIL_COLOR, CYAN_TRANSPARENT } from './policy.js';
|
bindTrackingClickGesture,
|
||||||
|
isTrackingSelectionGesture,
|
||||||
|
isTrackingClickGesture,
|
||||||
|
} from '../../data/trackingClickGesture.js';
|
||||||
|
import {
|
||||||
|
TRACKED_MODEL_MAX_LOAD_FAILS,
|
||||||
|
TRACKED_MODEL_RETRY_BACKOFF_MS,
|
||||||
|
TRAIL_MAX_POINTS,
|
||||||
|
RENDER_DELAY_SEC,
|
||||||
|
TRAIL_COLOR,
|
||||||
|
CYAN_TRANSPARENT,
|
||||||
|
} from './policy.js';
|
||||||
|
|
||||||
export function createTracking({ flightState, services, parts, layer, resolveAsset }) {
|
export function createTracking({
|
||||||
const { selectTrackedSubjectContext, clearTrackedSubjectContext, refreshTrackedSubjectContext } = services.context;
|
flightState,
|
||||||
const { isTr3b, tr3bTypeLabel, tr3bConvertedIds } = services.aircraftPresentation;
|
services,
|
||||||
|
parts,
|
||||||
|
layer,
|
||||||
|
resolveAsset,
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
selectTrackedSubjectContext,
|
||||||
|
clearTrackedSubjectContext,
|
||||||
|
refreshTrackedSubjectContext,
|
||||||
|
} = services.context;
|
||||||
|
const { isTr3b, tr3bTypeLabel, tr3bConvertedIds } =
|
||||||
|
services.aircraftPresentation;
|
||||||
const { createTrail } = services.trails;
|
const { createTrail } = services.trails;
|
||||||
const { ensureGeoidReady, geoidHeight } = services.geoid;
|
const { ensureGeoidReady, geoidHeight } = services.geoid;
|
||||||
const { resolveGroundFloorCellsBounded, floorAltitudeM, cachedGroundFloor } = services.groundFloor;
|
const { resolveGroundFloorCellsBounded, floorAltitudeM, cachedGroundFloor } =
|
||||||
|
services.groundFloor;
|
||||||
const { clearFocusTarget } = services.focus;
|
const { clearFocusTarget } = services.focus;
|
||||||
const { isMilitaryLayerActive, isMilitaryIcao } = services.militaryRegistry;
|
const { isMilitaryLayerActive, isMilitaryIcao } = services.militaryRegistry;
|
||||||
const { trackedLabelModelFromText, refreshTrackedReadout } = services.readout;
|
const { trackedLabelModelFromText, refreshTrackedReadout } = services.readout;
|
||||||
@@ -22,20 +48,22 @@ const { applyTrackedCameraFrame } = services.camera;
|
|||||||
const { resolvePickId, isOwnedByOtherLayer } = services.picking;
|
const { resolvePickId, isOwnedByOtherLayer } = services.picking;
|
||||||
const flightsLayer = layer;
|
const flightsLayer = layer;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _emitAwarenessEvent(type, detail) {
|
function _emitAwarenessEvent(type, detail) {
|
||||||
if (typeof window === 'undefined' || !window.dispatchEvent || typeof CustomEvent === 'undefined') return;
|
if (
|
||||||
|
typeof window === 'undefined' ||
|
||||||
|
!window.dispatchEvent ||
|
||||||
|
typeof CustomEvent === 'undefined'
|
||||||
|
)
|
||||||
|
return;
|
||||||
window.dispatchEvent(new CustomEvent(type, { detail }));
|
window.dispatchEvent(new CustomEvent(type, { detail }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _publishTrackedSelection(icao24, origin = 'programmatic') {
|
function _publishTrackedSelection(icao24, origin = 'programmatic') {
|
||||||
const bb = flightState._billboards.get(icao24);
|
const bb = flightState._billboards.get(icao24);
|
||||||
const info = flightState._flightData.get(icao24);
|
const info = flightState._flightData.get(icao24);
|
||||||
if (!bb?.position || !info) return false;
|
if (!bb?.position || !info) return false;
|
||||||
if (flightState._trackedEntity) flightState._trackedEntity.gevSelectionOrigin = origin;
|
if (flightState._trackedEntity)
|
||||||
|
flightState._trackedEntity.gevSelectionOrigin = origin;
|
||||||
_emitAwarenessEvent('gev:awareness-subject-selected', {
|
_emitAwarenessEvent('gev:awareness-subject-selected', {
|
||||||
layerId: 'flights',
|
layerId: 'flights',
|
||||||
id: icao24,
|
id: icao24,
|
||||||
@@ -51,7 +79,6 @@ function _publishTrackedSelection(icao24, origin = 'programmatic') {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describe the selected contact for the shared context slot the voice tools
|
* Describe the selected contact for the shared context slot the voice tools
|
||||||
* and Cockpit read. Values are the LIVE descriptor, not a selection-time
|
* and Cockpit read. Values are the LIVE descriptor, not a selection-time
|
||||||
@@ -64,7 +91,8 @@ function _contextSubjectMetadata(icao24) {
|
|||||||
const described = parts.queries._describeFlight(icao24);
|
const described = parts.queries._describeFlight(icao24);
|
||||||
if (!described) return null;
|
if (!described) return null;
|
||||||
const altFt = Math.round((described.altitudeM || 0) * 3.28084);
|
const altFt = Math.round((described.altitudeM || 0) * 3.28084);
|
||||||
const route = described.route && _routeIsPlausible(icao24, described.route)
|
const route =
|
||||||
|
described.route && _routeIsPlausible(icao24, described.route)
|
||||||
? `${described.route.origin.code} → ${described.route.destination.code}`
|
? `${described.route.origin.code} → ${described.route.destination.code}`
|
||||||
: null;
|
: null;
|
||||||
return {
|
return {
|
||||||
@@ -72,22 +100,32 @@ function _contextSubjectMetadata(icao24) {
|
|||||||
layerId: 'flights',
|
layerId: 'flights',
|
||||||
layerName: 'Live Flights',
|
layerName: 'Live Flights',
|
||||||
source: flightState._lastSource,
|
source: flightState._lastSource,
|
||||||
label: parts.queries._contactLabel(icao24, flightState._flightData.get(icao24)),
|
label: parts.queries._contactLabel(
|
||||||
|
icao24,
|
||||||
|
flightState._flightData.get(icao24),
|
||||||
|
),
|
||||||
latitude: described.latitude,
|
latitude: described.latitude,
|
||||||
longitude: described.longitude,
|
longitude: described.longitude,
|
||||||
// Flat text only: the voice payload compacts properties through a
|
// Flat text only: the voice payload compacts properties through a
|
||||||
// string cleaner that drops nested objects.
|
// string cleaner that drops nested objects.
|
||||||
properties: {
|
properties: {
|
||||||
name: parts.queries._contactLabel(icao24, flightState._flightData.get(icao24)),
|
name: parts.queries._contactLabel(
|
||||||
|
icao24,
|
||||||
|
flightState._flightData.get(icao24),
|
||||||
|
),
|
||||||
operator: described.airline || '',
|
operator: described.airline || '',
|
||||||
callsign: described.callsign || '',
|
callsign: described.callsign || '',
|
||||||
registration: described.registration || '',
|
registration: described.registration || '',
|
||||||
type: described.typeName || described.typeCode || '',
|
type: described.typeName || described.typeCode || '',
|
||||||
altitude: described.onGround ? 'on ground' : `${altFt.toLocaleString('en-US')} ft`,
|
altitude: described.onGround
|
||||||
|
? 'on ground'
|
||||||
|
: `${altFt.toLocaleString('en-US')} ft`,
|
||||||
speed: Number.isFinite(described.velocityMps)
|
speed: Number.isFinite(described.velocityMps)
|
||||||
? `${Math.round(described.velocityMps * 1.944)} kt`
|
? `${Math.round(described.velocityMps * 1.944)} kt`
|
||||||
: '',
|
: '',
|
||||||
heading: Number.isFinite(described.track) ? `${Math.round(described.track)}°` : '',
|
heading: Number.isFinite(described.track)
|
||||||
|
? `${Math.round(described.track)}°`
|
||||||
|
: '',
|
||||||
route: route || '',
|
route: route || '',
|
||||||
icao24,
|
icao24,
|
||||||
// Honesty cue: the contact is coasting on dead reckoning, so the
|
// Honesty cue: the contact is coasting on dead reckoning, so the
|
||||||
@@ -97,13 +135,10 @@ function _contextSubjectMetadata(icao24) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _isExplicitTrackingOrigin(origin) {
|
function _isExplicitTrackingOrigin(origin) {
|
||||||
return origin === 'user' || origin === 'voice' || origin === 'tool';
|
return origin === 'user' || origin === 'voice' || origin === 'tool';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh the Cockpit AIR near/far band without consulting model state.
|
* Refresh the Cockpit AIR near/far band without consulting model state.
|
||||||
* Near contacts keep their 2D aircraft silhouette when 3D is off, loading, or
|
* Near contacts keep their 2D aircraft silhouette when 3D is off, loading, or
|
||||||
@@ -111,8 +146,12 @@ function _isExplicitTrackingOrigin(origin) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function _refreshCockpitNearContacts() {
|
function _refreshCockpitNearContacts() {
|
||||||
if (!flightState._cockpitContactMode || !flightState._viewer?.camera?.positionWC) {
|
if (
|
||||||
if (flightState._cockpitNearContacts.size) flightState._cockpitNearContacts = new Set();
|
!flightState._cockpitContactMode ||
|
||||||
|
!flightState._viewer?.camera?.positionWC
|
||||||
|
) {
|
||||||
|
if (flightState._cockpitNearContacts.size)
|
||||||
|
flightState._cockpitNearContacts = new Set();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const previous = flightState._cockpitNearContacts;
|
const previous = flightState._cockpitNearContacts;
|
||||||
@@ -121,7 +160,10 @@ function _refreshCockpitNearContacts() {
|
|||||||
if (icao24 === flightState._trackedIcao || !bb?.position) continue;
|
if (icao24 === flightState._trackedIcao || !bb?.position) continue;
|
||||||
distancesSquared.push([
|
distancesSquared.push([
|
||||||
icao24,
|
icao24,
|
||||||
Cesium.Cartesian3.distanceSquared(flightState._viewer.camera.positionWC, bb.position),
|
Cesium.Cartesian3.distanceSquared(
|
||||||
|
flightState._viewer.camera.positionWC,
|
||||||
|
bb.position,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
const next = nextCockpitNearContacts(
|
const next = nextCockpitNearContacts(
|
||||||
@@ -140,7 +182,6 @@ function _refreshCockpitNearContacts() {
|
|||||||
if (presentationChanged) flightState._lastCamPoseSig = '';
|
if (presentationChanged) flightState._lastCamPoseSig = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Switch all current and future ambient contacts between silhouettes and cockpit pips. */
|
/** Switch all current and future ambient contacts between silhouettes and cockpit pips. */
|
||||||
|
|
||||||
function _setCockpitContactMode(active) {
|
function _setCockpitContactMode(active) {
|
||||||
@@ -156,22 +197,22 @@ function _setCockpitContactMode(active) {
|
|||||||
if (flightState._modelCollection) flightState._modelCollection.show = true;
|
if (flightState._modelCollection) flightState._modelCollection.show = true;
|
||||||
flightState._trail?.setVisible(!next);
|
flightState._trail?.setVisible(!next);
|
||||||
if (flightState._trailHeadEntity) flightState._trailHeadEntity.show = !next;
|
if (flightState._trailHeadEntity) flightState._trailHeadEntity.show = !next;
|
||||||
for (const [icao24, bb] of flightState._billboards) parts.rendering._applyFleetBillboardPresentation(icao24, bb);
|
for (const [icao24, bb] of flightState._billboards)
|
||||||
|
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
|
||||||
flightState._lastCamPoseSig = '';
|
flightState._lastCamPoseSig = '';
|
||||||
flightState._lastFleetTickMs = 0;
|
flightState._lastFleetTickMs = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _applyCockpitState(detail = {}) {
|
function _applyCockpitState(detail = {}) {
|
||||||
const active = detail?.active === true;
|
const active = detail?.active === true;
|
||||||
flightState._cockpitSubjectId = active
|
flightState._cockpitSubjectId = active
|
||||||
? String(detail?.subjectId || '').trim().toLowerCase() || null
|
? String(detail?.subjectId || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase() || null
|
||||||
: null;
|
: null;
|
||||||
_setCockpitContactMode(active);
|
_setCockpitContactMode(active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Clear every per-SELECTION tracked-model latch: the zoom hysteresis band and
|
/** Clear every per-SELECTION tracked-model latch: the zoom hysteresis band and
|
||||||
* the load-failure bound. Called from each path that changes which contact is
|
* the load-failure bound. Called from each path that changes which contact is
|
||||||
* selected — deselect, re-track, cross-layer handoff, init, destroy.
|
* selected — deselect, re-track, cross-layer handoff, init, destroy.
|
||||||
@@ -192,16 +233,16 @@ function _resetTrackedSelectionState() {
|
|||||||
flightState._trackedModelRetryAtMs = 0;
|
flightState._trackedModelRetryAtMs = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Whether the driver may start another tracked-model load this frame. */
|
/** Whether the driver may start another tracked-model load this frame. */
|
||||||
|
|
||||||
function _trackedModelLoadAllowed(nowMs = Date.now()) {
|
function _trackedModelLoadAllowed(nowMs = Date.now()) {
|
||||||
if (flightState._trackedModelFailIcao !== flightState._trackedIcao) return true; // untried selection
|
if (flightState._trackedModelFailIcao !== flightState._trackedIcao)
|
||||||
if (flightState._trackedModelFailCount >= TRACKED_MODEL_MAX_LOAD_FAILS) return false;
|
return true; // untried selection
|
||||||
|
if (flightState._trackedModelFailCount >= TRACKED_MODEL_MAX_LOAD_FAILS)
|
||||||
|
return false;
|
||||||
return nowMs >= flightState._trackedModelRetryAtMs;
|
return nowMs >= flightState._trackedModelRetryAtMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Record a rejected tracked-model load and arm the backoff / give-up latch. */
|
/** Record a rejected tracked-model load and arm the backoff / give-up latch. */
|
||||||
|
|
||||||
function _noteTrackedModelLoadFailure(url, err) {
|
function _noteTrackedModelLoadFailure(url, err) {
|
||||||
@@ -210,17 +251,17 @@ function _noteTrackedModelLoadFailure(url, err) {
|
|||||||
flightState._trackedModelFailCount = 0;
|
flightState._trackedModelFailCount = 0;
|
||||||
}
|
}
|
||||||
flightState._trackedModelFailCount += 1;
|
flightState._trackedModelFailCount += 1;
|
||||||
flightState._trackedModelRetryAtMs = Date.now() + TRACKED_MODEL_RETRY_BACKOFF_MS;
|
flightState._trackedModelRetryAtMs =
|
||||||
|
Date.now() + TRACKED_MODEL_RETRY_BACKOFF_MS;
|
||||||
if (flightState._trackedModelFailCount >= TRACKED_MODEL_MAX_LOAD_FAILS) {
|
if (flightState._trackedModelFailCount >= TRACKED_MODEL_MAX_LOAD_FAILS) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Data:Flights] tracked 3D model gave up after ${flightState._trackedModelFailCount} failed loads of ${url} — `
|
`[Data:Flights] tracked 3D model gave up after ${flightState._trackedModelFailCount} failed loads of ${url} — ` +
|
||||||
+ 'this contact stays 2D until another is selected',
|
'this contact stays 2D until another is selected',
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The TRACKED aircraft's own model regime — DEFAULT-ON, camera-distance driven
|
* The TRACKED aircraft's own model regime — DEFAULT-ON, camera-distance driven
|
||||||
* (owner directive 2026-08-19). Unlike the fleet, this does NOT consult the
|
* (owner directive 2026-08-19). Unlike the fleet, this does NOT consult the
|
||||||
@@ -249,7 +290,11 @@ function _trackedModelRegimeActive() {
|
|||||||
// A converted TR-3B has no 3D asset — suppressing the regime keeps its
|
// A converted TR-3B has no 3D asset — suppressing the regime keeps its
|
||||||
// tracked billboard fully opaque (the colour callback reads this too), so
|
// tracked billboard fully opaque (the colour callback reads this too), so
|
||||||
// the triangle stays the visual all the way in.
|
// the triangle stays the visual all the way in.
|
||||||
if (!flightState._trackedIcao || flightState._cockpitContactMode || isTr3b(flightState._trackedIcao)) {
|
if (
|
||||||
|
!flightState._trackedIcao ||
|
||||||
|
flightState._cockpitContactMode ||
|
||||||
|
isTr3b(flightState._trackedIcao)
|
||||||
|
) {
|
||||||
flightState._trackedZoomLatched = false;
|
flightState._trackedZoomLatched = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -260,12 +305,13 @@ function _trackedModelRegimeActive() {
|
|||||||
return flightState._trackedZoomLatched;
|
return flightState._trackedZoomLatched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Seed the tracked billboard's 2D orientation before a 3D→2D handoff. */
|
/** Seed the tracked billboard's 2D orientation before a 3D→2D handoff. */
|
||||||
|
|
||||||
function _syncTracked2dRotation() {
|
function _syncTracked2dRotation() {
|
||||||
if (!flightState._trackedIcao || !flightState._viewer) return;
|
if (!flightState._trackedIcao || !flightState._viewer) return;
|
||||||
const pos = parts.motion._trackedDisplayCached() || flightState._billboards.get(flightState._trackedIcao)?.position;
|
const pos =
|
||||||
|
parts.motion._trackedDisplayCached() ||
|
||||||
|
flightState._billboards.get(flightState._trackedIcao)?.position;
|
||||||
if (!pos) return;
|
if (!pos) return;
|
||||||
const projected = screenProjectedRotation(
|
const projected = screenProjectedRotation(
|
||||||
flightState._viewer.scene,
|
flightState._viewer.scene,
|
||||||
@@ -273,11 +319,14 @@ function _syncTracked2dRotation() {
|
|||||||
parts.motion._trackedDisplayCourse(),
|
parts.motion._trackedDisplayCourse(),
|
||||||
flightState._lastTrackedRotation,
|
flightState._lastTrackedRotation,
|
||||||
);
|
);
|
||||||
const rotation = stabilizeScreenRotation(flightState._lastTrackedRotation, projected, 0);
|
const rotation = stabilizeScreenRotation(
|
||||||
|
flightState._lastTrackedRotation,
|
||||||
|
projected,
|
||||||
|
0,
|
||||||
|
);
|
||||||
if (rotation !== null) flightState._lastTrackedRotation = rotation;
|
if (rotation !== null) flightState._lastTrackedRotation = rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append one fix to the tracked aircraft's trail accumulation and refresh
|
* Append one fix to the tracked aircraft's trail accumulation and refresh
|
||||||
* the rendered trail. Caller passes an owned (cloned) Cartesian3.
|
* the rendered trail. Caller passes an owned (cloned) Cartesian3.
|
||||||
@@ -286,11 +335,11 @@ function _syncTracked2dRotation() {
|
|||||||
|
|
||||||
function _appendTrailFix(position) {
|
function _appendTrailFix(position) {
|
||||||
flightState._trailPositions.push(position);
|
flightState._trailPositions.push(position);
|
||||||
if (flightState._trailPositions.length > TRAIL_MAX_POINTS) flightState._trailPositions.shift();
|
if (flightState._trailPositions.length > TRAIL_MAX_POINTS)
|
||||||
|
flightState._trailPositions.shift();
|
||||||
_refreshTrailDisplay();
|
_refreshTrailDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the trail with its head clamped to the render-behind display
|
* Renders the trail with its head clamped to the render-behind display
|
||||||
* position. Raw newest fixes run up to RENDER_DELAY_SEC ahead of the
|
* position. Raw newest fixes run up to RENDER_DELAY_SEC ahead of the
|
||||||
@@ -307,10 +356,13 @@ function _refreshTrailDisplay() {
|
|||||||
// delayed dead-reckoned head, so the body primitive only rebuilds on a real fix
|
// delayed dead-reckoned head, so the body primitive only rebuilds on a real fix
|
||||||
// (poll cadence), never at motion cadence.
|
// (poll cadence), never at motion cadence.
|
||||||
if (!flightState._trail) return;
|
if (!flightState._trail) return;
|
||||||
flightState._trail.setPositions(flightState._trailPositions.length > 1 ? flightState._trailPositions.slice(0, -1) : flightState._trailPositions);
|
flightState._trail.setPositions(
|
||||||
|
flightState._trailPositions.length > 1
|
||||||
|
? flightState._trailPositions.slice(0, -1)
|
||||||
|
: flightState._trailPositions,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the trail for a newly tracked aircraft: seed it with the short
|
* Start the trail for a newly tracked aircraft: seed it with the short
|
||||||
* dead-reckoning history (chronological), render immediately, then
|
* dead-reckoning history (chronological), render immediately, then
|
||||||
@@ -327,7 +379,9 @@ function _startTrail(icao24) {
|
|||||||
// would draw the trail in front of the plane. They join the trail via _appendTrailFix
|
// would draw the trail in front of the plane. They join the trail via _appendTrailFix
|
||||||
// as they age past the delay.
|
// as they age past the delay.
|
||||||
const seedRenderTime = Cesium.JulianDate.addSeconds(
|
const seedRenderTime = Cesium.JulianDate.addSeconds(
|
||||||
Cesium.JulianDate.now(), -RENDER_DELAY_SEC, flightState._scratchWarmupTime
|
Cesium.JulianDate.now(),
|
||||||
|
-RENDER_DELAY_SEC,
|
||||||
|
flightState._scratchWarmupTime,
|
||||||
);
|
);
|
||||||
for (const fix of history) {
|
for (const fix of history) {
|
||||||
if (Cesium.JulianDate.lessThanOrEquals(fix.time, seedRenderTime)) {
|
if (Cesium.JulianDate.lessThanOrEquals(fix.time, seedRenderTime)) {
|
||||||
@@ -335,7 +389,10 @@ function _startTrail(icao24) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!flightState._trail && flightState._viewer) {
|
if (!flightState._trail && flightState._viewer) {
|
||||||
flightState._trail = createTrail(flightState._viewer, { color: TRAIL_COLOR, width: 2.5 });
|
flightState._trail = createTrail(flightState._viewer, {
|
||||||
|
color: TRAIL_COLOR,
|
||||||
|
width: 2.5,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
flightState._trail?.setVisible(!flightState._cockpitContactMode);
|
flightState._trail?.setVisible(!flightState._cockpitContactMode);
|
||||||
// Live head segment: last fix → current dead-reckoned icon, updated every frame via
|
// Live head segment: last fix → current dead-reckoned icon, updated every frame via
|
||||||
@@ -355,12 +412,22 @@ function _startTrail(icao24) {
|
|||||||
// point would be the sole raw fix — which is ~now, AHEAD of the delayed icon —
|
// point would be the sole raw fix — which is ~now, AHEAD of the delayed icon —
|
||||||
// so the segment would draw IN FRONT of the plane. Likewise during warm-up the
|
// so the segment would draw IN FRONT of the plane. Likewise during warm-up the
|
||||||
// icon predates all real history, so there is no valid body point behind it.
|
// icon predates all real history, so there is no valid body point behind it.
|
||||||
if (!flightState._trackedIcao || flightState._trailPositions.length < 2 || parts.motion._isTrackWarmingUp()) return [];
|
if (
|
||||||
const head = parts.motion._trackedTrailCached() || parts.motion._trackedDisplayPosition(flightState._trackedIcao);
|
!flightState._trackedIcao ||
|
||||||
|
flightState._trailPositions.length < 2 ||
|
||||||
|
parts.motion._isTrackWarmingUp()
|
||||||
|
)
|
||||||
|
return [];
|
||||||
|
const head =
|
||||||
|
parts.motion._trackedTrailCached() ||
|
||||||
|
parts.motion._trackedDisplayPosition(flightState._trackedIcao);
|
||||||
if (!head) return [];
|
if (!head) return [];
|
||||||
// body[n−2] (last displayed body point) → delayed head: runs FORWARD, never a
|
// body[n−2] (last displayed body point) → delayed head: runs FORWARD, never a
|
||||||
// backward/reversing segment.
|
// backward/reversing segment.
|
||||||
const start = flightState._trailPositions[flightState._trailPositions.length - 2];
|
const start =
|
||||||
|
flightState._trailPositions[
|
||||||
|
flightState._trailPositions.length - 2
|
||||||
|
];
|
||||||
// On a contact that has not moved this segment runs from inside the
|
// On a contact that has not moved this segment runs from inside the
|
||||||
// model out to its own anchor — a line through the fuselage. The END
|
// model out to its own anchor — a line through the fuselage. The END
|
||||||
// never gives, so a moving trail still terminates on the tail; the
|
// never gives, so a moving trail still terminates on the tail; the
|
||||||
@@ -369,7 +436,11 @@ function _startTrail(icao24) {
|
|||||||
// See trailHeadStart. Read `head` in place and clone only on the draw
|
// See trailHeadStart. Read `head` in place and clone only on the draw
|
||||||
// path — a suppressed parked contact runs this every frame.
|
// path — a suppressed parked contact runs this every frame.
|
||||||
const from = trailHeadStart(
|
const from = trailHeadStart(
|
||||||
start, head, parts.motion._trackedModelCenterWorld(), parts.motion._trackedModelEnvelopeM(), flightState._scratchTrailHead,
|
start,
|
||||||
|
head,
|
||||||
|
parts.motion._trackedModelCenterWorld(),
|
||||||
|
parts.motion._trackedModelEnvelopeM(),
|
||||||
|
flightState._scratchTrailHead,
|
||||||
);
|
);
|
||||||
if (!from) return [];
|
if (!from) return [];
|
||||||
return [from, Cesium.Cartesian3.clone(head)];
|
return [from, Cesium.Cartesian3.clone(head)];
|
||||||
@@ -378,7 +449,8 @@ function _startTrail(icao24) {
|
|||||||
material: Cesium.Color.fromCssColorString(TRAIL_COLOR).withAlpha(0.9),
|
material: Cesium.Color.fromCssColorString(TRAIL_COLOR).withAlpha(0.9),
|
||||||
// Round 4: the head must never vanish into the mesh either (dimmed
|
// Round 4: the head must never vanish into the mesh either (dimmed
|
||||||
// when occluded so depth still reads).
|
// when occluded so depth still reads).
|
||||||
depthFailMaterial: Cesium.Color.fromCssColorString(TRAIL_COLOR).withAlpha(0.45),
|
depthFailMaterial:
|
||||||
|
Cesium.Color.fromCssColorString(TRAIL_COLOR).withAlpha(0.45),
|
||||||
arcType: Cesium.ArcType.GEODESIC, // round 8: consistent with the trail body (no chords)
|
arcType: Cesium.ArcType.GEODESIC, // round 8: consistent with the trail body (no chords)
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -391,7 +463,6 @@ function _startTrail(icao24) {
|
|||||||
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
|
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fire-and-forget OpenSky /tracks backfill (PRD F1). On success, splices
|
* Fire-and-forget OpenSky /tracks backfill (PRD F1). On success, splices
|
||||||
* waypoints strictly older than the oldest seeded fix AHEAD of the locally
|
* waypoints strictly older than the oldest seeded fix AHEAD of the locally
|
||||||
@@ -406,12 +477,25 @@ function _startTrail(icao24) {
|
|||||||
async function _backfillTrail(icao24, token, oldestFixEpochSec) {
|
async function _backfillTrail(icao24, token, oldestFixEpochSec) {
|
||||||
let path = null;
|
let path = null;
|
||||||
try {
|
try {
|
||||||
const track = await flightState._source.getTrack?.(flightState._flightData.get(icao24)?.sourceReference ?? icao24, { signal: AbortSignal.any([flightState.lifetime.signal, AbortSignal.timeout(8000)]) });
|
const track = await flightState._source.getTrack?.(
|
||||||
|
flightState._flightData.get(icao24)?.sourceReference ?? icao24,
|
||||||
|
{
|
||||||
|
signal: AbortSignal.any([
|
||||||
|
flightState.lifetime.signal,
|
||||||
|
AbortSignal.timeout(8000),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
);
|
||||||
path = track?.records ?? null;
|
path = track?.records ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
return; // silent fallback to the accumulated trail
|
return; // silent fallback to the accumulated trail
|
||||||
}
|
}
|
||||||
if (!path || token !== flightState._trailBackfillToken || icao24 !== flightState._trackedIcao) return;
|
if (
|
||||||
|
!path ||
|
||||||
|
token !== flightState._trailBackfillToken ||
|
||||||
|
icao24 !== flightState._trackedIcao
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
// OpenSky track waypoints: [time, latitude, longitude, baro_altitude, true_track, on_ground]
|
// OpenSky track waypoints: [time, latitude, longitude, baro_altitude, true_track, on_ground]
|
||||||
// Height-datum fix (Task 6): /tracks only ever reports barometric/MSL altitude
|
// Height-datum fix (Task 6): /tracks only ever reports barometric/MSL altitude
|
||||||
@@ -422,7 +506,12 @@ async function _backfillTrail(icao24, token, oldestFixEpochSec) {
|
|||||||
await ensureGeoidReady();
|
await ensureGeoidReady();
|
||||||
const parsed = [];
|
const parsed = [];
|
||||||
for (const waypoint of path) {
|
for (const waypoint of path) {
|
||||||
const { observedAtMs, latitude: lat, longitude: lon, baroAltitudeM: baroAlt } = waypoint;
|
const {
|
||||||
|
observedAtMs,
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lon,
|
||||||
|
baroAltitudeM: baroAlt,
|
||||||
|
} = waypoint;
|
||||||
const time = observedAtMs / 1000;
|
const time = observedAtMs / 1000;
|
||||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
|
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
|
||||||
if (!Number.isFinite(time) || time >= oldestFixEpochSec) continue;
|
if (!Number.isFinite(time) || time >= oldestFixEpochSec) continue;
|
||||||
@@ -443,12 +532,18 @@ async function _backfillTrail(icao24, token, oldestFixEpochSec) {
|
|||||||
await resolveGroundFloorCellsBounded(parsed);
|
await resolveGroundFloorCellsBounded(parsed);
|
||||||
// Re-check the backfill token after the await (same guard as post-fetch):
|
// Re-check the backfill token after the await (same guard as post-fetch):
|
||||||
// tracking may have moved on while the terrain race was in flight.
|
// tracking may have moved on while the terrain race was in flight.
|
||||||
if (token !== flightState._trailBackfillToken || icao24 !== flightState._trackedIcao) return;
|
if (
|
||||||
|
token !== flightState._trailBackfillToken ||
|
||||||
|
icao24 !== flightState._trackedIcao
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
const older = [];
|
const older = [];
|
||||||
let lastAltM = null; // carry-forward for no-baro points whose cell isn't warm yet
|
let lastAltM = null; // carry-forward for no-baro points whose cell isn't warm yet
|
||||||
for (const { lat, lon, baroAlt } of parsed) {
|
for (const { lat, lon, baroAlt } of parsed) {
|
||||||
const baroM = Number.isFinite(baroAlt) ? baroAlt + geoidHeight(lat, lon) : null;
|
const baroM = Number.isFinite(baroAlt)
|
||||||
|
? baroAlt + geoidHeight(lat, lon)
|
||||||
|
: null;
|
||||||
let altM = floorAltitudeM(baroM, cachedGroundFloor(lat, lon));
|
let altM = floorAltitudeM(baroM, cachedGroundFloor(lat, lon));
|
||||||
// No baro + unresolved floor: hold the previous waypoint's altitude
|
// No baro + unresolved floor: hold the previous waypoint's altitude
|
||||||
// (continuity — never a dive/spike to a made-up height). Leading points
|
// (continuity — never a dive/spike to a made-up height). Leading points
|
||||||
@@ -460,12 +555,13 @@ async function _backfillTrail(icao24, token, oldestFixEpochSec) {
|
|||||||
|
|
||||||
flightState._trailPositions = older.concat(flightState._trailPositions);
|
flightState._trailPositions = older.concat(flightState._trailPositions);
|
||||||
if (flightState._trailPositions.length > TRAIL_MAX_POINTS) {
|
if (flightState._trailPositions.length > TRAIL_MAX_POINTS) {
|
||||||
flightState._trailPositions = flightState._trailPositions.slice(flightState._trailPositions.length - TRAIL_MAX_POINTS);
|
flightState._trailPositions = flightState._trailPositions.slice(
|
||||||
|
flightState._trailPositions.length - TRAIL_MAX_POINTS,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_refreshTrailDisplay();
|
_refreshTrailDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the rendered trail and accumulation; invalidate pending backfills.
|
* Clear the rendered trail and accumulation; invalidate pending backfills.
|
||||||
*/
|
*/
|
||||||
@@ -474,13 +570,20 @@ function _clearTrail() {
|
|||||||
flightState._trailBackfillToken += 1;
|
flightState._trailBackfillToken += 1;
|
||||||
flightState._trailPositions = [];
|
flightState._trailPositions = [];
|
||||||
if (flightState._trail) flightState._trail.clear();
|
if (flightState._trail) flightState._trail.clear();
|
||||||
if (flightState._trailHeadEntity && flightState._viewer && !flightState._viewer.isDestroyed()) {
|
if (
|
||||||
try { flightState._viewer.entities.remove(flightState._trailHeadEntity); } catch { /* already gone */ }
|
flightState._trailHeadEntity &&
|
||||||
|
flightState._viewer &&
|
||||||
|
!flightState._viewer.isDestroyed()
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
flightState._viewer.entities.remove(flightState._trailHeadEntity);
|
||||||
|
} catch {
|
||||||
|
/* already gone */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
flightState._trailHeadEntity = null;
|
flightState._trailHeadEntity = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy the trail primitive entirely (layer disable/teardown).
|
* Destroy the trail primitive entirely (layer disable/teardown).
|
||||||
*/
|
*/
|
||||||
@@ -493,7 +596,6 @@ function _destroyTrail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop tracking the currently followed aircraft.
|
* Stop tracking the currently followed aircraft.
|
||||||
*
|
*
|
||||||
@@ -515,10 +617,10 @@ function _destroyTrail() {
|
|||||||
* only tear down on a deliberate clear.
|
* only tear down on a deliberate clear.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function _clearTracking(skipViewerUntrack = false, {
|
function _clearTracking(
|
||||||
evicted = false,
|
skipViewerUntrack = false,
|
||||||
origin = 'programmatic',
|
{ evicted = false, origin = 'programmatic' } = {},
|
||||||
} = {}) {
|
) {
|
||||||
flightState._trackedCameraFrameStop?.();
|
flightState._trackedCameraFrameStop?.();
|
||||||
flightState._trackedCameraFrameStop = null;
|
flightState._trackedCameraFrameStop = null;
|
||||||
if (!flightState._trackedIcao) {
|
if (!flightState._trackedIcao) {
|
||||||
@@ -541,7 +643,10 @@ function _clearTracking(skipViewerUntrack = false, {
|
|||||||
// Ground/military-aware restore (a plane untracked while taxiing must come
|
// Ground/military-aware restore (a plane untracked while taxiing must come
|
||||||
// back muted-gray at ground scale, not white at full scale).
|
// back muted-gray at ground scale, not white at full scale).
|
||||||
bb.color = parts.rendering._fleetBillboardColor(flightState._trackedIcao);
|
bb.color = parts.rendering._fleetBillboardColor(flightState._trackedIcao);
|
||||||
bb.scale = parts.rendering._fleetBillboardScale(flightState._trackedIcao, flightState._flightData.get(flightState._trackedIcao)?.klass);
|
bb.scale = parts.rendering._fleetBillboardScale(
|
||||||
|
flightState._trackedIcao,
|
||||||
|
flightState._flightData.get(flightState._trackedIcao)?.klass,
|
||||||
|
);
|
||||||
bb.rotation = flightState._lastTrackedRotation;
|
bb.rotation = flightState._lastTrackedRotation;
|
||||||
}
|
}
|
||||||
flightState._lastCamPoseSig = ''; // force a fleet rotation pass on the next tick
|
flightState._lastCamPoseSig = ''; // force a fleet rotation pass on the next tick
|
||||||
@@ -561,7 +666,10 @@ function _clearTracking(skipViewerUntrack = false, {
|
|||||||
parts.rendering._releaseTrackedModel();
|
parts.rendering._releaseTrackedModel();
|
||||||
_resetTrackedSelectionState(); // the zoom band + load-failure budget belong to the selection we just dropped
|
_resetTrackedSelectionState(); // the zoom band + load-failure budget belong to the selection we just dropped
|
||||||
flightState._trackedIcao = null;
|
flightState._trackedIcao = null;
|
||||||
parts.rendering._applyFleetBillboardPresentation(clearedIcao, flightState._billboards.get(clearedIcao));
|
parts.rendering._applyFleetBillboardPresentation(
|
||||||
|
clearedIcao,
|
||||||
|
flightState._billboards.get(clearedIcao),
|
||||||
|
);
|
||||||
clearTrackedSubjectContext('flights');
|
clearTrackedSubjectContext('flights');
|
||||||
_emitAwarenessEvent('gev:awareness-subject-cleared', {
|
_emitAwarenessEvent('gev:awareness-subject-cleared', {
|
||||||
layerId: 'flights',
|
layerId: 'flights',
|
||||||
@@ -575,8 +683,6 @@ function _clearTracking(skipViewerUntrack = false, {
|
|||||||
_clearTrail();
|
_clearTrail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the Military layer suppresses this civil duplicate right now.
|
* Whether the Military layer suppresses this civil duplicate right now.
|
||||||
*
|
*
|
||||||
@@ -604,25 +710,28 @@ function _militaryLayerSuppresses(icao24) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _applyPendingTrackingRestore() {
|
function _applyPendingTrackingRestore() {
|
||||||
const pending = flightState._pendingTrackingRestore;
|
const pending = flightState._pendingTrackingRestore;
|
||||||
if (!pending || pending.generation !== flightState._trackingIntentGeneration) return false;
|
if (
|
||||||
if (!flightState._billboardCollection?.show || !flightState._billboards.has(pending.id)) return false;
|
!pending ||
|
||||||
|
pending.generation !== flightState._trackingIntentGeneration
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
if (
|
||||||
|
!flightState._billboardCollection?.show ||
|
||||||
|
!flightState._billboards.has(pending.id)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
flightState._pendingTrackingRestore = null;
|
flightState._pendingTrackingRestore = null;
|
||||||
_trackFlight(pending.id, { origin: pending.origin });
|
_trackFlight(pending.id, { origin: pending.origin });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function _cancelPendingTrackingRestore() {
|
function _cancelPendingTrackingRestore() {
|
||||||
flightState._trackingIntentGeneration += 1;
|
flightState._trackingIntentGeneration += 1;
|
||||||
flightState._pendingTrackingRestore = null;
|
flightState._pendingTrackingRestore = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Multi-line tracked presentation text: "CS · FL · kts" + "Airline · Type" +
|
/** Multi-line tracked presentation text: "CS · FL · kts" + "Airline · Type" +
|
||||||
* "ORIG → DEST". The route line is gated by
|
* "ORIG → DEST". The route line is gated by
|
||||||
* routePlausible so a wrong-leg adsbdb route is hidden, not displayed.
|
* routePlausible so a wrong-leg adsbdb route is hidden, not displayed.
|
||||||
@@ -644,13 +753,18 @@ function _trackedLabelText(icao24) {
|
|||||||
const altFt = Math.round((info.altitude || 0) * 3.28084);
|
const altFt = Math.round((info.altitude || 0) * 3.28084);
|
||||||
const fl = altFt >= 18000 ? `FL${Math.round(altFt / 100)}` : `${altFt} ft`;
|
const fl = altFt >= 18000 ? `FL${Math.round(altFt / 100)}` : `${altFt} ft`;
|
||||||
const spd = info.velocity ? `${Math.round(info.velocity * 1.944)} kts` : '';
|
const spd = info.velocity ? `${Math.round(info.velocity * 1.944)} kts` : '';
|
||||||
const stale = (flightState._missingPolls.get(icao24) || flightState._backoff) ? 'STALE' : '';
|
const stale =
|
||||||
|
flightState._missingPolls.get(icao24) || flightState._backoff
|
||||||
|
? 'STALE'
|
||||||
|
: '';
|
||||||
const lines = [[cs, fl, spd, stale].filter(Boolean).join(' · ')];
|
const lines = [[cs, fl, spd, stale].filter(Boolean).join(' · ')];
|
||||||
// Converted contacts report their class as TR-3B and nothing else — the
|
// Converted contacts report their class as TR-3B and nothing else — the
|
||||||
// operator/type identity is exactly what the Easter egg is replacing.
|
// operator/type identity is exactly what the Easter egg is replacing.
|
||||||
const ident = isTr3b(icao24)
|
const ident = isTr3b(icao24)
|
||||||
? tr3bTypeLabel(icao24)
|
? tr3bTypeLabel(icao24)
|
||||||
: [info.airline, info.typeName || info.typeCode].filter(Boolean).join(' · ');
|
: [info.airline, info.typeName || info.typeCode]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' · ');
|
||||||
if (ident) lines.push(ident);
|
if (ident) lines.push(ident);
|
||||||
if (info.route && _routeIsPlausible(icao24, info.route)) {
|
if (info.route && _routeIsPlausible(icao24, info.route)) {
|
||||||
lines.push(`${info.route.origin.code} → ${info.route.destination.code}`);
|
lines.push(`${info.route.origin.code} → ${info.route.destination.code}`);
|
||||||
@@ -658,11 +772,11 @@ function _trackedLabelText(icao24) {
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Write the explicit tracked presentation model and refresh its host entry. */
|
/** Write the explicit tracked presentation model and refresh its host entry. */
|
||||||
|
|
||||||
function _updateTrackedLabelModel(icao24) {
|
function _updateTrackedLabelModel(icao24) {
|
||||||
if (!flightState._trackedEntity || icao24 !== flightState._trackedIcao) return;
|
if (!flightState._trackedEntity || icao24 !== flightState._trackedIcao)
|
||||||
|
return;
|
||||||
flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText(
|
flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText(
|
||||||
_trackedLabelText(icao24),
|
_trackedLabelText(icao24),
|
||||||
'#39d0ff',
|
'#39d0ff',
|
||||||
@@ -673,18 +787,20 @@ function _updateTrackedLabelModel(icao24) {
|
|||||||
refreshTrackedSubjectContext(_contextSubjectMetadata(icao24));
|
refreshTrackedSubjectContext(_contextSubjectMetadata(icao24));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Re-image the tracked entity's billboard from the current class/conversion. */
|
/** Re-image the tracked entity's billboard from the current class/conversion. */
|
||||||
|
|
||||||
function _syncTrackedBillboardImage() {
|
function _syncTrackedBillboardImage() {
|
||||||
if (!flightState._trackedIcao || !flightState._trackedEntity?.billboard) return;
|
if (!flightState._trackedIcao || !flightState._trackedEntity?.billboard)
|
||||||
|
return;
|
||||||
flightState._trackedEntity.billboard.image = aircraftIcon(
|
flightState._trackedEntity.billboard.image = aircraftIcon(
|
||||||
parts.rendering._iconKind(flightState._trackedIcao, flightState._flightData.get(flightState._trackedIcao)?.klass),
|
parts.rendering._iconKind(
|
||||||
|
flightState._trackedIcao,
|
||||||
|
flightState._flightData.get(flightState._trackedIcao)?.klass,
|
||||||
|
),
|
||||||
TRACKED_ICON_PX,
|
TRACKED_ICON_PX,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-render one contact after its TR-3B conversion (or the active IR style)
|
* Re-render one contact after its TR-3B conversion (or the active IR style)
|
||||||
* changed. Converting drops any 3D model so the triangle owns the visual; the
|
* changed. Converting drops any 3D model so the triangle owns the visual; the
|
||||||
@@ -694,12 +810,15 @@ function _syncTrackedBillboardImage() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function _refreshTr3bContact(icao24) {
|
function _refreshTr3bContact(icao24) {
|
||||||
const id = String(icao24 || '').trim().toLowerCase();
|
const id = String(icao24 || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
if (!id) return false;
|
if (!id) return false;
|
||||||
if (isTr3b(id)) {
|
if (isTr3b(id)) {
|
||||||
// Drop the 3D handoff for this contact — the fleet tick now skips it, so
|
// Drop the 3D handoff for this contact — the fleet tick now skips it, so
|
||||||
// an already-loaded model would otherwise linger with the billboard hidden.
|
// an already-loaded model would otherwise linger with the billboard hidden.
|
||||||
if (flightState._models.has(id) || flightState._modelPending.has(id)) parts.rendering._releaseModel(id);
|
if (flightState._models.has(id) || flightState._modelPending.has(id))
|
||||||
|
parts.rendering._releaseModel(id);
|
||||||
const bb = flightState._billboards.get(id);
|
const bb = flightState._billboards.get(id);
|
||||||
if (bb && id !== flightState._trackedIcao) bb.show = true; // horizon cull re-asserts next tick
|
if (bb && id !== flightState._trackedIcao) bb.show = true; // horizon cull re-asserts next tick
|
||||||
if (id === flightState._trackedIcao) {
|
if (id === flightState._trackedIcao) {
|
||||||
@@ -717,16 +836,15 @@ function _refreshTr3bContact(icao24) {
|
|||||||
return flightState._billboards.has(id) || id === flightState._trackedIcao;
|
return flightState._billboards.has(id) || id === flightState._trackedIcao;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Re-image every converted contact this layer owns (IR style flip). */
|
/** Re-image every converted contact this layer owns (IR style flip). */
|
||||||
|
|
||||||
function _refreshTr3bForStyle() {
|
function _refreshTr3bForStyle() {
|
||||||
for (const id of tr3bConvertedIds()) {
|
for (const id of tr3bConvertedIds()) {
|
||||||
if (flightState._billboards.has(id) || id === flightState._trackedIcao) _refreshTr3bContact(id);
|
if (flightState._billboards.has(id) || id === flightState._trackedIcao)
|
||||||
|
_refreshTr3bContact(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Plausibility check anchored to the plane's billboard position (coarse is
|
/** Plausibility check anchored to the plane's billboard position (coarse is
|
||||||
* fine here — this gates a LABEL, and it must not touch the tracked frame
|
* fine here — this gates a LABEL, and it must not touch the tracked frame
|
||||||
* cache). Missing data → true (never hide what we can't judge). */
|
* cache). Missing data → true (never hide what we can't judge). */
|
||||||
@@ -735,7 +853,11 @@ function _routeIsPlausible(icao24, route) {
|
|||||||
const info = flightState._flightData.get(icao24);
|
const info = flightState._flightData.get(icao24);
|
||||||
const bb = flightState._billboards.get(icao24);
|
const bb = flightState._billboards.get(icao24);
|
||||||
if (!info || !bb || !bb.position) return true;
|
if (!info || !bb || !bb.position) return true;
|
||||||
const carto = Cesium.Cartographic.fromCartesian(bb.position, Cesium.Ellipsoid.WGS84, flightState._scratchCarto);
|
const carto = Cesium.Cartographic.fromCartesian(
|
||||||
|
bb.position,
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
flightState._scratchCarto,
|
||||||
|
);
|
||||||
if (!carto) return true;
|
if (!carto) return true;
|
||||||
return routePlausible({
|
return routePlausible({
|
||||||
latDeg: Cesium.Math.toDegrees(carto.latitude),
|
latDeg: Cesium.Math.toDegrees(carto.latitude),
|
||||||
@@ -747,7 +869,6 @@ function _routeIsPlausible(icao24, route) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Begin tracking a specific aircraft by ICAO24 address.
|
* Begin tracking a specific aircraft by ICAO24 address.
|
||||||
*
|
*
|
||||||
@@ -788,7 +909,8 @@ function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
|
|||||||
// one computation shared by the position/alignedAxis/rotation/trail-head callbacks,
|
// one computation shared by the position/alignedAxis/rotation/trail-head callbacks,
|
||||||
// with discontinuity reconciliation). Falls back to the last billboard position when
|
// with discontinuity reconciliation). Falls back to the last billboard position when
|
||||||
// the aircraft has no fix.
|
// the aircraft has no fix.
|
||||||
const getTrackedPosition = () => parts.motion._trackedDisplayPosition(icao24) || bb.position;
|
const getTrackedPosition = () =>
|
||||||
|
parts.motion._trackedDisplayPosition(icao24) || bb.position;
|
||||||
|
|
||||||
// Dead-reckoning position property — smooth continuous motion between API updates.
|
// Dead-reckoning position property — smooth continuous motion between API updates.
|
||||||
const positionProperty = new Cesium.CallbackProperty(() => {
|
const positionProperty = new Cesium.CallbackProperty(() => {
|
||||||
@@ -820,15 +942,28 @@ function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
|
|||||||
// frames makes the target oscillate forward/back on screen.
|
// frames makes the target oscillate forward/back on screen.
|
||||||
trackingReferenceFrame: Cesium.TrackingReferenceFrame.ENU,
|
trackingReferenceFrame: Cesium.TrackingReferenceFrame.ENU,
|
||||||
billboard: {
|
billboard: {
|
||||||
image: aircraftIcon(parts.rendering._iconKind(flightState._trackedIcao, flightState._flightData.get(flightState._trackedIcao)?.klass), TRACKED_ICON_PX),
|
image: aircraftIcon(
|
||||||
|
parts.rendering._iconKind(
|
||||||
|
flightState._trackedIcao,
|
||||||
|
flightState._flightData.get(flightState._trackedIcao)?.klass,
|
||||||
|
),
|
||||||
|
TRACKED_ICON_PX,
|
||||||
|
),
|
||||||
width: 28,
|
width: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
scale: CLASS_SCALE_2D[flightState._flightData.get(flightState._trackedIcao)?.klass] || 1,
|
scale:
|
||||||
|
CLASS_SCALE_2D[
|
||||||
|
flightState._flightData.get(flightState._trackedIcao)?.klass
|
||||||
|
] || 1,
|
||||||
// Solid cyan when the billboard is the visual (zoomed out, 3D off, or model still loading);
|
// Solid cyan when the billboard is the visual (zoomed out, 3D off, or model still loading);
|
||||||
// transparent once the STANDALONE tracked model is actually up (ready + shown).
|
// transparent once the STANDALONE tracked model is actually up (ready + shown).
|
||||||
color: new Cesium.CallbackProperty(() => (
|
color: new Cesium.CallbackProperty(
|
||||||
parts.rendering._modelOwnsVisual(flightState._trackedIcao) ? CYAN_TRANSPARENT : Cesium.Color.CYAN
|
() =>
|
||||||
), false),
|
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
|
? CYAN_TRANSPARENT
|
||||||
|
: Cesium.Color.CYAN,
|
||||||
|
false,
|
||||||
|
),
|
||||||
sizeInMeters: false,
|
sizeInMeters: false,
|
||||||
scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5),
|
scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5),
|
||||||
alignedAxis: Cesium.Cartesian3.ZERO,
|
alignedAxis: Cesium.Cartesian3.ZERO,
|
||||||
@@ -842,14 +977,18 @@ function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
|
|||||||
rotation: new Cesium.CallbackProperty(() => {
|
rotation: new Cesium.CallbackProperty(() => {
|
||||||
const tracked = flightState._flightData.get(flightState._trackedIcao);
|
const tracked = flightState._flightData.get(flightState._trackedIcao);
|
||||||
const pos = getTrackedPosition();
|
const pos = getTrackedPosition();
|
||||||
if (!tracked || !pos || !flightState._viewer) return flightState._lastTrackedRotation;
|
if (!tracked || !pos || !flightState._viewer)
|
||||||
|
return flightState._lastTrackedRotation;
|
||||||
const projected = screenProjectedRotation(
|
const projected = screenProjectedRotation(
|
||||||
flightState._viewer.scene,
|
flightState._viewer.scene,
|
||||||
pos,
|
pos,
|
||||||
parts.motion._trackedDisplayCourse(),
|
parts.motion._trackedDisplayCourse(),
|
||||||
flightState._lastTrackedRotation,
|
flightState._lastTrackedRotation,
|
||||||
);
|
);
|
||||||
const rot = stabilizeScreenRotation(flightState._lastTrackedRotation, projected);
|
const rot = stabilizeScreenRotation(
|
||||||
|
flightState._lastTrackedRotation,
|
||||||
|
projected,
|
||||||
|
);
|
||||||
if (rot !== null) flightState._lastTrackedRotation = rot;
|
if (rot !== null) flightState._lastTrackedRotation = rot;
|
||||||
return flightState._lastTrackedRotation;
|
return flightState._lastTrackedRotation;
|
||||||
}, false),
|
}, false),
|
||||||
@@ -857,14 +996,24 @@ function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
|
|||||||
});
|
});
|
||||||
flightState._trackedEntity.gevSelectionOrigin = origin;
|
flightState._trackedEntity.gevSelectionOrigin = origin;
|
||||||
flightState._trackedEntity.gevTrackedId = `flights:${icao24}`;
|
flightState._trackedEntity.gevTrackedId = `flights:${icao24}`;
|
||||||
flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText(_trackedLabelText(icao24), '#39d0ff');
|
flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText(
|
||||||
|
_trackedLabelText(icao24),
|
||||||
|
'#39d0ff',
|
||||||
|
);
|
||||||
|
|
||||||
// A billboard has a ~zero bounding sphere, so Cesium's default follow distance is
|
// A billboard has a ~zero bounding sphere, so Cesium's default follow distance is
|
||||||
// far too tight (the user had to scroll out to read the plane). Give the entity a
|
// far too tight (the user had to scroll out to read the plane). Give the entity a
|
||||||
// calibrated viewFrom — behind + above, distance scaled to altitude — for a readable
|
// calibrated viewFrom — behind + above, distance scaled to altitude — for a readable
|
||||||
// initial frame with surrounding context. (ENU: east=+X, north=+Y, up=+Z.)
|
// initial frame with surrounding context. (ENU: east=+X, north=+Y, up=+Z.)
|
||||||
const followRange = Math.min(Math.max((info.altitude || 1500) * 1.1 + 2500, 3000), 30000);
|
const followRange = Math.min(
|
||||||
flightState._trackedEntity.viewFrom = new Cesium.Cartesian3(0, -followRange * 0.8, followRange * 0.55);
|
Math.max((info.altitude || 1500) * 1.1 + 2500, 3000),
|
||||||
|
30000,
|
||||||
|
);
|
||||||
|
flightState._trackedEntity.viewFrom = new Cesium.Cartesian3(
|
||||||
|
0,
|
||||||
|
-followRange * 0.8,
|
||||||
|
followRange * 0.55,
|
||||||
|
);
|
||||||
|
|
||||||
// Cancel any in-progress camera flight first — otherwise Cesium won't apply the
|
// Cancel any in-progress camera flight first — otherwise Cesium won't apply the
|
||||||
// tracked entity's viewFrom on the first frame, so voice-initiated tracking (which
|
// tracked entity's viewFrom on the first frame, so voice-initiated tracking (which
|
||||||
@@ -873,16 +1022,19 @@ function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
|
|||||||
// Expose the camera's already-settled position to cross-module HUD consumers (the tracked-target
|
// Expose the camera's already-settled position to cross-module HUD consumers (the tracked-target
|
||||||
// readout) so they draw at the SAME spot the camera framed, without recomputing the dead-reckon in
|
// readout) so they draw at the SAME spot the camera framed, without recomputing the dead-reckon in
|
||||||
// postRender (which would jitter the label against the now-stable plane).
|
// postRender (which would jitter the label against the now-stable plane).
|
||||||
flightState._trackedEntity.gevDisplayPosition = parts.motion._trackedDisplayCached;
|
flightState._trackedEntity.gevDisplayPosition =
|
||||||
|
parts.motion._trackedDisplayCached;
|
||||||
// Separate accessor on purpose: `gevDisplayPosition` carries the follow-camera
|
// Separate accessor on purpose: `gevDisplayPosition` carries the follow-camera
|
||||||
// anti-jitter contract and must keep returning the cached DR position. Presentation
|
// anti-jitter contract and must keep returning the cached DR position. Presentation
|
||||||
// that should weld to the AIRCRAFT YOU SEE reads `gevVisualPosition` instead.
|
// that should weld to the AIRCRAFT YOU SEE reads `gevVisualPosition` instead.
|
||||||
flightState._trackedEntity.gevVisualPosition = parts.motion._trackedVisualCached;
|
flightState._trackedEntity.gevVisualPosition =
|
||||||
|
parts.motion._trackedVisualCached;
|
||||||
refreshTrackedReadout(flightState._trackedEntity);
|
refreshTrackedReadout(flightState._trackedEntity);
|
||||||
flightState._viewer.camera.cancelFlight();
|
flightState._viewer.camera.cancelFlight();
|
||||||
// Camera follows the tracked entity
|
// Camera follows the tracked entity
|
||||||
flightState._viewer.trackedEntity = flightState._trackedEntity;
|
flightState._viewer.trackedEntity = flightState._trackedEntity;
|
||||||
flightState._trackedCameraFrameStop = applyTrackedCameraFrame(
|
flightState._trackedCameraFrameStop =
|
||||||
|
applyTrackedCameraFrame(
|
||||||
flightState._viewer,
|
flightState._viewer,
|
||||||
flightState._trackedEntity,
|
flightState._trackedEntity,
|
||||||
flightState._trackedEntity.viewFrom,
|
flightState._trackedEntity.viewFrom,
|
||||||
@@ -901,10 +1053,11 @@ function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
|
|||||||
|
|
||||||
_publishTrackedSelection(icao24, origin);
|
_publishTrackedSelection(icao24, origin);
|
||||||
|
|
||||||
console.log(`[Data:Flights] Tracking ${parts.queries._contactLabel(icao24, info)} (${icao24})`);
|
console.log(
|
||||||
|
`[Data:Flights] Tracking ${parts.queries._contactLabel(icao24, info)} (${icao24})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immediate military-suppression handoff sweep (pre-ship audit M2).
|
* Immediate military-suppression handoff sweep (pre-ship audit M2).
|
||||||
*
|
*
|
||||||
@@ -928,7 +1081,8 @@ function _onMilitaryActiveChange(active) {
|
|||||||
if (!flightState._viewer || !flightState._billboardCollection) return;
|
if (!flightState._viewer || !flightState._billboardCollection) return;
|
||||||
if (active) {
|
if (active) {
|
||||||
for (const [icao24, bb] of flightState._billboards) {
|
for (const [icao24, bb] of flightState._billboards) {
|
||||||
if (!isMilitaryIcao(icao24) || icao24 === flightState._trackedIcao) continue;
|
if (!isMilitaryIcao(icao24) || icao24 === flightState._trackedIcao)
|
||||||
|
continue;
|
||||||
flightState._billboardCollection.remove(bb);
|
flightState._billboardCollection.remove(bb);
|
||||||
flightState._billboards.delete(icao24);
|
flightState._billboards.delete(icao24);
|
||||||
parts.rendering._releaseModel(icao24); // military-suppression: drop any 3D model too
|
parts.rendering._releaseModel(icao24); // military-suppression: drop any 3D model too
|
||||||
@@ -945,7 +1099,6 @@ function _onMilitaryActiveChange(active) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global keydown handler — Escape deselects the tracked flight.
|
* Global keydown handler — Escape deselects the tracked flight.
|
||||||
* @param {KeyboardEvent} e
|
* @param {KeyboardEvent} e
|
||||||
@@ -958,7 +1111,6 @@ function _onKeyDown(e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a LEFT_CLICK handler on the scene canvas for flight selection.
|
* Install a LEFT_CLICK handler on the scene canvas for flight selection.
|
||||||
*
|
*
|
||||||
@@ -979,16 +1131,26 @@ function _installClickHandler(viewer) {
|
|||||||
// (the new owner controls it). Guarded so the intermediate untrack→retrack of OUR OWN switch
|
// (the new owner controls it). Guarded so the intermediate untrack→retrack of OUR OWN switch
|
||||||
// (viewer.trackedEntity briefly undefined) doesn't self-clear.
|
// (viewer.trackedEntity briefly undefined) doesn't self-clear.
|
||||||
if (!flightState._trackedEntityChangedRemove) {
|
if (!flightState._trackedEntityChangedRemove) {
|
||||||
flightState._trackedEntityChangedRemove = viewer.trackedEntityChanged.addEventListener(() => {
|
flightState._trackedEntityChangedRemove =
|
||||||
if (flightState._trackedIcao && flightState._viewer && flightState._viewer.trackedEntity && flightState._viewer.trackedEntity !== flightState._trackedEntity) {
|
viewer.trackedEntityChanged.addEventListener(() => {
|
||||||
|
if (
|
||||||
|
flightState._trackedIcao &&
|
||||||
|
flightState._viewer &&
|
||||||
|
flightState._viewer.trackedEntity &&
|
||||||
|
flightState._viewer.trackedEntity !== flightState._trackedEntity
|
||||||
|
) {
|
||||||
_clearTracking(true, {
|
_clearTracking(true, {
|
||||||
origin: flightState._viewer.trackedEntity?.gevSelectionOrigin || 'programmatic',
|
origin:
|
||||||
|
flightState._viewer.trackedEntity?.gevSelectionOrigin ||
|
||||||
|
'programmatic',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
flightState._clickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
flightState._clickHandler = new Cesium.ScreenSpaceEventHandler(
|
||||||
|
viewer.scene.canvas,
|
||||||
|
);
|
||||||
bindTrackingClickGesture(flightState._clickHandler, (click, gesture) => {
|
bindTrackingClickGesture(flightState._clickHandler, (click, gesture) => {
|
||||||
// Camera drags never select or deselect, even if they finish over a plane.
|
// Camera drags never select or deselect, even if they finish over a plane.
|
||||||
// Duration alone is allowed through so a stationary long press can still
|
// Duration alone is allowed through so a stationary long press can still
|
||||||
@@ -1010,19 +1172,32 @@ function _installClickHandler(viewer) {
|
|||||||
// above. H1: the model used to have no pick id, so this fell through to
|
// above. H1: the model used to have no pick id, so this fell through to
|
||||||
// "empty space" and deselected the very plane being tracked.
|
// "empty space" and deselected the very plane being tracked.
|
||||||
if (flightState._trackedIcao) {
|
if (flightState._trackedIcao) {
|
||||||
const rawPick = typeof picked.id === 'string' ? picked.id : picked.primitive?.id;
|
const rawPick =
|
||||||
if (picked.primitive === flightState._trackedModel || rawPick === flightState._trackedIcao) return;
|
typeof picked.id === 'string' ? picked.id : picked.primitive?.id;
|
||||||
|
if (
|
||||||
|
picked.primitive === flightState._trackedModel ||
|
||||||
|
rawPick === flightState._trackedIcao
|
||||||
|
)
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For BillboardCollection picks, the billboard may be at picked.primitive or picked.id
|
// For BillboardCollection picks, the billboard may be at picked.primitive or picked.id
|
||||||
const billboard = picked.primitive;
|
const billboard = picked.primitive;
|
||||||
if (billboard && billboard.id && flightState._billboards.has(billboard.id)) {
|
if (
|
||||||
|
billboard &&
|
||||||
|
billboard.id &&
|
||||||
|
flightState._billboards.has(billboard.id)
|
||||||
|
) {
|
||||||
_cancelPendingTrackingRestore();
|
_cancelPendingTrackingRestore();
|
||||||
_trackFlight(billboard.id, { origin: 'user' });
|
_trackFlight(billboard.id, { origin: 'user' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Some CesiumJS versions surface the id as a string on picked.id instead
|
// Some CesiumJS versions surface the id as a string on picked.id instead
|
||||||
if (picked.id && typeof picked.id === 'string' && flightState._billboards.has(picked.id)) {
|
if (
|
||||||
|
picked.id &&
|
||||||
|
typeof picked.id === 'string' &&
|
||||||
|
flightState._billboards.has(picked.id)
|
||||||
|
) {
|
||||||
_cancelPendingTrackingRestore();
|
_cancelPendingTrackingRestore();
|
||||||
_trackFlight(picked.id, { origin: 'user' });
|
_trackFlight(picked.id, { origin: 'user' });
|
||||||
return;
|
return;
|
||||||
@@ -1050,5 +1225,38 @@ function _installClickHandler(viewer) {
|
|||||||
|
|
||||||
document.addEventListener('keydown', _onKeyDown);
|
document.addEventListener('keydown', _onKeyDown);
|
||||||
}
|
}
|
||||||
return { _emitAwarenessEvent, _publishTrackedSelection, _contextSubjectMetadata, _isExplicitTrackingOrigin, _refreshCockpitNearContacts, _setCockpitContactMode, _applyCockpitState, _resetTrackedSelectionState, _trackedModelLoadAllowed, _noteTrackedModelLoadFailure, _trackedModelRegimeActive, _syncTracked2dRotation, _appendTrailFix, _refreshTrailDisplay, _startTrail, _backfillTrail, _clearTrail, _destroyTrail, _clearTracking, _militaryLayerSuppresses, _applyPendingTrackingRestore, _cancelPendingTrackingRestore, _trackedLabelText, _updateTrackedLabelModel, _syncTrackedBillboardImage, _refreshTr3bContact, _refreshTr3bForStyle, _routeIsPlausible, _trackFlight, _onMilitaryActiveChange, _onKeyDown, _installClickHandler };
|
return {
|
||||||
|
_emitAwarenessEvent,
|
||||||
|
_publishTrackedSelection,
|
||||||
|
_contextSubjectMetadata,
|
||||||
|
_isExplicitTrackingOrigin,
|
||||||
|
_refreshCockpitNearContacts,
|
||||||
|
_setCockpitContactMode,
|
||||||
|
_applyCockpitState,
|
||||||
|
_resetTrackedSelectionState,
|
||||||
|
_trackedModelLoadAllowed,
|
||||||
|
_noteTrackedModelLoadFailure,
|
||||||
|
_trackedModelRegimeActive,
|
||||||
|
_syncTracked2dRotation,
|
||||||
|
_appendTrailFix,
|
||||||
|
_refreshTrailDisplay,
|
||||||
|
_startTrail,
|
||||||
|
_backfillTrail,
|
||||||
|
_clearTrail,
|
||||||
|
_destroyTrail,
|
||||||
|
_clearTracking,
|
||||||
|
_militaryLayerSuppresses,
|
||||||
|
_applyPendingTrackingRestore,
|
||||||
|
_cancelPendingTrackingRestore,
|
||||||
|
_trackedLabelText,
|
||||||
|
_updateTrackedLabelModel,
|
||||||
|
_syncTrackedBillboardImage,
|
||||||
|
_refreshTr3bContact,
|
||||||
|
_refreshTr3bForStyle,
|
||||||
|
_routeIsPlausible,
|
||||||
|
_trackFlight,
|
||||||
|
_onMilitaryActiveChange,
|
||||||
|
_onKeyDown,
|
||||||
|
_installClickHandler,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -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