mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
158 lines
6.0 KiB
JavaScript
158 lines
6.0 KiB
JavaScript
/** Contact readouts and nearby-signal presentation for the Cockpit controller. */
|
|
import * as Cesium from 'cesium';
|
|
import {
|
|
bearingBetweenCoordinates,
|
|
formatCockpitContextScope,
|
|
relativeBearing,
|
|
resolveCockpitContextReadout,
|
|
} from '../cockpitMath.js';
|
|
|
|
export function updateContext(info, heading) {
|
|
if (!this.context) return;
|
|
const snapshot =
|
|
this.services.militaryAwarenessLayer.getContextSnapshot?.() || null;
|
|
const trackedId = info.icao24 || info.id;
|
|
const readout = resolveCockpitContextReadout({ snapshot, info });
|
|
if (!readout.visible) {
|
|
this.context.hidden = true;
|
|
this.hud?.classList.remove('context-active');
|
|
this.contextLayoutStamp = null;
|
|
this.pushCockpitSignal(
|
|
'context-status',
|
|
'info',
|
|
'CONTEXT STANDBY',
|
|
'ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS',
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.context.hidden = false;
|
|
this.hud?.classList.add('context-active');
|
|
if (this.contextSubject) {
|
|
const installationCoverage = snapshot.cohorts.find(
|
|
(cohort) => cohort.id === 'military-installations',
|
|
)?.coverage;
|
|
this.contextSubject.textContent = formatCockpitContextScope(
|
|
snapshot.subject.label || trackedId,
|
|
snapshot.radiusM,
|
|
installationCoverage,
|
|
);
|
|
}
|
|
// Navigation stays wired in every state — the operator must always be able
|
|
// to step off the current contact from the panel that hosts the controls.
|
|
if (this.contextPrevious)
|
|
this.contextPrevious.disabled = !snapshot.navigation?.canPrevious;
|
|
if (this.contextNext)
|
|
this.contextNext.disabled = !snapshot.navigation?.canNext;
|
|
|
|
if (readout.contactLost) {
|
|
// The subject left its source. Every number below is measured against a
|
|
// position that stopped updating, so hold the last rendered readout and
|
|
// say so instead of re-deriving stale geometry as if it were live.
|
|
const enteringLost = this.context.dataset.state !== 'lost';
|
|
this.context.dataset.state = 'lost';
|
|
if (this.contextUncertainty) {
|
|
this.contextUncertainty.textContent =
|
|
'CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR';
|
|
}
|
|
// The cue changes the footer's height; re-run layout once on the way in
|
|
// rather than every frame the contact stays lost.
|
|
if (enteringLost) this.scheduleContextLayout();
|
|
this.pushCockpitSignal(
|
|
'context-status',
|
|
'warning',
|
|
`CONTACT LOST · ${snapshot.subject.label || snapshot.subject.id || 'SUBJECT'}`,
|
|
'SUBJECT LEFT ITS FEED · READOUT HOLDING LAST KNOWN',
|
|
);
|
|
return;
|
|
}
|
|
|
|
let unknownCount = 0;
|
|
const nearest = [];
|
|
for (const cohort of snapshot.cohorts) {
|
|
const element = this.contextCohorts.get(cohort.id);
|
|
const value = element?.querySelector('strong');
|
|
if (value)
|
|
value.textContent = cohort.count === null ? '?' : String(cohort.count);
|
|
element?.classList.toggle('unknown', cohort.relationship === 'UNKNOWN');
|
|
if (cohort.count === null) unknownCount += 1;
|
|
for (const item of cohort.nearest) nearest.push({ ...item, cohort });
|
|
}
|
|
nearest.sort((a, b) => (a.distanceM ?? Infinity) - (b.distanceM ?? Infinity));
|
|
const closest = nearest[0] || null;
|
|
const closestLabel = this.services.formatAwarenessLabel(closest);
|
|
if (this.contextNearestLabel) {
|
|
this.contextNearestLabel.textContent = closest
|
|
? `${closest.cohort.label.toUpperCase()} · ${closestLabel}`
|
|
: 'NO AVAILABLE EXAMPLE';
|
|
this.contextNearestLabel.setAttribute(
|
|
'aria-label',
|
|
closest && closestLabel === '—'
|
|
? `${closest.cohort.label}, Unavailable`
|
|
: this.contextNearestLabel.textContent,
|
|
);
|
|
}
|
|
if (this.contextDistance) {
|
|
const distanceM = closest?.distanceM;
|
|
this.contextDistance.textContent = Number.isFinite(distanceM)
|
|
? `${distanceM < 10000 ? (distanceM / 1000).toFixed(1) : Math.round(distanceM / 1000)} KM`
|
|
: '—';
|
|
this.contextDistance.setAttribute(
|
|
'aria-label',
|
|
Number.isFinite(distanceM)
|
|
? this.contextDistance.textContent
|
|
: 'Unavailable',
|
|
);
|
|
}
|
|
|
|
// The arrow and BRG are nose-relative to the tracked aircraft. When the
|
|
// subject is a vessel, an installation, or another aircraft, the rest of
|
|
// this row is measured from that subject — so the aircraft-frame half is
|
|
// dashed rather than presented alongside subject-frame distances as if the
|
|
// two shared an origin.
|
|
let relative = null;
|
|
if (
|
|
readout.aircraftRelative &&
|
|
closest?.position &&
|
|
Number.isFinite(info.latitude) &&
|
|
Number.isFinite(info.longitude)
|
|
) {
|
|
const cartographic = Cesium.Cartographic.fromCartesian(closest.position);
|
|
const bearing = cartographic
|
|
? bearingBetweenCoordinates(
|
|
info.latitude,
|
|
info.longitude,
|
|
Cesium.Math.toDegrees(cartographic.latitude),
|
|
Cesium.Math.toDegrees(cartographic.longitude),
|
|
)
|
|
: null;
|
|
relative = relativeBearing(bearing, heading);
|
|
}
|
|
if (this.contextDirection) {
|
|
this.contextDirection.style.transform = `rotate(${relative ?? 0}deg)`;
|
|
this.contextDirection.classList.toggle('unknown', relative === null);
|
|
}
|
|
if (this.contextBearing) {
|
|
if (relative === null) this.contextBearing.textContent = 'BRG —';
|
|
else if (Math.abs(relative) < 8) this.contextBearing.textContent = 'AHEAD';
|
|
else
|
|
this.contextBearing.textContent = `${relative < 0 ? 'L' : 'R'} ${String(Math.round(Math.abs(relative))).padStart(3, '0')}°`;
|
|
}
|
|
if (this.contextUncertainty) {
|
|
this.contextUncertainty.textContent = unknownCount
|
|
? `${unknownCount} INPUT${unknownCount === 1 ? '' : 'S'} UNKNOWN · NOT AN ALL-CLEAR`
|
|
: 'AVAILABLE INPUTS CURRENT · NOT AN ALL-CLEAR';
|
|
}
|
|
if (this.contextUpdated) {
|
|
this.contextUpdated.textContent = Number.isFinite(snapshot.evaluatedAt)
|
|
? new Date(snapshot.evaluatedAt).toISOString().slice(11, 19) + 'Z'
|
|
: '--:--:--Z';
|
|
}
|
|
this.context.dataset.state = unknownCount ? 'uncertain' : 'current';
|
|
this.updateCockpitSignals(snapshot, unknownCount);
|
|
if (this.contextLayoutStamp !== snapshot.evaluatedAt) {
|
|
this.contextLayoutStamp = snapshot.evaluatedAt;
|
|
this.scheduleContextLayout();
|
|
}
|
|
}
|