mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
refactor(layers): W1 timestamps without JulianDate
Flights and military keep their fix history in plain milliseconds (timeMs, whole ms as the Date the JulianDate came from held) and read "now" from the app clock: the render-delay time, bracketing, extrapolation, the warm-up check and the trail seed are number arithmetic (motion.js, tracking.js, snapshotRenderer.js), as is the turn rate and the forward synthetic fix in data/motionModel.js, which no longer imports Cesium. The JulianDate scratch fields in state.js and the unused JulianDate.now() in queries.js go. Launches (mission frame, panel, selection), the tracked satellite's SGP4 epoch and the celestial ring's sun and moon read the app clock; where a Cesium property still takes a JulianDate it is built from the app clock's now (as JulianDate.now did from Date.now). Wall-clock throttles and feed receipt stamps stay on Date.now; property reads at viewer.clock.currentTime (render, frozen at boot) move with the render code in the *a tasks. Tests that built JulianDate history fixtures now build ms ones with the same instants (motionModel.test, flights.test, militaryFlights.test); new tests show the military delayed position and the tracked satellite follow the app clock (both fail on the previous code).
This commit is contained in:
1 parent
f1a2df5cfe
commit
c33ee31efc
21 files changed
+155
-130
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { appClock } from './time/clock.js';
|
||||
import { governorRequestRender } from './renderGovernor.js';
|
||||
|
||||
/** Outer edge of the existing NVG/FLIR keyhole in normalized shader space. */
|
||||
@@ -752,7 +753,9 @@ export class CelestialRing {
|
||||
|
||||
if (!wasVisible) this._ephemerisDirty = true;
|
||||
|
||||
const time = Cesium.JulianDate.now();
|
||||
// The sun and moon at the app clock's now (the wall clock unless QA
|
||||
// moves it), as Navara's sun.
|
||||
const time = Cesium.JulianDate.fromDate(new Date(appClock.now()));
|
||||
if (!this._updateEphemeris(time)) return;
|
||||
this._root.dataset.ephemerisUpdates = String(this._ephemerisUpdateCount);
|
||||
const camera = this.viewer.camera;
|
||||
|
||||
@@ -479,7 +479,7 @@ test('real civil track path creates no native label and publishes every cached h
|
||||
tracked: false,
|
||||
history: [
|
||||
{
|
||||
time: now,
|
||||
timeMs: Cesium.JulianDate.toDate(now).getTime(),
|
||||
epochMs: Date.now(),
|
||||
position,
|
||||
velocity: 250,
|
||||
|
||||
@@ -407,7 +407,7 @@ test('real military track path creates no native label and publishes every cache
|
||||
tracked: false,
|
||||
history: [
|
||||
{
|
||||
time: now,
|
||||
timeMs: Cesium.JulianDate.toDate(now).getTime(),
|
||||
epochMs: Date.now(),
|
||||
position,
|
||||
velocity: 231.5,
|
||||
|
||||
+13
-9
@@ -6,7 +6,6 @@
|
||||
* Turn-rate estimation + constant-rate-turn arc integration are adapted from
|
||||
* skylight (https://github.com/cpaczek/skylight, MIT) shared/src/aim.ts.
|
||||
*/
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../geo/index.js';
|
||||
|
||||
const DEG = Math.PI / 180;
|
||||
@@ -257,17 +256,17 @@ export function estimateTurnRateDps(
|
||||
return Math.max(-maxDps, Math.min(maxDps, rate));
|
||||
}
|
||||
|
||||
/** Adapter: fix history [{time: JulianDate, track, velocity}] →
|
||||
/** Adapter: fix history [{timeMs, track, velocity}] →
|
||||
* estimateTurnRateDps, guarded by TURN_MIN_SPEED_MPS (low-speed fix-track
|
||||
* jitter must not manufacture a turn rate). */
|
||||
export function turnRateFromFixHistory(history) {
|
||||
if (!history || history.length < 2) return 0;
|
||||
const t0 = history[0].time;
|
||||
const t0 = history[0].timeMs;
|
||||
const samples = [];
|
||||
for (const s of history) {
|
||||
if (!Number.isFinite(s.track)) continue;
|
||||
samples.push({
|
||||
tSec: Cesium.JulianDate.secondsDifference(s.time, t0),
|
||||
tSec: (s.timeMs - t0) / 1000,
|
||||
trackDeg: s.track,
|
||||
speedMps: s.velocity,
|
||||
});
|
||||
@@ -447,19 +446,23 @@ const _forwardFixOffset = geo.cartesian3.create();
|
||||
* kinematics. This prevents a late course change from reprojecting the entire
|
||||
* stale interval and snapping the rendered contact by kilometres.
|
||||
*
|
||||
* @param {{time: Cesium.JulianDate, epochMs?: number, position: Cesium.Cartesian3,
|
||||
* @param {{timeMs: number, epochMs?: number, position: Cesium.Cartesian3,
|
||||
* velocity?: number, track?: number}} newest Existing immutable history fix.
|
||||
* @param {{epochMs: number, velocity: number, track: number, turnRateDps?: number}} next
|
||||
* New kinematics and the wall/source epoch at which they become authoritative.
|
||||
* @returns {{time: Cesium.JulianDate, epochMs: number, position: Cesium.Cartesian3,
|
||||
* @returns {{timeMs: number, epochMs: number, position: Cesium.Cartesian3,
|
||||
* velocity: number, track: number}|null} Forward synthetic fix, or null.
|
||||
*/
|
||||
export function synthesizeForwardKinematicsFix(newest, next = {}) {
|
||||
if (!newest?.position || !newest?.time || !Number.isFinite(next.epochMs))
|
||||
if (
|
||||
!newest?.position ||
|
||||
newest?.timeMs == null ||
|
||||
!Number.isFinite(next.epochMs)
|
||||
)
|
||||
return null;
|
||||
const startEpochMs = Number.isFinite(newest.epochMs)
|
||||
? newest.epochMs
|
||||
: Cesium.JulianDate.toDate(newest.time).getTime();
|
||||
: newest.timeMs;
|
||||
if (!Number.isFinite(startEpochMs) || next.epochMs <= startEpochMs)
|
||||
return null;
|
||||
|
||||
@@ -492,7 +495,8 @@ export function synthesizeForwardKinematicsFix(newest, next = {}) {
|
||||
geo.cartesian3.create(),
|
||||
);
|
||||
return {
|
||||
time: Cesium.JulianDate.fromDate(new Date(next.epochMs)),
|
||||
// Whole milliseconds, as a Date holds the fix time.
|
||||
timeMs: Math.trunc(next.epochMs),
|
||||
epochMs: next.epochMs,
|
||||
position,
|
||||
velocity: Number.isFinite(next.velocity)
|
||||
|
||||
@@ -93,10 +93,10 @@ test('estimateTurnRateDps: 2°/s ramp detected; noise floored; clamped', () => {
|
||||
assert.equal(estimateTurnRateDps(wild), -4); // clamped
|
||||
});
|
||||
|
||||
test('turnRateFromFixHistory adapts JulianDate history', () => {
|
||||
const t0 = Cesium.JulianDate.now();
|
||||
test('turnRateFromFixHistory adapts the fix history (ms timestamps)', () => {
|
||||
const t0 = Date.now();
|
||||
const h = [0, 30, 60].map((s, i) => ({
|
||||
time: Cesium.JulianDate.addSeconds(t0, s, new Cesium.JulianDate()),
|
||||
timeMs: t0 + s * 1000,
|
||||
track: i * 60,
|
||||
}));
|
||||
assert.ok(Math.abs(turnRateFromFixHistory(h) - 2) < 1e-9);
|
||||
@@ -142,9 +142,9 @@ test('courseSlewCapDps: COURSE_MIN_DPS at low speed easing to the fleet cap at c
|
||||
});
|
||||
|
||||
test('turn-rate guard: hover fix-track jitter manufactures NO turn rate; a real slow turn still does', () => {
|
||||
const t0 = Cesium.JulianDate.now();
|
||||
const t0 = Date.now();
|
||||
const mk = (specs) => specs.map(([s, track, velocity]) => ({
|
||||
time: Cesium.JulianDate.addSeconds(t0, s, new Cesium.JulianDate()),
|
||||
timeMs: t0 + s * 1000,
|
||||
track, velocity,
|
||||
}));
|
||||
// Hovering heli: 1 m/s drift, reported track flipping ±45° — used to yield ±°/s
|
||||
@@ -180,7 +180,7 @@ test('repeated kinematics apply only from a forward synthetic fix', () => {
|
||||
const epochMs = 1_800_000_000_000;
|
||||
const originalPosition = Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 9000);
|
||||
const newest = {
|
||||
time: Cesium.JulianDate.fromDate(new Date(epochMs)),
|
||||
timeMs: epochMs,
|
||||
epochMs,
|
||||
position: Cesium.Cartesian3.clone(originalPosition),
|
||||
velocity: 100,
|
||||
@@ -195,6 +195,7 @@ test('repeated kinematics apply only from a forward synthetic fix', () => {
|
||||
assert.equal(newest.velocity, 100);
|
||||
assert.equal(newest.track, 90);
|
||||
assert.ok(Cesium.Cartesian3.equals(newest.position, originalPosition));
|
||||
assert.equal(synthetic.timeMs, epochMs + 10_000);
|
||||
assert.equal(synthetic.velocity, 220);
|
||||
assert.equal(synthetic.track, 15);
|
||||
assert.ok(Math.abs(Cesium.Cartesian3.distance(newest.position, synthetic.position) - 1000) < 0.5);
|
||||
@@ -202,7 +203,7 @@ test('repeated kinematics apply only from a forward synthetic fix', () => {
|
||||
|
||||
test('liftRepeatedGroundFix raises only grounded history and preserves the stored coordinates', () => {
|
||||
const newest = {
|
||||
time: Cesium.JulianDate.now(),
|
||||
timeMs: Date.now(),
|
||||
epochMs: 123,
|
||||
position: Cesium.Cartesian3.fromDegrees(-98.04, 29.71, -30),
|
||||
velocity: 0,
|
||||
|
||||
@@ -338,3 +338,33 @@ test('ISS overlay entry retains the native distance scale and source text', () =
|
||||
assert.equal(entry.edgeFade, 'keyhole');
|
||||
assert.equal(entry.horizonCull, true);
|
||||
});
|
||||
|
||||
test('the tracked satellite is propagated at the app clock time', async () => {
|
||||
const { appClock } = await import('../time/clock.js');
|
||||
const entity = { gevLabelModel: { title: 'OLD', details: ['? km'] } };
|
||||
const point = { position: new Cesium.Cartesian3() };
|
||||
const viewer = {
|
||||
camera: null,
|
||||
scene: { frameState: { frameNumber: 1 } },
|
||||
};
|
||||
_setTrackedSatelliteRefreshStateForTest({
|
||||
noradId: 25544,
|
||||
name: 'ISS (ZARYA)',
|
||||
satrec: twoline2satrec(L1, L2),
|
||||
entity,
|
||||
point,
|
||||
viewer,
|
||||
now: undefined,
|
||||
});
|
||||
try {
|
||||
appClock.freeze(Date.UTC(2008, 8, 20, 12, 30));
|
||||
_runSatellitePreRenderForTest();
|
||||
assert.equal(entity.gevLabelModel.details[1], '353 km · NORAD 25544');
|
||||
appClock.freeze(Date.UTC(2008, 8, 20, 13, 0));
|
||||
viewer.scene.frameState.frameNumber = 2;
|
||||
_runSatellitePreRenderForTest();
|
||||
assert.equal(entity.gevLabelModel.details[1], '366 km · NORAD 25544');
|
||||
} finally {
|
||||
appClock.reset();
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import {
|
||||
COURSE_HOLD_SPEED_MPS,
|
||||
lerpAngleDeg,
|
||||
@@ -150,25 +150,15 @@ export function createMotion({
|
||||
const out = result || geo.cartesian3.create();
|
||||
// Render one poll interval behind real time so we interpolate between
|
||||
// two KNOWN fixes whenever possible (see RENDER_DELAY_SEC rationale).
|
||||
const renderTime = Cesium.JulianDate.addSeconds(
|
||||
Cesium.JulianDate.now(),
|
||||
-RENDER_DELAY_SEC,
|
||||
flightState._scratchRenderTime,
|
||||
);
|
||||
const renderTimeMs = appClock.now() - RENDER_DELAY_SEC * 1000;
|
||||
|
||||
// Bracketing pair: interpolate — no extrapolation error, no snap-back.
|
||||
for (let i = history.length - 1; i >= 1; i--) {
|
||||
const a = history[i - 1];
|
||||
const b = history[i];
|
||||
if (
|
||||
Cesium.JulianDate.lessThanOrEquals(a.time, renderTime) &&
|
||||
Cesium.JulianDate.lessThanOrEquals(renderTime, b.time)
|
||||
) {
|
||||
const span = Cesium.JulianDate.secondsDifference(b.time, a.time);
|
||||
const t =
|
||||
span > 0
|
||||
? Cesium.JulianDate.secondsDifference(renderTime, a.time) / span
|
||||
: 1.0;
|
||||
if (a.timeMs <= renderTimeMs && renderTimeMs <= b.timeMs) {
|
||||
const span = (b.timeMs - a.timeMs) / 1000;
|
||||
const t = span > 0 ? (renderTimeMs - a.timeMs) / 1000 / span : 1.0;
|
||||
// Course of the DISPLAYED motion. The chord is only trustworthy when the
|
||||
// segment covers real ground (at hover its direction is GPS jitter; on a
|
||||
// slow tight turn it STEPS the whole per-segment turn at each boundary),
|
||||
@@ -199,10 +189,7 @@ export function createMotion({
|
||||
}
|
||||
|
||||
const newest = history[history.length - 1];
|
||||
const elapsedSec = Cesium.JulianDate.secondsDifference(
|
||||
renderTime,
|
||||
newest.time,
|
||||
);
|
||||
const elapsedSec = (renderTimeMs - newest.timeMs) / 1000;
|
||||
if (elapsedSec <= 0) {
|
||||
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
|
||||
// aircraft, before RENDER_DELAY_SEC of history has accumulated, so no bracketing
|
||||
@@ -213,10 +200,7 @@ export function createMotion({
|
||||
// the oldest fix froze the icon; extrapolating the NEWEST fix to wall-clock now made
|
||||
// the icon jump back ~one poll interval the instant interpolation took over.)
|
||||
const oldest = history[0];
|
||||
const lookbackSec = Cesium.JulianDate.secondsDifference(
|
||||
oldest.time,
|
||||
renderTime,
|
||||
); // ≥ 0
|
||||
const lookbackSec = (oldest.timeMs - renderTimeMs) / 1000; // ≥ 0
|
||||
return _extrapolateFix(
|
||||
oldest,
|
||||
info,
|
||||
@@ -236,7 +220,7 @@ export function createMotion({
|
||||
// a Date per aircraft on every 12 Hz fleet tick.
|
||||
fixEpochMs: Number.isFinite(newest.epochMs)
|
||||
? newest.epochMs
|
||||
: Cesium.JulianDate.toDate(newest.time).getTime(),
|
||||
: newest.timeMs,
|
||||
lastContactEpochMs: info?.lastContactEpochMs,
|
||||
// Permit one minute of contact grace but cap any cached-feed drift at
|
||||
// five minutes. Source backoff is exposed separately as a STALE cue.
|
||||
@@ -307,12 +291,7 @@ export function createMotion({
|
||||
if (!flightState._trackedIcao) return false;
|
||||
const history = flightState._positionHistory.get(flightState._trackedIcao);
|
||||
if (!history || history.length === 0) return true;
|
||||
const renderTime = Cesium.JulianDate.addSeconds(
|
||||
Cesium.JulianDate.now(),
|
||||
-RENDER_DELAY_SEC,
|
||||
flightState._scratchWarmupTime,
|
||||
);
|
||||
return Cesium.JulianDate.lessThan(renderTime, history[0].time);
|
||||
return appClock.now() - RENDER_DELAY_SEC * 1000 < history[0].timeMs;
|
||||
}
|
||||
|
||||
/** Resolve the selected aircraft's actual rendered square extent this frame. */
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { approxDistanceKm as _approxDistanceKm } from './recordPolicy.js';
|
||||
import { displayedKinematics } from '../../data/motionModel.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
|
||||
import { modelVisualAnchor } from '../../data/modelVisualAnchor.js';
|
||||
@@ -550,7 +549,6 @@ export function createQueries({
|
||||
const maxRange =
|
||||
Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY;
|
||||
|
||||
const now = Cesium.JulianDate.now();
|
||||
const nearby = [];
|
||||
|
||||
for (const [icao24, bb] of flightState._billboards) {
|
||||
|
||||
@@ -97,18 +97,19 @@ export function createFlightSnapshotRenderer({
|
||||
// 5-15s stale and receipt-time stamping is what caused the
|
||||
// back/forward oscillation. Only append when the fix actually
|
||||
// advances, so stale repeats don't create zero-dt segments.
|
||||
const fixTime = Cesium.JulianDate.fromDate(new Date(fixEpochMs));
|
||||
// Milliseconds since the epoch, whole as a Date holds them.
|
||||
const fixTimeMs = Math.trunc(fixEpochMs);
|
||||
if (!flightState._positionHistory.has(icao24)) {
|
||||
flightState._positionHistory.set(icao24, []);
|
||||
}
|
||||
const history = flightState._positionHistory.get(icao24);
|
||||
const newest = history[history.length - 1];
|
||||
if (!newest || Cesium.JulianDate.greaterThan(fixTime, newest.time)) {
|
||||
if (!newest || fixTimeMs > newest.timeMs) {
|
||||
// Per-fix kinematics: the fix's own velocity/track ride along so the
|
||||
// extrapolation paths use the values that BELONG to the fix they
|
||||
// extend, not whatever the latest poll reported.
|
||||
history.push({
|
||||
time: fixTime,
|
||||
timeMs: fixTimeMs,
|
||||
epochMs: fixEpochMs,
|
||||
position: geo.cartesian3.clone(position),
|
||||
velocity: meta.velocity,
|
||||
|
||||
@@ -117,7 +117,7 @@ export function createFlightState({ source, services }) {
|
||||
|
||||
flightState._focusEvidenceIds = new Set();
|
||||
|
||||
/** @type {Map<string, Array<{time:Cesium.JulianDate, position:Cesium.Cartesian3}>>} */
|
||||
/** @type {Map<string, Array<{timeMs: number, epochMs: number, position: Cesium.Cartesian3}>>} timeMs: the fix time in whole ms since the epoch (the app clock's unit) */
|
||||
|
||||
flightState._positionHistory = new Map();
|
||||
|
||||
@@ -247,16 +247,12 @@ export function createFlightState({ source, services }) {
|
||||
|
||||
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
||||
|
||||
flightState._scratchRenderTime = new Cesium.JulianDate();
|
||||
|
||||
flightState._scratchFleetPos = geo.cartesian3.create();
|
||||
|
||||
flightState._scratchSonarScreen = geo.cartesian2.create();
|
||||
|
||||
flightState._scratchDrRaw = geo.cartesian3.create();
|
||||
|
||||
flightState._scratchWarmupTime = new Cesium.JulianDate();
|
||||
|
||||
flightState._trackedPosHolder = geo.cartesian3.create();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js';
|
||||
import { trackedModelZoomActive } from '../../data/trackedModelRegime.js';
|
||||
import {
|
||||
@@ -378,13 +379,9 @@ export function createTracking({
|
||||
// newest ~RENDER_DELAY_SEC of fixes are AHEAD of the displayed icon; including them
|
||||
// would draw the trail in front of the plane. They join the trail via _appendTrailFix
|
||||
// as they age past the delay.
|
||||
const seedRenderTime = Cesium.JulianDate.addSeconds(
|
||||
Cesium.JulianDate.now(),
|
||||
-RENDER_DELAY_SEC,
|
||||
flightState._scratchWarmupTime,
|
||||
);
|
||||
const seedRenderTimeMs = appClock.now() - RENDER_DELAY_SEC * 1000;
|
||||
for (const fix of history) {
|
||||
if (Cesium.JulianDate.lessThanOrEquals(fix.time, seedRenderTime)) {
|
||||
if (fix.timeMs <= seedRenderTimeMs) {
|
||||
flightState._trailPositions.push(Cesium.Cartesian3.clone(fix.position));
|
||||
}
|
||||
}
|
||||
@@ -458,7 +455,7 @@ export function createTracking({
|
||||
_refreshTrailDisplay();
|
||||
|
||||
const oldestFixEpochSec = history.length
|
||||
? Cesium.JulianDate.toDate(history[0].time).getTime() / 1000
|
||||
? history[0].timeMs / 1000
|
||||
: Infinity;
|
||||
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import {
|
||||
MISSION_CLOSE_VIEW_RANGE_M,
|
||||
@@ -358,7 +359,10 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
`rocket-satellite:${layerState._selectedLaunchId}`,
|
||||
);
|
||||
const position = satellite?.position?.getValue(
|
||||
Cesium.JulianDate.now(layerState._declutterTime),
|
||||
Cesium.JulianDate.fromDate(
|
||||
new Date(appClock.now()),
|
||||
layerState._declutterTime,
|
||||
),
|
||||
);
|
||||
const altitudeM = position
|
||||
? geo.cartographic.fromCartesian(position)?.height
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import {
|
||||
TRAJECTORY_STAGE_COLORS,
|
||||
REPLAY_ORBIT_DURATION_SEC,
|
||||
@@ -32,7 +33,11 @@ export function createRendering({
|
||||
return;
|
||||
}
|
||||
parts.panel.updateMissionTelemetry();
|
||||
const time = Cesium.JulianDate.now(layerState._declutterTime);
|
||||
// The app clock's now, as the JulianDate Cesium's entity properties take.
|
||||
const time = Cesium.JulianDate.fromDate(
|
||||
new Date(appClock.now()),
|
||||
layerState._declutterTime,
|
||||
);
|
||||
layerState._declutterOccluder.cameraPosition =
|
||||
layerState._viewer.scene.camera.positionWC;
|
||||
parts.overlays.updateReplayVehicleOverlay(layerState._declutterOccluder);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { MISSION_GLOBE_VIEW_RANGE_M, MISSION_FOCUS_RANGE_M } from './policy.js';
|
||||
|
||||
@@ -126,7 +127,7 @@ export function createSelection({
|
||||
`rocket-orbit:${launch.id}`,
|
||||
);
|
||||
const orbitPositions = orbitEntity?.polyline?.positions?.getValue(
|
||||
Cesium.JulianDate.now(),
|
||||
Cesium.JulianDate.fromDate(new Date(appClock.now())),
|
||||
);
|
||||
const launchPosition = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
|
||||
let range = 18000000;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import {
|
||||
lerpAngleDeg,
|
||||
speedRamp,
|
||||
@@ -67,25 +67,15 @@ export function createMotion({
|
||||
const out = result || geo.cartesian3.create();
|
||||
// Render one poll interval behind real time so we interpolate between
|
||||
// two KNOWN fixes whenever possible (see RENDER_DELAY_SEC rationale).
|
||||
const renderTime = Cesium.JulianDate.addSeconds(
|
||||
Cesium.JulianDate.now(),
|
||||
-RENDER_DELAY_SEC,
|
||||
flightState._scratchRenderTime,
|
||||
);
|
||||
const renderTimeMs = appClock.now() - RENDER_DELAY_SEC * 1000;
|
||||
|
||||
// Bracketing pair: interpolate — no extrapolation error, no snap-back.
|
||||
for (let i = history.length - 1; i >= 1; i--) {
|
||||
const a = history[i - 1];
|
||||
const b = history[i];
|
||||
if (
|
||||
Cesium.JulianDate.lessThanOrEquals(a.time, renderTime) &&
|
||||
Cesium.JulianDate.lessThanOrEquals(renderTime, b.time)
|
||||
) {
|
||||
const span = Cesium.JulianDate.secondsDifference(b.time, a.time);
|
||||
const t =
|
||||
span > 0
|
||||
? Cesium.JulianDate.secondsDifference(renderTime, a.time) / span
|
||||
: 1.0;
|
||||
if (a.timeMs <= renderTimeMs && renderTimeMs <= b.timeMs) {
|
||||
const span = (b.timeMs - a.timeMs) / 1000;
|
||||
const t = span > 0 ? (renderTimeMs - a.timeMs) / 1000 / span : 1.0;
|
||||
// Course of the DISPLAYED motion. The chord is only trustworthy when the
|
||||
// segment covers real ground (at hover its direction is GPS jitter; on a
|
||||
// slow tight turn it STEPS the whole per-segment turn at each boundary),
|
||||
@@ -115,10 +105,7 @@ export function createMotion({
|
||||
}
|
||||
|
||||
const newest = history[history.length - 1];
|
||||
const elapsedSec = Cesium.JulianDate.secondsDifference(
|
||||
renderTime,
|
||||
newest.time,
|
||||
);
|
||||
const elapsedSec = (renderTimeMs - newest.timeMs) / 1000;
|
||||
if (elapsedSec <= 0) {
|
||||
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
|
||||
// aircraft, before RENDER_DELAY_SEC of history has accumulated, so no bracketing
|
||||
@@ -128,10 +115,7 @@ export function createMotion({
|
||||
// the bracketing interpolation above with NO freeze and NO backward snap. (Holding
|
||||
// the oldest fix froze the icon until enough history accrued, then jumped.)
|
||||
const oldest = history[0];
|
||||
const lookbackSec = Cesium.JulianDate.secondsDifference(
|
||||
oldest.time,
|
||||
renderTime,
|
||||
); // ≥ 0
|
||||
const lookbackSec = (oldest.timeMs - renderTimeMs) / 1000; // ≥ 0
|
||||
return _extrapolateFix(
|
||||
oldest,
|
||||
info,
|
||||
@@ -148,7 +132,7 @@ export function createMotion({
|
||||
// Date for every aircraft twelve times per second.
|
||||
fixEpochMs: Number.isFinite(newest.epochMs)
|
||||
? newest.epochMs
|
||||
: Cesium.JulianDate.toDate(newest.time).getTime(),
|
||||
: newest.timeMs,
|
||||
lastContactEpochMs: info?.lastContactEpochMs,
|
||||
});
|
||||
return _extrapolateFix(
|
||||
@@ -213,12 +197,7 @@ export function createMotion({
|
||||
if (!flightState._trackedIcao) return false;
|
||||
const history = flightState._positionHistory.get(flightState._trackedIcao);
|
||||
if (!history || history.length === 0) return true;
|
||||
const renderTime = Cesium.JulianDate.addSeconds(
|
||||
Cesium.JulianDate.now(),
|
||||
-RENDER_DELAY_SEC,
|
||||
flightState._scratchWarmupTime,
|
||||
);
|
||||
return Cesium.JulianDate.lessThan(renderTime, history[0].time);
|
||||
return appClock.now() - RENDER_DELAY_SEC * 1000 < history[0].timeMs;
|
||||
}
|
||||
|
||||
/** Resolve the selected aircraft's actual rendered square extent this frame. */
|
||||
|
||||
@@ -114,11 +114,9 @@ test('a normalized source can retain its stale reason without changing standalon
|
||||
assert.equal(layer.getStats().stale, true);
|
||||
});
|
||||
|
||||
test('a newly seen military contact appears at its delayed position, not ahead of it', async () => {
|
||||
// The military fleet renders RENDER_DELAY_SEC behind real time, so a fresh
|
||||
// contact's first displayed position is its fix projected back to that
|
||||
// delayed time. Creating the billboard at the raw fix drew it ahead until
|
||||
// the next fleet tick jumped it back.
|
||||
// A fresh contact's first billboard, with the app clock `appClockLagMs`
|
||||
// behind the wall clock (0: the app's default, the wall clock itself).
|
||||
async function firstPlacement(appClockLagMs) {
|
||||
const Cesium = await import('cesium');
|
||||
const { RENDER_DELAY_SEC } = await import('./policy.js');
|
||||
// The application's own scene services, as src/app/layers/militaryFlights.js
|
||||
@@ -146,6 +144,8 @@ test('a newly seen military contact appears at its delayed position, not ahead o
|
||||
const speedMps = 200;
|
||||
const fixAgeMs = 2_000;
|
||||
const nowMs = Date.now();
|
||||
const { appClock } = await import('../../time/clock.js');
|
||||
if (appClockLagMs) appClock.freeze(nowMs - appClockLagMs);
|
||||
const layer = createMilitaryFlightLayer({
|
||||
services: supplied,
|
||||
source: {
|
||||
@@ -193,7 +193,11 @@ test('a newly seen military contact appears at its delayed position, not ahead o
|
||||
viewer: { camera: { positionCartographic: null }, scene: {} },
|
||||
tracked: false,
|
||||
});
|
||||
await layer.update({ camera: { positionCartographic: null }, scene: {} });
|
||||
try {
|
||||
await layer.update({ camera: { positionCartographic: null }, scene: {} });
|
||||
} finally {
|
||||
appClock.reset();
|
||||
}
|
||||
const billboard = added.find((entry) => entry.id === 'ae0001');
|
||||
assert.ok(billboard, 'the new contact gets a billboard');
|
||||
const shown = Cesium.Cartographic.fromCartesian(billboard.position);
|
||||
@@ -201,11 +205,37 @@ test('a newly seen military contact appears at its delayed position, not ahead o
|
||||
Cesium.Cartesian3.fromRadians(shown.longitude, shown.latitude, 0),
|
||||
Cesium.Cartesian3.fromDegrees(fixLon, fixLat, 0),
|
||||
);
|
||||
return {
|
||||
behindM,
|
||||
westOfFix: Cesium.Math.toDegrees(shown.longitude) < fixLon,
|
||||
speedMps,
|
||||
fixAgeMs,
|
||||
RENDER_DELAY_SEC,
|
||||
};
|
||||
}
|
||||
|
||||
test('a newly seen military contact appears at its delayed position, not ahead of it', async () => {
|
||||
// The military fleet renders RENDER_DELAY_SEC behind real time, so a fresh
|
||||
// contact's first displayed position is its fix projected back to that
|
||||
// delayed time. Creating the billboard at the raw fix drew it ahead until
|
||||
// the next fleet tick jumped it back.
|
||||
const { behindM, westOfFix, speedMps, fixAgeMs, RENDER_DELAY_SEC } =
|
||||
await firstPlacement(0);
|
||||
const expectedM = speedMps * (RENDER_DELAY_SEC - fixAgeMs / 1000);
|
||||
assert.ok(westOfFix, 'drawn behind the fix');
|
||||
assert.ok(
|
||||
Cesium.Math.toDegrees(shown.longitude) < fixLon,
|
||||
'drawn behind the fix',
|
||||
Math.abs(behindM - expectedM) < speedMps * 3,
|
||||
`expected about ${Math.round(expectedM)} m behind the fix, got ${Math.round(behindM)} m`,
|
||||
);
|
||||
});
|
||||
|
||||
test('the delayed position is read from the app clock', async () => {
|
||||
// With the app clock 10 s behind the wall clock, the render time moves 10 s
|
||||
// further back from the fix.
|
||||
const { behindM, westOfFix, speedMps, fixAgeMs, RENDER_DELAY_SEC } =
|
||||
await firstPlacement(10_000);
|
||||
const expectedM = speedMps * (RENDER_DELAY_SEC + 10 - fixAgeMs / 1000);
|
||||
assert.ok(westOfFix, 'drawn behind the fix');
|
||||
assert.ok(
|
||||
Math.abs(behindM - expectedM) < speedMps * 3,
|
||||
`expected about ${Math.round(expectedM)} m behind the fix, got ${Math.round(behindM)} m`,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { displayedKinematics } from '../../data/motionModel.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
|
||||
import { aircraftIncludedInNearby } from '../../data/aircraftNearbyPolicy.js';
|
||||
@@ -338,7 +337,6 @@ export function createQueries({
|
||||
const maxRange =
|
||||
Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY;
|
||||
|
||||
const now = Cesium.JulianDate.now();
|
||||
const nearby = [];
|
||||
|
||||
for (const [icao24, bb] of flightState._billboards) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
||||
import {
|
||||
@@ -72,18 +71,19 @@ export function createMilitarySnapshotRenderer({
|
||||
// report, so the fix epoch is receipt time minus that age. Only
|
||||
// append when the fix actually advances, so stale repeats don't
|
||||
// create zero-dt segments.
|
||||
const fixTime = Cesium.JulianDate.fromDate(new Date(fixEpochMs));
|
||||
// Milliseconds since the epoch, whole as a Date holds them.
|
||||
const fixTimeMs = Math.trunc(fixEpochMs);
|
||||
if (!flightState._positionHistory.has(icao24)) {
|
||||
flightState._positionHistory.set(icao24, []);
|
||||
}
|
||||
const history = flightState._positionHistory.get(icao24);
|
||||
const newest = history[history.length - 1];
|
||||
if (!newest || Cesium.JulianDate.greaterThan(fixTime, newest.time)) {
|
||||
if (!newest || fixTimeMs > newest.timeMs) {
|
||||
// Per-fix kinematics: the fix's own velocity/track ride along so the
|
||||
// extrapolation paths use the values that BELONG to the fix they
|
||||
// extend, not whatever the latest poll reported.
|
||||
history.push({
|
||||
time: fixTime,
|
||||
timeMs: fixTimeMs,
|
||||
epochMs: fixEpochMs,
|
||||
position: geo.cartesian3.clone(position),
|
||||
velocity: meta.speedMps,
|
||||
|
||||
@@ -109,7 +109,7 @@ export function createFlightState({ source, services }) {
|
||||
|
||||
flightState._detectionObjects = new Map();
|
||||
|
||||
/** @type {Map<string, Array<{time: Cesium.JulianDate, position: Cesium.Cartesian3}>>} ICAO hex -> recent position samples for dead reckoning */
|
||||
/** @type {Map<string, Array<{timeMs: number, epochMs: number, position: Cesium.Cartesian3}>>} ICAO hex -> recent position samples for dead reckoning (timeMs: whole ms since the epoch, the app clock's unit) */
|
||||
|
||||
flightState._positionHistory = new Map();
|
||||
|
||||
@@ -224,16 +224,12 @@ export function createFlightState({ source, services }) {
|
||||
|
||||
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
||||
|
||||
flightState._scratchRenderTime = new Cesium.JulianDate();
|
||||
|
||||
flightState._scratchFleetPos = geo.cartesian3.create();
|
||||
|
||||
flightState._scratchSonarScreen = geo.cartesian2.create();
|
||||
|
||||
flightState._scratchDrRaw = geo.cartesian3.create();
|
||||
|
||||
flightState._scratchWarmupTime = new Cesium.JulianDate();
|
||||
|
||||
flightState._trackedPosHolder = geo.cartesian3.create();
|
||||
|
||||
// -- Per-frame dead-reckoning cache for the tracked entity --
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js';
|
||||
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
|
||||
import { trackedModelZoomActive } from '../../data/trackedModelRegime.js';
|
||||
@@ -497,13 +498,9 @@ export function createTracking({
|
||||
// Seed only fixes at/behind the DELAYED display time (now − RENDER_DELAY_SEC). The
|
||||
// newest ~RENDER_DELAY_SEC of fixes are AHEAD of the displayed icon; including them
|
||||
// would draw the trail in front of the plane. They join via _appendTrailFix as they age.
|
||||
const seedRenderTime = Cesium.JulianDate.addSeconds(
|
||||
Cesium.JulianDate.now(),
|
||||
-RENDER_DELAY_SEC,
|
||||
flightState._scratchWarmupTime,
|
||||
);
|
||||
const seedRenderTimeMs = appClock.now() - RENDER_DELAY_SEC * 1000;
|
||||
for (const fix of history) {
|
||||
if (Cesium.JulianDate.lessThanOrEquals(fix.time, seedRenderTime)) {
|
||||
if (fix.timeMs <= seedRenderTimeMs) {
|
||||
// Floor grounded-history seeds (round 2): a grounded contact's stored
|
||||
// fixes carry the pre-datum default height — the trail copy sits on
|
||||
// the surface instead (pure lift; airborne fixes pass through).
|
||||
@@ -586,7 +583,7 @@ export function createTracking({
|
||||
_refreshTrailDisplay();
|
||||
|
||||
const oldestFixEpochSec = history.length
|
||||
? Cesium.JulianDate.toDate(history[0].time).getTime() / 1000
|
||||
? history[0].timeMs / 1000
|
||||
: Infinity;
|
||||
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import { satelliteClassLabel } from '../../data/satelliteClass.js';
|
||||
import {
|
||||
ISS_NORAD,
|
||||
@@ -130,7 +131,7 @@ export function createTracking({ state: layerState, services, parts, source }) {
|
||||
* Get the tracked satellite's geodetic position, propagated at most once per
|
||||
* rendered frame (WS-D2). All tracked-satellite consumers (entity position
|
||||
* callback → camera, host model, point primitive, getTrackedInfo) share this
|
||||
* single `new Date()` epoch per frame, so they can never diverge by the old
|
||||
* single app-clock epoch per frame, so they can never diverge by the old
|
||||
* 200ms throttle. The matching ECEF position is left in
|
||||
* `_trackedFrameCartesian`. SGP4 for one satellite per frame is cheap.
|
||||
* @returns {{ longitude: number, latitude: number, altitude: number }|null}
|
||||
@@ -150,9 +151,11 @@ export function createTracking({ state: layerState, services, parts, source }) {
|
||||
) {
|
||||
const pos = parts.orbits.propagatePosition(
|
||||
sat.satrec,
|
||||
layerState._trackedFrameNowForTest
|
||||
? new Date(layerState._trackedFrameNowForTest())
|
||||
: new Date(),
|
||||
new Date(
|
||||
layerState._trackedFrameNowForTest
|
||||
? layerState._trackedFrameNowForTest()
|
||||
: appClock.now(),
|
||||
),
|
||||
);
|
||||
if (!pos) return layerState._trackedFrameGeo; // propagation hiccup — keep last good sample
|
||||
layerState._trackedFrameGeo = pos;
|
||||
@@ -422,7 +425,10 @@ export function createTracking({ state: layerState, services, parts, source }) {
|
||||
// Comfortable tracking landing: ~726 km back for LEO (user-validated
|
||||
// "slightly zoomed out" framing — no stutter, label reads cleanly), scaled
|
||||
// up for MEO/GEO so the camera doesn't land on top of a high-orbit dot.
|
||||
const initialPos = parts.orbits.propagatePosition(sat.satrec, new Date());
|
||||
const initialPos = parts.orbits.propagatePosition(
|
||||
sat.satrec,
|
||||
new Date(appClock.now()),
|
||||
);
|
||||
const viewScale =
|
||||
initialPos && initialPos.altitude > HIGH_ORBIT_ALTITUDE_M
|
||||
? TRACK_VIEW_FROM_HIGH_SCALE
|
||||
|
||||
Reference in new issue
Block a user