Files
gods-eye-view/src/layers/satellites/state.js
T
Sameh Khamis 0c4da3cdb2 Keep one world clock that every timed layer reads
Core told time eight ways: Date.now() in most layers, JulianDate.now() for
flights and military, Cesium's clock frozen at page load for the sky, the
weather clock for observed imagery. They now read one clock,
src/time/worldClock.js, exported as ./time.

Live is a mode, not a timestamp: while live the clock is the wall clock,
so every layer draws as before. Away from live only the layers that follow
the clock move with it. The weather products follow it, as they followed
the weather transport; a host opts others in with follow(id).

- The weather clock takes its time from the world clock and drives its
  replay, stepping its frames as before.
- Satellites, launches, flights and military (still 30 s and 15 s behind),
  transit, GNSS, Recent Imagery, street level, the HUD and the celestial
  ring read worldTimeFor(id). Cadences stay on the wall clock.
- Cesium's sky (stars, sun, moon, atmosphere, lighting) draws at the world
  time each frame instead of the moment the page loaded.
- Weather frames and satellites declare coverage() for a host's timeline.
2026-11-01 21:47:32 -08:00

175 lines
4.7 KiB
JavaScript

import * as Cesium from 'cesium';
import { worldTimeFor } from '../../time/worldClock.js';
export function createState({ services }) {
const { setOverlayEntries, setOverlaySourceVisible, clearOverlaySource } =
services.overlays;
const state = {};
state.DEFAULT_OVERLAY_HOST = Object.freeze({
setEntries: setOverlayEntries,
setVisible: setOverlaySourceVisible,
clearSource: clearOverlaySource,
});
state._overlayHost = state.DEFAULT_OVERLAY_HOST;
// Satellite catalog: { noradId → { name, satrec, group } }
state._catalog = new Map();
state._pointCollection = null;
state._points = new Map();
// noradId → point primitive
/** Stable lightweight records reused by the detection overlay between updates. */
state._detectionObjects = new Map();
// noradId → { primitive, gmstAtBake }
// primitive is a one-instance Cesium.Primitive holding the ring baked in ECEF
// at gmstAtBake; the preRender tick re-aligns it to current GMST by updating
// its modelMatrix (rigid Z-rotation — no geometry rebuild, WS-D1).
state._orbitPaths = new Map();
state._count = 0;
state._lastUpdate = null;
/** @type {string|null} Surfaced feed error (e.g. CelesTrak outage) for the layer chip. */
state._lastError = null;
// Cache provenance belongs to admitted core and dense records separately.
state._coreStale = false;
state._denseStale = false;
state._activeUpdateControllers = new Set();
state._denseLoadController = null;
state._viewer = null;
state._preRenderListener = null;
state._lastPropagation = 0;
state._lastRingRotation = 0;
state._lastFocusUpdate = 0;
/** Points whose animated emphasis remains outside the 1.0 deadband. */
state._activeFocusCount = 0;
state._scratchFocusScreen = new Cesium.Cartesian2();
state._enabled = false;
// Click-to-track state
state._trackedNorad = null;
state._pendingTrackingRestore = null;
state._trackingIntentGeneration = 0;
state._trackingRefreshEpoch = 0;
state._lastTrackingRefreshOutcome = {
epoch: 0,
status: 'unavailable',
failedGroups: [],
};
state._trackedEntity = null;
state._clickHandler = null;
/** @type {Cesium.Event.RemoveCallback|null} trackedEntityChanged listener disposer (cross-layer untrack) */
state._trackedEntityChangedRemove = null;
// Runtime params (DataLayerManager.setLayerParams path)
state._params = { catalog: 'core', showPoints: true, showOrbits: true };
// 'core' | 'dense'
state._denseIds = [];
// norad ids of dense extras, round-robin order
state._denseCursor = 0;
// next dense id to re-propagate
state._denseLoadToken = 0;
// invalidates in-flight dense loads on mode flip/reload
state._denseLoadPromise = null;
/**
* Dense-load lifecycle: 'idle' → 'loading' → 'ready' | 'failed'.
* The catalog param flips synchronously but the Starlink shell arrives over
* seconds and can fail outright (CelesTrak 502s this feed regularly), so the
* row chip reports THIS, not the param. An active chip must mean dense points
* are actually on screen.
*/
state._denseStatus = 'idle';
/** @type {string|null} Why the last dense load failed, for the chip tooltip. */
state._denseError = null;
/** Bumped on every bulk catalog mutation; keys the row-legend tally cache. */
state._catalogRevision = 0;
state._classTallyCache = { revision: -1, counts: null };
/** @type {(() => void)|null} Manager callback: "this layer's row controls changed". */
state._rowControlsListener = null;
/** @type {Set<number>} NORAD ids co-located with the tracked satellite. */
state._dockedCompanions = new Set();
state._lastDockedScanMs = Number.NEGATIVE_INFINITY;
// Per-frame cache for the tracked satellite (WS-D2): dot, host readout, camera, and
// getTrackedInfo all read one SGP4 sample per rendered frame, keyed on
// scene.frameState.frameNumber, so they never diverge in epoch.
state._trackedFrameNumber = -1;
state._trackedFrameGeo = null;
// { longitude, latitude, altitude } or null
state._trackedFrameCartesian = new Cesium.Cartesian3();
/** Optional deterministic clock used only by the production-frame test seam. */
state._trackedFrameNowForTest = null;
/** The time satellites are propagated to, in epoch ms. */
state.now = worldTimeFor('satellites');
// Scratch variables
state._scratchCartesian = new Cesium.Cartesian3();
state._scratchRingRotation = new Cesium.Matrix3();
/** Update the explicit model only when the rounded altitude line changes. */
/** Epoch of the last shared-context refresh for the tracked satellite. */
state._contextRefreshedAtMs = 0;
state._lookupTleText = null;
state._lookupTleEntries = [];
return state;
}