mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +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 * as Cesium from 'cesium';
|
||||||
|
import { appClock } from './time/clock.js';
|
||||||
import { governorRequestRender } from './renderGovernor.js';
|
import { governorRequestRender } from './renderGovernor.js';
|
||||||
|
|
||||||
/** Outer edge of the existing NVG/FLIR keyhole in normalized shader space. */
|
/** Outer edge of the existing NVG/FLIR keyhole in normalized shader space. */
|
||||||
@@ -752,7 +753,9 @@ export class CelestialRing {
|
|||||||
|
|
||||||
if (!wasVisible) this._ephemerisDirty = true;
|
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;
|
if (!this._updateEphemeris(time)) return;
|
||||||
this._root.dataset.ephemerisUpdates = String(this._ephemerisUpdateCount);
|
this._root.dataset.ephemerisUpdates = String(this._ephemerisUpdateCount);
|
||||||
const camera = this.viewer.camera;
|
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,
|
tracked: false,
|
||||||
history: [
|
history: [
|
||||||
{
|
{
|
||||||
time: now,
|
timeMs: Cesium.JulianDate.toDate(now).getTime(),
|
||||||
epochMs: Date.now(),
|
epochMs: Date.now(),
|
||||||
position,
|
position,
|
||||||
velocity: 250,
|
velocity: 250,
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ test('real military track path creates no native label and publishes every cache
|
|||||||
tracked: false,
|
tracked: false,
|
||||||
history: [
|
history: [
|
||||||
{
|
{
|
||||||
time: now,
|
timeMs: Cesium.JulianDate.toDate(now).getTime(),
|
||||||
epochMs: Date.now(),
|
epochMs: Date.now(),
|
||||||
position,
|
position,
|
||||||
velocity: 231.5,
|
velocity: 231.5,
|
||||||
|
|||||||
+13
-9
@@ -6,7 +6,6 @@
|
|||||||
* Turn-rate estimation + constant-rate-turn arc integration are adapted from
|
* Turn-rate estimation + constant-rate-turn arc integration are adapted from
|
||||||
* skylight (https://github.com/cpaczek/skylight, MIT) shared/src/aim.ts.
|
* skylight (https://github.com/cpaczek/skylight, MIT) shared/src/aim.ts.
|
||||||
*/
|
*/
|
||||||
import * as Cesium from 'cesium';
|
|
||||||
import * as geo from '../geo/index.js';
|
import * as geo from '../geo/index.js';
|
||||||
|
|
||||||
const DEG = Math.PI / 180;
|
const DEG = Math.PI / 180;
|
||||||
@@ -257,17 +256,17 @@ export function estimateTurnRateDps(
|
|||||||
return Math.max(-maxDps, Math.min(maxDps, rate));
|
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
|
* estimateTurnRateDps, guarded by TURN_MIN_SPEED_MPS (low-speed fix-track
|
||||||
* jitter must not manufacture a turn rate). */
|
* jitter must not manufacture a turn rate). */
|
||||||
export function turnRateFromFixHistory(history) {
|
export function turnRateFromFixHistory(history) {
|
||||||
if (!history || history.length < 2) return 0;
|
if (!history || history.length < 2) return 0;
|
||||||
const t0 = history[0].time;
|
const t0 = history[0].timeMs;
|
||||||
const samples = [];
|
const samples = [];
|
||||||
for (const s of history) {
|
for (const s of history) {
|
||||||
if (!Number.isFinite(s.track)) continue;
|
if (!Number.isFinite(s.track)) continue;
|
||||||
samples.push({
|
samples.push({
|
||||||
tSec: Cesium.JulianDate.secondsDifference(s.time, t0),
|
tSec: (s.timeMs - t0) / 1000,
|
||||||
trackDeg: s.track,
|
trackDeg: s.track,
|
||||||
speedMps: s.velocity,
|
speedMps: s.velocity,
|
||||||
});
|
});
|
||||||
@@ -447,19 +446,23 @@ const _forwardFixOffset = geo.cartesian3.create();
|
|||||||
* kinematics. This prevents a late course change from reprojecting the entire
|
* kinematics. This prevents a late course change from reprojecting the entire
|
||||||
* stale interval and snapping the rendered contact by kilometres.
|
* 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.
|
* velocity?: number, track?: number}} newest Existing immutable history fix.
|
||||||
* @param {{epochMs: number, velocity: number, track: number, turnRateDps?: number}} next
|
* @param {{epochMs: number, velocity: number, track: number, turnRateDps?: number}} next
|
||||||
* New kinematics and the wall/source epoch at which they become authoritative.
|
* 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.
|
* velocity: number, track: number}|null} Forward synthetic fix, or null.
|
||||||
*/
|
*/
|
||||||
export function synthesizeForwardKinematicsFix(newest, next = {}) {
|
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;
|
return null;
|
||||||
const startEpochMs = Number.isFinite(newest.epochMs)
|
const startEpochMs = Number.isFinite(newest.epochMs)
|
||||||
? newest.epochMs
|
? newest.epochMs
|
||||||
: Cesium.JulianDate.toDate(newest.time).getTime();
|
: newest.timeMs;
|
||||||
if (!Number.isFinite(startEpochMs) || next.epochMs <= startEpochMs)
|
if (!Number.isFinite(startEpochMs) || next.epochMs <= startEpochMs)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -492,7 +495,8 @@ export function synthesizeForwardKinematicsFix(newest, next = {}) {
|
|||||||
geo.cartesian3.create(),
|
geo.cartesian3.create(),
|
||||||
);
|
);
|
||||||
return {
|
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,
|
epochMs: next.epochMs,
|
||||||
position,
|
position,
|
||||||
velocity: Number.isFinite(next.velocity)
|
velocity: Number.isFinite(next.velocity)
|
||||||
|
|||||||
@@ -93,10 +93,10 @@ test('estimateTurnRateDps: 2°/s ramp detected; noise floored; clamped', () => {
|
|||||||
assert.equal(estimateTurnRateDps(wild), -4); // clamped
|
assert.equal(estimateTurnRateDps(wild), -4); // clamped
|
||||||
});
|
});
|
||||||
|
|
||||||
test('turnRateFromFixHistory adapts JulianDate history', () => {
|
test('turnRateFromFixHistory adapts the fix history (ms timestamps)', () => {
|
||||||
const t0 = Cesium.JulianDate.now();
|
const t0 = Date.now();
|
||||||
const h = [0, 30, 60].map((s, i) => ({
|
const h = [0, 30, 60].map((s, i) => ({
|
||||||
time: Cesium.JulianDate.addSeconds(t0, s, new Cesium.JulianDate()),
|
timeMs: t0 + s * 1000,
|
||||||
track: i * 60,
|
track: i * 60,
|
||||||
}));
|
}));
|
||||||
assert.ok(Math.abs(turnRateFromFixHistory(h) - 2) < 1e-9);
|
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', () => {
|
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]) => ({
|
const mk = (specs) => specs.map(([s, track, velocity]) => ({
|
||||||
time: Cesium.JulianDate.addSeconds(t0, s, new Cesium.JulianDate()),
|
timeMs: t0 + s * 1000,
|
||||||
track, velocity,
|
track, velocity,
|
||||||
}));
|
}));
|
||||||
// Hovering heli: 1 m/s drift, reported track flipping ±45° — used to yield ±°/s
|
// 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 epochMs = 1_800_000_000_000;
|
||||||
const originalPosition = Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 9000);
|
const originalPosition = Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 9000);
|
||||||
const newest = {
|
const newest = {
|
||||||
time: Cesium.JulianDate.fromDate(new Date(epochMs)),
|
timeMs: epochMs,
|
||||||
epochMs,
|
epochMs,
|
||||||
position: Cesium.Cartesian3.clone(originalPosition),
|
position: Cesium.Cartesian3.clone(originalPosition),
|
||||||
velocity: 100,
|
velocity: 100,
|
||||||
@@ -195,6 +195,7 @@ test('repeated kinematics apply only from a forward synthetic fix', () => {
|
|||||||
assert.equal(newest.velocity, 100);
|
assert.equal(newest.velocity, 100);
|
||||||
assert.equal(newest.track, 90);
|
assert.equal(newest.track, 90);
|
||||||
assert.ok(Cesium.Cartesian3.equals(newest.position, originalPosition));
|
assert.ok(Cesium.Cartesian3.equals(newest.position, originalPosition));
|
||||||
|
assert.equal(synthetic.timeMs, epochMs + 10_000);
|
||||||
assert.equal(synthetic.velocity, 220);
|
assert.equal(synthetic.velocity, 220);
|
||||||
assert.equal(synthetic.track, 15);
|
assert.equal(synthetic.track, 15);
|
||||||
assert.ok(Math.abs(Cesium.Cartesian3.distance(newest.position, synthetic.position) - 1000) < 0.5);
|
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', () => {
|
test('liftRepeatedGroundFix raises only grounded history and preserves the stored coordinates', () => {
|
||||||
const newest = {
|
const newest = {
|
||||||
time: Cesium.JulianDate.now(),
|
timeMs: Date.now(),
|
||||||
epochMs: 123,
|
epochMs: 123,
|
||||||
position: Cesium.Cartesian3.fromDegrees(-98.04, 29.71, -30),
|
position: Cesium.Cartesian3.fromDegrees(-98.04, 29.71, -30),
|
||||||
velocity: 0,
|
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.edgeFade, 'keyhole');
|
||||||
assert.equal(entry.horizonCull, true);
|
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 * as geo from '../../geo/index.js';
|
||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
import {
|
import {
|
||||||
COURSE_HOLD_SPEED_MPS,
|
COURSE_HOLD_SPEED_MPS,
|
||||||
lerpAngleDeg,
|
lerpAngleDeg,
|
||||||
@@ -150,25 +150,15 @@ export function createMotion({
|
|||||||
const out = result || geo.cartesian3.create();
|
const out = result || geo.cartesian3.create();
|
||||||
// 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 renderTimeMs = appClock.now() - RENDER_DELAY_SEC * 1000;
|
||||||
Cesium.JulianDate.now(),
|
|
||||||
-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--) {
|
||||||
const a = history[i - 1];
|
const a = history[i - 1];
|
||||||
const b = history[i];
|
const b = history[i];
|
||||||
if (
|
if (a.timeMs <= renderTimeMs && renderTimeMs <= b.timeMs) {
|
||||||
Cesium.JulianDate.lessThanOrEquals(a.time, renderTime) &&
|
const span = (b.timeMs - a.timeMs) / 1000;
|
||||||
Cesium.JulianDate.lessThanOrEquals(renderTime, b.time)
|
const t = span > 0 ? (renderTimeMs - a.timeMs) / 1000 / span : 1.0;
|
||||||
) {
|
|
||||||
const span = Cesium.JulianDate.secondsDifference(b.time, a.time);
|
|
||||||
const t =
|
|
||||||
span > 0
|
|
||||||
? Cesium.JulianDate.secondsDifference(renderTime, a.time) / span
|
|
||||||
: 1.0;
|
|
||||||
// Course of the DISPLAYED motion. The chord is only trustworthy when the
|
// 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
|
// 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),
|
// 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 newest = history[history.length - 1];
|
||||||
const elapsedSec = Cesium.JulianDate.secondsDifference(
|
const elapsedSec = (renderTimeMs - newest.timeMs) / 1000;
|
||||||
renderTime,
|
|
||||||
newest.time,
|
|
||||||
);
|
|
||||||
if (elapsedSec <= 0) {
|
if (elapsedSec <= 0) {
|
||||||
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
|
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
|
||||||
// aircraft, before RENDER_DELAY_SEC of history has accumulated, so no bracketing
|
// 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 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.)
|
// the icon jump back ~one poll interval the instant interpolation took over.)
|
||||||
const oldest = history[0];
|
const oldest = history[0];
|
||||||
const lookbackSec = Cesium.JulianDate.secondsDifference(
|
const lookbackSec = (oldest.timeMs - renderTimeMs) / 1000; // ≥ 0
|
||||||
oldest.time,
|
|
||||||
renderTime,
|
|
||||||
); // ≥ 0
|
|
||||||
return _extrapolateFix(
|
return _extrapolateFix(
|
||||||
oldest,
|
oldest,
|
||||||
info,
|
info,
|
||||||
@@ -236,7 +220,7 @@ export function createMotion({
|
|||||||
// a Date per aircraft on every 12 Hz fleet tick.
|
// a Date per aircraft on every 12 Hz fleet tick.
|
||||||
fixEpochMs: Number.isFinite(newest.epochMs)
|
fixEpochMs: Number.isFinite(newest.epochMs)
|
||||||
? newest.epochMs
|
? newest.epochMs
|
||||||
: Cesium.JulianDate.toDate(newest.time).getTime(),
|
: newest.timeMs,
|
||||||
lastContactEpochMs: info?.lastContactEpochMs,
|
lastContactEpochMs: info?.lastContactEpochMs,
|
||||||
// Permit one minute of contact grace but cap any cached-feed drift at
|
// Permit one minute of contact grace but cap any cached-feed drift at
|
||||||
// five minutes. Source backoff is exposed separately as a STALE cue.
|
// five minutes. Source backoff is exposed separately as a STALE cue.
|
||||||
@@ -307,12 +291,7 @@ export function createMotion({
|
|||||||
if (!flightState._trackedIcao) return false;
|
if (!flightState._trackedIcao) return false;
|
||||||
const history = flightState._positionHistory.get(flightState._trackedIcao);
|
const history = flightState._positionHistory.get(flightState._trackedIcao);
|
||||||
if (!history || history.length === 0) return true;
|
if (!history || history.length === 0) return true;
|
||||||
const renderTime = Cesium.JulianDate.addSeconds(
|
return appClock.now() - RENDER_DELAY_SEC * 1000 < history[0].timeMs;
|
||||||
Cesium.JulianDate.now(),
|
|
||||||
-RENDER_DELAY_SEC,
|
|
||||||
flightState._scratchWarmupTime,
|
|
||||||
);
|
|
||||||
return Cesium.JulianDate.lessThan(renderTime, history[0].time);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve the selected aircraft's actual rendered square extent this frame. */
|
/** Resolve the selected aircraft's actual rendered square extent this frame. */
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { approxDistanceKm as _approxDistanceKm } from './recordPolicy.js';
|
import { approxDistanceKm as _approxDistanceKm } from './recordPolicy.js';
|
||||||
import { displayedKinematics } from '../../data/motionModel.js';
|
import { displayedKinematics } from '../../data/motionModel.js';
|
||||||
import * as Cesium from 'cesium';
|
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
|
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
|
||||||
import { modelVisualAnchor } from '../../data/modelVisualAnchor.js';
|
import { modelVisualAnchor } from '../../data/modelVisualAnchor.js';
|
||||||
@@ -550,7 +549,6 @@ export function createQueries({
|
|||||||
const maxRange =
|
const maxRange =
|
||||||
Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY;
|
Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
const now = Cesium.JulianDate.now();
|
|
||||||
const nearby = [];
|
const nearby = [];
|
||||||
|
|
||||||
for (const [icao24, bb] of flightState._billboards) {
|
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
|
// 5-15s stale and receipt-time stamping is what caused the
|
||||||
// back/forward oscillation. Only append when the fix actually
|
// back/forward oscillation. Only append when the fix actually
|
||||||
// advances, so stale repeats don't create zero-dt segments.
|
// 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)) {
|
if (!flightState._positionHistory.has(icao24)) {
|
||||||
flightState._positionHistory.set(icao24, []);
|
flightState._positionHistory.set(icao24, []);
|
||||||
}
|
}
|
||||||
const history = flightState._positionHistory.get(icao24);
|
const history = flightState._positionHistory.get(icao24);
|
||||||
const newest = history[history.length - 1];
|
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
|
// Per-fix kinematics: the fix's own velocity/track ride along so the
|
||||||
// extrapolation paths use the values that BELONG to the fix they
|
// extrapolation paths use the values that BELONG to the fix they
|
||||||
// extend, not whatever the latest poll reported.
|
// extend, not whatever the latest poll reported.
|
||||||
history.push({
|
history.push({
|
||||||
time: fixTime,
|
timeMs: fixTimeMs,
|
||||||
epochMs: fixEpochMs,
|
epochMs: fixEpochMs,
|
||||||
position: geo.cartesian3.clone(position),
|
position: geo.cartesian3.clone(position),
|
||||||
velocity: meta.velocity,
|
velocity: meta.velocity,
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._focusEvidenceIds = new Set();
|
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();
|
flightState._positionHistory = new Map();
|
||||||
|
|
||||||
@@ -247,16 +247,12 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
||||||
|
|
||||||
flightState._scratchRenderTime = new Cesium.JulianDate();
|
|
||||||
|
|
||||||
flightState._scratchFleetPos = geo.cartesian3.create();
|
flightState._scratchFleetPos = geo.cartesian3.create();
|
||||||
|
|
||||||
flightState._scratchSonarScreen = geo.cartesian2.create();
|
flightState._scratchSonarScreen = geo.cartesian2.create();
|
||||||
|
|
||||||
flightState._scratchDrRaw = geo.cartesian3.create();
|
flightState._scratchDrRaw = geo.cartesian3.create();
|
||||||
|
|
||||||
flightState._scratchWarmupTime = new Cesium.JulianDate();
|
|
||||||
|
|
||||||
flightState._trackedPosHolder = geo.cartesian3.create();
|
flightState._trackedPosHolder = geo.cartesian3.create();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js';
|
import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js';
|
||||||
import { trackedModelZoomActive } from '../../data/trackedModelRegime.js';
|
import { trackedModelZoomActive } from '../../data/trackedModelRegime.js';
|
||||||
import {
|
import {
|
||||||
@@ -378,13 +379,9 @@ export function createTracking({
|
|||||||
// newest ~RENDER_DELAY_SEC of fixes are AHEAD of the displayed icon; including them
|
// 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
|
// would draw the trail in front of the plane. They join the trail via _appendTrailFix
|
||||||
// as they age past the delay.
|
// as they age past the delay.
|
||||||
const seedRenderTime = Cesium.JulianDate.addSeconds(
|
const seedRenderTimeMs = appClock.now() - RENDER_DELAY_SEC * 1000;
|
||||||
Cesium.JulianDate.now(),
|
|
||||||
-RENDER_DELAY_SEC,
|
|
||||||
flightState._scratchWarmupTime,
|
|
||||||
);
|
|
||||||
for (const fix of history) {
|
for (const fix of history) {
|
||||||
if (Cesium.JulianDate.lessThanOrEquals(fix.time, seedRenderTime)) {
|
if (fix.timeMs <= seedRenderTimeMs) {
|
||||||
flightState._trailPositions.push(Cesium.Cartesian3.clone(fix.position));
|
flightState._trailPositions.push(Cesium.Cartesian3.clone(fix.position));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,7 +455,7 @@ export function createTracking({
|
|||||||
_refreshTrailDisplay();
|
_refreshTrailDisplay();
|
||||||
|
|
||||||
const oldestFixEpochSec = history.length
|
const oldestFixEpochSec = history.length
|
||||||
? Cesium.JulianDate.toDate(history[0].time).getTime() / 1000
|
? history[0].timeMs / 1000
|
||||||
: Infinity;
|
: Infinity;
|
||||||
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
|
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import {
|
import {
|
||||||
MISSION_CLOSE_VIEW_RANGE_M,
|
MISSION_CLOSE_VIEW_RANGE_M,
|
||||||
@@ -358,7 +359,10 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
|||||||
`rocket-satellite:${layerState._selectedLaunchId}`,
|
`rocket-satellite:${layerState._selectedLaunchId}`,
|
||||||
);
|
);
|
||||||
const position = satellite?.position?.getValue(
|
const position = satellite?.position?.getValue(
|
||||||
Cesium.JulianDate.now(layerState._declutterTime),
|
Cesium.JulianDate.fromDate(
|
||||||
|
new Date(appClock.now()),
|
||||||
|
layerState._declutterTime,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const altitudeM = position
|
const altitudeM = position
|
||||||
? geo.cartographic.fromCartesian(position)?.height
|
? geo.cartographic.fromCartesian(position)?.height
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
import {
|
import {
|
||||||
TRAJECTORY_STAGE_COLORS,
|
TRAJECTORY_STAGE_COLORS,
|
||||||
REPLAY_ORBIT_DURATION_SEC,
|
REPLAY_ORBIT_DURATION_SEC,
|
||||||
@@ -32,7 +33,11 @@ export function createRendering({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
parts.panel.updateMissionTelemetry();
|
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._declutterOccluder.cameraPosition =
|
||||||
layerState._viewer.scene.camera.positionWC;
|
layerState._viewer.scene.camera.positionWC;
|
||||||
parts.overlays.updateReplayVehicleOverlay(layerState._declutterOccluder);
|
parts.overlays.updateReplayVehicleOverlay(layerState._declutterOccluder);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import { MISSION_GLOBE_VIEW_RANGE_M, MISSION_FOCUS_RANGE_M } from './policy.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}`,
|
`rocket-orbit:${launch.id}`,
|
||||||
);
|
);
|
||||||
const orbitPositions = orbitEntity?.polyline?.positions?.getValue(
|
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);
|
const launchPosition = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
|
||||||
let range = 18000000;
|
let range = 18000000;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
import {
|
import {
|
||||||
lerpAngleDeg,
|
lerpAngleDeg,
|
||||||
speedRamp,
|
speedRamp,
|
||||||
@@ -67,25 +67,15 @@ export function createMotion({
|
|||||||
const out = result || geo.cartesian3.create();
|
const out = result || geo.cartesian3.create();
|
||||||
// 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 renderTimeMs = appClock.now() - RENDER_DELAY_SEC * 1000;
|
||||||
Cesium.JulianDate.now(),
|
|
||||||
-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--) {
|
||||||
const a = history[i - 1];
|
const a = history[i - 1];
|
||||||
const b = history[i];
|
const b = history[i];
|
||||||
if (
|
if (a.timeMs <= renderTimeMs && renderTimeMs <= b.timeMs) {
|
||||||
Cesium.JulianDate.lessThanOrEquals(a.time, renderTime) &&
|
const span = (b.timeMs - a.timeMs) / 1000;
|
||||||
Cesium.JulianDate.lessThanOrEquals(renderTime, b.time)
|
const t = span > 0 ? (renderTimeMs - a.timeMs) / 1000 / span : 1.0;
|
||||||
) {
|
|
||||||
const span = Cesium.JulianDate.secondsDifference(b.time, a.time);
|
|
||||||
const t =
|
|
||||||
span > 0
|
|
||||||
? Cesium.JulianDate.secondsDifference(renderTime, a.time) / span
|
|
||||||
: 1.0;
|
|
||||||
// Course of the DISPLAYED motion. The chord is only trustworthy when the
|
// 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
|
// 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),
|
// 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 newest = history[history.length - 1];
|
||||||
const elapsedSec = Cesium.JulianDate.secondsDifference(
|
const elapsedSec = (renderTimeMs - newest.timeMs) / 1000;
|
||||||
renderTime,
|
|
||||||
newest.time,
|
|
||||||
);
|
|
||||||
if (elapsedSec <= 0) {
|
if (elapsedSec <= 0) {
|
||||||
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
|
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
|
||||||
// aircraft, before RENDER_DELAY_SEC of history has accumulated, so no bracketing
|
// 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 bracketing interpolation above with NO freeze and NO backward snap. (Holding
|
||||||
// the oldest fix froze the icon until enough history accrued, then jumped.)
|
// the oldest fix froze the icon until enough history accrued, then jumped.)
|
||||||
const oldest = history[0];
|
const oldest = history[0];
|
||||||
const lookbackSec = Cesium.JulianDate.secondsDifference(
|
const lookbackSec = (oldest.timeMs - renderTimeMs) / 1000; // ≥ 0
|
||||||
oldest.time,
|
|
||||||
renderTime,
|
|
||||||
); // ≥ 0
|
|
||||||
return _extrapolateFix(
|
return _extrapolateFix(
|
||||||
oldest,
|
oldest,
|
||||||
info,
|
info,
|
||||||
@@ -148,7 +132,7 @@ export function createMotion({
|
|||||||
// Date for every aircraft twelve times per second.
|
// Date for every aircraft twelve times per second.
|
||||||
fixEpochMs: Number.isFinite(newest.epochMs)
|
fixEpochMs: Number.isFinite(newest.epochMs)
|
||||||
? newest.epochMs
|
? newest.epochMs
|
||||||
: Cesium.JulianDate.toDate(newest.time).getTime(),
|
: newest.timeMs,
|
||||||
lastContactEpochMs: info?.lastContactEpochMs,
|
lastContactEpochMs: info?.lastContactEpochMs,
|
||||||
});
|
});
|
||||||
return _extrapolateFix(
|
return _extrapolateFix(
|
||||||
@@ -213,12 +197,7 @@ export function createMotion({
|
|||||||
if (!flightState._trackedIcao) return false;
|
if (!flightState._trackedIcao) return false;
|
||||||
const history = flightState._positionHistory.get(flightState._trackedIcao);
|
const history = flightState._positionHistory.get(flightState._trackedIcao);
|
||||||
if (!history || history.length === 0) return true;
|
if (!history || history.length === 0) return true;
|
||||||
const renderTime = Cesium.JulianDate.addSeconds(
|
return appClock.now() - RENDER_DELAY_SEC * 1000 < history[0].timeMs;
|
||||||
Cesium.JulianDate.now(),
|
|
||||||
-RENDER_DELAY_SEC,
|
|
||||||
flightState._scratchWarmupTime,
|
|
||||||
);
|
|
||||||
return Cesium.JulianDate.lessThan(renderTime, history[0].time);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve the selected aircraft's actual rendered square extent this frame. */
|
/** 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);
|
assert.equal(layer.getStats().stale, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a newly seen military contact appears at its delayed position, not ahead of it', async () => {
|
// A fresh contact's first billboard, with the app clock `appClockLagMs`
|
||||||
// The military fleet renders RENDER_DELAY_SEC behind real time, so a fresh
|
// behind the wall clock (0: the app's default, the wall clock itself).
|
||||||
// contact's first displayed position is its fix projected back to that
|
async function firstPlacement(appClockLagMs) {
|
||||||
// delayed time. Creating the billboard at the raw fix drew it ahead until
|
|
||||||
// the next fleet tick jumped it back.
|
|
||||||
const Cesium = await import('cesium');
|
const Cesium = await import('cesium');
|
||||||
const { RENDER_DELAY_SEC } = await import('./policy.js');
|
const { RENDER_DELAY_SEC } = await import('./policy.js');
|
||||||
// The application's own scene services, as src/app/layers/militaryFlights.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 speedMps = 200;
|
||||||
const fixAgeMs = 2_000;
|
const fixAgeMs = 2_000;
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
|
const { appClock } = await import('../../time/clock.js');
|
||||||
|
if (appClockLagMs) appClock.freeze(nowMs - appClockLagMs);
|
||||||
const layer = createMilitaryFlightLayer({
|
const layer = createMilitaryFlightLayer({
|
||||||
services: supplied,
|
services: supplied,
|
||||||
source: {
|
source: {
|
||||||
@@ -193,7 +193,11 @@ test('a newly seen military contact appears at its delayed position, not ahead o
|
|||||||
viewer: { camera: { positionCartographic: null }, scene: {} },
|
viewer: { camera: { positionCartographic: null }, scene: {} },
|
||||||
tracked: false,
|
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');
|
const billboard = added.find((entry) => entry.id === 'ae0001');
|
||||||
assert.ok(billboard, 'the new contact gets a billboard');
|
assert.ok(billboard, 'the new contact gets a billboard');
|
||||||
const shown = Cesium.Cartographic.fromCartesian(billboard.position);
|
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.fromRadians(shown.longitude, shown.latitude, 0),
|
||||||
Cesium.Cartesian3.fromDegrees(fixLon, fixLat, 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);
|
const expectedM = speedMps * (RENDER_DELAY_SEC - fixAgeMs / 1000);
|
||||||
|
assert.ok(westOfFix, 'drawn behind the fix');
|
||||||
assert.ok(
|
assert.ok(
|
||||||
Cesium.Math.toDegrees(shown.longitude) < fixLon,
|
Math.abs(behindM - expectedM) < speedMps * 3,
|
||||||
'drawn behind the fix',
|
`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(
|
assert.ok(
|
||||||
Math.abs(behindM - expectedM) < speedMps * 3,
|
Math.abs(behindM - expectedM) < speedMps * 3,
|
||||||
`expected about ${Math.round(expectedM)} m behind the fix, got ${Math.round(behindM)} m`,
|
`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 { displayedKinematics } from '../../data/motionModel.js';
|
||||||
import * as Cesium from 'cesium';
|
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
|
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
|
||||||
import { aircraftIncludedInNearby } from '../../data/aircraftNearbyPolicy.js';
|
import { aircraftIncludedInNearby } from '../../data/aircraftNearbyPolicy.js';
|
||||||
@@ -338,7 +337,6 @@ export function createQueries({
|
|||||||
const maxRange =
|
const maxRange =
|
||||||
Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY;
|
Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
const now = Cesium.JulianDate.now();
|
|
||||||
const nearby = [];
|
const nearby = [];
|
||||||
|
|
||||||
for (const [icao24, bb] of flightState._billboards) {
|
for (const [icao24, bb] of flightState._billboards) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Cesium from 'cesium';
|
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
||||||
import {
|
import {
|
||||||
@@ -72,18 +71,19 @@ export function createMilitarySnapshotRenderer({
|
|||||||
// report, so the fix epoch is receipt time minus that age. Only
|
// report, so the fix epoch is receipt time minus that age. Only
|
||||||
// append when the fix actually advances, so stale repeats don't
|
// append when the fix actually advances, so stale repeats don't
|
||||||
// create zero-dt segments.
|
// 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)) {
|
if (!flightState._positionHistory.has(icao24)) {
|
||||||
flightState._positionHistory.set(icao24, []);
|
flightState._positionHistory.set(icao24, []);
|
||||||
}
|
}
|
||||||
const history = flightState._positionHistory.get(icao24);
|
const history = flightState._positionHistory.get(icao24);
|
||||||
const newest = history[history.length - 1];
|
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
|
// Per-fix kinematics: the fix's own velocity/track ride along so the
|
||||||
// extrapolation paths use the values that BELONG to the fix they
|
// extrapolation paths use the values that BELONG to the fix they
|
||||||
// extend, not whatever the latest poll reported.
|
// extend, not whatever the latest poll reported.
|
||||||
history.push({
|
history.push({
|
||||||
time: fixTime,
|
timeMs: fixTimeMs,
|
||||||
epochMs: fixEpochMs,
|
epochMs: fixEpochMs,
|
||||||
position: geo.cartesian3.clone(position),
|
position: geo.cartesian3.clone(position),
|
||||||
velocity: meta.speedMps,
|
velocity: meta.speedMps,
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._detectionObjects = new Map();
|
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();
|
flightState._positionHistory = new Map();
|
||||||
|
|
||||||
@@ -224,16 +224,12 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
|
||||||
|
|
||||||
flightState._scratchRenderTime = new Cesium.JulianDate();
|
|
||||||
|
|
||||||
flightState._scratchFleetPos = geo.cartesian3.create();
|
flightState._scratchFleetPos = geo.cartesian3.create();
|
||||||
|
|
||||||
flightState._scratchSonarScreen = geo.cartesian2.create();
|
flightState._scratchSonarScreen = geo.cartesian2.create();
|
||||||
|
|
||||||
flightState._scratchDrRaw = geo.cartesian3.create();
|
flightState._scratchDrRaw = geo.cartesian3.create();
|
||||||
|
|
||||||
flightState._scratchWarmupTime = new Cesium.JulianDate();
|
|
||||||
|
|
||||||
flightState._trackedPosHolder = geo.cartesian3.create();
|
flightState._trackedPosHolder = geo.cartesian3.create();
|
||||||
|
|
||||||
// -- Per-frame dead-reckoning cache for the tracked entity --
|
// -- Per-frame dead-reckoning cache for the tracked entity --
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js';
|
import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js';
|
||||||
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
|
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
|
||||||
import { trackedModelZoomActive } from '../../data/trackedModelRegime.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
|
// 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
|
// 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.
|
// would draw the trail in front of the plane. They join via _appendTrailFix as they age.
|
||||||
const seedRenderTime = Cesium.JulianDate.addSeconds(
|
const seedRenderTimeMs = appClock.now() - RENDER_DELAY_SEC * 1000;
|
||||||
Cesium.JulianDate.now(),
|
|
||||||
-RENDER_DELAY_SEC,
|
|
||||||
flightState._scratchWarmupTime,
|
|
||||||
);
|
|
||||||
for (const fix of history) {
|
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
|
// Floor grounded-history seeds (round 2): a grounded contact's stored
|
||||||
// fixes carry the pre-datum default height — the trail copy sits on
|
// fixes carry the pre-datum default height — the trail copy sits on
|
||||||
// the surface instead (pure lift; airborne fixes pass through).
|
// the surface instead (pure lift; airborne fixes pass through).
|
||||||
@@ -586,7 +583,7 @@ export function createTracking({
|
|||||||
_refreshTrailDisplay();
|
_refreshTrailDisplay();
|
||||||
|
|
||||||
const oldestFixEpochSec = history.length
|
const oldestFixEpochSec = history.length
|
||||||
? Cesium.JulianDate.toDate(history[0].time).getTime() / 1000
|
? history[0].timeMs / 1000
|
||||||
: Infinity;
|
: Infinity;
|
||||||
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
|
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
import { satelliteClassLabel } from '../../data/satelliteClass.js';
|
import { satelliteClassLabel } from '../../data/satelliteClass.js';
|
||||||
import {
|
import {
|
||||||
ISS_NORAD,
|
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
|
* Get the tracked satellite's geodetic position, propagated at most once per
|
||||||
* rendered frame (WS-D2). All tracked-satellite consumers (entity position
|
* rendered frame (WS-D2). All tracked-satellite consumers (entity position
|
||||||
* callback → camera, host model, point primitive, getTrackedInfo) share this
|
* 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
|
* 200ms throttle. The matching ECEF position is left in
|
||||||
* `_trackedFrameCartesian`. SGP4 for one satellite per frame is cheap.
|
* `_trackedFrameCartesian`. SGP4 for one satellite per frame is cheap.
|
||||||
* @returns {{ longitude: number, latitude: number, altitude: number }|null}
|
* @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(
|
const pos = parts.orbits.propagatePosition(
|
||||||
sat.satrec,
|
sat.satrec,
|
||||||
layerState._trackedFrameNowForTest
|
new Date(
|
||||||
? new Date(layerState._trackedFrameNowForTest())
|
layerState._trackedFrameNowForTest
|
||||||
: new Date(),
|
? layerState._trackedFrameNowForTest()
|
||||||
|
: appClock.now(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (!pos) return layerState._trackedFrameGeo; // propagation hiccup — keep last good sample
|
if (!pos) return layerState._trackedFrameGeo; // propagation hiccup — keep last good sample
|
||||||
layerState._trackedFrameGeo = pos;
|
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
|
// Comfortable tracking landing: ~726 km back for LEO (user-validated
|
||||||
// "slightly zoomed out" framing — no stutter, label reads cleanly), scaled
|
// "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.
|
// 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 =
|
const viewScale =
|
||||||
initialPos && initialPos.altitude > HIGH_ORBIT_ALTITUDE_M
|
initialPos && initialPos.altitude > HIGH_ORBIT_ALTITUDE_M
|
||||||
? TRACK_VIEW_FROM_HIGH_SCALE
|
? TRACK_VIEW_FROM_HIGH_SCALE
|
||||||
|
|||||||
Reference in new issue
Block a user