mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
361 lines
12 KiB
JavaScript
361 lines
12 KiB
JavaScript
/** Camera placement, ground acquisition and motion correction for the Cockpit controller. */
|
|
import * as Cesium from 'cesium';
|
|
import {
|
|
cockpitAnchorCorrectionStep,
|
|
cockpitGroundSafeHeight,
|
|
cockpitSurfaceWaitExpired,
|
|
cockpitUiUpdateDue,
|
|
slewHeading,
|
|
} from '../cockpitMath.js';
|
|
import {
|
|
COCKPIT_HEADING_SLEW_DPS,
|
|
COCKPIT_FORWARD_OFFSET_M,
|
|
COCKPIT_UP_OFFSET_M,
|
|
COCKPIT_MIN_GROUND_CLEARANCE_M,
|
|
COCKPIT_VIEW_PITCH_DEG,
|
|
COCKPIT_CAMERA_UPDATE_MS,
|
|
COCKPIT_HUD_UPDATE_MS,
|
|
COCKPIT_GROUND_PROBE_MS,
|
|
COCKPIT_GROUND_WAIT_TIMEOUT_MS,
|
|
} from './cockpitPresentation.js';
|
|
|
|
export function update() {
|
|
if (this.destroyed) return false;
|
|
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 = this.services.cachedGroundFloor(
|
|
info.latitude,
|
|
info.longitude,
|
|
);
|
|
if (info.onGround === true) {
|
|
const groundPoint = [{ lat: info.latitude, lon: info.longitude }];
|
|
this.services.warmGroundFloor(groundPoint);
|
|
const meshFloorM = this.services.cachedMeshFloor(
|
|
info.latitude,
|
|
info.longitude,
|
|
);
|
|
if (this.services.meshFloorPreferred() && !Number.isFinite(meshFloorM)) {
|
|
if (
|
|
cockpitUiUpdateDue(
|
|
nowMs,
|
|
this.lastGroundProbeMs,
|
|
COCKPIT_GROUND_PROBE_MS,
|
|
)
|
|
) {
|
|
this.lastGroundProbeMs = nowMs;
|
|
const viewerCartographic = this.viewer.camera.positionCartographic;
|
|
this.services.sampleMeshFloorCells(this.viewer.scene, groundPoint, {
|
|
excludeObjects: [this.trackedEntity],
|
|
viewerLat: Cesium.Math.toDegrees(viewerCartographic.latitude),
|
|
viewerLon: Cesium.Math.toDegrees(viewerCartographic.longitude),
|
|
});
|
|
}
|
|
cockpitFloorM = this.services.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 - this.services.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 = this.services.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);
|
|
}
|
|
}
|