/** Instrument values, routes and vision controls for the Cockpit controller. */ import { COCKPIT_VISION_MODES, normalizeCockpitVisionMode, } from '../cockpitVisionPolicy.js'; import { altitudeRulerCurveInset, altitudeRulerTicks, bearingBetweenCoordinates, cockpitAltitudeDisplayFt, cockpitUiUpdateDue, compassDivisions, formatAltitudeRulerTick, formatCompassDivision, formatSpeedRulerTick, normalizeHeading, relativeBearing, speedRulerTicks, } from '../cockpitMath.js'; import { COCKPIT_CONTEXT_UPDATE_MS, setCockpitRollingValue, } from './cockpitPresentation.js'; export function updateHud( info, nowMs = performance.now(), forceContext = false, ) { this.lastAircraftInfo = info; const heading = normalizeHeading(this.heading ?? info.track ?? 0); if (this.callsign) { this.callsign.textContent = info.callsign || info.registration || info.icao24 || 'AIRCRAFT'; } const speedKt = Number.isFinite(info.velocityMps) ? info.velocityMps * 1.94384 : null; setCockpitRollingValue(this.speed, formatSpeedRulerTick(speedKt), speedKt, { immediate: forceContext, }); if (this.speedRim) this.speedRim.classList.toggle('unavailable', speedKt === null); if (this.speedRimValue) this.speedRimValue.textContent = formatSpeedRulerTick(speedKt); const speedTicks = speedRulerTicks(speedKt, this.speedRimTicks.length); this.speedRimTicks.forEach((element, index) => { const tick = speedTicks[index]; element.hidden = !tick; if (!tick) return; element.style.setProperty('--slot', tick.slot.toFixed(4)); element.style.setProperty('--depth', tick.depth.toFixed(4)); element.style.setProperty( '--curve', altitudeRulerCurveInset(tick.slot).toFixed(5), ); element.classList.toggle('major', tick.major); const label = element.querySelector('b'); if (label) label.textContent = formatSpeedRulerTick(tick.valueKt); }); const altitudeFt = cockpitAltitudeDisplayFt(info.altitudeM, info.onGround); if (this.altitude) { const displayedAltitudeFt = Number.isFinite(altitudeFt) ? Math.round(altitudeFt) : null; setCockpitRollingValue( this.altitude, displayedAltitudeFt !== null ? displayedAltitudeFt.toLocaleString('en-US') : '-----', displayedAltitudeFt, { immediate: forceContext }, ); } if (this.altitudeRim) this.altitudeRim.classList.toggle('unavailable', altitudeFt === null); if (this.altitudeRimValue) { this.altitudeRimValue.textContent = formatAltitudeRulerTick(altitudeFt); } const altitudeTicks = altitudeRulerTicks( altitudeFt, this.altitudeRimTicks.length, ); this.altitudeRimTicks.forEach((element, index) => { const tick = altitudeTicks[index]; element.hidden = !tick; if (!tick) return; element.style.setProperty('--slot', tick.slot.toFixed(4)); element.style.setProperty('--depth', tick.depth.toFixed(4)); element.style.setProperty( '--curve', altitudeRulerCurveInset(tick.slot).toFixed(5), ); element.classList.toggle('major', tick.major); const label = element.querySelector('b'); if (label) label.textContent = formatAltitudeRulerTick(tick.valueFt); }); setCockpitRollingValue( this.headingValue, String(Math.round(heading) % 360).padStart(3, '0'), heading, { circularRange: 360, immediate: forceContext }, ); if (this.compassTape) { const divisions = compassDivisions(heading); const signature = divisions.join(','); if (signature !== this.lastCompassSignature) { this.lastCompassSignature = signature; this.compassTape.innerHTML = divisions .map((division, index) => { const slot = index - 3; return `${formatCompassDivision(division)}`; }) .join(''); } } if (this.clock) this.clock.textContent = new Date().toISOString().slice(11, 19) + 'Z'; if (this.position) { const lat = Number.isFinite(info.latitude) ? `${Math.abs(info.latitude).toFixed(3)}°${info.latitude >= 0 ? 'N' : 'S'}` : '--'; const lon = Number.isFinite(info.longitude) ? `${Math.abs(info.longitude).toFixed(3)}°${info.longitude >= 0 ? 'E' : 'W'}` : '--'; this.position.textContent = `${lat} · ${lon}`; } if (this.aircraftMeta) { const feedState = this.surfaceAcquiring ? 'ACQUIRING SURFACE' : this.surfaceFallback ? 'SURFACE FALLBACK' : info.stale ? 'STALE FEED' : 'LIVE TRACK'; this.aircraftMeta.textContent = `${info.layerId === 'military' ? 'MILITARY' : 'COMMERCIAL'} · ${feedState} · COURSE ALIGNED`; } this.updateRoute(info); if ( forceContext || cockpitUiUpdateDue( nowMs, this.lastContextUpdateMs, COCKPIT_CONTEXT_UPDATE_MS, ) ) { this.lastContextUpdateMs = nowMs; this.updateLocalPosition(info); this.maybeRefreshRegionalBrief(info); this.updateContext(info, heading); } if (this.hud) this.hud.dataset.layer = info.layerId || 'flights'; } export function updateRoute(info) { const origin = info?.route?.origin; const destination = info?.route?.destination; const validDestination = Number.isFinite(destination?.lat) && Number.isFinite(destination?.lon); const routeLabel = (airport) => [airport?.code, airport?.name].filter(Boolean).join(' · ') || 'UNKNOWN'; if (this.routeFrom) this.routeFrom.textContent = routeLabel(origin); if (this.routeTo) this.routeTo.textContent = routeLabel(destination); if (this.routeStatus) { this.routeStatus.textContent = validDestination ? 'ARROW · ESTIMATED DIRECTION' : 'ROUTE DATA UNAVAILABLE'; } if (this.route) this.route.hidden = !origin && !destination; if ( !validDestination || !Number.isFinite(info?.longitude) || !Number.isFinite(info?.latitude) ) { this.clearPredictiveRoute(); return; } const destinationBearing = bearingBetweenCoordinates( info.latitude, info.longitude, destination.lat, destination.lon, ); const relative = relativeBearing( destinationBearing, this.heading ?? info.track ?? 0, ); if (!Number.isFinite(destinationBearing) || !Number.isFinite(relative)) { this.clearPredictiveRoute(); return; } if (this.routeDirection) { const displayedRelative = Math.max(-120, Math.min(120, relative)); this.routeDirection.hidden = false; this.routeDirection.style.setProperty( '--route-angle', `${displayedRelative.toFixed(2)}deg`, ); } if (this.routeDirectionLabel) { this.routeDirectionLabel.textContent = `DEST ${String(Math.round(destinationBearing)).padStart(3, '0')}°`; } } export function syncWeatherToggle(enabled) { if (!this.weatherToggle) return; const active = !!enabled; this.weatherToggle.setAttribute('aria-pressed', String(active)); this.weatherToggle.setAttribute( 'aria-label', `${active ? 'Disable' : 'Enable'} cockpit weather effects`, ); this.weatherToggle.title = `${active ? 'Disable' : 'Enable'} cockpit weather effects`; if (this.weatherState) this.weatherState.textContent = active ? 'ON' : 'OFF'; } export function setVisionMode(mode, { revealParameters = false } = {}) { const next = normalizeCockpitVisionMode(mode); this.visionMode = next; const labels = { optical: 'NORMAL', crt: 'CRT', nvg: 'NVG', thermal: 'FLIR', anime: 'ANIME', noir: 'NOIR', snow: 'SNOW', }; const names = { optical: 'Normal', crt: 'CRT', nvg: 'Night vision', thermal: 'Thermal', anime: 'Anime', noir: 'Noir', snow: 'Snow', }; if (this.visionCurrent) { this.visionCurrent.dataset.cockpitVision = next; this.visionCurrent.setAttribute( 'aria-label', `Current cockpit vision style: ${names[next]}. Activate for next style.`, ); this.visionCurrent.title = `Current style: ${names[next]} — click for next`; } if (this.visionCurrentLabel) this.visionCurrentLabel.textContent = labels[next]; this.onVisionChange?.(next, this.active, { revealParameters }); } export function cycleVisionMode(direction = 1) { const modes = COCKPIT_VISION_MODES; const currentIndex = Math.max(0, modes.indexOf(this.visionMode)); const step = direction < 0 ? -1 : 1; const nextIndex = (currentIndex + step + modes.length) % modes.length; this.setVisionMode(modes[nextIndex], { revealParameters: true }); } export function clearPredictiveRoute() { if (this.routeDirection) this.routeDirection.hidden = true; }