mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge pull request #1378 from bilawalsidhu/fix/touch-zoom-aircraft-performance
Improve touch zoom and aircraft update performance
This commit is contained in:
7 files changed
+255
-14
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
- Increase touch-first globe zoom sensitivity to three times the default while
|
||||||
|
retaining Cesium collision and distance limits.
|
||||||
|
- Reuse one delayed display timestamp per civil-aircraft fleet update and cache
|
||||||
|
extrapolation coordinate frames until their fix coordinates change. Preserve
|
||||||
|
contact counts, movement cadence, tracked motion and ground corrections.
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
# God's Eye View Current State
|
# God's Eye View Current State
|
||||||
|
|
||||||
|
Civil aircraft share one delayed render timestamp per fleet update. Extrapolated
|
||||||
|
fixes reuse their local coordinate frame until their coordinates change, including
|
||||||
|
in-place ground-height corrections. Update cadence, contact counts, interpolation,
|
||||||
|
turn rates and tracked-aircraft motion are unchanged. Cached frames are weakly
|
||||||
|
keyed by each position fix and do not retain retired history.
|
||||||
|
|
||||||
|
Touch-first devices use three times the default Cesium zoom sensitivity, so
|
||||||
|
pinching traverses globe and street scales with fewer gestures. Cesium retains
|
||||||
|
its distance-aware zoom, collision handling and minimum/maximum distance limits.
|
||||||
|
Fine-pointer desktop mouse and trackpad sensitivity is unchanged.
|
||||||
|
|
||||||
Analyst region queries resolve unique Natural Earth names offline without
|
Analyst region queries resolve unique Natural Earth names offline without
|
||||||
geocoding, so Gold Coast and Ivory Coast name the West African natural regions.
|
geocoding, so Gold Coast and Ivory Coast name the West African natural regions.
|
||||||
Duplicate names require a geocoded containment anchor; when several entries for
|
Duplicate names require a geocoded containment anchor; when several entries for
|
||||||
|
|||||||
@@ -137,6 +137,11 @@ export function createApplicationViewer({ container, creditContainer }) {
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
viewer.targetFrameRate = 60;
|
viewer.targetFrameRate = 60;
|
||||||
|
// A phone pinch spans fewer screen pixels than desktop zoom gestures.
|
||||||
|
// Keep Cesium's distance-aware zoom and collision limits, with less travel
|
||||||
|
// required on touch-first screens. Mouse/trackpad sensitivity stays native.
|
||||||
|
if (globalThis.matchMedia?.('(pointer: coarse)').matches)
|
||||||
|
viewer.scene.screenSpaceCameraController.zoomFactor *= 3;
|
||||||
// Before any tile builds a draw command: Cesium's per-vertex model
|
// Before any tile builds a draw command: Cesium's per-vertex model
|
||||||
// atmosphere fails to LINK on Apple's Metal backend and kills the
|
// atmosphere fails to LINK on Apple's Metal backend and kills the
|
||||||
// render loop. See app/atmosphereCompat.js.
|
// render loop. See app/atmosphereCompat.js.
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ export function createMotion({
|
|||||||
publishFocusTargetFromCachedPosition,
|
publishFocusTargetFromCachedPosition,
|
||||||
} = services.focus;
|
} = services.focus;
|
||||||
|
|
||||||
|
// Fixes leave the cache with their history; corrected positions invalidate
|
||||||
|
// the local frame even when the existing Cartesian is mutated in place.
|
||||||
|
const fixFrames = new WeakMap();
|
||||||
|
|
||||||
// Round 5: the old `_warmGroundedAircraftSurfaceCache` (global exact-5-decimal
|
// Round 5: the old `_warmGroundedAircraftSurfaceCache` (global exact-5-decimal
|
||||||
// warm for every grounded contact on Earth) is GONE. Parked-aircraft GPS
|
// warm for every grounded contact on Earth) is GONE. Parked-aircraft GPS
|
||||||
// jitter minted fresh keys every poll → thousands of upstream points per
|
// jitter minted fresh keys every poll → thousands of upstream points per
|
||||||
@@ -124,7 +128,7 @@ export function createMotion({
|
|||||||
* @returns {Cesium.Cartesian3|null} Estimated ECEF position, or null if no history exists.
|
* @returns {Cesium.Cartesian3|null} Estimated ECEF position, or null if no history exists.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function _deadReckon(icao24, result) {
|
function _deadReckon(icao24, result, sampledRenderTime) {
|
||||||
const info = flightState.records.data.get(icao24);
|
const info = flightState.records.data.get(icao24);
|
||||||
if (FOCUS_EVIDENCE_DEV && flightState._focusEvidenceIds.has(icao24)) {
|
if (FOCUS_EVIDENCE_DEV && flightState._focusEvidenceIds.has(icao24)) {
|
||||||
const position = flightState._billboards.get(icao24)?.position;
|
const position = flightState._billboards.get(icao24)?.position;
|
||||||
@@ -149,11 +153,13 @@ export function createMotion({
|
|||||||
const out = result || new Cesium.Cartesian3();
|
const out = result || new Cesium.Cartesian3();
|
||||||
// Render one poll interval behind real time so we interpolate between
|
// Render one poll interval behind real time so we interpolate between
|
||||||
// two KNOWN fixes whenever possible (see RENDER_DELAY_SEC rationale).
|
// two KNOWN fixes whenever possible (see RENDER_DELAY_SEC rationale).
|
||||||
const renderTime = Cesium.JulianDate.addSeconds(
|
const renderTime =
|
||||||
Cesium.JulianDate.now(),
|
sampledRenderTime ??
|
||||||
-RENDER_DELAY_SEC,
|
Cesium.JulianDate.addSeconds(
|
||||||
flightState._scratchRenderTime,
|
Cesium.JulianDate.now(flightState._scratchRenderTime),
|
||||||
);
|
-RENDER_DELAY_SEC,
|
||||||
|
flightState._scratchRenderTime,
|
||||||
|
);
|
||||||
|
|
||||||
// Bracketing pair: interpolate — no extrapolation error, no snap-back.
|
// Bracketing pair: interpolate — no extrapolation error, no snap-back.
|
||||||
for (let i = history.length - 1; i >= 1; i--) {
|
for (let i = history.length - 1; i >= 1; i--) {
|
||||||
@@ -286,12 +292,31 @@ export function createMotion({
|
|||||||
0,
|
0,
|
||||||
flightState._scratchOffset,
|
flightState._scratchOffset,
|
||||||
);
|
);
|
||||||
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(
|
let frame = fixFrames.get(fix);
|
||||||
fix.position,
|
if (!frame) {
|
||||||
Cesium.Ellipsoid.WGS84,
|
frame = {
|
||||||
flightState._scratchEnu,
|
position: new Cesium.Cartesian3(),
|
||||||
|
matrix: new Cesium.Matrix4(),
|
||||||
|
};
|
||||||
|
fixFrames.set(fix, frame);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!frame.valid ||
|
||||||
|
!Cesium.Cartesian3.equals(frame.position, fix.position)
|
||||||
|
) {
|
||||||
|
Cesium.Transforms.eastNorthUpToFixedFrame(
|
||||||
|
fix.position,
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
frame.matrix,
|
||||||
|
);
|
||||||
|
Cesium.Cartesian3.clone(fix.position, frame.position);
|
||||||
|
frame.valid = true;
|
||||||
|
}
|
||||||
|
return Cesium.Matrix4.multiplyByPoint(
|
||||||
|
frame.matrix,
|
||||||
|
flightState._scratchOffset,
|
||||||
|
out,
|
||||||
);
|
);
|
||||||
return Cesium.Matrix4.multiplyByPoint(enu, flightState._scratchOffset, out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import { createMotion } from './motion.js';
|
||||||
|
import { arcOffsetEnu } from '../../data/motionModel.js';
|
||||||
|
|
||||||
|
function fixture() {
|
||||||
|
const flightState = {
|
||||||
|
records: { data: new Map() },
|
||||||
|
_positionHistory: new Map(),
|
||||||
|
_focusEvidenceIds: new Set(),
|
||||||
|
_scratchArc: {},
|
||||||
|
_scratchOffset: new Cesium.Cartesian3(),
|
||||||
|
_scratchRenderTime: new Cesium.JulianDate(),
|
||||||
|
};
|
||||||
|
const motion = createMotion({
|
||||||
|
flightState,
|
||||||
|
services: { groundFloor: {}, camera: {}, focus: {} },
|
||||||
|
parts: {},
|
||||||
|
});
|
||||||
|
return { flightState, motion };
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectedPosition(fix, dt, turnRate, transform) {
|
||||||
|
const arc = {};
|
||||||
|
arcOffsetEnu(fix.velocity, fix.track, turnRate, dt, arc);
|
||||||
|
return Cesium.Matrix4.multiplyByPoint(
|
||||||
|
transform(fix.position, Cesium.Ellipsoid.WGS84, new Cesium.Matrix4()),
|
||||||
|
new Cesium.Cartesian3(arc.east, arc.north, 0),
|
||||||
|
new Cesium.Cartesian3(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(actual, expected) {
|
||||||
|
assert.ok(Cesium.Cartesian3.distance(actual, expected) < 1e-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('extrapolation reuses fixed frames and invalidates corrected coordinates', (t) => {
|
||||||
|
const { motion } = fixture();
|
||||||
|
const transform = Cesium.Transforms.eastNorthUpToFixedFrame;
|
||||||
|
const spy = t.mock.method(
|
||||||
|
Cesium.Transforms,
|
||||||
|
'eastNorthUpToFixedFrame',
|
||||||
|
transform,
|
||||||
|
);
|
||||||
|
const fix = {
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(-97, 30, 200),
|
||||||
|
velocity: 90,
|
||||||
|
track: 70,
|
||||||
|
};
|
||||||
|
const out = new Cesium.Cartesian3();
|
||||||
|
for (const dt of [-30, 5, 30, 60]) {
|
||||||
|
motion._extrapolateFix(fix, {}, dt, out, 1.5);
|
||||||
|
close(out, expectedPosition(fix, dt, 1.5, transform));
|
||||||
|
}
|
||||||
|
assert.equal(spy.mock.callCount(), 1);
|
||||||
|
|
||||||
|
// Ground reflooring mutates an existing fix's Cartesian in place.
|
||||||
|
Cesium.Cartesian3.fromDegrees(-97, 30, 350, undefined, fix.position);
|
||||||
|
motion._extrapolateFix(fix, {}, 30, out, 1.5);
|
||||||
|
close(out, expectedPosition(fix, 30, 1.5, transform));
|
||||||
|
assert.equal(spy.mock.callCount(), 2);
|
||||||
|
|
||||||
|
fix.position = Cesium.Cartesian3.fromDegrees(140, 80, 1000);
|
||||||
|
motion._extrapolateFix(fix, {}, 30, out, -2);
|
||||||
|
close(out, expectedPosition(fix, 30, -2, transform));
|
||||||
|
assert.equal(spy.mock.callCount(), 3);
|
||||||
|
|
||||||
|
fix.velocity = 210;
|
||||||
|
fix.track = 359;
|
||||||
|
motion._extrapolateFix(fix, {}, 15, out, -1);
|
||||||
|
close(out, expectedPosition(fix, 15, -1, transform));
|
||||||
|
assert.equal(spy.mock.callCount(), 3);
|
||||||
|
|
||||||
|
const replacement = { ...fix };
|
||||||
|
motion._extrapolateFix(replacement, {}, 15, out, -1);
|
||||||
|
close(out, expectedPosition(replacement, 15, -1, transform));
|
||||||
|
assert.equal(spy.mock.callCount(), 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stationary fixes do not build a frame until motion needs it', (t) => {
|
||||||
|
const { motion } = fixture();
|
||||||
|
const spy = t.mock.method(Cesium.Transforms, 'eastNorthUpToFixedFrame');
|
||||||
|
const fix = {
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
|
||||||
|
velocity: 0,
|
||||||
|
track: 90,
|
||||||
|
};
|
||||||
|
const out = new Cesium.Cartesian3();
|
||||||
|
close(motion._extrapolateFix(fix, {}, 30, out), fix.position);
|
||||||
|
fix.velocity = 10;
|
||||||
|
close(motion._extrapolateFix(fix, {}, 0, out), fix.position);
|
||||||
|
assert.equal(spy.mock.callCount(), 0);
|
||||||
|
motion._extrapolateFix(fix, {}, 30, out);
|
||||||
|
assert.equal(spy.mock.callCount(), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fleet render time is shared without resampling or mutating the supplied instant', (t) => {
|
||||||
|
const { flightState, motion } = fixture();
|
||||||
|
const start = Cesium.JulianDate.fromIso8601('2026-10-24T12:00:00Z');
|
||||||
|
const end = Cesium.JulianDate.addSeconds(start, 60, new Cesium.JulianDate());
|
||||||
|
const renderTime = Cesium.JulianDate.addSeconds(
|
||||||
|
start,
|
||||||
|
30,
|
||||||
|
new Cesium.JulianDate(),
|
||||||
|
);
|
||||||
|
const savedTime = Cesium.JulianDate.clone(renderTime);
|
||||||
|
const history = [
|
||||||
|
{
|
||||||
|
time: start,
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(-97, 30, 1000),
|
||||||
|
track: 90,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: end,
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(-96.99, 30, 1000),
|
||||||
|
track: 90,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const id of ['one', 'two']) {
|
||||||
|
flightState.records.data.set(id, { velocity: 100, true_track: 90 });
|
||||||
|
flightState._positionHistory.set(id, history);
|
||||||
|
}
|
||||||
|
t.mock.method(Cesium.JulianDate, 'now', () => {
|
||||||
|
throw new Error('unexpected per-contact clock read');
|
||||||
|
});
|
||||||
|
const expected = Cesium.Cartesian3.lerp(
|
||||||
|
history[0].position,
|
||||||
|
history[1].position,
|
||||||
|
0.5,
|
||||||
|
new Cesium.Cartesian3(),
|
||||||
|
);
|
||||||
|
for (const id of ['one', 'two'])
|
||||||
|
close(
|
||||||
|
motion._deadReckon(id, new Cesium.Cartesian3(), renderTime),
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
assert.ok(Cesium.JulianDate.equals(renderTime, savedTime));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tracked callers still sample a fresh delayed time without a fleet epoch', (t) => {
|
||||||
|
const { flightState, motion } = fixture();
|
||||||
|
const start = Cesium.JulianDate.fromIso8601('2026-10-24T12:00:00Z');
|
||||||
|
const history = [
|
||||||
|
{
|
||||||
|
time: start,
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(-97, 30, 1000),
|
||||||
|
track: 90,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: Cesium.JulianDate.addSeconds(start, 60, new Cesium.JulianDate()),
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(-96.99, 30, 1000),
|
||||||
|
track: 90,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
flightState.records.data.set('tracked', { velocity: 100, true_track: 90 });
|
||||||
|
flightState._positionHistory.set('tracked', history);
|
||||||
|
let elapsed = 60;
|
||||||
|
const spy = t.mock.method(Cesium.JulianDate, 'now', (result) => {
|
||||||
|
assert.equal(result, flightState._scratchRenderTime);
|
||||||
|
return Cesium.JulianDate.addSeconds(start, elapsed, result);
|
||||||
|
});
|
||||||
|
const out = new Cesium.Cartesian3();
|
||||||
|
close(
|
||||||
|
motion._deadReckon('tracked', out),
|
||||||
|
Cesium.Cartesian3.lerp(
|
||||||
|
history[0].position,
|
||||||
|
history[1].position,
|
||||||
|
0.5,
|
||||||
|
new Cesium.Cartesian3(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
elapsed = 75;
|
||||||
|
close(
|
||||||
|
motion._deadReckon('tracked', out),
|
||||||
|
Cesium.Cartesian3.lerp(
|
||||||
|
history[0].position,
|
||||||
|
history[1].position,
|
||||||
|
0.75,
|
||||||
|
new Cesium.Cartesian3(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.equal(spy.mock.callCount(), 2);
|
||||||
|
});
|
||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
TRACKED_MODEL_MIN_PX,
|
TRACKED_MODEL_MIN_PX,
|
||||||
TRACKED_MODEL_MAX_PX,
|
TRACKED_MODEL_MAX_PX,
|
||||||
FLEET_DR_INTERVAL_MS,
|
FLEET_DR_INTERVAL_MS,
|
||||||
|
RENDER_DELAY_SEC,
|
||||||
COURSE_SLEW_DT_MAX_SEC,
|
COURSE_SLEW_DT_MAX_SEC,
|
||||||
ROTATION_REFRESH_MS,
|
ROTATION_REFRESH_MS,
|
||||||
COURSE_MAX_DPS,
|
COURSE_MAX_DPS,
|
||||||
@@ -854,6 +855,12 @@ export function createRendering({
|
|||||||
)
|
)
|
||||||
: 0.08;
|
: 0.08;
|
||||||
flightState._lastFleetTickMs = nowMs;
|
flightState._lastFleetTickMs = nowMs;
|
||||||
|
// Every contact in this pass shares the same delayed display instant.
|
||||||
|
const renderTime = Cesium.JulianDate.addSeconds(
|
||||||
|
Cesium.JulianDate.now(flightState._scratchFleetRenderTime),
|
||||||
|
-RENDER_DELAY_SEC,
|
||||||
|
flightState._scratchFleetRenderTime,
|
||||||
|
);
|
||||||
|
|
||||||
_drainIrReloadQueue(); // bounded per-tick slice of any pending boost-flip reload
|
_drainIrReloadQueue(); // bounded per-tick slice of any pending boost-flip reload
|
||||||
if (flightState._cockpitContactMode)
|
if (flightState._cockpitContactMode)
|
||||||
@@ -957,7 +964,11 @@ export function createRendering({
|
|||||||
|
|
||||||
const info = flightState.records.data.get(icao24);
|
const info = flightState.records.data.get(icao24);
|
||||||
|
|
||||||
const dr = parts.motion._deadReckon(icao24, flightState._scratchFleetPos);
|
const dr = parts.motion._deadReckon(
|
||||||
|
icao24,
|
||||||
|
flightState._scratchFleetPos,
|
||||||
|
renderTime,
|
||||||
|
);
|
||||||
// The dead-reckoned point drifts away from the fix whose cell supplied the
|
// The dead-reckoned point drifts away from the fix whose cell supplied the
|
||||||
// height, so a grounded contact's sprite ends up under the mesh it taxied
|
// height, so a grounded contact's sprite ends up under the mesh it taxied
|
||||||
// (or coasted) over. Re-floor at the DISPLAYED coordinate — read-only, and
|
// (or coasted) over. Re-floor at the DISPLAYED coordinate — read-only, and
|
||||||
|
|||||||
@@ -242,12 +242,12 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._scratchCarto = new Cesium.Cartographic();
|
flightState._scratchCarto = new Cesium.Cartographic();
|
||||||
|
|
||||||
flightState._scratchEnu = new Cesium.Matrix4();
|
|
||||||
|
|
||||||
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
||||||
|
|
||||||
flightState._scratchRenderTime = new Cesium.JulianDate();
|
flightState._scratchRenderTime = new Cesium.JulianDate();
|
||||||
|
|
||||||
|
flightState._scratchFleetRenderTime = new Cesium.JulianDate();
|
||||||
|
|
||||||
flightState._scratchFleetPos = new Cesium.Cartesian3();
|
flightState._scratchFleetPos = new Cesium.Cartesian3();
|
||||||
|
|
||||||
flightState._scratchSonarScreen = new Cesium.Cartesian2();
|
flightState._scratchSonarScreen = new Cesium.Cartesian2();
|
||||||
|
|||||||
Reference in new issue
Block a user