mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
677 lines
29 KiB
JavaScript
677 lines
29 KiB
JavaScript
import { displayedKinematics } from '../../data/motionModel.js';
|
|
import * as Cesium from 'cesium';
|
|
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
|
|
import { aircraftIncludedInNearby } from '../../data/aircraftNearbyPolicy.js';
|
|
import { modelVisualAnchor } from '../../data/modelVisualAnchor.js';
|
|
import { rankContactMatch, contactMatchWins, CONTACT_MATCH_TIER } from '../../data/contactMatch.js';
|
|
import { LANDED_ALT_MAX_FT, LANDED_SPEED_MAX_MPS } from './policy.js';
|
|
|
|
export function createQueries({ flightState, services, parts, layer, resolveAsset }) {
|
|
const { tr3bAircraftClass, tr3bTypeLabel } = services.aircraftPresentation;
|
|
const { formatFlightLevel } = services.labels;
|
|
const { applyTrackedCameraFrame } = services.camera;
|
|
|
|
|
|
/**
|
|
* Coerce a value to a finite number, returning null if not possible.
|
|
* Handles both numeric and string inputs (e.g. API fields that may arrive as strings).
|
|
* @param {*} value - Raw value from the API response
|
|
* @returns {number|null} Finite number or null
|
|
*/
|
|
|
|
function _toFiniteNumber(value) {
|
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
if (typeof value === 'string') {
|
|
const n = Number.parseFloat(value.trim());
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Safely convert a value to a trimmed string, defaulting to '' for falsy inputs.
|
|
* @param {*} value - Raw value to stringify
|
|
* @returns {string} Trimmed string
|
|
*/
|
|
|
|
function _toCleanText(value) {
|
|
return String(value || '').trim();
|
|
}
|
|
|
|
|
|
/**
|
|
* Format an altitude value in feet for display, with fallback text.
|
|
* @param {number|null|undefined} altitudeFt - Barometric altitude in feet
|
|
* @returns {string} Formatted altitude string (e.g. "35000 ft" or "Alt unknown")
|
|
*/
|
|
|
|
function _formatAltitude(altitudeFt) {
|
|
if (!Number.isFinite(altitudeFt)) return 'Alt unknown';
|
|
return `${Math.round(altitudeFt)} ft`;
|
|
}
|
|
|
|
|
|
/**
|
|
* True when the aircraft's latest metadata reads "on or about the runway"
|
|
* (low + slow) — see the landed fast-cull rationale above. Both gates must
|
|
* hold, so a plane missing either datum keeps the normal grace.
|
|
* @param {string} icao24 - ICAO hex identifier of the aircraft
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
function _likelyLanded(icao24) {
|
|
const info = flightState._flightData.get(icao24);
|
|
if (!info) return false;
|
|
// Round 7 (mirror of flights.js): fast cull only for contacts seen
|
|
// AIRBORNE this session — parked contacts ride the normal grace so feed
|
|
// flaps don't churn their identity/floor state.
|
|
if (info.wasAirborne !== true) return false;
|
|
if (info.onGround) return true;
|
|
return Number.isFinite(info.altitudeFt) && info.altitudeFt < LANDED_ALT_MAX_FT
|
|
&& Number.isFinite(info.speedMps) && info.speedMps < LANDED_SPEED_MAX_MPS;
|
|
}
|
|
|
|
|
|
/**
|
|
* Build a public descriptor for one aircraft using its best current position
|
|
* (dead-reckoned when history exists, billboard position otherwise).
|
|
* @param {string} icao24 - ICAO hex identifier of the aircraft.
|
|
* @returns {{icao24: string, callsign: string|null, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
|
|
* Descriptor with a cloned position, or null if the aircraft is unknown.
|
|
*/
|
|
|
|
function _describeFlight(icao24) {
|
|
const info = flightState._flightData.get(icao24);
|
|
const bb = flightState._billboards.get(icao24);
|
|
const basePos = parts.motion._deadReckon(icao24) || (bb ? bb.position : null);
|
|
if (!basePos) return null;
|
|
const displayed = displayedKinematics({
|
|
derivedSpeedMps: flightState._drSpeedMps,
|
|
derivedTrackDeg: flightState._drCourseDeg,
|
|
reportedSpeedMps: info?.speedMps,
|
|
reportedTrackDeg: info?.track,
|
|
});
|
|
const carto = Cesium.Cartographic.fromCartesian(basePos, Cesium.Ellipsoid.WGS84, flightState._scratchCarto);
|
|
if (!carto) return null;
|
|
return {
|
|
icao24,
|
|
callsign: _toCleanText(info?.callsign) || null,
|
|
// Additive: the label chain's middle link, trimmed like `callsign`, so
|
|
// getTrackedSubject and the voice narration can read it straight off the
|
|
// descriptor instead of reaching back into `_flightData`.
|
|
registration: _toCleanText(info?.registration) || null,
|
|
position: Cesium.Cartesian3.clone(basePos),
|
|
latitude: Cesium.Math.toDegrees(carto.latitude),
|
|
longitude: Cesium.Math.toDegrees(carto.longitude),
|
|
// Keep the cockpit readout on the reported aviation altitude. Render
|
|
// terrain height is a separate visual datum and may be below zero.
|
|
altitudeM: Number.isFinite(info?.altitudeM) ? info.altitudeM : carto.height,
|
|
renderAltitudeM: Number.isFinite(info?.renderAltitudeM) ? info.renderAltitudeM : carto.height,
|
|
onGround: info?.onGround === true,
|
|
velocityMps: displayed.speedMps,
|
|
track: displayed.trackDeg,
|
|
stale: Boolean(flightState._missingPolls.get(icao24) || flightState._backoff),
|
|
};
|
|
}
|
|
|
|
|
|
|
|
function _normalizeTrackedIcao(candidate) {
|
|
const normalized = String(candidate ?? '').trim().toLowerCase();
|
|
return normalized || null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Map one military aircraft's internal poll record to a plain JSON-safe
|
|
* analyst record (analyst query engine seam) — same shape as the flights
|
|
* layer's mapAnalystRecord, with `military` always true. Pure — no Cesium
|
|
* types, no fetches. Missing/unknown fields are null, never NaN/undefined.
|
|
* adsb.lol carries no origin-country field and this layer has no route
|
|
* enrichment, so originCountry/routeOrigin/routeDestination are always null.
|
|
* @param {string} icao24 - ICAO hex identifier of the aircraft.
|
|
* @param {Object|null|undefined} info - `_flightData` record for this aircraft.
|
|
* @returns {{id: string, icao24: string, callsign: string|null, lat: number|null,
|
|
* lon: number|null, altitudeM: number|null, speedMps: number|null,
|
|
* heading: number|null, verticalRateMps: number|null, onGround: boolean,
|
|
* military: boolean, aircraftClass: string|null, originCountry: null,
|
|
* operator: string|null, routeOrigin: null, routeDestination: null}}
|
|
*/
|
|
|
|
function mapAnalystRecord(icao24, info) {
|
|
const num = (v) => (Number.isFinite(v) ? v : null);
|
|
const text = (v) => { const t = String(v ?? '').trim(); return t || null; };
|
|
const callsign = text(info?.callsign);
|
|
return {
|
|
id: callsign || text(info?.registration) || icao24,
|
|
icao24,
|
|
callsign,
|
|
lat: num(info?.rawLat),
|
|
lon: num(info?.rawLon),
|
|
// altitudeFt is the sticky barometric/MSL aviation field — converted to
|
|
// meters here for shape parity with the flights layer.
|
|
altitudeM: Number.isFinite(info?.altitudeFt) ? info.altitudeFt * 0.3048 : null,
|
|
speedMps: num(info?.speedMps),
|
|
heading: num(info?.track),
|
|
verticalRateMps: num(info?.verticalRateMps),
|
|
onGround: info?.onGround === true,
|
|
military: true,
|
|
// A converted contact reports the class it RENDERS as (mirror of
|
|
// flights.js), so an analyst filter/superlative agrees with the triangle.
|
|
aircraftClass: tr3bAircraftClass(icao24, text(info?.klass)),
|
|
originCountry: null,
|
|
operator: text(info?.operator),
|
|
routeOrigin: null,
|
|
routeDestination: null,
|
|
};
|
|
}
|
|
const methods = { mapAnalystRecord,
|
|
|
|
id: 'military',
|
|
|
|
name: 'Military Flights',
|
|
|
|
icon: '🎖️',
|
|
|
|
source: flightState._lastSource,
|
|
|
|
/** @type {number} Polling interval in ms between API fetches */
|
|
updateInterval: 15000,
|
|
|
|
|
|
/** @deprecated Compatibility alias for {@link enable}. */
|
|
show(viewer) {
|
|
this.enable(viewer);
|
|
},
|
|
|
|
|
|
/** @deprecated Compatibility alias for {@link disable}. */
|
|
hide(viewer) {
|
|
this.disable(viewer);
|
|
},
|
|
|
|
|
|
/**
|
|
* Live layer params.
|
|
* `models3d` toggles 3D glTF jet-model rendering for the FLEET (altitude-gated): when on,
|
|
* surrounding military aircraft become 3D models once the camera is zoomed in past
|
|
* MODEL_ALT_CEIL_M. The TRACKED contact is NOT gated by this — it takes its 3D model by
|
|
* camera distance regardless (see `_trackedModelRegimeActive` / trackedModelRegime.js).
|
|
* `models3dMode` is 'proximity' (nearest MODEL_MAX in view) or 'all' (every in-view plane).
|
|
* @param {{models3d?: boolean, models3dMode?: 'proximity'|'all', selectedMilitaryTrackingId?: string|null}} params
|
|
*/
|
|
setParams(params = {}, { origin = 'programmatic' } = {}) {
|
|
if (isExplicitLayerStateOrigin(origin)
|
|
&& !Object.hasOwn(params, 'selectedMilitaryTrackingId')) {
|
|
parts.tracking._cancelPendingTrackingRestore();
|
|
}
|
|
if (typeof params.models3d === 'boolean' && params.models3d !== flightState._models3dEnabled) {
|
|
flightState._models3dEnabled = params.models3d;
|
|
if (!flightState._models3dEnabled) {
|
|
parts.rendering._releaseModels();
|
|
parts.tracking._syncTracked2dRotation();
|
|
// Restore fleet billboards (horizon-cull re-asserts next tick), but NEVER the tracked
|
|
// plane's own fleet billboard — its tracked entity is the visual (avoids a double-image).
|
|
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) {
|
|
// The next fleet tick re-derives the eligible set under the new cap and releases the overflow.
|
|
flightState._models3dMode = params.models3dMode;
|
|
if (flightState._cockpitContactMode) {
|
|
parts.tracking._refreshCockpitNearContacts();
|
|
flightState._lastFleetTickMs = 0;
|
|
}
|
|
}
|
|
if (typeof params.irBoost === 'boolean' && params.irBoost !== flightState._irBoost) {
|
|
flightState._irBoost = params.irBoost;
|
|
parts.rendering._reloadModelsForIrBoost();
|
|
// Sprites don't reload with the models — swap the TR-3B glyph between its
|
|
// cold and thermal-reactive variants directly (mirror of flights.js).
|
|
parts.tracking._refreshTr3bForStyle();
|
|
}
|
|
if (Object.hasOwn(params, 'selectedMilitaryTrackingId')) {
|
|
const requested = _normalizeTrackedIcao(params.selectedMilitaryTrackingId);
|
|
if (requested === flightState._trackedIcao) {
|
|
flightState._pendingTrackingRestore = null;
|
|
} else if (requested === null) {
|
|
parts.tracking._cancelPendingTrackingRestore();
|
|
if (flightState._trackedIcao) parts.tracking._clearTracking(false, { origin });
|
|
} else {
|
|
const generation = ++flightState._trackingIntentGeneration;
|
|
flightState._pendingTrackingRestore = { id: requested, generation, origin };
|
|
if (flightState._trackedIcao) parts.tracking._clearTracking(false, { origin });
|
|
parts.tracking._applyPendingTrackingRestore();
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
getParams() {
|
|
return {
|
|
models3d: flightState._models3dEnabled,
|
|
models3dMode: flightState._models3dMode,
|
|
irBoost: flightState._irBoost,
|
|
selectedMilitaryTrackingId: flightState._trackedIcao,
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* Re-render a contact whose TR-3B conversion just flipped (Easter egg).
|
|
* Callers own the registry write; this only re-derives what renders.
|
|
* @param {string} icao24 - ICAO 24-bit address.
|
|
* @returns {boolean} True when this layer owns the contact.
|
|
*/
|
|
refreshTr3b(icao24) { return parts.tracking._refreshTr3bContact(icao24); },
|
|
|
|
|
|
/**
|
|
* Find military aircraft near a given ECEF position, sorted by distance.
|
|
* Used by proximity-based features (e.g. detection overlay, spatial queries).
|
|
* @param {Cesium.Cartesian3} center - Reference position in ECEF coordinates
|
|
* @param {number} range - Maximum distance in meters (Infinity if not specified)
|
|
* @param {number} [maxCount=50] - Maximum number of results to return
|
|
* @param {object} [options] Query membership options.
|
|
* @param {boolean} [options.includeHidden=false] Include loaded horizon-hidden aircraft.
|
|
* @returns {Array<Object>} Sorted array of nearby aircraft descriptors
|
|
*/
|
|
getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) {
|
|
if (!center || !flightState._billboardCollection || !flightState._billboardCollection.show) return [];
|
|
|
|
const limit = Number.isFinite(maxCount)
|
|
? Math.max(1, Math.floor(maxCount))
|
|
: 50;
|
|
const maxRange = Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY;
|
|
|
|
const now = Cesium.JulianDate.now();
|
|
const nearby = [];
|
|
|
|
for (const [icao24, bb] of flightState._billboards) {
|
|
const isTracked = icao24 === flightState._trackedIcao;
|
|
// Keep planes rendered as a 3D model (billboard hidden) so proximity counts
|
|
// don't drop to zero on the 2D→3D handoff; bb.position stays current while hidden.
|
|
if (!aircraftIncludedInNearby({
|
|
isTracked,
|
|
billboardShown: bb.show,
|
|
modelRendering: parts.rendering._modelOwnsVisual(icao24),
|
|
includeHidden,
|
|
})) continue;
|
|
|
|
const trackedPos = isTracked ? parts.motion._trackedDisplayCached() : null; // cached, no recompute (anti-jitter)
|
|
const pos = trackedPos || bb.position;
|
|
if (!pos) continue;
|
|
|
|
const distance = Cesium.Cartesian3.distance(center, pos);
|
|
if (distance > maxRange) continue;
|
|
|
|
const info = flightState._flightData.get(icao24);
|
|
nearby.push({
|
|
id: info?.callsign?.trim() || info?.registration?.trim() || icao24,
|
|
icao24,
|
|
position: pos,
|
|
distance,
|
|
// Filter surface (mirror of flights.js): the cockpit next/previous path
|
|
// matches on this field, so it follows the conversion.
|
|
aircraftClass: tr3bAircraftClass(icao24, String(info?.klass || info?.type || '').trim().toLowerCase() || null),
|
|
track: info?.track ?? null,
|
|
// Display type — converted too, so a Contacts row can't still name the
|
|
// airframe the triangle replaced (the filter matcher reads this as a
|
|
// fallback candidate as well).
|
|
type: tr3bTypeLabel(icao24, info?.type || null),
|
|
registration: info?.registration || null,
|
|
operator: info?.operator || null,
|
|
altitudeFt: info?.altitudeFt ?? null,
|
|
});
|
|
}
|
|
|
|
nearby.sort((a, b) => a.distance - b.distance);
|
|
return nearby.slice(0, limit);
|
|
},
|
|
|
|
|
|
/**
|
|
* Return a subset of aircraft suitable for the detection overlay system.
|
|
* Uses a deterministic stride-based sampling to keep the count manageable
|
|
* while distributing selections evenly across the collection.
|
|
* @param {Object} [options={}] - Options
|
|
* @param {number} [options.maxCount] - Maximum objects to return (defaults to all)
|
|
* @param {number} [options.seed] - Seed offset for stride sampling (for frame variation)
|
|
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
|
|
*/
|
|
getDetectableObjects(options = {}) {
|
|
if (!flightState._billboardCollection || !flightState._billboardCollection.show) return [];
|
|
const maxCount = Number.isFinite(options.maxCount)
|
|
? Math.max(1, Math.floor(options.maxCount))
|
|
: flightState._billboards.size;
|
|
const seed = Number.isFinite(options.seed) ? Math.floor(options.seed) : 0;
|
|
// Deterministic stride: evenly space selections across the billboard map
|
|
const stride = Math.max(1, Math.ceil(flightState._billboards.size / maxCount));
|
|
const start = seed % stride;
|
|
|
|
const result = [];
|
|
let idx = 0;
|
|
for (const [icao24, bb] of flightState._billboards) {
|
|
const shouldTake = ((idx - start) % stride) === 0;
|
|
idx++;
|
|
if (!shouldTake) continue;
|
|
if (flightState._cockpitContactMode && icao24.toLowerCase() === flightState._cockpitSubjectId) continue;
|
|
const isTracked = icao24 === flightState._trackedIcao;
|
|
// Keep planes rendered as a 3D model (billboard hidden) so the detection box
|
|
// doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden.
|
|
const model = flightState._models.get(icao24);
|
|
const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24);
|
|
if (!isTracked && !bb.show && !modelOwnsVisual) continue;
|
|
const info = flightState._flightData.get(icao24);
|
|
const callsign = info?.callsign?.trim();
|
|
const registration = info?.registration?.trim();
|
|
let object = flightState._detectionObjects.get(icao24);
|
|
if (!object) {
|
|
object = {
|
|
sourceId: icao24, type: 'AIR', tier: 'military', _weldPos: new Cesium.Cartesian3(),
|
|
};
|
|
flightState._detectionObjects.set(icao24, object);
|
|
}
|
|
// WELD (mirror of flights.js): anchor to whatever owns the visual. Model-owned
|
|
// contacts read the translation the fleet tick already wrote into their
|
|
// modelMatrix; sprite-owned contacts keep `bb.position`, where sprite and bracket
|
|
// are co-located anyway.
|
|
const spec = modelOwnsVisual ? parts.rendering._modelSpec(info?.klass) : null;
|
|
const pos = isTracked
|
|
? (parts.motion._trackedVisualCached() || bb.position)
|
|
: (modelOwnsVisual
|
|
? modelVisualAnchor(
|
|
model.modelMatrix,
|
|
spec.visualCenterNative,
|
|
Number.isFinite(model.computedScale) ? model.computedScale : spec.scale,
|
|
object._weldPos || (object._weldPos = new Cesium.Cartesian3()),
|
|
)
|
|
: bb.position);
|
|
if (!pos) continue;
|
|
object.position = pos;
|
|
object.skipLabel = isTracked;
|
|
const id = callsign || registration || icao24;
|
|
if (object.id !== id) object.id = id;
|
|
// Card surface: detectionDraw composes its secondary line from
|
|
// `[src.klass, src.metric]`, so a converted contact's card must name the
|
|
// TR-3B, not the airframe underneath it.
|
|
const klass = tr3bTypeLabel(icao24, info?.type || 'MIL');
|
|
if (object.klass !== klass) object.klass = klass;
|
|
const altitudeFt = info?.altitudeFt ?? 0;
|
|
if (object._altitudeFt !== altitudeFt) {
|
|
object._altitudeFt = altitudeFt;
|
|
// military metadata stores altitudeFt (feet); the helper takes metres
|
|
object.metric = formatFlightLevel(altitudeFt * 0.3048);
|
|
}
|
|
result.push(object);
|
|
if (result.length >= maxCount) break;
|
|
}
|
|
if (flightState._detectionObjects.size > flightState._billboards.size + 512) {
|
|
for (const icao24 of flightState._detectionObjects.keys()) {
|
|
if (!flightState._billboards.has(icao24)) flightState._detectionObjects.delete(icao24);
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
|
|
/**
|
|
* Find a single aircraft by free-text query.
|
|
* Match priority: exact icao24 hex, exact callsign, callsign prefix,
|
|
* then callsign substring (all case-insensitive, trimmed).
|
|
* @param {string} query - ICAO hex or full/partial callsign.
|
|
* @returns {{icao24: string, callsign: string|null, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
|
|
* Best match with a cloned, dead-reckoned position, or null if none.
|
|
*/
|
|
findByQuery(query) {
|
|
if (!flightState._flightData || flightState._flightData.size === 0) return null;
|
|
const q = String(query || '').trim().toLowerCase();
|
|
if (!q) return null;
|
|
|
|
// Registration is searched alongside callsign because it is what the
|
|
// operator SEES: a callsign-less contact reads as its tail number on the
|
|
// card, in the analyst's answer, and in the Contacts list. Matching only
|
|
// callsigns meant "follow 6606" — and the analyst → track_entity handoff
|
|
// the tool instructions prescribe — answered "nothing matched" for the
|
|
// very identity the app had just shown (owner field session 2026-08-21,
|
|
// 23:48: three failed track_entity retries before a fallback stuck).
|
|
// Ranking is shared with the flights layer (contactMatch.js) so the two
|
|
// cannot disagree, and it is strictly tiered so a registration can never
|
|
// out-rank a real callsign on feed order alone.
|
|
let best = null;
|
|
for (const [icao24, info] of flightState._flightData) {
|
|
const candidate = {
|
|
tier: rankContactMatch({
|
|
query: q,
|
|
hex: icao24,
|
|
callsign: info?.callsign,
|
|
registration: info?.registration,
|
|
}),
|
|
id: icao24,
|
|
};
|
|
if (!contactMatchWins(candidate, best)) continue;
|
|
best = candidate;
|
|
if (candidate.tier === CONTACT_MATCH_TIER.HEX_EXACT) break;
|
|
}
|
|
return best ? _describeFlight(best.id) : null;
|
|
},
|
|
|
|
|
|
/**
|
|
* Return id/label/position for up to maxCount currently rendered aircraft.
|
|
* Cheap snapshot for voice-tool framing — billboard positions only, no
|
|
* dead reckoning and no cloning.
|
|
* @param {number} [maxCount=500] - Maximum number of entries to return.
|
|
* @returns {Array<{id: string, label: string, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number}>}
|
|
*/
|
|
/**
|
|
* Whether this layer still carries a contact, in O(1).
|
|
*
|
|
* Mirror of `flights.hasContact`: presence consumers must not infer absence
|
|
* from the capped `getAllPositions` rows. Id matching follows trackById —
|
|
* exact key first, then lowercase.
|
|
* A disabled layer keeps its records but hides the collection, so it must
|
|
* decline rather than answer from data the user can no longer see.
|
|
* @param {string} icao24 Contact identifier.
|
|
* @returns {boolean|null} Presence, or null when the layer is disabled or
|
|
* holds no data and therefore cannot answer.
|
|
*/
|
|
hasContact(icao24) {
|
|
if (!flightState._billboardCollection || !flightState._billboardCollection.show || flightState._billboards.size === 0) return null;
|
|
if (!icao24) return false;
|
|
const id = String(icao24).trim();
|
|
return flightState._billboards.has(id) || flightState._billboards.has(id.toLowerCase());
|
|
},
|
|
|
|
|
|
getAllPositions(maxCount = 500) {
|
|
if (!flightState._billboardCollection || flightState._billboards.size === 0) return [];
|
|
const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 500;
|
|
|
|
const result = [];
|
|
for (const [icao24, bb] of flightState._billboards) {
|
|
const pos = bb.position;
|
|
if (!pos) continue;
|
|
const carto = Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84, flightState._scratchCarto);
|
|
if (!carto) continue;
|
|
const info = flightState._flightData.get(icao24);
|
|
result.push({
|
|
id: icao24, // identity (trackById/Context resolve this)
|
|
// Last surface still reading as the raw hex for a callsign-less
|
|
// contact — same chain as getNearby/getDetectableObjects/getTrackedSubject.
|
|
label: _toCleanText(info?.callsign) || _toCleanText(info?.registration) || icao24,
|
|
position: pos,
|
|
latitude: Cesium.Math.toDegrees(carto.latitude),
|
|
longitude: Cesium.Math.toDegrees(carto.longitude),
|
|
altitudeM: carto.height,
|
|
});
|
|
if (result.length >= limit) break;
|
|
}
|
|
return result;
|
|
},
|
|
|
|
|
|
/**
|
|
* Snapshot the layer's in-memory records as plain JSON-safe objects for
|
|
* the analyst query engine. On-demand only (called at most once per
|
|
* spoken query) — zero per-frame cost, no listeners, no caching. Returns
|
|
* [] while the layer is disabled or empty.
|
|
* @param {number} [maxCount=2000] - Maximum records to return (truncation).
|
|
* @returns {Array<Object>} See mapAnalystRecord for the record shape.
|
|
*/
|
|
getAnalystRecords(maxCount = 2000) {
|
|
if (!flightState._billboardCollection || !flightState._billboardCollection.show || flightState._flightData.size === 0) return [];
|
|
const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 2000;
|
|
const result = [];
|
|
for (const [icao24, info] of flightState._flightData) {
|
|
result.push(mapAnalystRecord(icao24, info));
|
|
if (result.length >= limit) break;
|
|
}
|
|
return result;
|
|
},
|
|
|
|
|
|
/**
|
|
* Start camera-tracking an aircraft by ICAO hex identifier.
|
|
* @param {string} icao24 - ICAO hex identifier of the aircraft.
|
|
* @returns {boolean} True if the aircraft exists and tracking started.
|
|
*/
|
|
trackById(icao24, { origin = 'programmatic' } = {}) {
|
|
if (!icao24) return false;
|
|
let id = String(icao24).trim();
|
|
if (!flightState._billboards.has(id)) id = id.toLowerCase();
|
|
if (!flightState._billboards.has(id)) return false;
|
|
if (parts.tracking._isExplicitTrackingOrigin(origin)) parts.tracking._cancelPendingTrackingRestore();
|
|
if (flightState._trackedIcao === id) return parts.tracking._publishTrackedSelection(id, origin);
|
|
parts.tracking._trackFlight(id, { origin });
|
|
return true;
|
|
},
|
|
|
|
|
|
/** Resolve a shared Follow target only against the latest accepted refresh. */
|
|
async resolveTrackingRestoreTarget(icao24, { signal = null, origin = 'share-restore' } = {}) {
|
|
if (signal?.aborted) return { status: 'cancelled', reason: String(signal.reason || 'aborted') };
|
|
const id = String(icao24 ?? '').trim().toLowerCase();
|
|
if (!id) return { status: 'missing', reason: 'invalid-target' };
|
|
const outcome = flightState._lastTrackingRefreshOutcome;
|
|
if (outcome.status !== 'accepted') {
|
|
return {
|
|
status: 'source-unavailable',
|
|
reason: `${flightState._lastSource} snapshot unavailable`,
|
|
refreshEpoch: outcome.epoch,
|
|
source: outcome.source,
|
|
};
|
|
}
|
|
if (!outcome.ids.has(id)) {
|
|
return {
|
|
status: 'missing',
|
|
reason: 'target-absent-from-snapshot',
|
|
refreshEpoch: outcome.epoch,
|
|
source: outcome.source,
|
|
};
|
|
}
|
|
if (signal?.aborted) return { status: 'cancelled', reason: String(signal.reason || 'aborted') };
|
|
const followed = this.trackById(id, { origin });
|
|
return followed
|
|
? { status: 'found', refreshEpoch: outcome.epoch, source: outcome.source }
|
|
: { status: 'source-unavailable', reason: 'target-not-renderable', refreshEpoch: outcome.epoch };
|
|
},
|
|
|
|
|
|
/** Reapply the canonical follow frame without recreating the selected flight. */
|
|
refocusTrackedById(icao24, { origin = 'programmatic' } = {}) {
|
|
if (!icao24 || flightState._cockpitContactMode || !flightState._viewer || !flightState._trackedEntity) return false;
|
|
let id = String(icao24).trim();
|
|
if (!flightState._billboards.has(id)) id = id.toLowerCase();
|
|
if (
|
|
id !== flightState._trackedIcao
|
|
|| !flightState._viewer.entities?.contains?.(flightState._trackedEntity)
|
|
|| flightState._viewer.trackedEntity !== flightState._trackedEntity
|
|
) return false;
|
|
flightState._trackedCameraFrameStop?.();
|
|
flightState._viewer.camera.cancelFlight();
|
|
flightState._viewer.trackedEntity = flightState._trackedEntity;
|
|
flightState._trackedCameraFrameStop = applyTrackedCameraFrame(
|
|
flightState._viewer,
|
|
flightState._trackedEntity,
|
|
flightState._trackedEntity.viewFrom,
|
|
) || null;
|
|
parts.tracking._publishTrackedSelection(id, origin);
|
|
return true;
|
|
},
|
|
|
|
|
|
/**
|
|
* Stop tracking the currently followed aircraft (no-op if none).
|
|
* @returns {boolean} Always true.
|
|
*/
|
|
stopTracking({ origin = 'programmatic' } = {}) {
|
|
parts.tracking._cancelPendingTrackingRestore();
|
|
parts.tracking._clearTracking(false, { origin });
|
|
return true;
|
|
},
|
|
|
|
|
|
cancelPendingTrackingRestore() {
|
|
parts.tracking._cancelPendingTrackingRestore();
|
|
},
|
|
|
|
|
|
/**
|
|
* Describe the currently tracked aircraft at its dead-reckoned position.
|
|
* @returns {{icao24: string, callsign: string|null, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
|
|
* Tracked aircraft info, or null when nothing is tracked.
|
|
*/
|
|
getTrackedInfo() {
|
|
if (!flightState._trackedIcao) return null;
|
|
const described = _describeFlight(flightState._trackedIcao);
|
|
if (!described) return null;
|
|
const { position, ...rest } = described;
|
|
return rest;
|
|
},
|
|
|
|
|
|
/**
|
|
* Return the current aircraft as a Context subject without changing
|
|
* tracking or camera ownership.
|
|
* @returns {{layerId: string, id: string, label: string, position: Cesium.Cartesian3}|null}
|
|
* Detached subject descriptor, or null when no aircraft is tracked.
|
|
*/
|
|
getTrackedSubject() {
|
|
if (!flightState._trackedIcao) return null;
|
|
const described = _describeFlight(flightState._trackedIcao);
|
|
if (!described?.position) return null;
|
|
return {
|
|
layerId: 'military',
|
|
id: described.icao24,
|
|
// Same label chain as getNearby/getDetectableObjects: a callsign-less
|
|
// contact reads as its registration, never as the raw ICAO hex.
|
|
label: described.callsign || described.registration || described.icao24,
|
|
position: Cesium.Cartesian3.clone(described.position),
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* Return current layer health/status for the HUD status chip.
|
|
* @returns {{count: number, lastUpdate: number|null, stale: boolean, error: string|null, status: number|null, retryInSec: number}}
|
|
*/
|
|
getStats() {
|
|
const retryInSec = flightState._retryAt ? Math.max(0, Math.ceil((flightState._retryAt - Date.now()) / 1000)) : 0;
|
|
return {
|
|
count: flightState._count,
|
|
lastUpdate: flightState._lastUpdate,
|
|
stale: flightState._backoff,
|
|
error: flightState._lastError,
|
|
status: flightState._lastStatus,
|
|
retryInSec,
|
|
source: flightState._lastSource,
|
|
fallback: false,
|
|
};
|
|
}
|
|
};
|
|
|
|
return { _toFiniteNumber, _toCleanText, _formatAltitude, _likelyLanded, _describeFlight, _normalizeTrackedIcao, mapAnalystRecord, methods };
|
|
}
|