mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
433 lines
16 KiB
JavaScript
433 lines
16 KiB
JavaScript
/** Coordinate Cockpit lifecycle through supplied aircraft, terrain, rendering and briefing operations. */
|
|
import {
|
|
readAircraftInfo,
|
|
dispatchCockpitModeChanged,
|
|
toggleTrackedTr3b,
|
|
syncTr3bToggle,
|
|
syncEntry,
|
|
navigateContext,
|
|
_adoptTrackedEntity,
|
|
enter,
|
|
exit,
|
|
} from './cockpitTrackingController.js';
|
|
import { update } from './cockpitCamera.js';
|
|
import {
|
|
updateHud,
|
|
updateRoute,
|
|
syncWeatherToggle,
|
|
setVisionMode,
|
|
cycleVisionMode,
|
|
clearPredictiveRoute,
|
|
} from './cockpitInstruments.js';
|
|
import { updateContext } from './cockpitContext.js';
|
|
import {
|
|
showBriefPage,
|
|
setBriefAutoRotate,
|
|
startBriefRotation,
|
|
stopBriefRotation,
|
|
updateLocalPosition,
|
|
maybeRefreshRegionalBrief,
|
|
renderRegionalBriefStatus,
|
|
renderRegionalBrief,
|
|
} from './cockpitBriefing.js';
|
|
import {
|
|
handleSignalClick,
|
|
renderCockpitSignals,
|
|
pushCockpitSignal,
|
|
updateCockpitSignals,
|
|
} from './cockpitSignals.js';
|
|
import {
|
|
scheduleContextLayout,
|
|
setContextCollapsed,
|
|
setSignalCollapsed,
|
|
syncContextLayout,
|
|
syncSignalLayout,
|
|
} from './cockpitLayout.js';
|
|
import { onKeyDown } from './cockpitInput.js';
|
|
import * as Cesium from 'cesium';
|
|
|
|
export class CockpitViewController {
|
|
constructor(
|
|
viewer,
|
|
{
|
|
services,
|
|
onVisionChange = null,
|
|
getInitialVisionMode = null,
|
|
onCameraTakeover = null,
|
|
isEntryAllowed = null,
|
|
onEntered = null,
|
|
onExited = null,
|
|
restoreTrackingFrame = null,
|
|
} = {},
|
|
) {
|
|
this.viewer = viewer;
|
|
this.services = services;
|
|
this.destroyed = false;
|
|
this.disposed = false;
|
|
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.getInitialVisionMode =
|
|
typeof getInitialVisionMode === 'function'
|
|
? getInitialVisionMode
|
|
: () => 'optical';
|
|
this.onCameraTakeover = onCameraTakeover;
|
|
this.onEntered = onEntered;
|
|
this.onExited = onExited;
|
|
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) =>
|
|
this.handleSignalClick(event),
|
|
);
|
|
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),
|
|
);
|
|
}
|
|
|
|
readAircraftInfo() {
|
|
return readAircraftInfo.call(this);
|
|
}
|
|
dispatchCockpitModeChanged(active, info) {
|
|
return dispatchCockpitModeChanged.call(this, active, info);
|
|
}
|
|
toggleTrackedTr3b() {
|
|
return toggleTrackedTr3b.call(this);
|
|
}
|
|
syncTr3bToggle(info) {
|
|
return syncTr3bToggle.call(this, info);
|
|
}
|
|
syncEntry() {
|
|
return syncEntry.call(this);
|
|
}
|
|
navigateContext(direction, options) {
|
|
return navigateContext.call(this, direction, options);
|
|
}
|
|
_adoptTrackedEntity(nowMs, suppliedInfo) {
|
|
return _adoptTrackedEntity.call(this, nowMs, suppliedInfo);
|
|
}
|
|
enter() {
|
|
return enter.call(this);
|
|
}
|
|
exit(options) {
|
|
return exit.call(this, options);
|
|
}
|
|
update() {
|
|
return update.call(this);
|
|
}
|
|
updateHud(info, nowMs, forceContext) {
|
|
return updateHud.call(this, info, nowMs, forceContext);
|
|
}
|
|
updateRoute(info) {
|
|
return updateRoute.call(this, info);
|
|
}
|
|
syncWeatherToggle(enabled) {
|
|
return syncWeatherToggle.call(this, enabled);
|
|
}
|
|
setVisionMode(mode, options) {
|
|
return setVisionMode.call(this, mode, options);
|
|
}
|
|
cycleVisionMode(direction) {
|
|
return cycleVisionMode.call(this, direction);
|
|
}
|
|
clearPredictiveRoute() {
|
|
return clearPredictiveRoute.call(this);
|
|
}
|
|
updateContext(info, heading) {
|
|
return updateContext.call(this, info, heading);
|
|
}
|
|
showBriefPage(index, options) {
|
|
return showBriefPage.call(this, index, options);
|
|
}
|
|
setBriefAutoRotate(enabled) {
|
|
return setBriefAutoRotate.call(this, enabled);
|
|
}
|
|
startBriefRotation(options) {
|
|
return startBriefRotation.call(this, options);
|
|
}
|
|
stopBriefRotation() {
|
|
return stopBriefRotation.call(this);
|
|
}
|
|
updateLocalPosition(info) {
|
|
return updateLocalPosition.call(this, info);
|
|
}
|
|
maybeRefreshRegionalBrief(info) {
|
|
return maybeRefreshRegionalBrief.call(this, info);
|
|
}
|
|
renderRegionalBriefStatus(status, info) {
|
|
return renderRegionalBriefStatus.call(this, status, info);
|
|
}
|
|
renderRegionalBrief(payload, info) {
|
|
return renderRegionalBrief.call(this, payload, info);
|
|
}
|
|
renderCockpitSignals() {
|
|
return renderCockpitSignals.call(this);
|
|
}
|
|
pushCockpitSignal(key, tone, title, detail, target) {
|
|
return pushCockpitSignal.call(this, key, tone, title, detail, target);
|
|
}
|
|
updateCockpitSignals(snapshot, unknownCount) {
|
|
return updateCockpitSignals.call(this, snapshot, unknownCount);
|
|
}
|
|
scheduleContextLayout() {
|
|
return scheduleContextLayout.call(this);
|
|
}
|
|
setContextCollapsed(collapsed) {
|
|
return setContextCollapsed.call(this, collapsed);
|
|
}
|
|
setSignalCollapsed(collapsed, options) {
|
|
return setSignalCollapsed.call(this, collapsed, options);
|
|
}
|
|
syncContextLayout() {
|
|
return syncContextLayout.call(this);
|
|
}
|
|
syncSignalLayout() {
|
|
return syncSignalLayout.call(this);
|
|
}
|
|
onKeyDown(event) {
|
|
return onKeyDown.call(this, event);
|
|
}
|
|
|
|
handleSignalClick(event) {
|
|
return handleSignalClick.call(this, event);
|
|
}
|
|
|
|
/** Revoke new work before the shell awaits layer restoration. */
|
|
stop() {
|
|
if (this.destroyed) return;
|
|
this.destroyed = true;
|
|
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?.();
|
|
}
|
|
|
|
dispose() {
|
|
if (this.disposed) return;
|
|
this.disposed = true;
|
|
this.stop();
|
|
this.exit({ restoreTracking: false });
|
|
}
|
|
}
|