import * as Cesium from 'cesium';
import { retroShader } from './styles/retro.js';
import { animeShader } from './styles/anime.js';
import { noirShader } from './styles/noir.js';
import { snowShader } from './styles/snow.js';
import { nightVisionShader } from './styles/surveillance.js';
import { thermalShader } from './styles/thermal.js';
import {
BLOOM_INTENSITY_DEFAULT,
BLOOM_SCALE_VERSION,
bloomStrengthFromIntensity,
clampBloomIntensity,
decodeBloomIntensity,
} from './bloom.js';
import { LOCATIONS, CITY_POIS, GLOBE_VIEW, flyToGlobeView, flyToPresetLocation, flyToPOI, searchAndFlyTo } from './locations.js';
import { locationMiniStatus } from './locationStatus.js';
import { interruptCameraMotion } from './cameraVerbs.js';
import {
aircraftTrackingTarget,
enterCockpitWithTracking,
} from './cockpitTracking.js';
import { IntelHUD } from './hud.js';
import { ShareLinkManager } from './sharelink.js';
import {
isExplicitLayerStateOrigin,
LayerStateCoordinator,
} from './data/layerState.js';
import { renderMapStackChips, syncMapStackChips } from './mapStackChips.js';
import { OrbitController } from './orbit.js';
import {
CelestialRing,
getKeyholeFadeTuning,
isCelestialRingStyleSupported,
setKeyholeFadeTuning,
} from './celestialRing.js';
import { destroyTrackedReadout, initTrackedReadout } from './data/trackedReadout.js';
import { destroyWorldOverlay, initWorldOverlay } from './overlays/worldOverlay.js';
import {
destroyDetection,
initDetection,
cycleMode as cycleDetectionMode,
getDetectionDiagnostics as readDetectionDiagnostics,
getDetectionTuning,
getMode as getDetectionMode,
setMode as setDetectionModeByLabel,
suspendDetection,
resumeDetection,
setDetectionStyle,
setDetectionTuning,
} from './data/detection.js';
import {
ALLOCATION_STRATEGIES,
canonicalizeDensity,
defaultDensityForProfile,
normalizeAllocationStrategy,
normalizeProfile,
profileForDensity,
} from './data/detectionPolicy.js';
import trafficLayer from './data/traffic.js';
import flightsLayer from './data/flights.js';
import militaryFlightsLayer from './data/militaryFlights.js';
import { isTr3b, toggleTr3b } from './data/tr3bRegistry.js';
import satellitesLayer from './data/satellites.js';
import cctvLayer from './data/cctv.js';
import radioLayer, {
buildRadioTunerTicks,
radioTunerCommitSlot,
radioTunerPointerPosition,
radioTunerSlot,
} from './data/radio.js';
import bikeshareLayer from './data/bikeshare.js';
import aisLiveVesselsLayer from './data/aisLiveVessels.js';
import militaryAwarenessLayer from './data/militaryAwareness.js';
import militaryInstallationsLayer from './data/militaryInstallations.js';
import rocketLaunchesLayer from './data/rocketLaunches.js';
import {
aggregateLayerLoading,
canPresentDeferredStatusNotice,
createGlobalStatusNotice,
createLoadingFeedbackState,
createTrafficSyncFeedbackState,
presentGlobalLoadingStatus,
presentGlobalStatusNotice,
presentLoadingFeedback,
reduceLoadingFeedback,
reduceTrafficSyncFeedback,
} from './loadingFeedback.js';
import { setSplitFlapText } from './splitFlap.js';
import {
cockpitEntryAllowed,
contextAllowedLayerIds,
contextLayerEnableBlockReason,
contextRestoreLayerIds,
contextSnapshotLayerIds,
isExplicitUserIntentOrigin,
mergeContextTransitionErrors,
recordContextRestoreExplicitChange,
recordContextSessionUserChange,
runWithContextModeChanging,
settleContextModeChange,
settleContextIntentReplay,
settleUserFacingContextAction,
shouldCaptureContextSession,
shouldDeferContextEntryDuringClear,
shouldExitContextForLayerChange,
spaceMissionEntryCancellationDisposition,
contextModeWord,
} from './contextModePolicy.js';
import {
shouldExpandGlobalContextPanel,
shouldHideCollapsedRightPanels,
} from './rightRailPolicy.js';
import {
allocatePanelStackHeights,
panelStackAutoCollapseIndices,
resolveLeftStackBottomBoundary,
resolvePanelStackCorridor,
} from './panelStackLayout.js';
import {
resolveCockpitUtilityAnchor,
resolveCockpitUtilityLayout,
} from './cockpitUtilityLayout.js';
import {
applyCockpitVisionStageIntensities,
captureCockpitVisionBaseline,
COCKPIT_VISION_MODES,
normalizeCockpitVisionMode,
} from './cockpitVisionPolicy.js';
import {
applyContactsDetection,
shareCacheNeedsHeal,
shareableDetectionState,
} from './contactsDetectionPolicy.js';
import { formatAwarenessLabel } from './data/militaryAwarenessEngine.js';
import { runCctvLayerEnableTransition } from './cctvFocusPolicy.js';
import {
registerCctvFocusRequestListener,
routeCctvFocusRequest,
} from './cctvFocusRequest.js';
import {
flyToWorldTarget,
registerWorldFocusRequestListener,
routeWorldFocusRequest,
} from './worldFocus.js';
import {
beginDeferredNavigation,
reassertNavigationHandoff,
registerNavigationAuthorityListener,
runExplicitNavigation,
stampInitialShareGesture,
} from './navigationPolicy.js';
import {
cachedGroundFloor,
cachedMeshFloor,
GROUND_FLOOR_LIFT_M,
meshFloorPreferred,
warmGroundFloor,
} from './data/groundFloor.js';
import { sampleMeshFloorCells } from './data/meshFloorSampler.js';
import { holdContinuousRender, releaseContinuousRender, governorRequestRender } from './renderGovernor.js';
import {
setScopeMaskEnabled,
isScopeMaskEnabled,
setScopeMaskFeather,
getScopeMaskFeather,
setScopeTerminusOverride,
getScopeTerminusOverride,
clampScopeTerminusPct,
} from './scopeMask.js';
import {
fetchRegionalBrief,
regionalDistanceM,
weatherCodeLabel,
} from './data/regionalBrief.js';
import {
altitudeRulerCurveInset,
altitudeRulerTicks,
bearingBetweenCoordinates,
cockpitAnchorCorrectionStep,
cockpitAltitudeDisplayFt,
cockpitGroundSafeHeight,
cockpitSurfaceWaitExpired,
cockpitUiUpdateDue,
compassDivisions,
formatAltitudeRulerTick,
formatCockpitContextScope,
formatCompassDivision,
formatSpeedRulerTick,
normalizeHeading,
relativeBearing,
resolveCockpitContextReadout,
resolveHudRailLayout,
resolveTrackedAircraftInfo,
slewHeading,
speedRulerTicks,
} from './cockpitMath.js';
/** Duration (ms) for shader intensity crossfade between style presets. */
const TRANSITION_DURATION_MS = 500;
/** Map of style name to its GLSL shader module for post-process stages. */
const STYLES = { retro: retroShader, surveillance: nightVisionShader, thermal: thermalShader, anime: animeShader, noir: noirShader, snow: snowShader };
/** Versioned localStorage namespace prefix to invalidate stale panel layouts. */
const PANEL_LAYOUT_STORAGE_VERSION = 'v6';
const SHARE_PANEL_STATE_SPECS = Object.freeze([
{ id: 'control-panel', pinnable: true },
{ id: 'location-bar', pinnable: true },
{ id: 'data-panel' },
{ id: 'cctv-panel' },
{ id: 'radio-panel' },
{ id: 'scene-panel' },
{ id: 'global-context-panel' },
{ id: 'pp-toggles' },
{ id: 'param-slider-panel' },
]);
/** Standard map-view panels cleared out of the way on a fresh Cockpit entry. */
const COCKPIT_ENTRY_COLLAPSE_PANEL_IDS = Object.freeze([
'data-panel',
'cctv-panel',
'scene-panel',
'pp-toggles',
'global-context-panel',
'radio-panel',
]);
/**
* Position keys are versioned separately from collapsed-state keys so layout
* default changes (e.g. right-rail origin) can reset positions without also
* resetting every panel's open/closed preference.
*/
const PANEL_POSITION_STORAGE_VERSION = 'v8';
const DETECTION_ALLOCATION_STORAGE_KEY = 'gev:detection-allocation:v1';
/** Z ladder: panels promote within [100, 139]; voice pill 150, toast 200, clean-view-exit 300. */
const PANEL_Z_BASE = 100;
const PANEL_Z_MAX = 139;
const COCKPIT_HEADING_SLEW_DPS = 28;
const COCKPIT_FORWARD_OFFSET_M = 7;
const COCKPIT_UP_OFFSET_M = 2.6;
const COCKPIT_MIN_GROUND_CLEARANCE_M = 12;
const COCKPIT_VIEW_PITCH_DEG = -4;
const COCKPIT_CAMERA_UPDATE_MS = 50;
const COCKPIT_HUD_UPDATE_MS = 100;
const COCKPIT_CONTEXT_UPDATE_MS = 250;
const COCKPIT_LAYOUT_SETTLE_MS = 240;
// Cockpit's Display/Radio strip hangs off the REC readout it shares the right
// margin with, and never touches the briefing card underneath it.
const COCKPIT_UTILITY_REC_GAP_PX = 12;
const COCKPIT_UTILITY_SIGNAL_GAP_PX = 8;
const COCKPIT_UTILITY_MIN_TOP_PX = 96;
const COCKPIT_UTILITY_MIN_TOP_RATIO = 0.12;
const COCKPIT_UTILITY_LAUNCHER_MIN_HEIGHT_PX = 50;
const COCKPIT_GROUND_PROBE_MS = 500;
const COCKPIT_GROUND_WAIT_TIMEOUT_MS = 5000;
const COCKPIT_BRIEF_ROTATE_MS = 9000;
const COCKPIT_BRIEF_CYCLE_OFF_HELP = 'Cycle briefing pages automatically every 9 seconds (Signals → News → Local). Pauses while you hover or focus the panel. Live signal data refreshes continuously either way.';
const COCKPIT_BRIEF_CYCLE_ON_HELP = 'Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.';
const COCKPIT_REGIONAL_REFRESH_MS = 5 * 60_000;
const COCKPIT_REGIONAL_REFRESH_DISTANCE_M = 25_000;
const COCKPIT_BRIEF_PAGES = [
{
id: 'signals',
kicker: 'LIVE SIGNALS',
subtitle: 'OBSERVED / MAPPED PINGS',
source: 'SOURCE-BACKED EVENTS · NO SYNTHETIC NEWS',
},
{
id: 'news',
kicker: 'REGIONAL NEWS',
subtitle: 'LATEST LOCATION-MATCHED REPORTING',
source: 'GOOGLE NEWS RSS · LOCATION QUERY · RECENT',
},
{
id: 'local',
kicker: 'LOCAL INFO',
subtitle: 'PLACE / CONDITIONS / POSITION',
source: 'OPENSTREETMAP · OPEN-METEO · UTC',
},
];
/**
* Fixed UI regions that can occupy the left accordion's vertical lane.
* Rectangles are filtered at runtime for visibility and horizontal overlap,
* so right-side/center controls do not reduce the lane unless they actually
* intersect it at the current viewport size.
*/
const LEFT_STACK_OBSTACLE_SELECTOR = [
'#cockpit-hud .cockpit-topline',
'#cockpit-hud .cockpit-topline > div',
'#title-bar',
'#style-indicator',
'#top-center-actions',
'#traffic-sync-chip',
'#cctv-sync-chip',
'#intel-hud .hud-top-left',
'#intel-hud .hud-top-right',
'#intel-hud .hud-bottom-left',
'#intel-hud .hud-bottom-right',
'#intel-hud .hud-top-bar',
'#intel-hud .hud-bottom-bar',
'#intel-hud .hud-left-edge',
'#intel-hud .hud-right-edge',
'#cockpit-context',
'#cesium-credits .cesium-credit-logoContainer',
'#cesium-credits .cesium-credit-textContainer',
'#location-bar',
'#control-panel',
'#gev-voice-control',
'#pp-toggles',
'#param-slider-panel',
].join(', ');
/**
* Whether an element currently occupies screen space a layout must respect.
*
* A rect alone is not enough: `visibility: hidden` and `opacity: 0` (how the
* Intel HUD retires as a whole) leave the rect intact, so anything anchoring to
* a HUD readout must walk the ancestors as well.
*
* @param {Element|null|undefined} element - Candidate element.
* @returns {boolean} True when the element is painted and has area.
*/
function isRenderedOnScreen(element) {
if (!element) return false;
for (let node = element; node instanceof Element; node = node.parentElement) {
const style = getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) {
return false;
}
}
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
/**
* Fixed UI regions that can occupy the right control lane. Runtime rectangle
* filtering keeps the rail clear of whichever HUD variant is currently
* visible without tying the layout to one screen height.
*/
const RIGHT_STACK_OBSTACLE_SELECTOR = [
'#cockpit-hud .cockpit-topline',
'#cockpit-hud .cockpit-topline > div',
'#title-bar',
'#style-indicator',
'#top-center-actions',
'#traffic-sync-chip',
'#cctv-sync-chip',
'#intel-hud .hud-top-left',
'#intel-hud .hud-top-right',
'#intel-hud .hud-bottom-left',
'#intel-hud .hud-bottom-right',
'#intel-hud .hud-top-bar',
'#intel-hud .hud-bottom-bar',
'#intel-hud .hud-left-edge',
'#intel-hud .hud-right-edge',
'#cockpit-context',
'#cockpit-signal-stream',
'#cesium-credits .cesium-credit-logoContainer',
'#cesium-credits .cesium-credit-textContainer',
'#command-dock',
'#gev-voice-control',
].join(', ');
/** Display labels shown in the mini-status readout for each active style. */
const STYLE_STATUS_LABELS = {
normal: 'NORMAL',
retro: 'CRT',
surveillance: 'NVG',
thermal: 'FLIR',
anime: 'ANIME',
noir: 'NOIR',
snow: 'SNOW',
};
/**
* The tactical detection look: Dense at 75%.
*
* Field test 2026-08-18: "detection mode… 75% weighted, with the 16% fade
* and 5% outside, whatever we had. I want that as the default. It should just
* happen." Fade and outside opacity live in GLOBAL_POST_DEFAULTS, so "whatever
* we had" still needs nothing here — but they are 7% and 1% now, the outside
* default having moved 5 → 3 → 1 during final field tuning (2026-08-24).
* What the quote asked for is the
* baseline of the day, not the two numbers it happened to name.
*
* ONE object, shared by the first-load baseline below, by every military style,
* AND by the Contacts context mode (which OWNS detection while active and
* restores the prior state on exit — see contactsDetectionPolicy.js). Cockpit
* deliberately does NOT touch detection: entering it with SPARSE selected leaves
* SPARSE. Declared ahead of GLOBAL_POST_DEFAULTS because that baseline now reads
* from it.
*/
const MILITARY_DETECTION_PRESET = Object.freeze({ mode: 'dense', densityPct: 75 });
/** Baseline post-processing settings applied on first load (before share-link restore). */
const GLOBAL_POST_DEFAULTS = {
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
sharpen: { enabled: true, intensity: 49 },
hudVariant: 'tactical',
hudVisible: true,
// Detection is ON for EVERY style on a first run, Normal included (owner
// directive 2026-08-22: "detect should also be on by default"). It is the
// same preset object the military styles and Contacts already apply, so there
// is one tactical look, not several that can drift.
//
// This is a first-LOAD baseline, not an override: `_applyGlobalPostDefaults`
// runs before any share-link restore, so a link's `dm`/`dd` still lands on top
// of it. It also deliberately leaves `_detectionUserOverridden` alone — the
// flag means the OPERATOR hand-edited detection, and a factory default is not
// that. Turning detection off by hand therefore still sets the flag and still
// suppresses the military-style auto-enable for the rest of the session.
detectionMode: MILITARY_DETECTION_PRESET.mode.toUpperCase(),
detectionDensity: MILITARY_DETECTION_PRESET.densityPct,
detectionAllocation: 'ELASTIC',
detectionFadePct: 7,
detectionOutsideOpacityPct: 1,
celestialRing: false,
};
// Tactical style defaults applied when users select military style presets.
const STYLE_PRESET_DEFAULTS = {
retro: {
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
sharpen: { enabled: true, intensity: 49 },
styleParams: {
retro: {
pixelation: 1.0,
distortion: 0,
instability: 0.42,
},
},
hudVariant: 'tactical',
hudVisible: true,
detection: MILITARY_DETECTION_PRESET,
},
surveillance: {
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
sharpen: { enabled: true, intensity: 49 },
styleParams: {
surveillance: {
gain: 0.18,
bloom: 0.22,
scanlineStr: 0.96,
pixelation: 1.0,
},
},
hudVariant: 'tactical',
hudVisible: true,
detection: MILITARY_DETECTION_PRESET,
},
thermal: {
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
sharpen: { enabled: true, intensity: 49 },
styleParams: {
thermal: {
sensitivity: 0.85,
bloom: 0.2,
mode: 0.33,
pixelation: 1.0,
},
},
hudVariant: 'tactical',
hudVisible: true,
detection: MILITARY_DETECTION_PRESET,
},
};
/**
* GLSL fragment shader implementing an unsharp-mask sharpening filter.
* Samples a 3x3 neighborhood, computes box blur, then adds the
* difference (center - blur) scaled by `amount` for edge enhancement.
*/
const SHARPEN_SHADER = /* glsl */ `
uniform sampler2D colorTexture;
uniform vec2 colorTextureDimensions;
uniform float amount;
in vec2 v_textureCoordinates;
void main() {
vec2 uv = v_textureCoordinates;
vec2 texel = 1.0 / colorTextureDimensions;
vec4 center = texture(colorTexture, uv);
vec4 blur = (
texture(colorTexture, uv + vec2(-texel.x, -texel.y)) +
texture(colorTexture, uv + vec2( 0.0, -texel.y)) +
texture(colorTexture, uv + vec2( texel.x, -texel.y)) +
texture(colorTexture, uv + vec2(-texel.x, 0.0)) +
center +
texture(colorTexture, uv + vec2( texel.x, 0.0)) +
texture(colorTexture, uv + vec2(-texel.x, texel.y)) +
texture(colorTexture, uv + vec2( 0.0, texel.y)) +
texture(colorTexture, uv + vec2( texel.x, texel.y))
) / 9.0;
vec4 sharpened = center + (center - blur) * amount;
out_FragColor = vec4(clamp(sharpened.rgb, 0.0, 1.0), center.a);
}
`;
/**
* Central UI orchestrator for the God's Eye View application.
*
* Responsibilities:
* - CesiumJS PostProcessStage pipeline: registers per-style GLSL stages
* (NVG, FLIR, CRT, anime, noir, snow) and manages intensity crossfades.
* - Bloom and sharpen post-processing toggle/intensity control.
* - Draggable/collapsible panel system with localStorage persistence,
* z-order stacking, and viewport-clamped positioning.
* - CCTV panel: camera selection, coverage toggle, projection, calibration
* sliders, auto-hop, and summary typewriter effect.
* - Location bar with city/POI preset pills, QWERTY key navigation,
* geocoding search, and inter-city world-jump transitions.
* - Orbit controller integration for POI fly-around.
* - Recording mode with safe-frame overlay and HUD mode switching.
* - Share link encoding/decoding (delegates to ShareLinkManager).
* - Detection overlay mode cycling and density tuning.
* - Toast notification system.
* - Intel HUD lifecycle and variant switching.
*/
/** Shortest-wrap signed degrees, for heading offsets typed as absolute values. */
const signedNormalizeDeg = (deg) => ((((deg + 180) % 360) + 360) % 360) - 180;
/**
* Field definitions for the CCTV click-to-edit pose readout. Chips DISPLAY the
* camera's EFFECTIVE pose (not raw offsets — "HDG 135.0°" instead of the old
* "HEADING 0°" nonsense); typed values convert back to calibration offsets
* against the frozen basePose. ΔN/ΔE stay offset-denominated (absolute lat/lon
* typing is user-hostile).
*/
const CCTV_CAL_FIELDS = {
heading: {
label: 'HDG', unit: '°', decimals: 1,
get: (cam) => cam.headingDeg,
toPatch: (value, base) => ({ headingDeg: signedNormalizeDeg(value - base.headingDeg) }),
},
pitch: {
label: 'PITCH', unit: '°', decimals: 1,
get: (cam) => cam.pitchDeg,
toPatch: (value, base) => ({ pitchDeg: value - base.pitchDeg }),
},
fov: {
label: 'FOV', unit: '°', decimals: 0,
get: (cam) => cam.fovDeg,
toPatch: (value, base) => ({ fovDeg: value - base.fovDeg }),
},
range: {
label: 'RANGE', unit: 'm', decimals: 0,
get: (cam) => cam.rangeM,
toPatch: (value, base) => ({ rangeScale: base.rangeM > 0 ? value / base.rangeM : 1 }),
},
height: {
label: 'HGT', unit: 'm', decimals: 0,
get: (cam) => cam.mountHeightM,
toPatch: (value, base) => ({ heightM: value - base.mountHeightM }),
},
north: {
label: 'ΔN', unit: 'm', decimals: 1,
get: (cam) => cam.calibration?.offsetNorthM || 0,
toPatch: (value) => ({ offsetNorthM: value }),
},
east: {
label: 'ΔE', unit: 'm', decimals: 1,
get: (cam) => cam.calibration?.offsetEastM || 0,
toPatch: (value) => ({ offsetEastM: value }),
},
};
function formatCockpitBriefAge(value) {
const timestamp = Date.parse(value);
if (!Number.isFinite(timestamp)) return 'TIME UNKNOWN';
const minutes = Math.max(0, Math.round((Date.now() - timestamp) / 60_000));
if (minutes < 60) return `${minutes}M AGO`;
const hours = Math.round(minutes / 60);
return hours < 48 ? `${hours}H AGO` : `${Math.round(hours / 24)}D AGO`;
}
function formatCockpitWindDirection(value) {
if (!Number.isFinite(value)) return 'DIR UNKNOWN';
const labels = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
const normalized = ((value % 360) + 360) % 360;
return `${labels[Math.round(normalized / 45) % labels.length]} · ${Math.round(normalized)}°`;
}
function setCockpitRollingValue(element, text, numericValue, {
circularRange = null,
immediate = false,
} = {}) {
if (!element) return;
const nextText = String(text);
const previousText = element.dataset.rollingText;
const previousValue = Number(element.dataset.rollingValue);
const nowMs = performance.now();
const lastRollMs = Number(element.dataset.rollingAt);
if (!immediate
&& previousText !== undefined
&& previousText !== nextText
&& Number.isFinite(lastRollMs)
&& nowMs - lastRollMs < 220) {
return;
}
element.dataset.rollingText = nextText;
element.dataset.rollingAt = String(nowMs);
if (Number.isFinite(numericValue)) element.dataset.rollingValue = String(numericValue);
else delete element.dataset.rollingValue;
element.setAttribute('aria-label', nextText);
if (immediate || previousText === undefined || previousText === nextText) {
if (previousText !== nextText || !element.querySelector('.cockpit-roll-token')) {
element.replaceChildren(...Array.from(nextText, (character) => {
const token = document.createElement('span');
token.className = 'cockpit-roll-token';
token.setAttribute('aria-hidden', 'true');
token.textContent = character;
return token;
}));
}
return;
}
let delta = Number.isFinite(numericValue) && Number.isFinite(previousValue)
? numericValue - previousValue
: 0;
if (Number.isFinite(circularRange) && circularRange > 0) {
const halfRange = circularRange / 2;
if (delta > halfRange) delta -= circularRange;
else if (delta < -halfRange) delta += circularRange;
}
const direction = delta < 0 ? 'down' : 'up';
const width = Math.max(previousText.length, nextText.length);
const from = previousText.padStart(width, ' ');
const to = nextText.padStart(width, ' ');
const fragment = document.createDocumentFragment();
for (let index = 0; index < width; index += 1) {
const previousCharacter = from[index];
const nextCharacter = to[index];
const token = document.createElement('span');
token.className = 'cockpit-roll-token';
token.setAttribute('aria-hidden', 'true');
if (previousCharacter === nextCharacter
|| !/\d/.test(previousCharacter)
|| !/\d/.test(nextCharacter)) {
token.textContent = nextCharacter === ' ' ? '\u00a0' : nextCharacter;
fragment.append(token);
continue;
}
token.classList.add('is-rolling', `roll-${direction}`);
const track = document.createElement('span');
track.className = 'cockpit-roll-track';
const first = document.createElement('span');
const second = document.createElement('span');
first.textContent = direction === 'up' ? previousCharacter : nextCharacter;
second.textContent = direction === 'up' ? nextCharacter : previousCharacter;
track.append(first, second);
token.append(track);
fragment.append(token);
}
element.replaceChildren(fragment);
}
class CockpitViewController {
constructor(viewer, {
onVisionChange = null,
onCameraTakeover = null,
isEntryAllowed = null,
onEntered = null,
onExited = null,
getInheritedVisionLabel = null,
restoreTrackingFrame = null,
} = {}) {
this.viewer = viewer;
this.active = false;
this.trackedEntity = null;
this.trackedEntityWasShown = true;
this.heading = null;
this.lastFrameMs = 0;
this.lastCameraUpdateMs = 0;
this.lastHudUpdateMs = 0;
this.lastContextUpdateMs = 0;
this.lastGroundProbeMs = 0;
this.contextNavigationDeadlineMs = 0;
this.surfaceWaitStartedMs = 0;
this.surfaceAcquiring = false;
this.surfaceFallback = false;
this.lastCompassSignature = '';
this.entry = document.getElementById('cockpit-entry');
this.tr3bToggle = document.getElementById('tr3b-toggle');
this._tr3bSignature = null;
this.mapViewButton = document.getElementById('map-view-switch');
this.resetGlobeButton = document.getElementById('cockpit-reset-globe');
this.hud = document.getElementById('cockpit-hud');
this.entryFocusOrigin = null;
this.callsign = document.getElementById('cockpit-callsign');
this.speed = document.getElementById('cockpit-speed-value');
this.speedRim = document.getElementById('cockpit-speed-rim');
this.speedRimValue = document.getElementById('cockpit-speed-rim-value');
this.speedRimTicks = Array.from(document.querySelectorAll('[data-speed-rim-tick]'));
this.altitude = document.getElementById('cockpit-altitude-value');
this.altitudeRim = document.getElementById('cockpit-altitude-rim');
this.altitudeRimValue = document.getElementById('cockpit-altitude-rim-value');
this.altitudeRimTicks = Array.from(document.querySelectorAll('[data-altitude-rim-tick]'));
this.headingValue = document.getElementById('cockpit-heading-value');
this.compassTape = document.getElementById('cockpit-compass-tape');
this.clock = document.getElementById('cockpit-clock');
this.position = document.getElementById('cockpit-position');
this.aircraftMeta = document.getElementById('cockpit-aircraft-meta');
this.route = document.getElementById('cockpit-route');
this.routeFrom = document.getElementById('cockpit-route-from');
this.routeTo = document.getElementById('cockpit-route-to');
this.routeStatus = document.getElementById('cockpit-route-status');
this.routeDirection = document.getElementById('cockpit-route-direction');
this.routeDirectionLabel = document.getElementById('cockpit-route-direction-label');
this.visionPrevious = document.getElementById('cockpit-vision-previous');
this.visionCurrent = document.getElementById('cockpit-vision-current');
this.visionCurrentLabel = document.getElementById('cockpit-vision-current-label');
this.visionNext = document.getElementById('cockpit-vision-next');
this.visionMode = 'optical';
this.onVisionChange = onVisionChange;
this.onCameraTakeover = onCameraTakeover;
this.onEntered = onEntered;
this.onExited = onExited;
this.getInheritedVisionLabel = typeof getInheritedVisionLabel === 'function'
? getInheritedVisionLabel
: () => 'NORMAL';
this.restoreTrackingFrame = typeof restoreTrackingFrame === 'function'
? restoreTrackingFrame
: () => false;
this.isEntryAllowed = typeof isEntryAllowed === 'function' ? isEntryAllowed : () => true;
this.context = document.getElementById('cockpit-context');
this.contextSubject = document.getElementById('cockpit-context-subject');
this.contextNearestLabel = document.getElementById('cockpit-context-nearest-label');
this.contextBearing = document.getElementById('cockpit-context-bearing');
this.contextDistance = document.getElementById('cockpit-context-distance');
this.contextDirection = document.getElementById('cockpit-context-direction');
this.contextUncertainty = document.getElementById('cockpit-context-uncertainty');
this.contextUpdated = document.getElementById('cockpit-context-updated');
this.contextCohorts = new Map(Array.from(document.querySelectorAll('[data-context-cohort]'))
.map((element) => [element.dataset.contextCohort, element]));
this.contextPrevious = document.getElementById('cockpit-context-previous');
this.contextNext = document.getElementById('cockpit-context-next');
this.contextToggle = document.getElementById('cockpit-context-toggle');
this.weatherToggle = document.getElementById('cockpit-weather-toggle');
this.weatherState = document.getElementById('cockpit-weather-state');
this.contextCollapsed = false;
this.signalStream = document.getElementById('cockpit-signal-stream');
this.signalList = document.getElementById('cockpit-signal-list');
this.signalToggle = document.getElementById('cockpit-signal-toggle');
this.briefKicker = document.getElementById('cockpit-brief-kicker');
this.briefSubtitle = document.getElementById('cockpit-brief-subtitle');
this.briefPrevious = document.getElementById('cockpit-brief-previous');
this.briefNext = document.getElementById('cockpit-brief-next');
this.briefAutoToggle = document.getElementById('cockpit-brief-auto');
this.briefPosition = document.getElementById('cockpit-brief-position');
this.briefSource = document.getElementById('cockpit-brief-source');
this.briefPages = Array.from(document.querySelectorAll('[data-cockpit-brief-page]'));
this.briefTabs = Array.from(document.querySelectorAll('[data-cockpit-brief-index]'));
this.newsStatus = document.getElementById('cockpit-news-status');
this.newsList = document.getElementById('cockpit-news-list');
this.localPlace = document.getElementById('cockpit-local-place');
this.localCoordinates = document.getElementById('cockpit-local-coordinates');
this.localTemperature = document.getElementById('cockpit-local-temperature');
this.localWind = document.getElementById('cockpit-local-wind');
this.localWindDirection = document.getElementById('cockpit-local-wind-direction');
this.localCondition = document.getElementById('cockpit-local-condition');
this.localCloud = document.getElementById('cockpit-local-cloud');
this.localPrecipitation = document.getElementById('cockpit-local-precipitation');
this.signalCollapsed = false;
this.signalUserCollapsed = false;
this.signalItems = [];
this.signalSignatures = new Map();
this.briefPageIndex = 0;
this.briefAutoRotateEnabled = false;
this.briefTimer = null;
this.lastAircraftInfo = null;
this.regionalBrief = null;
this.regionalBriefAnchor = null;
this.regionalBriefFetchedAt = 0;
this.regionalBriefAbort = null;
this.regionalBriefRequestToken = 0;
this.regionalBriefSubjectId = null;
this.contextLayoutFrame = null;
this.contextLayoutStamp = null;
this.scratchTarget = new Cesium.Cartesian3();
this.cockpitAnchor = new Cesium.Cartesian3();
this.cockpitAnchorValid = false;
this.scratchCamera = new Cesium.Cartesian3();
this.scratchAdvance = new Cesium.Cartesian3();
this.scratchCorrection = new Cesium.Cartesian3();
this.scratchForward = new Cesium.Cartesian3();
this.scratchHorizontal = new Cesium.Cartesian3();
this.scratchUp = new Cesium.Cartesian3();
this.scratchLocal = new Cesium.Cartesian3();
this.scratchEnu = new Cesium.Matrix4();
this.scratchAnchorCartographic = new Cesium.Cartographic();
this.scratchCameraCartographic = new Cesium.Cartographic();
this.scratchTargetCartographic = new Cesium.Cartographic();
this._listenerRemovers = [];
// Camera mutations belong before scene update/culling. Changing the camera
// from preRender makes 3D Tiles discover a new view after traversal and can
// create a self-sustaining refinement loop under a moving cockpit camera.
this._listenerRemovers.push(
viewer.scene.preUpdate.addEventListener(() => this.update()),
viewer.trackedEntityChanged.addEventListener(() => {
if (this.active) this._adoptTrackedEntity(performance.now());
else this.syncEntry();
}),
);
this._listen(this.entry, 'click', () => this.enter());
this._listen(this.tr3bToggle, 'click', () => this.toggleTrackedTr3b());
this._listen(this.mapViewButton, 'click', () => this.exit());
this._listen(this.visionPrevious, 'click', () => this.cycleVisionMode(-1));
this._listen(this.visionCurrent, 'click', () => this.cycleVisionMode(1));
this._listen(this.visionNext, 'click', () => this.cycleVisionMode(1));
this._listen(this.contextPrevious, 'click', () => this.navigateContext(-1, { origin: 'user' }));
this._listen(this.contextNext, 'click', () => this.navigateContext(1, { origin: 'user' }));
this._listen(this.contextToggle, 'click', () => this.setContextCollapsed(!this.contextCollapsed));
this._listen(this.weatherToggle, 'click', () => {
const enabled = this.weatherToggle.getAttribute('aria-pressed') !== 'true';
this.syncWeatherToggle(enabled);
window.dispatchEvent(new CustomEvent('gev:cockpit-weather-toggle', {
detail: { enabled },
}));
});
this._listen(window, 'gev:cockpit-weather-state', (event) => {
this.syncWeatherToggle(event?.detail?.enabled !== false);
});
this._listen(this.signalToggle, 'click', () => this.setSignalCollapsed(
!this.signalCollapsed,
{ user: true },
));
this._listen(this.signalList, 'click', (event) => {
const target = event.target.closest('button[data-signal-layer][data-signal-id]');
if (!target) return;
event.preventDefault();
militaryAwarenessLayer.focusTarget?.(
target.dataset.signalLayer,
target.dataset.signalId,
{ origin: 'user' },
);
});
this._listen(this.briefPrevious, 'click', () => this.showBriefPage(this.briefPageIndex - 1, { manual: true }));
this._listen(this.briefNext, 'click', () => this.showBriefPage(this.briefPageIndex + 1, { manual: true }));
this._listen(this.briefAutoToggle, 'click', () => {
this.setBriefAutoRotate(!this.briefAutoRotateEnabled);
});
this.briefTabs.forEach((button) => this._listen(button, 'click', () => {
this.showBriefPage(Number(button.dataset.cockpitBriefIndex), { manual: true });
}));
this._listen(document, 'visibilitychange', () => {
if (document.hidden) this.stopBriefRotation();
else if (this.briefAutoRotateEnabled) this.startBriefRotation();
});
this._listen(window, 'resize', () => this.scheduleContextLayout());
this._listen(document, 'keydown', (event) => this.onKeyDown(event), true);
}
_listen(target, type, handler, options) {
if (!target?.addEventListener) return;
target.addEventListener(type, handler, options);
this._listenerRemovers.push(() => target.removeEventListener(type, handler, options));
}
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';
}
readAircraftInfo() {
// In cockpit mode the controller takes the entity off `viewer.trackedEntity`
// (see update()), so the cockpit's own handle is the tracked identity there.
const trackedEntity = this.viewer?.trackedEntity || this.trackedEntity;
return resolveTrackedAircraftInfo({
civilian: flightsLayer.getTrackedInfo?.() || null,
military: militaryFlightsLayer.getTrackedInfo?.() || null,
trackedId: trackedEntity?.gevTrackedId || '',
});
}
dispatchCockpitModeChanged(active, info = null) {
const subjectId = active
? String(info?.icao24 || '').trim().toLowerCase() || null
: null;
const layerId = active && ['flights', 'military'].includes(info?.layerId)
? info.layerId
: null;
window.dispatchEvent(new CustomEvent('gev:cockpit-mode-changed', {
detail: { active: active === true, subjectId, layerId },
}));
}
/**
* TR-3B Easter egg: flip the tracked contact between its real silhouette and
* the black triangle. The registry owns the session state; the owning layer
* re-derives what renders.
* @returns {boolean} True when a tracked contact was converted/restored.
*/
toggleTrackedTr3b() {
const info = this.readAircraftInfo();
const icao24 = String(info?.icao24 || '').trim();
if (!icao24) return false;
toggleTr3b(icao24);
const layer = info.layerId === 'military' ? militaryFlightsLayer : flightsLayer;
layer.refreshTr3b?.(icao24);
this._tr3bSignature = null; // force the chip to repaint on the next sync
this.syncTr3bToggle(info);
return true;
}
/**
* Show the 🛸 chip whenever a contact is tracked (its own gate — converting
* does not depend on the cockpit entry policy) and mirror the conversion
* state. Change-only DOM writes: this runs on the preUpdate cadence.
* @param {object|null} info Tracked-aircraft descriptor, or null.
*/
syncTr3bToggle(info) {
if (!this.tr3bToggle) return;
const icao24 = String(info?.icao24 || '').trim();
const converted = !!icao24 && isTr3b(icao24);
const signature = icao24 ? `${icao24}:${converted ? 1 : 0}` : '';
if (this._tr3bSignature === signature) return;
this._tr3bSignature = signature;
this.tr3bToggle.hidden = !icao24;
this.tr3bToggle.setAttribute('aria-pressed', converted ? 'true' : 'false');
this.tr3bToggle.title = converted ? 'Restore real aircraft' : 'Reclassify as TR-3B';
}
syncEntry() {
if (this.active) return;
const info = this.readAircraftInfo();
const trackedContact = !!(info && this.viewer.trackedEntity?.position);
this.syncTr3bToggle(trackedContact ? info : null);
const available = !!(this.isEntryAllowed() && trackedContact);
// Change-only DOM writes: this runs on a preUpdate cadence, and
// unconditional `hidden` assignments invalidate style/layout every frame
// even when nothing changed. (perf item 9)
if (this._entryAvailable === available) return;
this._entryAvailable = available;
if (this.entry) this.entry.hidden = !available;
if (this.mapViewButton) this.mapViewButton.hidden = true;
if (this.resetGlobeButton) this.resetGlobeButton.hidden = true;
}
/**
* Navigate Contacts through one Cockpit-owned funnel. A short grace window
* covers feed-refresh handoffs where the old source releases before the new
* source publishes its entity; trackedEntityChanged adopts synchronously as
* soon as the replacement exists, so the normal path does not wait.
*/
navigateContext(direction, options = {}) {
const method = direction < 0 ? 'navigatePrevious' : 'navigateNext';
const wasActive = this.active;
if (wasActive) this.contextNavigationDeadlineMs = performance.now() + 1500;
const navigationOptions = wasActive ? { ...options, aircraftOnly: true } : options;
const changed = Boolean(militaryAwarenessLayer?.[method]?.(navigationOptions));
if (!changed) {
this.contextNavigationDeadlineMs = 0;
return false;
}
if (wasActive) this._adoptTrackedEntity(performance.now());
return true;
}
/** Adopt a newly selected aircraft without ever leaving Cockpit. */
_adoptTrackedEntity(nowMs, suppliedInfo = null) {
const nextEntity = this.viewer.trackedEntity;
if (!this.active || !nextEntity?.position || nextEntity === this.trackedEntity) return false;
const info = suppliedInfo || this.readAircraftInfo();
if (!info) return false;
if (this.trackedEntity && this.viewer.entities.contains(this.trackedEntity)) {
this.trackedEntity.show = this.trackedEntityWasShown;
}
this.trackedEntity = nextEntity;
this.trackedEntityWasShown = nextEntity.show;
nextEntity.show = false;
this.viewer.trackedEntity = undefined;
this.cockpitAnchorValid = false;
this.heading = normalizeHeading(info.track ?? 0);
this.lastFrameMs = nowMs;
this.lastHudUpdateMs = 0;
this.lastContextUpdateMs = 0;
this.lastCameraUpdateMs = 0;
this.contextNavigationDeadlineMs = 0;
this.dispatchCockpitModeChanged(true, info);
return true;
}
setVisionMode(mode, { revealParameters = false } = {}) {
const next = normalizeCockpitVisionMode(mode);
this.visionMode = next;
const inherited = String(this.getInheritedVisionLabel?.() || 'NORMAL').toUpperCase();
const labels = { optical: inherited, crt: 'CRT', nvg: 'NVG', thermal: 'FLIR', noir: 'NOIR' };
const names = { optical: inherited, crt: 'CRT', nvg: 'Night vision', thermal: 'Thermal', noir: 'Noir' };
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 });
}
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 });
}
clearPredictiveRoute() {
if (this.routeDirection) this.routeDirection.hidden = true;
}
onKeyDown(event) {
if (event.repeat || event.isComposing) return;
if (event.key === 'Escape' && this.active) {
if (document.getElementById('context-radio-dock')?.classList.contains('disclosure-open')) return;
if (document.querySelector('#cockpit-utility-controls [aria-expanded="true"]')) return;
event.preventDefault();
event.stopImmediatePropagation();
this.exit();
return;
}
if (event.target?.closest?.('input, textarea, select, [contenteditable]')) return;
const key = event.key?.toLowerCase();
if (key === 'c' && !event.metaKey && !event.ctrlKey && !event.altKey) {
if (!this.active) {
const cockpitAttempt = !!(this.readAircraftInfo() && this.viewer.trackedEntity?.position);
if (!cockpitAttempt) return;
}
event.preventDefault();
event.stopImmediatePropagation();
if (!this.active && !this.isEntryAllowed()) return;
const changed = this.active ? this.exit() : this.enter();
return;
}
}
enter() {
if (this.active) return false;
if (!this.isEntryAllowed()) return false;
const info = this.readAircraftInfo();
const entity = this.viewer.trackedEntity;
if (!info || !entity?.position) return false;
this.entryFocusOrigin = document.activeElement instanceof HTMLElement
? document.activeElement
: null;
// Retire deferred navigation before cancelFlight can run its callbacks.
this.onCameraTakeover?.();
this.viewer.camera.cancelFlight();
this.trackedEntity = entity;
this.trackedEntityWasShown = entity.show;
entity.show = false;
this.heading = normalizeHeading(info.track ?? 0);
this.lastFrameMs = performance.now();
this.lastCameraUpdateMs = 0;
this.lastHudUpdateMs = 0;
this.lastContextUpdateMs = 0;
this.contextNavigationDeadlineMs = 0;
this.lastGroundProbeMs = 0;
this.surfaceWaitStartedMs = performance.now();
this.surfaceAcquiring = false;
this.surfaceFallback = false;
this.lastCompassSignature = '';
this.cockpitAnchorValid = false;
this.lastCameraUpdateMs = 0;
this.active = true;
// Cockpit animates the camera from preUpdate every frame — preUpdate only
// runs on rendered frames, so idle mode would freeze the cockpit solid.
// (perf wave 2)
holdContinuousRender('cockpit');
this.viewer.trackedEntity = undefined;
this.viewer.scene.screenSpaceCameraController.enableInputs = false;
document.body.classList.add('cockpit-mode');
// Activation writes entry/quick/map visibility directly, bypassing
// syncEntry's change-only cache — invalidate it so the exit-path
// syncEntry re-applies every write (notably re-hiding mapViewButton).
this._entryAvailable = undefined;
if (this.entry) this.entry.hidden = true;
if (this.tr3bToggle) {
this.tr3bToggle.hidden = true;
this._tr3bSignature = null;
}
if (this.mapViewButton) this.mapViewButton.hidden = false;
if (this.resetGlobeButton) this.resetGlobeButton.hidden = false;
if (this.hud) this.hud.hidden = false;
if (this.signalStream) this.signalStream.hidden = false;
this.hud?.classList.add('signals-active');
this.signalItems = [];
this.signalSignatures.clear();
this.showBriefPage(0);
this.startBriefRotation();
const trackLabel = info.callsign || info.registration || info.icao24 || 'AIRCRAFT';
const trackHeading = String(Math.round(normalizeHeading(info.track ?? 0))).padStart(3, '0');
this.pushCockpitSignal(
'track',
'track',
'TRACK ACQUIRED',
`${trackLabel} · COURSE ${trackHeading}°`,
);
this.updateHud(info, performance.now(), true);
this.setVisionMode(this.visionMode);
this.scheduleContextLayout();
this.mapViewButton?.focus({ preventScroll: true });
this.onEntered?.();
this.dispatchCockpitModeChanged(true, info);
return true;
}
exit({ restoreTracking = true } = {}) {
if (!this.active) return false;
const entity = this.trackedEntity;
this.active = false;
releaseContinuousRender('cockpit');
this.trackedEntity = null;
this.heading = null;
this.cockpitAnchorValid = false;
this.surfaceWaitStartedMs = 0;
this.surfaceAcquiring = false;
this.surfaceFallback = false;
this.lastHudUpdateMs = 0;
this.lastContextUpdateMs = 0;
this.contextNavigationDeadlineMs = 0;
this.lastCompassSignature = '';
this.stopBriefRotation();
this.regionalBriefAbort?.abort();
this.regionalBriefAbort = null;
this.regionalBriefRequestToken += 1;
this.regionalBriefSubjectId = null;
document.body.classList.remove('cockpit-mode');
this.onExited?.();
this.hud?.style.removeProperty('--cockpit-utility-top');
this.hud?.style.removeProperty('--cockpit-utility-max-height');
if (this.hud) this.hud.hidden = true;
if (this.route) this.route.hidden = true;
this.clearPredictiveRoute();
this.setVisionMode('optical');
if (this.signalStream) this.signalStream.hidden = true;
this.hud?.classList.remove('signals-active');
this.viewer.scene.screenSpaceCameraController.enableInputs = true;
if (entity && this.viewer.entities.contains(entity)) entity.show = this.trackedEntityWasShown;
this.trackedEntityWasShown = true;
this.dispatchCockpitModeChanged(false);
if (restoreTracking && entity && this.viewer.entities.contains(entity)) {
this.viewer.trackedEntity = entity;
this.restoreTrackingFrame(entity);
}
this.syncEntry();
const restoreTarget = this.entryFocusOrigin === this.entry
? this.entry
: (this.entry || this.entryFocusOrigin);
this.entryFocusOrigin = null;
if (restoreTarget?.isConnected && !restoreTarget.hidden) {
restoreTarget.focus({ preventScroll: true });
}
return true;
}
update() {
if (!this.active) {
// Entry availability changes on a human timescale (tracking start/stop,
// info arriving after a poll) — polling it every rendered frame ran
// readAircraftInfo() + DOM pokes at display rate in plain map mode.
// 250 ms keeps the chip imperceptibly fresh; trackedEntityChanged still
// fires syncEntry immediately on the events that matter. (perf item 9)
const nowMs = performance.now();
if (nowMs - (this._lastEntrySyncMs || 0) >= 250) {
this._lastEntrySyncMs = nowMs;
this.syncEntry();
}
return;
}
const nowMs = performance.now();
const info = this.readAircraftInfo();
// Adopt a newly selected track before the camera cadence gate. A context
// NEXT/PREV selection can otherwise spend one frame driving the old
// aircraft with the new aircraft's metadata.
this._adoptTrackedEntity(nowMs, info);
if (!info || !this.trackedEntity || !this.viewer.entities.contains(this.trackedEntity)) {
if (nowMs < this.contextNavigationDeadlineMs) return;
this.exit({ restoreTracking: false });
return;
}
if (!cockpitUiUpdateDue(nowMs, this.lastCameraUpdateMs, COCKPIT_CAMERA_UPDATE_MS)) return;
this.lastCameraUpdateMs = nowMs;
const target = this.trackedEntity.position.getValue(this.viewer.clock.currentTime, this.scratchTarget);
if (!target) return;
const dtSec = Math.min(0.1, Math.max(0, (nowMs - this.lastFrameMs) / 1000));
this.lastFrameMs = nowMs;
if (Number.isFinite(info.track)) {
this.heading = slewHeading(
this.heading ?? info.track, info.track, COCKPIT_HEADING_SLEW_DPS * dtSec,
);
}
if (!this.cockpitAnchorValid) {
Cesium.Cartesian3.clone(target, this.cockpitAnchor);
this.cockpitAnchorValid = true;
}
// First-person motion cannot use the delayed feed correction as a raw
// camera destination: a harmless icon re-anchor becomes a whole-world
// surge/reversal in cockpit view. Advance the camera anchor inertially
// from the reported course/speed and converge on the authoritative layer
// display position at a bounded rate. This preserves the layer's required
// 15/30-second interpolation and per-frame cache without exposing its
// sample-boundary corrections to the camera.
const headingRad = Cesium.Math.toRadians(this.heading ?? 0);
const pitchRad = Cesium.Math.toRadians(COCKPIT_VIEW_PITCH_DEG);
const speedMps = Number.isFinite(info.velocityMps) ? Math.max(0, info.velocityMps) : 0;
if (info.stale) {
// A feed backoff has no authoritative velocity epoch to advance from.
// Hold the cockpit on the exact layer-rendered position so the camera
// cannot coast away while the icon correctly remains fixed.
Cesium.Cartesian3.clone(target, this.cockpitAnchor);
} else {
Cesium.Transforms.eastNorthUpToFixedFrame(
this.cockpitAnchor, Cesium.Ellipsoid.WGS84, this.scratchEnu,
);
this.scratchLocal.x = Math.sin(headingRad);
this.scratchLocal.y = Math.cos(headingRad);
this.scratchLocal.z = 0;
Cesium.Matrix4.multiplyByPointAsVector(this.scratchEnu, this.scratchLocal, this.scratchHorizontal);
Cesium.Cartesian3.normalize(this.scratchHorizontal, this.scratchHorizontal);
Cesium.Cartesian3.multiplyByScalar(
this.scratchHorizontal, speedMps * dtSec, this.scratchAdvance,
);
Cesium.Cartesian3.add(this.cockpitAnchor, this.scratchAdvance, this.cockpitAnchor);
Cesium.Cartesian3.subtract(target, this.cockpitAnchor, this.scratchCorrection);
const correctionDistanceM = Cesium.Cartesian3.magnitude(this.scratchCorrection);
const correctionStepM = cockpitAnchorCorrectionStep(correctionDistanceM, speedMps, dtSec);
if (correctionStepM > 0 && correctionDistanceM > 0) {
Cesium.Cartesian3.multiplyByScalar(
this.scratchCorrection, correctionStepM / correctionDistanceM, this.scratchCorrection,
);
Cesium.Cartesian3.add(this.cockpitAnchor, this.scratchCorrection, this.cockpitAnchor);
}
}
// The inertial anchor is independent of the layer's render-floor clamp and
// can otherwise coast into a photoreal mesh while a landing contact is
// between fixes. Clamp it against the same mesh-first shared floor used by
// aircraft rendering. For a slow contact whose floor cell is still cold,
// its already-clamped render position is a conservative temporary floor.
const anchorCartographic = Cesium.Cartographic.fromCartesian(
this.cockpitAnchor, Cesium.Ellipsoid.WGS84, this.scratchAnchorCartographic,
);
const targetCartographic = Cesium.Cartographic.fromCartesian(
target, Cesium.Ellipsoid.WGS84, this.scratchTargetCartographic,
);
let cockpitFloorM = cachedGroundFloor(info.latitude, info.longitude);
if (info.onGround === true) {
const groundPoint = [{ lat: info.latitude, lon: info.longitude }];
warmGroundFloor(groundPoint);
const meshFloorM = cachedMeshFloor(info.latitude, info.longitude);
if (meshFloorPreferred() && !Number.isFinite(meshFloorM)) {
if (cockpitUiUpdateDue(nowMs, this.lastGroundProbeMs, COCKPIT_GROUND_PROBE_MS)) {
this.lastGroundProbeMs = nowMs;
const viewerCartographic = this.viewer.camera.positionCartographic;
sampleMeshFloorCells(this.viewer.scene, groundPoint, {
excludeObjects: [this.trackedEntity],
viewerLat: Cesium.Math.toDegrees(viewerCartographic.latitude),
viewerLon: Cesium.Math.toDegrees(viewerCartographic.longitude),
});
}
cockpitFloorM = cachedMeshFloor(info.latitude, info.longitude);
if (!Number.isFinite(cockpitFloorM)) {
if (!this.surfaceWaitStartedMs) this.surfaceWaitStartedMs = nowMs;
if (!cockpitSurfaceWaitExpired(nowMs, this.surfaceWaitStartedMs, COCKPIT_GROUND_WAIT_TIMEOUT_MS)) {
// Keep the already-safe map camera in place while the photoreal
// surface under a parked aircraft is acquired. The bounded wait
// prevents a permanently cold mesh cell from freezing cockpit.
this.surfaceAcquiring = true;
this.surfaceFallback = false;
if (cockpitUiUpdateDue(nowMs, this.lastHudUpdateMs, COCKPIT_HUD_UPDATE_MS)) {
this.lastHudUpdateMs = nowMs;
this.updateHud(info, nowMs);
}
return;
}
this.surfaceAcquiring = false;
this.surfaceFallback = true;
} else {
this.surfaceWaitStartedMs = 0;
this.surfaceAcquiring = false;
this.surfaceFallback = false;
}
}
} else {
this.surfaceWaitStartedMs = 0;
this.surfaceAcquiring = false;
this.surfaceFallback = false;
}
if (!Number.isFinite(cockpitFloorM)
&& speedMps < 90
&& Number.isFinite(targetCartographic?.height)) {
cockpitFloorM = targetCartographic.height - GROUND_FLOOR_LIFT_M;
}
if (anchorCartographic && Number.isFinite(cockpitFloorM)) {
const minimumAnchorHeightM = cockpitGroundSafeHeight(
anchorCartographic.height,
cockpitFloorM,
COCKPIT_MIN_GROUND_CLEARANCE_M - COCKPIT_UP_OFFSET_M,
);
if (minimumAnchorHeightM !== anchorCartographic.height) {
anchorCartographic.height = minimumAnchorHeightM;
Cesium.Ellipsoid.WGS84.cartographicToCartesian(anchorCartographic, this.cockpitAnchor);
}
}
// Rebuild the local frame at the stabilized anchor after advancing it.
Cesium.Transforms.eastNorthUpToFixedFrame(
this.cockpitAnchor, Cesium.Ellipsoid.WGS84, this.scratchEnu,
);
this.scratchLocal.x = Math.sin(headingRad);
this.scratchLocal.y = Math.cos(headingRad);
this.scratchLocal.z = 0;
Cesium.Matrix4.multiplyByPointAsVector(this.scratchEnu, this.scratchLocal, this.scratchHorizontal);
Cesium.Cartesian3.normalize(this.scratchHorizontal, this.scratchHorizontal);
this.scratchLocal.x = Math.sin(headingRad) * Math.cos(pitchRad);
this.scratchLocal.y = Math.cos(headingRad) * Math.cos(pitchRad);
this.scratchLocal.z = Math.sin(pitchRad);
Cesium.Matrix4.multiplyByPointAsVector(this.scratchEnu, this.scratchLocal, this.scratchForward);
Cesium.Cartesian3.normalize(this.scratchForward, this.scratchForward);
this.scratchLocal.x = -Math.sin(headingRad) * Math.sin(pitchRad);
this.scratchLocal.y = -Math.cos(headingRad) * Math.sin(pitchRad);
this.scratchLocal.z = Math.cos(pitchRad);
Cesium.Matrix4.multiplyByPointAsVector(this.scratchEnu, this.scratchLocal, this.scratchUp);
Cesium.Cartesian3.normalize(this.scratchUp, this.scratchUp);
Cesium.Cartesian3.multiplyByScalar(
this.scratchHorizontal, COCKPIT_FORWARD_OFFSET_M, this.scratchCamera,
);
Cesium.Cartesian3.add(this.cockpitAnchor, this.scratchCamera, this.scratchCamera);
Cesium.Matrix4.getTranslation(this.scratchEnu, this.scratchTarget);
Cesium.Cartesian3.normalize(this.scratchTarget, this.scratchTarget);
Cesium.Cartesian3.multiplyByScalar(this.scratchTarget, COCKPIT_UP_OFFSET_M, this.scratchTarget);
Cesium.Cartesian3.add(this.scratchCamera, this.scratchTarget, this.scratchCamera);
// Recheck at the final forward-offset camera coordinate because a taxiing
// aircraft can cross into an adjacent coarse floor cell between updates.
const cameraCartographic = Cesium.Cartographic.fromCartesian(
this.scratchCamera, Cesium.Ellipsoid.WGS84, this.scratchCameraCartographic,
);
if (cameraCartographic) {
const cameraLat = Cesium.Math.toDegrees(cameraCartographic.latitude);
const cameraLon = Cesium.Math.toDegrees(cameraCartographic.longitude);
const cameraFloorM = cachedGroundFloor(cameraLat, cameraLon);
if (Number.isFinite(cameraFloorM)) {
cockpitFloorM = Math.max(cockpitFloorM ?? Number.NEGATIVE_INFINITY, cameraFloorM);
}
const safeHeightM = cockpitGroundSafeHeight(
cameraCartographic.height,
cockpitFloorM,
COCKPIT_MIN_GROUND_CLEARANCE_M,
);
if (safeHeightM !== cameraCartographic.height) {
cameraCartographic.height = safeHeightM;
Cesium.Ellipsoid.WGS84.cartographicToCartesian(cameraCartographic, this.scratchCamera);
}
}
this.viewer.camera.setView({
destination: this.scratchCamera,
orientation: { direction: this.scratchForward, up: this.scratchUp },
});
if (cockpitUiUpdateDue(nowMs, this.lastHudUpdateMs, COCKPIT_HUD_UPDATE_MS)) {
this.lastHudUpdateMs = nowMs;
this.updateHud(info, nowMs);
}
}
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';
}
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')}°`;
}
}
updateContext(info, heading) {
if (!this.context) return;
const snapshot = 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 = 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();
}
}
scheduleContextLayout() {
const contextVisible = this.context && !this.context.hidden;
const signalVisible = this.signalStream && !this.signalStream.hidden;
if ((!contextVisible && !signalVisible) || this.contextLayoutFrame !== null) return;
this.contextLayoutFrame = requestAnimationFrame(() => {
this.contextLayoutFrame = null;
this.syncContextLayout();
this.syncSignalLayout();
});
}
showBriefPage(index, { manual = false } = {}) {
const count = COCKPIT_BRIEF_PAGES.length;
this.briefPageIndex = ((Number(index) % count) + count) % count;
const page = COCKPIT_BRIEF_PAGES[this.briefPageIndex];
this.briefPages.forEach((element) => {
element.hidden = element.dataset.cockpitBriefPage !== page.id;
});
this.briefTabs.forEach((button) => {
const current = Number(button.dataset.cockpitBriefIndex) === this.briefPageIndex;
button.setAttribute('aria-current', current ? 'true' : 'false');
});
if (this.briefKicker) {
const indicator = this.briefKicker.querySelector('i');
this.briefKicker.replaceChildren(...[indicator, document.createTextNode(` ${page.kicker}`)].filter(Boolean));
}
if (this.briefSubtitle) this.briefSubtitle.textContent = page.subtitle;
if (this.briefPosition) this.briefPosition.textContent = `${this.briefPageIndex + 1} / ${count}`;
if (this.briefSource) this.briefSource.textContent = page.source;
if (this.signalStream) this.signalStream.dataset.briefPage = page.id;
if (manual && this.briefAutoRotateEnabled) this.startBriefRotation({ reset: true });
this.scheduleContextLayout();
}
setBriefAutoRotate(enabled) {
this.briefAutoRotateEnabled = Boolean(enabled);
if (this.briefAutoToggle) {
this.briefAutoToggle.setAttribute('aria-pressed', String(this.briefAutoRotateEnabled));
const label = this.briefAutoRotateEnabled ? 'CYCLE ON' : 'CYCLE OFF';
this.briefAutoToggle.textContent = label;
const help = this.briefAutoRotateEnabled
? COCKPIT_BRIEF_CYCLE_ON_HELP
: COCKPIT_BRIEF_CYCLE_OFF_HELP;
this.briefAutoToggle.setAttribute('aria-label', label);
this.briefAutoToggle.title = help;
}
if (this.briefAutoRotateEnabled) this.startBriefRotation({ reset: true });
else this.stopBriefRotation();
}
startBriefRotation({ reset = false } = {}) {
if (reset) this.stopBriefRotation();
if (!this.briefAutoRotateEnabled
|| this.briefTimer
|| !this.active
|| this.signalCollapsed
|| document.hidden) return;
this.briefTimer = window.setTimeout(() => {
this.briefTimer = null;
const hasPointer = this.signalStream?.matches(':hover') === true;
const hasFocus = this.signalStream?.contains(document.activeElement) === true;
const isInteracting = hasPointer || hasFocus;
if (!isInteracting) {
this.showBriefPage(this.briefPageIndex + 1);
}
this.startBriefRotation();
}, COCKPIT_BRIEF_ROTATE_MS);
}
stopBriefRotation() {
if (this.briefTimer) window.clearTimeout(this.briefTimer);
this.briefTimer = null;
}
updateLocalPosition(info) {
if (!this.localCoordinates) return;
if (!Number.isFinite(info.latitude) || !Number.isFinite(info.longitude)) {
this.localCoordinates.textContent = 'POSITION UNAVAILABLE';
return;
}
const lat = `${Math.abs(info.latitude).toFixed(3)}°${info.latitude >= 0 ? 'N' : 'S'}`;
const lon = `${Math.abs(info.longitude).toFixed(3)}°${info.longitude >= 0 ? 'E' : 'W'}`;
this.localCoordinates.textContent = `${lat} · ${lon}`;
}
maybeRefreshRegionalBrief(info) {
if (!this.active || !Number.isFinite(info.latitude) || !Number.isFinite(info.longitude)) return;
const subjectId = `${info.layerId || 'aircraft'}:${info.icao24 || info.registration || info.callsign || 'unknown'}`;
if (subjectId !== this.regionalBriefSubjectId) {
this.regionalBriefAbort?.abort();
this.regionalBriefAbort = null;
this.regionalBriefRequestToken += 1;
this.regionalBriefSubjectId = subjectId;
this.regionalBrief = null;
this.regionalBriefAnchor = null;
this.regionalBriefFetchedAt = 0;
}
const point = { latitude: info.latitude, longitude: info.longitude };
const ageMs = Date.now() - this.regionalBriefFetchedAt;
const distanceM = regionalDistanceM(this.regionalBriefAnchor, point);
if (this.regionalBriefAbort || (ageMs < COCKPIT_REGIONAL_REFRESH_MS
&& distanceM < COCKPIT_REGIONAL_REFRESH_DISTANCE_M)) return;
this.regionalBriefAnchor = point;
this.regionalBriefFetchedAt = Date.now();
const controller = new AbortController();
const requestToken = ++this.regionalBriefRequestToken;
this.regionalBriefAbort = controller;
if (!this.regionalBrief) this.renderRegionalBriefStatus('loading', info);
fetchRegionalBrief(point.latitude, point.longitude, { signal: controller.signal })
.then((payload) => {
if (!this.active
|| requestToken !== this.regionalBriefRequestToken
|| subjectId !== this.regionalBriefSubjectId) return;
this.regionalBrief = payload;
this.renderRegionalBrief(payload, info);
})
.catch((error) => {
if (error?.name !== 'AbortError'
&& this.active
&& requestToken === this.regionalBriefRequestToken
&& subjectId === this.regionalBriefSubjectId) {
this.renderRegionalBriefStatus('unavailable', info);
}
})
.finally(() => {
if (this.regionalBriefAbort === controller) this.regionalBriefAbort = null;
});
}
renderRegionalBriefStatus(status, info) {
if (this.newsStatus) {
this.newsStatus.hidden = false;
this.newsStatus.dataset.state = status;
this.newsStatus.textContent = status === 'loading'
? 'ACQUIRING REGIONAL NEWS'
: 'REGIONAL NEWS UNAVAILABLE';
}
if (status === 'unavailable') this.newsList?.replaceChildren();
if (this.localPlace && status === 'loading') this.localPlace.textContent = 'RESOLVING REGION';
if (this.localPlace && status === 'unavailable') this.localPlace.textContent = 'REGION UNAVAILABLE';
this.updateLocalPosition(info);
}
renderRegionalBrief(payload, info) {
const articles = Array.isArray(payload?.articles) ? payload.articles : [];
if (this.newsStatus) {
this.newsStatus.hidden = articles.length > 0;
this.newsStatus.dataset.state = payload?.newsStatus || 'unavailable';
this.newsStatus.textContent = payload?.newsStatus === 'empty'
? 'NO RECENT LOCATION MATCHES'
: 'REGIONAL NEWS UNAVAILABLE';
}
if (this.newsList) {
this.newsList.replaceChildren(...articles.slice(0, 4).map((article) => {
const entry = document.createElement('li');
const link = document.createElement('a');
link.href = article.url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
const title = document.createElement('strong');
title.textContent = article.title;
const metadata = document.createElement('span');
metadata.textContent = `${article.domain || 'SOURCE'} · ${formatCockpitBriefAge(article.publishedAt)}`;
link.append(title, metadata);
entry.append(link);
return entry;
}));
}
const placeLabel = payload?.place?.label || payload?.place?.country || 'REGION UNAVAILABLE';
if (this.localPlace) this.localPlace.textContent = placeLabel.toUpperCase();
this.updateLocalPosition(info);
const weather = payload?.weather;
if (this.localTemperature) {
this.localTemperature.textContent = Number.isFinite(weather?.temperatureC)
? `${Math.round(weather.temperatureC)}°C` : '—';
}
if (this.localWind) {
this.localWind.textContent = Number.isFinite(weather?.windKph)
? `${Math.round(weather.windKph)} KM/H` : '—';
}
if (this.localWindDirection) {
this.localWindDirection.textContent = formatCockpitWindDirection(weather?.windDirectionDeg);
}
if (this.localCondition) this.localCondition.textContent = weatherCodeLabel(weather?.weatherCode);
if (this.localCloud) {
this.localCloud.textContent = Number.isFinite(weather?.cloudCoverPct)
? `CLOUD ${Math.round(weather.cloudCoverPct)}%` : 'CLOUD UNKNOWN';
}
if (this.localPrecipitation) {
this.localPrecipitation.textContent = Number.isFinite(weather?.precipitationMm)
? weather.precipitationMm.toFixed(1) : '—';
}
if (this.signalStream) this.signalStream.dataset.regionalStatus = payload?.status || 'partial';
if (this.briefPageIndex === 1 && this.briefSource) {
this.briefSource.textContent = `${String(payload?.newsSource || 'REGIONAL NEWS').toUpperCase()} · LOCATION QUERY`;
}
this.scheduleContextLayout();
}
renderCockpitSignals() {
if (!this.signalList) return;
this.signalList.replaceChildren(...this.signalItems.map((item) => {
const entry = document.createElement('li');
entry.className = item.tone;
const time = document.createElement('time');
time.textContent = new Date(item.timestamp).toISOString().slice(11, 19) + 'Z';
const body = document.createElement('div');
const heading = item.target ? document.createElement('button') : document.createElement('strong');
if (item.target) {
heading.type = 'button';
heading.className = 'cockpit-signal-target';
heading.dataset.signalLayer = item.target.layerId;
heading.dataset.signalId = item.target.id;
heading.setAttribute('aria-label', `Select flight ${item.title}`);
const label = document.createElement('span');
label.className = 'cockpit-signal-target-label';
label.textContent = item.title;
const rule = document.createElement('span');
rule.className = 'cockpit-signal-target-rule';
rule.setAttribute('aria-hidden', 'true');
const chevron = document.createElement('span');
chevron.className = 'material-symbols-outlined cockpit-signal-target-chevron';
chevron.setAttribute('aria-hidden', 'true');
chevron.textContent = 'chevron_right';
heading.append(label, rule, chevron);
entry.classList.add('actionable');
} else {
heading.textContent = item.title;
}
const copy = document.createElement('span');
copy.textContent = item.detail;
body.append(heading, copy);
entry.append(time, body);
return entry;
}));
this.scheduleContextLayout();
}
pushCockpitSignal(key, tone, title, detail, target = null) {
if (!this.signalList || !title || !detail) return;
const signature = `${title}|${detail}|${target?.layerId || ''}|${target?.id || ''}`;
if (this.signalSignatures.get(key) === signature) return;
this.signalSignatures.set(key, signature);
this.signalItems.unshift({ key, tone, title, detail, target, timestamp: Date.now() });
this.signalItems = this.signalItems.slice(0, 5);
this.renderCockpitSignals();
}
updateCockpitSignals(snapshot, unknownCount) {
const previous = new Map(this.signalItems.map((item) => [item.key, item]));
const contacts = [];
const subject = snapshot.subject;
if (['flights', 'military'].includes(subject?.layerId) && subject?.id) {
contacts.push({
key: `flight:${subject.layerId}:${subject.id}`,
tone: 'track',
title: subject.label || subject.id,
detail: `${subject.layerId === 'military' ? 'MILITARY FLIGHT' : 'COMMERCIAL FLIGHT'} · CURRENT`,
target: { layerId: subject.layerId, id: String(subject.id) },
distanceM: -1,
});
}
for (const cohort of snapshot.cohorts) {
if (!['flights', 'military'].includes(cohort.id)) continue;
for (const item of cohort.nearest) {
const id = item.icao24 || item.id;
if (!id) continue;
contacts.push({
key: `flight:${cohort.id}:${id}`,
tone: cohort.id === 'military' ? 'nearby military' : 'nearby',
// `id` above is IDENTITY (the ICAO hex). Display must not reuse it:
// the row's own `id` already carries the layer's label convention
// (callsign → registration → hex), so a callsign-less enriched
// contact reads as its registration here too. Same helper the
// Context panel's nearest list uses.
title: formatAwarenessLabel(item),
detail: `${cohort.id === 'military' ? 'MILITARY FLIGHT' : 'COMMERCIAL FLIGHT'} · ${
Number.isFinite(item.distanceM)
? `${item.distanceM < 10000 ? (item.distanceM / 1000).toFixed(1) : Math.round(item.distanceM / 1000)} KM`
: 'DISTANCE UNKNOWN'
}`,
target: { layerId: cohort.id, id: String(id) },
distanceM: item.distanceM ?? Infinity,
});
}
}
contacts.sort((a, b) => a.distanceM - b.distanceM);
const nextItems = contacts.slice(0, 5).map((item) => ({
...item,
timestamp: previous.get(item.key)?.timestamp || snapshot.evaluatedAt || Date.now(),
}));
if (unknownCount) {
const sources = snapshot.cohorts
.filter((cohort) => cohort.count === null)
.map((cohort) => cohort.source)
.join(' · ');
nextItems.splice(4, Math.max(0, nextItems.length - 4), {
key: 'input-status',
tone: 'warning',
title: `${unknownCount} INPUT${unknownCount === 1 ? '' : 'S'} UNKNOWN`,
detail: sources || 'SOURCE STATUS UNAVAILABLE',
target: null,
timestamp: previous.get('input-status')?.timestamp || snapshot.evaluatedAt || Date.now(),
});
}
this.signalItems = nextItems;
this.signalSignatures.clear();
this.renderCockpitSignals();
}
setContextCollapsed(collapsed) {
const wasCollapsed = this.contextCollapsed;
this.contextCollapsed = Boolean(collapsed);
if (this.context) this.context.dataset.collapsed = String(this.contextCollapsed);
if (this.contextToggle) {
const expanded = !this.contextCollapsed;
this.contextToggle.setAttribute('aria-expanded', String(expanded));
this.contextToggle.setAttribute('aria-label', `${expanded ? 'Collapse' : 'Expand'} Contact panel`);
this.contextToggle.title = `${expanded ? 'Collapse' : 'Expand'} contact panel`;
const icon = this.contextToggle.querySelector('.material-symbols-outlined');
if (icon) icon.textContent = expanded ? 'chevron_left' : 'chevron_right';
}
if (this.active && wasCollapsed && !this.contextCollapsed) {
window.dispatchEvent(new CustomEvent('gev:cockpit-context-expanded'));
}
this.scheduleContextLayout();
}
setSignalCollapsed(collapsed, { user = false } = {}) {
const wasCollapsed = this.signalCollapsed;
this.signalCollapsed = Boolean(collapsed);
if (user) this.signalUserCollapsed = this.signalCollapsed;
if (this.signalStream) this.signalStream.dataset.collapsed = String(this.signalCollapsed);
if (this.signalToggle) {
const expanded = !this.signalCollapsed;
this.signalToggle.setAttribute('aria-expanded', String(expanded));
this.signalToggle.setAttribute('aria-label', `${expanded ? 'Collapse' : 'Expand'} cockpit briefing panel`);
this.signalToggle.title = `${expanded ? 'Collapse' : 'Expand'} briefing panel`;
const icon = this.signalToggle.querySelector('.material-symbols-outlined');
if (icon) icon.textContent = expanded ? 'right_panel_close' : 'right_panel_open';
}
if (this.signalCollapsed) this.stopBriefRotation();
else this.startBriefRotation({ reset: true });
if (this.active && wasCollapsed && !this.signalCollapsed) {
window.dispatchEvent(new CustomEvent('gev:cockpit-signal-expanded'));
}
this.scheduleContextLayout();
}
syncContextLayout() {
if (!this.context || this.context.hidden) return;
if (window.matchMedia('(max-width: 760px)').matches) {
this.context.dataset.layoutMode = 'compact-bottom';
this.context.style.removeProperty('--cockpit-context-left');
this.context.style.removeProperty('--cockpit-context-top');
this.context.style.removeProperty('--cockpit-context-max-height');
return;
}
const desktopInset = Math.max(24, Math.min(58, window.innerWidth * 0.04));
this.context.dataset.layoutMode = 'bottom-left';
this.context.style.setProperty('--cockpit-context-left', `${desktopInset.toFixed(1)}px`);
this.context.style.removeProperty('--cockpit-context-top');
this.context.style.removeProperty('--cockpit-context-max-height');
}
syncSignalLayout() {
if (!this.signalStream || this.signalStream.hidden) return;
const utilityControls = document.getElementById('cockpit-utility-controls');
if (window.matchMedia('(max-width: 760px)').matches) {
this.signalStream.dataset.layoutMode = 'compact-top';
utilityControls?.classList.remove('layout-primary-only');
utilityControls?.querySelectorAll('.cockpit-utility-control').forEach((control) => {
const hiddenSibling = Boolean(
utilityControls.querySelector('.cockpit-utility-control.is-expanded')
&& !control.classList.contains('is-expanded')
);
control.setAttribute('aria-hidden', String(hiddenSibling));
});
this.hud?.style.removeProperty('--cockpit-utility-top');
this.hud?.style.removeProperty('--cockpit-utility-max-height');
this.hud?.style.removeProperty('--cockpit-utility-expanded-max-height');
this.signalStream.style.removeProperty('--cockpit-signal-right');
this.signalStream.style.removeProperty('--cockpit-signal-top');
this.signalStream.style.removeProperty('--cockpit-signal-max-height');
return;
}
const desktopInset = Math.max(24, Math.min(58, window.innerWidth * 0.04));
this.signalStream.dataset.layoutMode = 'bottom-right';
this.signalStream.style.setProperty('--cockpit-signal-right', `${desktopInset.toFixed(1)}px`);
this.signalStream.style.removeProperty('--cockpit-signal-top');
this.signalStream.style.removeProperty('--cockpit-signal-max-height');
const signalBounds = this.signalStream.getBoundingClientRect();
const utilityBounds = utilityControls?.getBoundingClientRect();
if (utilityBounds) {
const expandedControl = utilityControls?.querySelector('.cockpit-utility-control.is-expanded');
const collapsedControl = utilityControls?.querySelector(
'.cockpit-utility-control:not(.is-expanded)',
);
const collapsedLauncher = collapsedControl?.querySelector('.cockpit-utility-launcher');
const expandedHeight = expandedControl
? Math.max(expandedControl.scrollHeight, expandedControl.getBoundingClientRect().height)
: 0;
const collapsedHeight = collapsedLauncher
? Math.max(COCKPIT_UTILITY_LAUNCHER_MIN_HEIGHT_PX, collapsedLauncher.scrollHeight)
: 0;
// Cockpit owns this anchor outright. The strip used to inherit the left
// accordion's committed top, which is solved against left-lane obstacles
// and dropped the strip straight through the briefing card below it.
// The readout only anchors the strip while it is genuinely on screen:
// the Minimal variant drops it with `display:none`, but HUD Off hides the
// whole Intel HUD with `visibility`/`opacity`, which keeps its rect.
const recReadout = document.querySelector('#intel-hud .hud-top-right');
const recBounds = isRenderedOnScreen(recReadout) ? recReadout.getBoundingClientRect() : null;
const utilityAnchor = resolveCockpitUtilityAnchor({
recBottom: recBounds ? recBounds.bottom : 0,
signalTop: signalBounds.top,
stripHeight: utilityBounds.height,
viewportHeight: window.innerHeight,
collapsedHeight,
recGap: COCKPIT_UTILITY_REC_GAP_PX,
signalGap: COCKPIT_UTILITY_SIGNAL_GAP_PX,
minTopFloor: COCKPIT_UTILITY_MIN_TOP_PX,
minTopRatio: COCKPIT_UTILITY_MIN_TOP_RATIO,
});
const availableHeight = utilityAnchor.maxHeight;
this.hud?.style.setProperty('--cockpit-utility-top', `${utilityAnchor.top.toFixed(1)}px`);
this.hud?.style.setProperty('--cockpit-utility-max-height', `${availableHeight.toFixed(2)}px`);
const utilityLayout = expandedControl && collapsedControl
? resolveCockpitUtilityLayout({ availableHeight, expandedHeight, collapsedHeight })
: { primaryOnly: false, expandedMaxHeight: availableHeight };
utilityControls?.classList.toggle('layout-primary-only', utilityLayout.primaryOnly);
this.hud?.style.setProperty(
'--cockpit-utility-expanded-max-height',
`${utilityLayout.expandedMaxHeight.toFixed(2)}px`,
);
utilityControls?.querySelectorAll('.cockpit-utility-control').forEach((control) => {
const hiddenSibling = utilityLayout.primaryOnly && control === collapsedControl;
control.setAttribute('aria-hidden', String(hiddenSibling));
if (hiddenSibling && control.contains(document.activeElement)) {
expandedControl?.querySelector('.cockpit-utility-glyph')?.focus({ preventScroll: true });
}
});
}
}
dispose() {
this.exit({ restoreTracking: false });
this.regionalBriefAbort?.abort();
this.regionalBriefAbort = null;
this.regionalBriefRequestToken += 1;
this.stopBriefRotation();
if (this.contextLayoutFrame !== null) cancelAnimationFrame(this.contextLayoutFrame);
this.contextLayoutFrame = null;
for (const removeListener of this._listenerRemovers.splice(0)) removeListener?.();
}
}
export class StyleManager {
/**
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
* @param {object} [options]
*/
constructor(viewer, { mapStackController = null } = {}) {
this.viewer = viewer;
this.mapStackController = mapStackController;
this.stages = {};
this.activeStyle = 'normal';
document.documentElement.dataset.gevStyle = this.activeStyle;
this.transitions = new Map();
this.startTime = Date.now();
// True once the user manually changes detection (button/key/slider/voice).
// Gates per-style detection defaults so they never stomp an explicit choice.
this._detectionUserOverridden = false;
// Bloom/sharpen state
this.bloomEnabled = false;
this.sharpenEnabled = false;
this._bloomStage = null;
this._sharpenStage = null;
this._recordingMode = false;
this._recordingConfig = { hidePanels: true, hudMode: 'minimal', safeFrame: '16:9' };
this._preRecordingHudState = null;
this._panelZCounter = PANEL_Z_BASE + 10;
this._animFrameId = null;
this._lastLoadingFeedbackUpdateAt = 0;
this._loadingFeedbackState = createLoadingFeedbackState();
this._loadingFeedbackEvent = null;
this._loadingFeedbackTicker = null;
this._globalStatusNotice = null;
this._shareTrackingAcquiringKey = null;
this._shareTrackingNoticeGeneration = 0;
this._globeResetPromise = null;
this._globeResetHandler = null;
this._clearSelectedLayersPromise = null;
this._clearSelectedLayersManagerPromise = null;
this._clearSelectedLayersHandler = null;
this._dataManager = null;
this._cctvUnsubscribe = null;
this._radioUnsubscribe = null;
this._radioState = null;
this._radioCategorySignature = '';
this._radioTunerStations = [];
this._radioTunerPool = [];
this._radioTunerDragging = false;
this._radioTunerDragStartSlot = 0;
this._radioTunerDragSnapshot = null;
this._radioTunerLastSlot = 0;
this._radioTunerDragDirection = 0;
this._radioTunerCoordinate = 0;
this._radioTunerPointerId = null;
this._radioTunerKeyboardKey = null;
this._radioTunerAbort = null;
this._radioTunerBandSignature = '';
this._radioTunerSelectedId = null;
this._radioTunerBandPinnedForNavigation = false;
this._radioTunerCameraRemove = null;
this._refreshRadioTunerBand = null;
this._cctvState = null;
this._cctvSummaryTypingTimer = null;
this._lastCctvSummaryText = '';
// Auto-expand guard: last active camera id seen while the layer was
// enabled; routine state notifications with the same id never re-expand.
this._lastSeenCctvActiveId = null;
this._cctvChipHideTimer = null;
this._cctvChipWasBusy = false;
this._leftStackLayoutFrame = null;
this._leftStackReconsiderAutoCollapse = false;
this._leftStackResizeObserver = null;
this._leftStackMutationObserver = null;
this._leftStackHudTransitionHandler = null;
this._leftStackCollapsedHeights = new Map();
this._leftStackPreferredPanelId = null;
this._rightPanelStack = document.getElementById('right-context-rail');
this._rightStackLayoutFrame = null;
this._rightStackReconsiderAutoCollapse = false;
this._rightStackResizeObserver = null;
this._rightStackMutationObserver = null;
this._rightStackHudTransitionHandler = null;
this._rightStackPreferredPanelId = null;
this._adaptivePanelSettleTimer = null;
this._windowResizeHandler = null;
this._loadingVisibilityHandler = null;
this._cctvRequestFocusHandler = null;
this._removeCctvRequestFocusListener = null;
this._worldRequestFocusHandler = null;
this._removeWorldRequestFocusListener = null;
this._removeNavigationAuthorityListener = null;
this._navigationOwnerChangedRemover = null;
this._navigationGeneration = 0;
this._activeLocationSearchGeneration = null;
this._initialShareState = null;
this._initialShareNavigationGeneration = null;
this._initialShareRestoreTimeout = null;
this._layerStateCoordinator = null;
this._layerStateRestorePromise = null;
this._awarenessSelectedHandler = null;
this._awarenessClearedHandler = null;
this._disposed = false;
this._draggableResizeObserver = null;
// DOM refs
this._styleIndicator = document.getElementById('active-style-name');
this._sliderPanel = document.getElementById('param-slider-panel');
this._sliderContainer = document.getElementById('param-sliders');
this._ppToggles = document.getElementById('pp-toggles');
this._bloomBtn = document.getElementById('bloom-toggle');
this._bloomSliderRow = document.getElementById('bloom-slider-row');
this._bloomSlider = document.getElementById('bloom-intensity-slider');
this._bloomSliderValue = document.getElementById('bloom-intensity-value');
this._sharpenBtn = document.getElementById('sharpen-toggle');
this._sharpenSliderRow = document.getElementById('sharpen-slider-row');
this._sharpenSlider = document.getElementById('sharpen-intensity-slider');
this._sharpenSliderValue = document.getElementById('sharpen-intensity-value');
this._hudBtn = document.getElementById('hud-toggle');
this._hudLayoutRow = document.getElementById('hud-layout-row');
this._hudLayoutSelect = document.getElementById('hud-layout-select');
this._detectionSliderRow = document.getElementById('detection-slider-row');
this._detectionDensitySlider = document.getElementById('detection-density-slider');
this._detectionDensityValue = document.getElementById('detection-density-value');
this._detectionAllocationRow = document.getElementById('detection-allocation-row');
this._detectionAllocationBtns = [
document.getElementById('detection-allocation-elastic'),
document.getElementById('detection-allocation-weighted'),
].filter(Boolean);
this._detectionFadeRow = document.getElementById('detection-fade-row');
this._detectionFadeSlider = document.getElementById('detection-fade-slider');
this._detectionFadeValue = document.getElementById('detection-fade-value');
this._detectionOpacityRow = document.getElementById('detection-opacity-row');
this._detectionOpacitySlider = document.getElementById('detection-opacity-slider');
this._detectionOpacityValue = document.getElementById('detection-opacity-value');
let storedDetectionAllocation = 'ELASTIC';
try {
storedDetectionAllocation = localStorage.getItem(DETECTION_ALLOCATION_STORAGE_KEY) || 'ELASTIC';
} catch { /* storage can be unavailable in privacy/test contexts */ }
this._detectionAllocationPreference = normalizeAllocationStrategy(storedDetectionAllocation);
this._celestialBtn = document.getElementById('celestial-toggle');
this._scopeBtn = document.getElementById('scope-toggle');
this._scopeFeatherSlider = document.getElementById('scope-feather-slider');
this._scopeFeatherValue = document.getElementById('scope-feather-value');
this._mapStackChips = document.getElementById('map-stack-chips');
this._mapStackStatus = document.getElementById('map-stack-status');
this._cleanViewBtn = document.getElementById('clean-view-toggle');
this._cleanViewExitBtn = document.getElementById('clean-view-exit');
this._dataPanel = document.getElementById('data-panel');
this._scenePanel = document.getElementById('scene-panel');
this._cctvPanel = document.getElementById('cctv-panel');
this._radioPanel = document.getElementById('radio-panel');
this._contextRadioDock = document.getElementById('context-radio-dock');
this._contextRadioToggleBtn = document.getElementById('context-radio-toggle-btn');
this._contextRadioMini = document.getElementById('context-radio-mini');
this._contextRadioMiniEnableBtn = document.getElementById('context-radio-mini-enable-btn');
this._contextRadioDetailsBtn = document.getElementById('context-radio-details-btn');
this._contextRadioMiniCloseBtn = document.getElementById('context-radio-mini-close-btn');
this._contextRadioMiniStation = document.getElementById('context-radio-mini-station');
this._contextRadioMiniPrevBtn = document.getElementById('context-radio-mini-prev-btn');
this._contextRadioMiniPlayBtn = document.getElementById('context-radio-mini-play-btn');
this._contextRadioMiniNextBtn = document.getElementById('context-radio-mini-next-btn');
this._contextRadioMiniVolume = document.getElementById('context-radio-mini-volume');
this._contextRadioMiniVolumeValue = document.getElementById('context-radio-mini-volume-value');
this._cockpitUtilityControls = document.getElementById('cockpit-utility-controls');
this._cockpitDisplayToggleBtn = document.getElementById('cockpit-display-toggle-btn');
this._cockpitDisplayPanel = document.getElementById('cockpit-display-panel');
this._cockpitDisplayPortalRecords = [];
this._cockpitDisplayPortalActive = false;
this._cockpitDisplayModeHandler = null;
this._cockpitRadioToggleBtn = document.getElementById('cockpit-radio-toggle-btn');
this._cockpitRadioPanel = document.getElementById('cockpit-radio-panel');
this._cockpitRadioEnableBtn = document.getElementById('cockpit-radio-enable-btn');
this._cockpitRadioStation = document.getElementById('cockpit-radio-station');
this._cockpitRadioPrevBtn = document.getElementById('cockpit-radio-prev-btn');
this._cockpitRadioPlayBtn = document.getElementById('cockpit-radio-play-btn');
this._cockpitRadioNextBtn = document.getElementById('cockpit-radio-next-btn');
this._cockpitRadioVolume = document.getElementById('cockpit-radio-volume');
this._cockpitRadioVolumeValue = document.getElementById('cockpit-radio-volume-value');
this._radioLayerState = document.getElementById('radio-layer-state');
this._radioEnableBtn = document.getElementById('radio-enable-btn');
this._radioFilter = document.getElementById('radio-filter');
this._radioStationName = document.getElementById('radio-station-name');
this._radioStationMeta = document.getElementById('radio-station-meta');
this._radioStationTags = document.getElementById('radio-station-tags');
this._radioTuner = document.getElementById('radio-tuner');
this._radioTunerSlider = document.getElementById('radio-tuner-slider');
this._radioTunerNeedle = document.getElementById('radio-tuner-needle');
this._radioTunerBandLabel = document.getElementById('radio-tuner-band-label');
this._radioTunerValue = document.getElementById('radio-tuner-value');
this._radioTunerStation = document.getElementById('radio-tuner-station');
this._radioPrevBtn = document.getElementById('radio-prev-btn');
this._radioPlayBtn = document.getElementById('radio-play-btn');
this._radioNextBtn = document.getElementById('radio-next-btn');
this._radioStopBtn = document.getElementById('radio-stop-btn');
this._radioVolume = document.getElementById('radio-volume');
this._radioVolumeValue = document.getElementById('radio-volume-value');
this._radioPlaybackState = document.getElementById('radio-playback-state');
this._radioStationHomepage = document.getElementById('radio-station-homepage');
this._globalContextFlightsBtn = document.getElementById('global-context-flights-btn');
this._globalContextMissionsBtn = document.getElementById('global-context-missions-btn');
this._contextModeStandby = document.getElementById('context-mode-standby');
this._contextFlightsView = document.getElementById('context-flights-view');
this._contextMissionsView = document.getElementById('context-missions-view');
this._contextMode = null;
this._contextModeChanging = false;
this._contextModeGeneration = 0;
this._contextModeEntering = null;
this._contextModeEntryIntent = null;
this._contextModeReplacementIntent = null;
this._contextModeDeferredEntryIntent = null;
this._contextSessionSnapshot = null;
this._contextRestoreState = null;
this._contextLayerReactionPromises = new Set();
this._preservePanelStateDuringLayerClear = false;
this._userFacingContextNotificationTokens = new Set();
this._dataManagerBeforeDestroyUnsubscribe = null;
this._dataManagerVisibilityGuardUnsubscribe = null;
this._dataManagerVisibilityRequestUnsubscribe = null;
this._installationsSearchBtn = document.getElementById('installations-search-btn');
this._leftPanelStack = document.getElementById('left-panel-stack');
this._cctvEnableBtn = document.getElementById('cctv-enable-btn');
this._cctvNearestBtn = document.getElementById('cctv-nearest-btn');
this._cctvPrevBtn = document.getElementById('cctv-prev-btn');
this._cctvNextBtn = document.getElementById('cctv-next-btn');
this._cctvSelect = document.getElementById('cctv-camera-select');
this._cctvFocusBtn = document.getElementById('cctv-focus-btn');
this._cctvCoverageBtn = document.getElementById('cctv-coverage-btn');
this._cctvAutoHopBtn = document.getElementById('cctv-auto-hop-btn');
this._cctvProjectionBtn = document.getElementById('cctv-projection-btn');
this._cctvQualityChip = document.getElementById('cctv-quality-chip');
this._cctvAdjustBtn = document.getElementById('cctv-adjust-btn');
this._cctvCalReadout = document.getElementById('cctv-cal-readout');
this._cctvCalibSaveBtn = document.getElementById('cctv-calib-save-btn');
this._cctvCalibResetBtn = document.getElementById('cctv-calib-reset-btn');
this._cctvFrame = document.getElementById('cctv-frame');
this._cctvFrameWrap = document.getElementById('cctv-frame-wrap');
this._cctvFrameRequestToken = 0;
this._cctvFramePreloader = null;
this._cctvSourceBadge = document.getElementById('cctv-source-badge');
this._cctvMeta = document.getElementById('cctv-meta');
this._cctvSummary = document.getElementById('cctv-summary');
this._shareBtn = document.getElementById('share-btn');
this._clearSelectedLayersBtn = document.getElementById('clear-selected-layers');
this._globalLoadingStatus = document.getElementById('global-loading-status');
this._globalLoadingLabel = document.getElementById('global-loading-label');
this._globalLoadingDetail = document.getElementById('global-loading-detail');
this._resetGlobeBtn = document.getElementById('reset-globe-view');
this._cockpitResetGlobeBtn = document.getElementById('cockpit-reset-globe');
this._styleButtons = document.getElementById('style-buttons');
this._trafficSyncChip = document.getElementById('traffic-sync-chip');
this._trafficSyncLabel = document.getElementById('traffic-sync-label');
this._trafficSyncProgress = document.getElementById('traffic-sync-progress');
this._cctvSyncChip = document.getElementById('cctv-sync-chip');
this._cctvSyncLabel = document.getElementById('cctv-sync-label');
this._cctvSyncProgress = document.getElementById('cctv-sync-progress');
this._toast = document.getElementById('toast');
this._locationSearch = document.getElementById('location-search');
this._searchToggle = document.getElementById('search-toggle');
this._locationPills = document.getElementById('location-pills');
this._poiRow = document.getElementById('poi-row');
this._locationBarDivider = document.getElementById('location-bar-divider');
this._styleMiniValue = document.getElementById('style-mini-value');
this._locationMiniCity = document.getElementById('location-mini-city');
this._locationMiniPoi = document.getElementById('location-mini-poi');
this._safeFrameOverlay = document.getElementById('safe-frame-overlay');
this._safeFrameBox = document.getElementById('safe-frame-box');
this._activeLocationId = null;
this._expandedCityId = null;
this._activePoiIndex = null;
this._currentTarget = null; // Cesium.Cartesian3 of current POI target
this._currentPoi = null; // Current POI data object
// Formatted address of the last free-text geocode search. Preset pills set
// _activeLocationId instead; a search has no preset record, so this is the
// only thing the mini-status can report for it.
this._searchedLocationLabel = null;
this._trafficSyncFeedbackState = createTrafficSyncFeedbackState();
this._trafficTransitionTimer = null;
this._lastTrafficChipUpdateAt = 0;
// Orbit controller
this.orbitController = new OrbitController(viewer);
this._orbitIndicator = null;
// Intel HUD
this.hud = new IntelHUD(viewer);
this._cockpitVisionMode = 'optical';
this._cockpitVisionRestore = null;
this._cockpitPanelRestore = null;
// True only while the open Data Layers panel is the reason Cockpit's
// Contact panel is collapsed. A user-collapsed Contact panel must remain
// collapsed when Data Layers closes.
this._cockpitContextCollapsedForDataPanel = false;
/** Pre-Contacts detection state, restored on deactivation (see _syncContactsDetection). */
this._contactsDetectionRestore = null;
this.cockpitView = new CockpitViewController(viewer, {
onVisionChange: (mode, active, options) => this._setCockpitVision(mode, active, options),
onCameraTakeover: () => this._stampNavigation({ cancelPendingSelection: false }),
getInheritedVisionLabel: () => (
STYLE_STATUS_LABELS[this.activeStyle]
|| String(this.activeStyle || 'normal').toUpperCase()
),
isEntryAllowed: () => cockpitEntryAllowed({
contextMode: this._contextMode,
contextModeChanging: this._contextModeChanging,
flightsEnabled: !!this._dataManager?.isEnabled('flights'),
militaryEnabled: !!this._dataManager?.isEnabled('military'),
}),
onEntered: () => {
// A new Cockpit session owns both side rails. Clear standard map-view
// panels once on entry; NEXT/PREVIOUS never reaches this callback, so
// panels the operator opens while already inside remain untouched.
this._cockpitPanelRestore = new Map();
this._cockpitContextCollapsedForDataPanel = false;
for (const panelId of COCKPIT_ENTRY_COLLAPSE_PANEL_IDS) {
const panel = document.getElementById(panelId);
if (panel) {
this._cockpitPanelRestore.set(panelId, panel.classList.contains('collapsed'));
}
this.setPanelCollapsed(panelId, true, {
persist: false,
syncShare: false,
});
}
this.cockpitView?.setContextCollapsed(false);
this.cockpitView?.setSignalCollapsed(false, { user: true });
},
onExited: () => {
const restore = this._cockpitPanelRestore;
this._cockpitPanelRestore = null;
this._cockpitContextCollapsedForDataPanel = false;
if (!restore) return;
for (const [panelId, wasCollapsed] of restore) {
this.setPanelCollapsed(panelId, wasCollapsed, {
persist: false,
syncShare: false,
});
}
},
restoreTrackingFrame: (entity) => {
const [layerId, ...idParts] = String(entity?.gevTrackedId || '').split(':');
const trackedId = idParts.join(':');
if (!trackedId) return false;
if (layerId === 'flights') return flightsLayer.refocusTrackedById?.(trackedId) === true;
if (layerId === 'military') return militaryFlightsLayer.refocusTrackedById?.(trackedId) === true;
return false;
},
});
// Full-globe sun/moon ring. It is a crisp screen-space overlay above the
// Cesium canvas but below the HUD/detection/readout z ladder.
this.celestialRing = new CelestialRing(viewer, {
enabled: false,
onAutoDisable: () => this.setCelestialRingEnabled(false, {
syncShare: !!this.shareLinkManager,
focus: false,
}),
});
// Share Link Manager
this.shareLinkManager = new ShareLinkManager(viewer, {
onRestore: async (state) => {
const {
style,
bloom,
sharpen,
bloomIntensity,
bloomVersion,
sharpenIntensity,
hudVariant,
hudVisible,
detectionMode,
detectionDensity,
detectionAllocation,
detectionFadePct,
detectionOutsideOpacityPct,
celestialRing,
scopeEnabled,
scopeFeatherPct,
scopeTerminusPct,
mapStack,
panelState,
styleParams,
} = state || {};
// Ignore the retired 'ai-edit' style from older share links.
if (style && style !== 'normal' && style !== 'ai-edit') {
this.setStyle(style, { applyPreset: true, revealParameters: false, restore: true });
}
if (styleParams && style && this.stages[style] && STYLES[style]?.uniforms) {
for (const [uniformName, uniformValue] of Object.entries(styleParams)) {
if (!Object.hasOwn(STYLES[style].uniforms, uniformName)) continue;
this.stages[style].uniforms[uniformName] = uniformValue;
}
this._updateSliderPanel(style, { reveal: false });
}
if (typeof bloomIntensity === 'number' && this._bloomSlider) {
const intensity = decodeBloomIntensity(bloomIntensity, bloomVersion);
this._setBloomIntensity(intensity, { syncShare: false });
}
if (typeof sharpenIntensity === 'number' && this._sharpenSlider) {
const pct = Math.max(0, Math.min(100, Math.round(sharpenIntensity)));
this._sharpenSlider.value = String(pct);
this._sharpenSliderValue.textContent = `${pct}%`;
this._applySharpenIntensity(pct / 100);
}
if (typeof bloom === 'boolean') this._setBloomEnabled(bloom);
if (typeof sharpen === 'boolean') this._setSharpenEnabled(sharpen);
if (hudVariant) this._setHudVariant(hudVariant);
if (typeof hudVisible === 'boolean') {
this.hud.setMode(hudVisible ? 'on' : 'off');
this._updateHudButtonState();
}
if (typeof detectionDensity === 'number' && this._detectionDensitySlider) {
const pct = canonicalizeDensity(detectionDensity);
this._detectionDensitySlider.value = String(pct);
this._detectionDensityValue.textContent = `${pct}%`;
this._applyDetectionDensityFromUi();
}
if (detectionAllocation) {
this._setDetectionAllocation(detectionAllocation, { syncShare: false, persist: false });
}
if (typeof detectionFadePct === 'number' && this._detectionFadeSlider) {
this._detectionFadeSlider.value = String(detectionFadePct);
}
if (typeof detectionOutsideOpacityPct === 'number' && this._detectionOpacitySlider) {
this._detectionOpacitySlider.value = String(detectionOutsideOpacityPct);
}
this._applyDetectionFadeFromUi();
if (detectionMode) this._setDetectionMode(detectionMode);
if (typeof celestialRing === 'boolean') {
this.setCelestialRingEnabled(celestialRing, { syncShare: false, focus: false });
}
if (typeof scopeEnabled === 'boolean') {
setScopeMaskEnabled(scopeEnabled);
this._scopeBtn?.classList.toggle('active', scopeEnabled);
this._scopeBtn?.setAttribute('aria-pressed', String(scopeEnabled));
}
if (typeof scopeFeatherPct === 'number' && this._scopeFeatherSlider) {
const pct = Math.max(0, Math.min(100, Math.round(scopeFeatherPct)));
this._scopeFeatherSlider.value = String(pct);
if (this._scopeFeatherValue) this._scopeFeatherValue.textContent = `${pct}%`;
setScopeMaskFeather(pct / 100);
}
// null restores the altitude-adaptive ramp; a number pins the terminus
// (clamped to the supported 94..100 band, same as the `sce` hash key).
if (scopeTerminusPct === null) setScopeTerminusOverride(null);
else if (typeof scopeTerminusPct === 'number') {
const pinned = clampScopeTerminusPct(scopeTerminusPct);
setScopeTerminusOverride(pinned == null ? null : pinned / 100);
}
const mapStackRestore = mapStack
? this._setMapStack(mapStack, { syncShare: false })
: Promise.resolve();
if (panelState) this._restorePanelState(panelState);
await mapStackRestore;
this._syncShareState();
},
isNavigationCurrent: (generation) => generation === this._navigationGeneration,
cancelOwnedNavigation: () => this.viewer.camera.cancelFlight(),
});
this.shareLinkManager.setPanelStateProvider(() => this._buildSharePanelState());
this.shareLinkManager.setStyleParamStateProvider((styleName) => {
const shader = STYLES[styleName];
const stage = this.stages[styleName];
if (!shader?.uniforms || !stage) return null;
return Object.fromEntries(
Object.keys(shader.uniforms).map((uniformName) => [uniformName, stage.uniforms[uniformName]]),
);
});
// Parse before panel chrome initializes so every valid share URL starts
// from deterministic markup defaults instead of recipient-local panel
// preferences. Encoded panel fields are applied after all panels exist.
this._initialShareState = this.shareLinkManager.parseInitialHash();
this._detectionBtn = document.getElementById('detection-toggle');
this._models3dBtn = document.getElementById('models3d-toggle');
this._models3dModeRow = document.getElementById('models3d-mode-row');
this._models3dModeBtns = [
document.getElementById('models3d-mode-proximity'),
document.getElementById('models3d-mode-all'),
];
// DISPLAY-rail 3D-aircraft toggle (flights layer param). DEFAULT-ON in
// PROXIMITY (product invariant 2026-08-22) — mirrors the `models3d` default in
// layerState.js and `_models3dEnabled` in both flight layers, and the `active`
// class the button carries in index.html. A fresh boot skips layer-state
// restoration, so these initializers are the only thing keeping the lit
// button and the armed layer in agreement.
this._models3dEnabled = true;
this._models3dMode = 'proximity'; // 'proximity' (nearest in view) | 'all' (every in-view plane)
// The shared world-overlay host must own its one postRender lane before
// detection and tracked-readout initialize. It stays transparent until a
// production source explicitly registers entries.
initWorldOverlay(viewer);
// Initialize detection overlay BEFORE style stages so the composite
// stage is first in the post-process pipeline
initDetection(viewer, [trafficLayer, flightsLayer, militaryFlightsLayer, satellitesLayer, cctvLayer, bikeshareLayer, aisLiveVesselsLayer], (modeLabel) => {
this._updateDetectionButton(modeLabel);
});
initTrackedReadout(viewer);
setDetectionStyle(this.activeStyle);
this._applyDetectionDensityFromUi();
this._initStages();
this._initBloomSharpen();
this._initUI();
this._initMapStackControl();
this._initPanelChrome();
this._initLeftPanelAdaptiveLayout();
this._initRightPanelAdaptiveLayout();
this._initRadioPanel();
this._initCctvPanel();
this._initGlobalContextPanel();
this._initLocationBar();
this._initShareButton();
this._initClearSelectedLayersButton();
this._initResetGlobeButton();
this._initHUDToggle();
this._initModels3dToggle();
this._applyGlobalPostDefaults();
this._initOrbit();
this._initRecordingOverlay();
this._startAnimationLoop();
this._startTrafficChipTicker();
this._updateStyleMiniStatus();
this._updateLocationMiniStatus();
// Restore from URL hash if present
const savedState = this._initialShareState;
this._initialShareRestorePromise = savedState
? new Promise((resolve) => { this._resolveInitialShareRestore = resolve; })
: Promise.resolve({ status: 'not-requested', share: null, layers: [] });
if (savedState) {
this._hasShareState = true;
// Reserve camera authority now; the delayed mesh-friendly flight may
// run only if no newer user, voice, or tracking navigation has won.
this._initialShareNavigationGeneration = this._beginDeferredNavigation(
'shared view',
{ cancelPendingSelection: false },
);
this._initialShareRestoreTimeout = setTimeout(() => {
this._initialShareRestoreTimeout = null;
if (this._disposed) return;
const generation = this._initialShareNavigationGeneration;
const applyCamera = Number.isInteger(generation)
&& this._reassertNavigationHandoff(generation);
void (async () => {
try {
const share = await this.shareLinkManager.applyState(savedState, {
applyCamera,
navigationToken: generation,
});
const layers = await (this._layerStateRestorePromise || Promise.resolve([]));
const tracking = share.camera === 'applied'
? await this._layerStateCoordinator?.restoreShareTrackingSelection?.()
: {
status: 'superseded',
cleared: this._layerStateCoordinator?.cancelPendingShareTracking?.(
'shared-camera-superseded',
{ clearSelection: true },
) === true,
};
this.shareLinkManager.completeInitialRestore();
this._settleInitialShareRestore({ status: 'settled', share, layers, tracking });
} catch (error) {
this.shareLinkManager.completeInitialRestore();
this._settleInitialShareRestore({
status: 'failed',
error: String(error?.message || error),
share: null,
layers: [],
});
}
})();
}, 1500);
} else {
this._syncShareState();
}
// A recipient can orbit before or during the delayed share flight. That
// gesture keeps ordinary layer state but revokes the passive base camera
// and selected-subject Follow so delayed work cannot seize navigation.
this._initialShareGestureHandler = () => {
if (
this._disposed
|| !this._hasShareState
|| !this._resolveInitialShareRestore
) return;
stampInitialShareGesture((options) => this._stampNavigation(options));
};
this.viewer?.canvas?.addEventListener('pointerdown', this._initialShareGestureHandler, {
passive: true,
});
this.viewer?.canvas?.addEventListener('wheel', this._initialShareGestureHandler, {
passive: true,
});
// Keep the parameter panel from overlapping toggle controls.
this._layoutRightPanels();
this._syncCctvPanelViewport();
this._windowResizeHandler = () => {
this._scheduleRightPanelLayout({ reconsiderAutoCollapse: true });
this._syncCctvPanelViewport();
this._scheduleLeftPanelLayout({ reconsiderAutoCollapse: true });
};
window.addEventListener('resize', this._windowResizeHandler);
// The loading-chip ticker is stopped while the tab is hidden (it can do no
// useful work off-screen and must not hold a 60ms timer there). Resample on
// return so the time-driven reducer catches up on real elapsed time — and
// re-arms its own ticker if the batch is still running.
this._loadingVisibilityHandler = () => {
if (!document.hidden) this._updateGlobalLoadingFeedback();
};
document.addEventListener('visibilitychange', this._loadingVisibilityHandler);
this._cctvRequestFocusHandler = (event) => routeCctvFocusRequest(
event,
(activate, focus) => this._runExplicitCctvFocus(activate, focus),
(cameraId, durationSec) => cctvLayer.focusCamera(cameraId, durationSec),
);
this._removeCctvRequestFocusListener = registerCctvFocusRequestListener(
window,
this._cctvRequestFocusHandler,
);
this._worldRequestFocusHandler = (event) => routeWorldFocusRequest(
event,
(detail, fly) => this._runExplicitWorldFocus(detail, fly),
(detail) => flyToWorldTarget(this.viewer, detail),
);
this._removeWorldRequestFocusListener = registerWorldFocusRequestListener(
window,
this._worldRequestFocusHandler,
);
this._navigationOwnerChangedRemover = viewer.trackedEntityChanged.addEventListener((entity) => {
if (entity && !this._disposed) this._stampNavigation({ cancelPendingSelection: false });
});
// Vessel/installation focus flies without ever assigning a tracked entity,
// so it cannot reach the listener above. It announces instead.
this._removeNavigationAuthorityListener = registerNavigationAuthorityListener(
window,
(event) => {
if (this._disposed) return;
this._stampNavigation({
cancelPendingSelection: event?.detail?.cancelPendingSelection !== false,
});
},
);
}
/** Advance camera authority and settle any older search UI immediately. */
_stampNavigation({ cancelPendingSelection = true, clearSearchedLocation = true } = {}) {
this._navigationGeneration += 1;
// A newer destination owns the camera, so the last free-text search is no
// longer where we are. DEFERRED navigation opts out here and clears at the
// reassert seam instead: a geocode that never resolves moves no camera, and
// a lookup that fails must not blank a readout that is still true.
if (clearSearchedLocation) this.clearSearchedLocation();
if (cancelPendingSelection) {
if (this._hasShareState && this._resolveInitialShareRestore && !this._layerStateCoordinator) {
this._initialShareSelectionSuperseded = true;
}
const passivelyClearedShareSelection = this._layerStateCoordinator
?.cancelPendingShareTracking?.(
'superseded-by-explicit-navigation',
{ clearSelection: true },
) === true;
try { flightsLayer.cancelPendingTrackingRestore?.(); } catch { /* best effort */ }
try { militaryFlightsLayer.cancelPendingTrackingRestore?.(); } catch { /* best effort */ }
try { satellitesLayer.cancelPendingTrackingRestore?.(); } catch { /* best effort */ }
// A deliberate destination supersedes share-selected entities that have
// not arrived yet. Active owners publish their clear when released.
if (!passivelyClearedShareSelection && !flightsLayer.getTrackedInfo?.()) {
this._dataManager?.setLayerParams('flights', {
selectedFlightsTrackingId: null,
}, { origin: 'tool' });
}
if (!passivelyClearedShareSelection && !militaryFlightsLayer.getTrackedInfo?.()) {
this._dataManager?.setLayerParams('military', {
selectedMilitaryTrackingId: null,
}, { origin: 'tool' });
}
if (!passivelyClearedShareSelection && !satellitesLayer.getTrackedInfo?.()) {
this._dataManager?.setLayerParams('satellites', {
selectedSatTrackingId: null,
}, { origin: 'tool' });
}
}
if (this._activeLocationSearchGeneration !== null) {
this._settleLocationSearchUi(this._activeLocationSearchGeneration);
}
return this._navigationGeneration;
}
/** Settle only the search generation that still owns the shared input UI. */
_settleLocationSearchUi(generation) {
if (this._activeLocationSearchGeneration !== generation) return;
this._activeLocationSearchGeneration = null;
this._locationSearch?.classList.remove('searching', 'expanded');
if (this._locationSearch) this._locationSearch.value = '';
this._locationSearch?.blur();
}
/** Release every follow owner while preserving Contact and vessel selection. */
_releaseFollowCamera({
preserveVesselSelection = true,
preserveCameraFlight = false,
trackingOrigin = 'tool',
} = {}) {
let contactSelected = false;
try {
contactSelected = Boolean(
militaryAwarenessLayer.releaseCameraOwnership?.({
preserveVesselSelection,
origin: trackingOrigin,
}),
);
} catch {
try { flightsLayer.stopTracking?.({ origin: trackingOrigin }); } catch { /* best-effort release */ }
try { militaryFlightsLayer.stopTracking?.({ origin: trackingOrigin }); } catch { /* best-effort release */ }
if (!preserveVesselSelection) {
try { aisLiveVesselsLayer.clearSelection?.(); } catch { /* best-effort release */ }
}
}
try { satellitesLayer.stopTracking?.({ origin: trackingOrigin }); } catch { /* best-effort release */ }
try { rocketLaunchesLayer.releaseCameraOwnership?.(); } catch { /* best-effort release */ }
this.viewer.trackedEntity = undefined;
interruptCameraMotion('explicit-navigation');
this._stopOrbit();
if (!preserveCameraFlight) this.viewer.camera.cancelFlight();
try {
this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
} catch { /* teardown race */ }
return contactSelected;
}
/** Run one immediate destination through the shared ownership policy. */
_runExplicitNavigation(noun, navigate, releaseOptions = undefined) {
return runExplicitNavigation({
disposed: this._disposed,
cockpitActive: !!this.cockpitView?.active,
noun,
showToast: (text) => this._showToast(text),
stamp: () => this._stampNavigation(),
release: () => this._releaseFollowCamera(releaseOptions),
navigate,
});
}
/** Accept a delayed lookup without releasing its current camera owner. */
_beginDeferredNavigation(noun = 'location', { cancelPendingSelection = true } = {}) {
return beginDeferredNavigation({
disposed: this._disposed,
cockpitActive: !!this.cockpitView?.active,
noun,
showToast: (text) => this._showToast(text),
// The searched-location readout survives the STAMP; only a flight that
// actually starts invalidates it (see the release hook below).
stamp: () => this._stampNavigation({ cancelPendingSelection, clearSearchedLocation: false }),
});
}
/** Final authority check and release immediately before a delayed flight. */
_reassertNavigationHandoff(generation) {
return reassertNavigationHandoff({
generation,
currentGeneration: this._navigationGeneration,
cockpitActive: !!this.cockpitView?.active,
disposed: this._disposed,
showToast: (text) => this._showToast(text),
// Reached only once the handoff is granted, immediately before the
// deferred flight starts — so a lookup that failed, was superseded, or
// was refused by the cockpit leaves the old readout standing.
release: () => {
this.clearSearchedLocation();
return this._releaseFollowCamera();
},
});
}
/** Public lifecycle seam used by voice location navigation. */
beginDeferredLocationNavigation() {
return this._beginDeferredNavigation('location');
}
/** Public final-authority seam used by voice geocoding. */
reassertDeferredLocationNavigation(generation) {
return this._reassertNavigationHandoff(generation);
}
/** Public immediate route used by voice destinations. */
runImmediateLocationNavigation(navigate) {
return this.runImmediateNavigation('location', navigate);
}
/** Public authority facade used by validated voice camera destinations. */
runImmediateNavigation(noun, navigate, releaseOptions = undefined) {
return this._runExplicitNavigation(noun, navigate, releaseOptions);
}
/** Supersede deferred work when an owner-specific route handles release. */
supersedeDeferredNavigation() {
return this._stampNavigation();
}
/** Route a valid vessel/fire request through the shared navigation policy. */
_runExplicitWorldFocus(detail, fly) {
return this._runExplicitNavigation(detail?.kind || 'target', fly);
}
/** Return the aircraft tracker owned before a multi-step Cockpit transaction. */
getAircraftTrackingTarget() {
return aircraftTrackingTarget(this.cockpitView?.readAircraftInfo?.());
}
/**
* On window resize, keep the draggable panel on-screen — a panel positioned near an edge can fall
* outside a now-smaller viewport (audit U2). pp-toggles is right-pinned, so re-pin (horizontal) and
* clamp its top. No-op until the panel has been positioned (explicit inline top).
* @returns {void}
*/
_reclampDraggablePanels() {
const el = this._ppToggles;
if (!el || !el.style.top || el.style.top === 'auto') return;
const top = parseInt(el.style.top, 10);
if (!Number.isFinite(top)) return;
el.style.top = `${this._clampToViewport(0, top, el).top}px`;
this._pinPanelToRight(el);
}
/**
* Creates one CesiumJS PostProcessStage per visual style and registers
* it with the scene. Each stage starts with intensity 0 (invisible)
* so crossfade transitions can animate it in later.
* @returns {void}
*/
_initStages() {
for (const [name, shader] of Object.entries(STYLES)) {
const uniforms = { intensity: 0.0 };
// Auto-detect time uniform — animated shaders (CRT scanlines, snow, etc.)
// declare `uniform float time` and receive elapsed seconds each frame.
if (shader.fragmentShader.includes('uniform float time')) {
uniforms.time = 0.0;
}
// Initialize custom uniforms from shader metadata (e.g. gain, pixelation)
if (shader.uniforms) {
for (const [uName, uMeta] of Object.entries(shader.uniforms)) {
uniforms[uName] = uMeta.default;
}
}
const stage = new Cesium.PostProcessStage({
name: `godsEyeView_${name}`,
fragmentShader: shader.fragmentShader,
uniforms,
});
// Zero-intensity stages are DISABLED (perf wave 1). History: the
// first attempt at this deleted the product's signature scope — the
// circular starfield mask was an EMERGENT artifact of these six
// stacked "identity" passes, not an implemented feature. The owner
// ruled to reimplement the scope explicitly (src/scopeMask.js, a
// featherable zero-per-frame canvas), which frees these passes for
// real. If the scope ever looks wrong, look there — not here.
stage.enabled = false;
this.viewer.scene.postProcessStages.add(stage);
this.stages[name] = stage;
}
// Frozen after init — cached so the per-frame animation loop doesn't
// rebuild Object.entries arrays every frame.
this._stageEntries = Object.entries(this.stages);
}
/**
* Single write path for style-stage intensity: keeps `enabled` in
* lockstep so zero-intensity stages cost nothing (safe now that the
* scope is explicit — see _initStages). The stage enables on the same
* frame the first non-zero intensity lands, so crossfades never pop.
* @param {Cesium.PostProcessStage} stage - Style post-process stage.
* @param {number} value - Intensity in [0, 1].
* @returns {void}
*/
_setStageIntensity(stage, value) {
if (!stage) return;
stage.uniforms.intensity = value;
stage.enabled = value > 0.001;
// An animated shader becoming visible needs the style loop (its clock)
// running again; the loop self-stops when nothing visible animates.
if (stage.enabled && stage.uniforms.time !== undefined) this._startAnimationLoop();
governorRequestRender('style-stage');
}
/**
* Re-sync every stage's `enabled` flag from its CURRENT intensity.
*
* The cockpit-vision policy helpers (src/cockpitVisionPolicy.js) are pure
* intensity math — they write `uniforms.intensity` directly and know
* nothing about the enabled/intensity lockstep _setStageIntensity owns.
* Without this sweep a stage the policy raised to 1 would stay DISABLED
* and cockpit NVG/FLIR/CRT would render nothing at all. (Inert while the
* chain is permanently enabled; load-bearing again once the explicit
* scope frees the zero-intensity stages — see _initStages.)
* @returns {void}
*/
_syncStagesEnabledFromIntensity() {
if (!this.stages) return;
for (const stage of Object.values(this.stages)) {
this._setStageIntensity(stage, stage.uniforms.intensity);
}
}
/**
* Contacts-scoped detection (field test 2026-08-18: "when you click on
* Contacts, detections should just turn on, and they should stay on in
* Cockpit or in third-person tracking inside Contacts").
*
* The scope is the CONTACTS SESSION, not Cockpit. Cockpit enter/exit and
* third-person tracking are moves WITHIN that session and deliberately do not
* touch detection — an earlier build hooked this to cockpit enter/exit, which
* is exactly what turned detections off when the owner left the cockpit.
*
* Called from `_syncContextModeButtons`, the single funnel every
* `_contextMode` mutation routes through, and gated on the transaction having
* SETTLED (`!_contextModeChanging`) so a failed activation can never strand
* detection on.
* @returns {void}
*/
_syncContactsDetection() {
if (this._contextModeChanging) return;
const result = applyContactsDetection({
active: this._contextMode === 'flights',
restore: this._contactsDetectionRestore,
// A map style picked DURING the session owns detection on the way out —
// its auto-enable preset is younger than the entry snapshot.
styleOwnsDetection: !this._detectionUserOverridden
&& Boolean(STYLE_PRESET_DEFAULTS[this.activeStyle]?.detection),
// The snapshot must cover everything activation mutates — the preset
// writes DENSITY as well as mode, so a mode-only snapshot returned
// OFF @ 25% as OFF @ 75% and the next manual enable came back Dense.
getState: () => {
const state = this.getDetectionState();
return { mode: state.detectionMode, densityPct: state.densityPct };
},
// Field test: the force-on lands on the tactical look the military
// styles apply — the SAME preset object — not on whatever profile the
// operator last happened to leave detection at.
applyPreset: () => this._applyDetectionPreset(MILITARY_DETECTION_PRESET),
// The preset applier IS the state replayer: same density-then-mode order,
// same slider writes, so a restore round-trips exactly.
restoreState: (state) => this._applyDetectionPreset(state),
});
const hadOwnership = Boolean(this._contactsDetectionRestore);
this._contactsDetectionRestore = result.restore;
// Serialization reads that ownership: while Contacts holds it the link
// carries the SAVED snapshot, and once released it carries live state. The
// share cache therefore goes stale on any ownership transition, whether or
// not the detection engine itself moved — and it does not always move.
// Exiting while a military style owns detection returns changed:false (the
// style's preset already matches), and returning early there left a copied
// link claiming the operator's pre-Contacts values while the map showed
// Dense @ 75%.
if (!shareCacheNeedsHeal({
changed: result.changed,
hadOwnership,
hasOwnership: Boolean(result.restore),
})) return;
if (result.changed) this._syncDetectionUiFromEngine();
this._syncShareState();
}
/** Apply a temporary cockpit-only CRT/NVG/FLIR/NOIR post-process override. */
_setCockpitVision(mode, active, { revealParameters = false } = {}) {
const next = active ? normalizeCockpitVisionMode(mode) : 'optical';
if (!this.stages) return;
if (!active) {
if (this._cockpitVisionRestore) {
for (const [name, intensity] of Object.entries(this._cockpitVisionRestore)) {
if (this.stages[name]) this._setStageIntensity(this.stages[name], intensity);
}
}
this._cockpitVisionRestore = null;
this._cockpitVisionMode = 'optical';
this._syncIrBoost(); // Cockpit exit: fall back to the map preset's IR state
this._updateSliderPanel(this.activeStyle, { reveal: false });
this._revealCockpitStyleParameters({ openDisplay: revealParameters });
return;
}
if (!this._cockpitVisionRestore) {
this._cockpitVisionRestore = captureCockpitVisionBaseline(this.stages, this.transitions);
}
if (next === 'optical') {
applyCockpitVisionStageIntensities(this.stages, next, this._cockpitVisionRestore);
this._syncStagesEnabledFromIntensity();
this._cockpitVisionMode = next;
this._syncIrBoost();
this._updateSliderPanel(this.activeStyle, { reveal: false });
this._revealCockpitStyleParameters({ openDisplay: revealParameters });
return;
}
const target = applyCockpitVisionStageIntensities(this.stages, next, this._cockpitVisionRestore);
this._syncStagesEnabledFromIntensity();
this._cockpitVisionMode = next;
this._syncIrBoost(); // Cockpit vision override ('nvg'/'thermal' boost; CRT/NOIR clear)
this._updateSliderPanel(target || null, { reveal: false });
this._revealCockpitStyleParameters({ openDisplay: revealParameters });
}
/** IR hot-target boost (field test 2026-08-16): under the luminance-
* mapped NVG/FLIR looks the 3D fleets flip to flat white so contacts read
* HOT instead of vanishing mid-gray; restored when the look exits. The
* EFFECTIVE look is Cockpit's vision override while Cockpit is active
* ('nvg'/'thermal', which can differ from the map preset in BOTH
* directions), otherwise the map preset ('surveillance'/'thermal'). */
_syncIrBoost() {
const cockpitMode = this.cockpitView?.active ? this._cockpitVisionMode : null;
const effective = cockpitMode && cockpitMode !== 'optical' ? cockpitMode : this.activeStyle;
const irBoost = effective === 'surveillance' || effective === 'thermal' || effective === 'nvg';
this._dataManager?.setLayerParams('flights', { irBoost });
this._dataManager?.setLayerParams('military', { irBoost });
// Fog blends distant geometry toward an effectively-BLACK color in this
// app (the Cesium globe is hidden), so beyond ~100 km every 3D aircraft
// fogs to a black silhouette — lighting and shaders can't reach past it
// (owner cockpit-FLIR field rounds, 2026-08-16). IR sensors see through
// haze, so the boost styles simply turn fog off; the prior state restores
// on exit. Transition-guarded so repeated syncs don't clobber the saved value.
const scene = this.viewer?.scene;
if (scene?.fog && irBoost !== this._irBoostActive) {
this._irBoostActive = irBoost;
if (irBoost) {
this._irFogWasEnabled = scene.fog.enabled;
scene.fog.enabled = false;
} else if (this._irFogWasEnabled != null) {
scene.fog.enabled = this._irFogWasEnabled;
this._irFogWasEnabled = null;
}
scene.requestRender?.();
}
}
/** Keep Cockpit's inherited label and restore target aligned with the active map preset. */
_syncCockpitInheritedStyle() {
if (!this.cockpitView?.active || !this.stages) return;
this._cockpitVisionRestore = Object.fromEntries(
Object.keys(this.stages).map((name) => [name, name === this.activeStyle ? 1 : 0]),
);
for (const name of Object.keys(this.stages)) this.transitions.delete(name);
this.cockpitView.setVisionMode(this.cockpitView.visionMode);
}
/** Reveal shared style parameters, optionally opening Cockpit Display first. */
_revealCockpitStyleParameters({ openDisplay = false } = {}) {
if (!this.cockpitView?.active || !this._sliderPanel?.classList.contains('active')) return;
if (openDisplay && this._cockpitDisplayToggleBtn?.getAttribute('aria-expanded') !== 'true') {
this._setCockpitDisclosure?.('display', true);
return;
}
if (this._cockpitDisplayToggleBtn?.getAttribute('aria-expanded') !== 'true') return;
this._sliderPanel.classList.remove('collapsed');
this._syncPanelCollapseButton(this._sliderPanel);
requestAnimationFrame(() => requestAnimationFrame(() => {
this._sliderPanel?.scrollIntoView?.({ block: 'nearest' });
}));
}
/**
* Configures Cesium's built-in bloom stage and adds a custom unsharp-mask
* sharpen stage to the post-process pipeline. Both start disabled.
* @returns {void}
*/
_initBloomSharpen() {
// Bloom — use Cesium's built-in bloom
this._bloomStage = this.viewer.scene.postProcessStages.bloom;
this._bloomStage.enabled = false;
this._bloomStage.uniforms.glowOnly = false;
this._bloomStage.uniforms.contrast = 256.0;
this._bloomStage.uniforms.brightness = -0.35;
this._bloomStage.uniforms.delta = 0.25;
this._bloomStage.uniforms.sigma = 0.35;
this._bloomStage.uniforms.stepSize = 1.0;
// Sharpen — custom unsharp mask PostProcessStage
this._sharpenStage = new Cesium.PostProcessStage({
name: 'godsEyeView_sharpen',
fragmentShader: SHARPEN_SHADER,
uniforms: {
amount: 1.3,
},
});
this._sharpenStage.enabled = false;
this.viewer.scene.postProcessStages.add(this._sharpenStage);
if (this._sharpenSlider) {
this._applySharpenIntensity(parseInt(this._sharpenSlider.value, 10) / 100);
}
}
/**
* Reads the current bloom intensity percentage from the UI slider.
* @returns {number} Clamped bloom intensity (0-200).
*/
_getBloomIntensity() {
return clampBloomIntensity(parseInt(this._bloomSlider?.value || `${BLOOM_INTENSITY_DEFAULT}`, 10));
}
/**
* Enables or disables the Cesium bloom stage based on both the user toggle
* and whether the computed strength exceeds the perceptual threshold (0.06).
* @returns {void}
*/
_syncBloomStageEnabled() {
if (!this._bloomStage) return;
const strength = bloomStrengthFromIntensity(this._getBloomIntensity());
this._bloomStage.enabled = this.bloomEnabled && strength > 0.06;
}
/**
* Sets the bloom intensity, updates the slider UI, and applies the value.
* @param {number} intensity - Raw intensity percentage.
* @param {object} [options]
* @param {boolean} [options.syncShare=true] - Whether to push state to the share link.
* @returns {void}
*/
_setBloomIntensity(intensity, { syncShare = true } = {}) {
governorRequestRender('bloom');
const clamped = clampBloomIntensity(intensity);
if (this._bloomSlider) this._bloomSlider.value = String(clamped);
if (this._bloomSliderValue) this._bloomSliderValue.textContent = `${clamped}%`;
this._applyBloomIntensity(clamped);
if (syncShare) this._syncShareState();
}
/**
* Maps a bloom intensity percentage to Cesium bloom stage uniforms.
* Uses smoothstep easing (Hermite interpolation: 3t^2 - 2t^3) to
* produce a perceptually linear glow ramp from zero to full strength.
* @param {number} intensity - Bloom intensity percentage (0-200).
* @returns {void}
*/
_applyBloomIntensity(intensity) {
if (!this._bloomStage) return;
const rawStrength = bloomStrengthFromIntensity(intensity);
// Dead-zone: strengths below 0.06 are imperceptible, clamp to zero.
const strength = rawStrength <= 0.06 ? 0.0 : ((rawStrength - 0.06) / 0.94);
// Smoothstep easing for perceptually linear bloom ramp
const eased = strength * strength * (3.0 - 2.0 * strength);
// Mapping tuned for intuitive UX:
// 0 => effectively no glow, 200 => strong glow.
// Keep threshold strict at low values so only very bright highlights bloom.
this._bloomStage.uniforms.contrast = 255.0 - (eased * 168.0);
this._bloomStage.uniforms.brightness = -0.5 + (eased * 0.36);
this._bloomStage.uniforms.sigma = 0.28 + (eased * 6.3);
this._bloomStage.uniforms.delta = 0.2 + (eased * 2.25);
this._bloomStage.uniforms.stepSize = 1.0 + (eased * 1.25);
this._syncBloomStageEnabled();
}
/**
* Toggles bloom on/off, syncs button state, and reveals/hides the intensity slider row.
* @param {boolean} enabled - Whether bloom should be active.
* @returns {void}
*/
_setBloomEnabled(enabled) {
governorRequestRender('bloom');
this.bloomEnabled = !!enabled;
this._syncBloomStageEnabled();
this._bloomBtn.classList.toggle('active', this.bloomEnabled);
this._bloomSliderRow.classList.toggle('visible', this.bloomEnabled);
if (this.bloomEnabled) {
this._applyBloomIntensity(this._getBloomIntensity());
}
this._syncShareState();
this._layoutRightPanels();
}
/**
* Maps a normalized sharpen value (0-1) to the unsharp-mask `amount` uniform.
* Range: 0.1 (subtle) to 2.1 (aggressive edge enhancement).
* @param {number} val - Normalized sharpen intensity (0.0 to 1.0).
* @returns {void}
*/
_applySharpenIntensity(val) {
governorRequestRender('sharpen');
if (!this._sharpenStage || !this._sharpenStage.uniforms) return;
this._sharpenStage.uniforms.amount = 0.1 + val * 2.0;
}
/**
* Toggles sharpening on/off, syncs button state, and reveals/hides the intensity slider row.
* @param {boolean} enabled - Whether sharpening should be active.
* @returns {void}
*/
_setSharpenEnabled(enabled) {
governorRequestRender('sharpen');
this.sharpenEnabled = !!enabled;
this._sharpenStage.enabled = this.sharpenEnabled;
this._sharpenBtn.classList.toggle('active', this.sharpenEnabled);
if (this._sharpenSliderRow) {
this._sharpenSliderRow.classList.toggle('visible', this.sharpenEnabled);
}
if (this.sharpenEnabled && this._sharpenSlider) {
this._applySharpenIntensity(parseInt(this._sharpenSlider.value, 10) / 100);
}
this._syncShareState();
this._layoutRightPanels();
}
/**
* Wires up all primary UI event listeners: style buttons, keyboard shortcuts
* (1-8 style keys, H/O/V/F/D/C hotkeys, Escape), AI prompt input with
* debounce, bloom/sharpen/HUD toggles, detection density slider, and
* clean-view toggle.
* @returns {void}
*/
_initUI() {
// Style buttons
document.querySelectorAll('.style-btn').forEach(btn => {
btn.addEventListener('click', () => this.setStyle(btn.dataset.style));
});
// Keyboard shortcuts: 1-7, H, Escape
this._globalKeydownHandler = (e) => {
// Ignore when interacting with a form control (except Escape). Global
// hotkeys ('1'-'7', 'h', 'o', 'v', 'd', 'c', 'f') otherwise fire while a
//