mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
refactor(satellites): Cesium rendering behind a renderer interface
Move native points, synchronous orbit primitives, tracked entities, follow and picking into the Cesium renderer. Use appClock for satellite physical epochs and isolate the unchanged CelestialRing ephemeris. Synthetic core/dense and ISS/MEO/GEO snapshots match the baseline exactly; all gates pass. The pointer-ownership source assertion follows scene.pick into render/cesium.js; registry and import-boundary assertions now reflect the migrated satellite layer.
This commit is contained in:
1 parent
83fbe6949f
commit
000ef99464
27 files changed
+459
-196
No files matched your search
@@ -38,10 +38,13 @@ const RENDERED_LAYER_DIRS = [
|
||||
'src/layers/military/',
|
||||
'src/layers/localAdsb/',
|
||||
'src/layers/vessels/',
|
||||
'src/layers/satellites/',
|
||||
'src/layers/aircraft/',
|
||||
'src/layers/shared/',
|
||||
];
|
||||
const renderedLayer = (file) =>
|
||||
file === 'src/celestialRing.js' ||
|
||||
file.startsWith('src/celestialRing/') ||
|
||||
RENDERED_LAYER_DIRS.some((dir) => file.startsWith(dir));
|
||||
const cesiumLayerRenderer = (file) => /\/render\/cesium[^/]*\.js$/.test(file);
|
||||
const navaraLayerRenderer = (file) => /\/render\/navara[^/]*\.js$/.test(file);
|
||||
|
||||
@@ -1138,5 +1138,10 @@
|
||||
"scripts/qa-flights-engines.mjs",
|
||||
"src/bench/w1Feeds.js",
|
||||
"scripts/qa-map-switch.mjs",
|
||||
"scripts/qa-engine-notice.mjs"
|
||||
"scripts/qa-engine-notice.mjs",
|
||||
"src/bench/satelliteFixture.js",
|
||||
"src/bench/satelliteFixture.test.mjs",
|
||||
"src/layers/satellites/render/cesium.js",
|
||||
"src/celestialRing/render/cesium.js",
|
||||
"scripts/qa-satellites.mjs"
|
||||
]
|
||||
@@ -1146,7 +1146,8 @@
|
||||
"src/geo/check.js",
|
||||
"src/geo/debug.js",
|
||||
"src/geo/developerError.js",
|
||||
"src/time/clock.js"
|
||||
"src/time/clock.js",
|
||||
"src/layers/shared/color.js"
|
||||
],
|
||||
"external": ["cesium", "satellite.js"]
|
||||
},
|
||||
@@ -2608,7 +2609,9 @@
|
||||
"src/layers/vessels/render/cesium.js",
|
||||
"src/layers/localAdsb/render/navara.js",
|
||||
"src/layers/vessels/render/navara.js",
|
||||
"src/layers/shared/render/navaraView.js"
|
||||
"src/layers/shared/render/navaraView.js",
|
||||
"src/celestialRing/render/cesium.js",
|
||||
"src/layers/satellites/render/cesium.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -3188,7 +3191,9 @@
|
||||
"src/layers/military/render/cesium.js",
|
||||
"src/layers/aircraft/qaDrawn.js",
|
||||
"src/layers/localAdsb/render/cesium.js",
|
||||
"src/layers/vessels/render/cesium.js"
|
||||
"src/layers/vessels/render/cesium.js",
|
||||
"src/celestialRing/render/cesium.js",
|
||||
"src/layers/satellites/render/cesium.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -3753,7 +3758,8 @@
|
||||
"src/geo/debug.js",
|
||||
"src/geo/developerError.js",
|
||||
"src/time/clock.js",
|
||||
"src/globe/surfaceDetail.js"
|
||||
"src/globe/surfaceDetail.js",
|
||||
"src/celestialRing/render/cesium.js"
|
||||
],
|
||||
"external": ["cesium", "egm96-universal", "pbf"]
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// military:synthetic[:count] readsb rows, `count` aircraft (1 000); the
|
||||
// first 25 are also synthetic civil flights
|
||||
// vessels:fixture 12 000 AIS rows, or those within an area
|
||||
// satellites:fixture 839 core TLEs + 6000 dense points
|
||||
// local-adsb:fixture the local receivers' answer (server format,
|
||||
// /api/local-receivers/aircraft): 40 aircraft
|
||||
// near JFK, 1090 MHz and 978 MHz UAT
|
||||
@@ -23,12 +24,15 @@ import {
|
||||
syntheticMilitary,
|
||||
syntheticVessels,
|
||||
} from '../../bench/w1Feeds.js';
|
||||
import { satelliteFixture } from '../../bench/satelliteFixture.js';
|
||||
import { createSatelliteSource } from '../../layers/satellites/source.js';
|
||||
import { createLocalReceiverFeeds } from '../../layers/localAdsb/feeds.js';
|
||||
|
||||
const KINDS = Object.freeze({
|
||||
flights: new Set(['synthetic']),
|
||||
military: new Set(['synthetic']),
|
||||
vessels: new Set(['fixture']),
|
||||
satellites: new Set(['fixture']),
|
||||
'local-adsb': new Set(['fixture']),
|
||||
});
|
||||
|
||||
@@ -44,7 +48,7 @@ export function parseQaFeed(search = '') {
|
||||
(count !== null && !(Number.isInteger(count) && count > 0))
|
||||
)
|
||||
throw new TypeError(
|
||||
`qaFeed: unknown entry "${item}" (flights:synthetic[:count], military:synthetic[:count], vessels:fixture, local-adsb:fixture)`,
|
||||
`qaFeed: unknown entry "${item}" (flights:synthetic[:count], military:synthetic[:count], vessels:fixture, satellites:fixture, local-adsb:fixture)`,
|
||||
);
|
||||
return { layer, kind, count };
|
||||
});
|
||||
@@ -60,6 +64,11 @@ const json = (payload) =>
|
||||
export function createQaSources(entries, { now = () => Date.now() } = {}) {
|
||||
const sources = {};
|
||||
for (const { layer, count } of entries) {
|
||||
if (layer === 'satellites')
|
||||
sources.satellites = createSatelliteSource({
|
||||
fetchImpl: async (url) =>
|
||||
new Response(satelliteFixture(String(url).split('/').at(-1))),
|
||||
});
|
||||
if (layer === 'flights')
|
||||
sources.flights = createFlightSource({
|
||||
now,
|
||||
|
||||
@@ -27,6 +27,7 @@ import { APPLICATION_LAYER_DIRECTORY } from './directory.js';
|
||||
import { createFlightsCesiumRenderer } from '../../layers/flights/render/cesium.js';
|
||||
import { createMilitaryCesiumRenderer } from '../../layers/military/render/cesium.js';
|
||||
import { createLocalAdsbCesiumRenderer } from '../../layers/localAdsb/render/cesium.js';
|
||||
import { createSatellitesCesiumRenderer } from '../../layers/satellites/render/cesium.js';
|
||||
import { createVesselsCesiumRenderer } from '../../layers/vessels/render/cesium.js';
|
||||
|
||||
const ENGINE_KINDS = new Set(['cesium', 'navara']);
|
||||
@@ -87,6 +88,7 @@ export const layerAvailableOn = (layerId, engineKind) =>
|
||||
|
||||
/** Cesium renderers extracted from their layers (render/cesium.js). */
|
||||
export const CESIUM_LAYER_RENDERERS = Object.freeze({
|
||||
satellites: createSatellitesCesiumRenderer,
|
||||
flights: createFlightsCesiumRenderer,
|
||||
military: createMilitaryCesiumRenderer,
|
||||
'local-adsb': createLocalAdsbCesiumRenderer,
|
||||
|
||||
@@ -56,8 +56,9 @@ test('the default registry: every layer on Cesium, none yet on Navara', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('the aircraft layers and vessels have extracted Cesium renderers; the others still draw themselves', () => {
|
||||
test('the aircraft, vessels and satellites have extracted Cesium renderers; the others still draw themselves', () => {
|
||||
assert.deepEqual(Object.keys(CESIUM_LAYER_RENDERERS), [
|
||||
'satellites',
|
||||
'flights',
|
||||
'military',
|
||||
'local-adsb',
|
||||
@@ -78,7 +79,7 @@ test('the aircraft layers and vessels have extracted Cesium renderers; the other
|
||||
const military = rendererFor('military', { kind: 'cesium' }, { viewer });
|
||||
assert.equal(military.kind, 'cesium');
|
||||
assert.equal(military.viewer, viewer);
|
||||
assert.equal(rendererFor('satellites', { kind: 'cesium' }), null);
|
||||
assert.equal(rendererFor('satellites', { kind: 'cesium' }).kind, 'cesium');
|
||||
});
|
||||
|
||||
test('the layer availability API: engine, isAvailable, available, unavailable', () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { rendererFor } from './renderers.js';
|
||||
import { createSatellitesCesiumRenderer } from '../../layers/satellites/render/cesium.js';
|
||||
import { createSatellitesLayer } from '../../layers/satellites/index.js';
|
||||
import * as picking from '../../data/pickRegistry.js';
|
||||
import * as focus from '../../data/focusDeemphasis.js';
|
||||
@@ -8,10 +10,19 @@ import * as render from '../../renderGovernor.js';
|
||||
import * as layerState from '../../data/layerState.js';
|
||||
|
||||
/** Construct one layer using the application scene owners and a supplied source. */
|
||||
export function createApplicationSatellites({ source }) {
|
||||
export function createApplicationSatellites({
|
||||
source,
|
||||
renderer = {
|
||||
create: ({ engine = null, ...options }) =>
|
||||
engine
|
||||
? rendererFor('satellites', engine, options)
|
||||
: createSatellitesCesiumRenderer(null, options),
|
||||
},
|
||||
}) {
|
||||
return createSatellitesLayer({
|
||||
source,
|
||||
services: {
|
||||
renderer,
|
||||
picking,
|
||||
focus,
|
||||
readout,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Deterministic TLE catalogs for phase 3 browser QA; no live feed requests.
|
||||
import { W1_FROZEN_MS } from './w1Feeds.js';
|
||||
|
||||
const checksum = (line) => {
|
||||
const body = line.padEnd(68).slice(0, 68);
|
||||
let sum = 0;
|
||||
for (const c of body) sum += /[0-9]/.test(c) ? Number(c) : c === '-' ? 1 : 0;
|
||||
return body + (sum % 10);
|
||||
};
|
||||
const tle = (id, name, index, inclination, motion) => {
|
||||
const number = String(id).padStart(5, '0');
|
||||
const angle = (value) => (value % 360).toFixed(4).padStart(8);
|
||||
return [
|
||||
name,
|
||||
checksum(
|
||||
`1 ${number}U 26001A 26289.50000000 .00000000 00000-0 00000-0 0 999`,
|
||||
),
|
||||
checksum(
|
||||
`2 ${number} ${angle(inclination)} ${angle(index * 137.508)} 0003000 ${angle(0)} ${angle(index * 47.3)} ${motion.toFixed(8).padStart(11)}00001`,
|
||||
),
|
||||
].join('\n');
|
||||
};
|
||||
const GROUPS = {
|
||||
stations: [25544, 1, 51.64, 15.5, 'ISS (ZARYA)'],
|
||||
visual: [30000, 720, 53, 15.2, 'QA VISUAL'],
|
||||
'gps-ops': [40000, 32, 55, 2.0056, 'QA GPS'],
|
||||
'glo-ops': [41000, 24, 64.8, 2.131, 'QA GLONASS'],
|
||||
galileo: [42000, 30, 56, 1.7047, 'QA GALILEO'],
|
||||
geo: [45000, 32, 0.1, 1.0027, 'QA GEO'],
|
||||
starlink: [50000, 6000, 53, 15.2, 'QA STARLINK'],
|
||||
};
|
||||
export const SATELLITE_FIXTURE_EPOCH = W1_FROZEN_MS;
|
||||
export const SATELLITE_FIXTURE_CORE_COUNT = 839;
|
||||
export const SATELLITE_FIXTURE_DENSE_COUNT = 6839;
|
||||
export function satelliteFixture(group) {
|
||||
const spec = GROUPS[group];
|
||||
if (!spec) throw new TypeError('Unknown fixture group');
|
||||
const [first, count, inclination, motion, prefix] = spec;
|
||||
return (
|
||||
Array.from({ length: count }, (_, i) =>
|
||||
tle(
|
||||
first + i,
|
||||
count === 1 ? prefix : `${prefix} ${i + 1}`,
|
||||
i,
|
||||
inclination,
|
||||
motion,
|
||||
),
|
||||
).join('\n') + '\n'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { twoline2satrec, propagate } from 'satellite.js';
|
||||
import {
|
||||
satelliteFixture,
|
||||
SATELLITE_FIXTURE_EPOCH,
|
||||
} from './satelliteFixture.js';
|
||||
|
||||
test('satellite fixture has distinct, valid SGP4 catalogs including LEO, MEO and GEO', () => {
|
||||
const ids = new Set();
|
||||
const counts = {
|
||||
stations: 1,
|
||||
visual: 720,
|
||||
'gps-ops': 32,
|
||||
'glo-ops': 24,
|
||||
galileo: 30,
|
||||
geo: 32,
|
||||
starlink: 6000,
|
||||
};
|
||||
for (const [group, count] of Object.entries(counts)) {
|
||||
const lines = satelliteFixture(group).trim().split('\n');
|
||||
assert.equal(lines.length, count * 3);
|
||||
for (let i = 0; i < lines.length; i += 3) {
|
||||
for (const line of [lines[i + 1], lines[i + 2]]) {
|
||||
assert.equal(line.length, 69);
|
||||
const sum = [...line.slice(0, 68)].reduce(
|
||||
(n, c) => n + (/\d/.test(c) ? Number(c) : c === '-' ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
assert.equal(Number(line[68]), sum % 10);
|
||||
}
|
||||
const rec = twoline2satrec(lines[i + 1], lines[i + 2]);
|
||||
assert.equal(rec.error, 0);
|
||||
assert.ok(!ids.has(rec.satnum));
|
||||
ids.add(rec.satnum);
|
||||
const position = propagate(
|
||||
rec,
|
||||
new Date(SATELLITE_FIXTURE_EPOCH),
|
||||
).position;
|
||||
assert.ok(position && Number.isFinite(position.x));
|
||||
const radius = Math.hypot(position.x, position.y, position.z);
|
||||
if (group === 'geo') assert.ok(radius > 41000 && radius < 43000);
|
||||
if (group === 'gps-ops') assert.ok(radius > 26000 && radius < 27000);
|
||||
}
|
||||
}
|
||||
});
|
||||
+33
-51
@@ -1,4 +1,10 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from './geo/index.js';
|
||||
import { engineFromViewer } from './globe/cesium/engineFromViewer.js';
|
||||
import {
|
||||
createCelestialEphemeris,
|
||||
projectCelestialPoint,
|
||||
flyCelestialViewer,
|
||||
} from './celestialRing/render/cesium.js';
|
||||
import { appClock } from './time/clock.js';
|
||||
import { governorRequestRender } from './renderGovernor.js';
|
||||
|
||||
@@ -32,7 +38,7 @@ export const KEYHOLE_OUTSIDE_OPACITY_DEFAULT = 0.01;
|
||||
const RING_INSET_PX = 11;
|
||||
const MARKER_INSET_PX = 36;
|
||||
const COLLIDING_MARKER_EXTRA_INSET_PX = 32;
|
||||
const MARKER_COLLISION_ANGLE = Cesium.Math.toRadians(6);
|
||||
const MARKER_COLLISION_ANGLE = geo.math.toRadians(6);
|
||||
const FULL_GLOBE_RADIUS_RATIO = 0.61;
|
||||
const TAU = Math.PI * 2;
|
||||
// This overlay is a secondary HUD treatment. Keep it smooth while reserving
|
||||
@@ -212,7 +218,7 @@ export function isFullGlobeInsideKeyhole(geometry, wasVisible = false) {
|
||||
|
||||
/** Return the Earth-disc radius in CSS pixels for a perspective camera. */
|
||||
export function earthDiscScreenRadius(cameraDistance, viewportHeight, fovy) {
|
||||
const earthRadiusM = Cesium.Ellipsoid.WGS84.maximumRadius;
|
||||
const earthRadiusM = geo.ellipsoid.WGS84.maximumRadius;
|
||||
if (
|
||||
!Number.isFinite(cameraDistance) ||
|
||||
cameraDistance <= earthRadiusM ||
|
||||
@@ -250,24 +256,23 @@ export function projectEarthDiscToViewport(
|
||||
const scene = viewer?.scene;
|
||||
if (!camera || !scene || !(width > 0) || !(height > 0)) return null;
|
||||
|
||||
const distance = Cesium.Cartesian3.magnitude(camera.positionWC);
|
||||
const distance = geo.cartesian3.magnitude(camera.positionWC);
|
||||
if (
|
||||
!Number.isFinite(distance) ||
|
||||
distance <= Cesium.Ellipsoid.WGS84.maximumRadius
|
||||
distance <= geo.ellipsoid.WGS84.maximumRadius
|
||||
)
|
||||
return null;
|
||||
|
||||
const toCenter = Cesium.Cartesian3.negate(
|
||||
const toCenter = geo.cartesian3.negate(
|
||||
camera.positionWC,
|
||||
scratchToCenter || new Cesium.Cartesian3(),
|
||||
scratchToCenter || geo.cartesian3.create(),
|
||||
);
|
||||
if (Cesium.Cartesian3.dot(camera.directionWC, toCenter) <= 0) return null;
|
||||
if (geo.cartesian3.dot(camera.directionWC, toCenter) <= 0) return null;
|
||||
|
||||
const center = Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||
scene,
|
||||
Cesium.Cartesian3.ZERO,
|
||||
scratchCenter,
|
||||
);
|
||||
const projector = engineFromViewer(viewer)?.projector;
|
||||
const center = projector
|
||||
? projector.worldToWindow(geo.cartesian3.ZERO, scratchCenter)
|
||||
: projectCelestialPoint(scene, geo.cartesian3.ZERO, scratchCenter);
|
||||
if (!center || !Number.isFinite(center.x) || !Number.isFinite(center.y))
|
||||
return null;
|
||||
|
||||
@@ -408,13 +413,11 @@ export class CelestialRing {
|
||||
this._moonRenderKey = '';
|
||||
this._outlineRenderKey = '';
|
||||
|
||||
this._sunInertial = new Cesium.Cartesian3();
|
||||
this._moonInertial = new Cesium.Cartesian3();
|
||||
this._sunFixed = new Cesium.Cartesian3();
|
||||
this._moonFixed = new Cesium.Cartesian3();
|
||||
this._toCenter = new Cesium.Cartesian3();
|
||||
this._screenCenter = new Cesium.Cartesian2();
|
||||
this._fixedMatrix = new Cesium.Matrix3();
|
||||
this._sunFixed = geo.cartesian3.create();
|
||||
this._moonFixed = geo.cartesian3.create();
|
||||
this._toCenter = geo.cartesian3.create();
|
||||
this._screenCenter = geo.cartesian2.create();
|
||||
this._ephemeris = createCelestialEphemeris();
|
||||
|
||||
this._buildDOM();
|
||||
this._removePostRender = viewer.scene.postRender.addEventListener(() =>
|
||||
@@ -531,7 +534,7 @@ export class CelestialRing {
|
||||
return false;
|
||||
}
|
||||
|
||||
const earthRadius = Cesium.Ellipsoid.WGS84.maximumRadius;
|
||||
const earthRadius = geo.ellipsoid.WGS84.maximumRadius;
|
||||
const keyholeRadius = getKeyholeGeometry(
|
||||
canvas.clientWidth || canvas.width,
|
||||
height,
|
||||
@@ -548,19 +551,18 @@ export class CelestialRing {
|
||||
this._focusInProgress = false;
|
||||
this.viewer.scene.requestRender?.();
|
||||
};
|
||||
this.viewer.camera.flyTo({
|
||||
destination: Cesium.Cartesian3.fromRadians(
|
||||
flyCelestialViewer(this.viewer, {
|
||||
destination: geo.cartesian3.fromRadians(
|
||||
cartographic.longitude,
|
||||
cartographic.latitude,
|
||||
altitude,
|
||||
),
|
||||
orientation: {
|
||||
heading: this.viewer.camera.heading,
|
||||
pitch: -Cesium.Math.PI_OVER_TWO,
|
||||
pitch: -geo.math.PI_OVER_TWO,
|
||||
roll: 0,
|
||||
},
|
||||
duration,
|
||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||
complete: finishFocus,
|
||||
cancel: () => {
|
||||
finishFocus();
|
||||
@@ -622,27 +624,7 @@ export class CelestialRing {
|
||||
_updateEphemeris(time) {
|
||||
if (!this._ephemerisDirty) return true;
|
||||
|
||||
Cesium.Simon1994PlanetaryPositions.computeSunPositionInEarthInertialFrame(
|
||||
time,
|
||||
this._sunInertial,
|
||||
);
|
||||
Cesium.Simon1994PlanetaryPositions.computeMoonPositionInEarthInertialFrame(
|
||||
time,
|
||||
this._moonInertial,
|
||||
);
|
||||
const matrix =
|
||||
Cesium.Transforms.computeIcrfToFixedMatrix(time, this._fixedMatrix) ||
|
||||
Cesium.Transforms.computeTemeToPseudoFixedMatrix(time, this._fixedMatrix);
|
||||
if (!matrix) return false;
|
||||
|
||||
Cesium.Matrix3.multiplyByVector(matrix, this._sunInertial, this._sunFixed);
|
||||
Cesium.Matrix3.multiplyByVector(
|
||||
matrix,
|
||||
this._moonInertial,
|
||||
this._moonFixed,
|
||||
);
|
||||
Cesium.Cartesian3.normalize(this._sunFixed, this._sunFixed);
|
||||
Cesium.Cartesian3.normalize(this._moonFixed, this._moonFixed);
|
||||
if (!this._ephemeris(time, this._sunFixed, this._moonFixed)) return false;
|
||||
this._ephemerisDirty = false;
|
||||
this._ephemerisUpdateCount += 1;
|
||||
return true;
|
||||
@@ -755,18 +737,18 @@ export class CelestialRing {
|
||||
|
||||
// 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()));
|
||||
const time = appClock.now();
|
||||
if (!this._updateEphemeris(time)) return;
|
||||
this._root.dataset.ephemerisUpdates = String(this._ephemerisUpdateCount);
|
||||
const camera = this.viewer.camera;
|
||||
const sunProjection = celestialScreenAngle(
|
||||
Cesium.Cartesian3.dot(this._sunFixed, camera.rightWC),
|
||||
Cesium.Cartesian3.dot(this._sunFixed, camera.upWC),
|
||||
geo.cartesian3.dot(this._sunFixed, camera.rightWC),
|
||||
geo.cartesian3.dot(this._sunFixed, camera.upWC),
|
||||
this._sunAngle,
|
||||
);
|
||||
const moonProjection = celestialScreenAngle(
|
||||
Cesium.Cartesian3.dot(this._moonFixed, camera.rightWC),
|
||||
Cesium.Cartesian3.dot(this._moonFixed, camera.upWC),
|
||||
geo.cartesian3.dot(this._moonFixed, camera.rightWC),
|
||||
geo.cartesian3.dot(this._moonFixed, camera.upWC),
|
||||
this._moonAngle,
|
||||
);
|
||||
if (sunProjection.stable) this._sunAngle = sunProjection.angle;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Exact legacy planetary model and bare-viewer fallback for CelestialRing.
|
||||
import * as Cesium from 'cesium';
|
||||
export function createCelestialEphemeris() {
|
||||
const sunInertial = new Cesium.Cartesian3();
|
||||
const moonInertial = new Cesium.Cartesian3();
|
||||
const fixedMatrix = new Cesium.Matrix3();
|
||||
return (epochMs, sunFixed, moonFixed) => {
|
||||
const time = Cesium.JulianDate.fromDate(new Date(epochMs));
|
||||
Cesium.Simon1994PlanetaryPositions.computeSunPositionInEarthInertialFrame(
|
||||
time,
|
||||
sunInertial,
|
||||
);
|
||||
Cesium.Simon1994PlanetaryPositions.computeMoonPositionInEarthInertialFrame(
|
||||
time,
|
||||
moonInertial,
|
||||
);
|
||||
const matrix =
|
||||
Cesium.Transforms.computeIcrfToFixedMatrix(time, fixedMatrix) ||
|
||||
Cesium.Transforms.computeTemeToPseudoFixedMatrix(time, fixedMatrix);
|
||||
if (!matrix) return false;
|
||||
Cesium.Matrix3.multiplyByVector(matrix, sunInertial, sunFixed);
|
||||
Cesium.Matrix3.multiplyByVector(matrix, moonInertial, moonFixed);
|
||||
Cesium.Cartesian3.normalize(sunFixed, sunFixed);
|
||||
Cesium.Cartesian3.normalize(moonFixed, moonFixed);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
export const projectCelestialPoint = (scene, position, result) =>
|
||||
Cesium.SceneTransforms.worldToWindowCoordinates(scene, position, result);
|
||||
export const flyCelestialViewer = (viewer, options) =>
|
||||
viewer.camera.flyTo({
|
||||
...options,
|
||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||
});
|
||||
@@ -127,7 +127,7 @@ test('every scene click handler consults ownership before it picks', () => {
|
||||
['src/layers/launches/lifecycle.js', 'drillPick(movement.position'],
|
||||
['src/layers/radio/interaction.js', 'pickedRadioStationAt(click.position)'],
|
||||
[
|
||||
'src/layers/satellites/interaction.js',
|
||||
'src/layers/satellites/render/cesium.js',
|
||||
'viewer.scene.pick(click.position)',
|
||||
],
|
||||
[
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import { nearFarScale } from '../shared/color.js';
|
||||
import { twoline2satrec } from 'satellite.js';
|
||||
import {
|
||||
DENSE_REFRESH_FRAMES,
|
||||
@@ -31,7 +33,7 @@ export function createCatalog({ state: layerState, services, parts, source }) {
|
||||
1,
|
||||
Math.ceil(layerState._denseIds.length / DENSE_REFRESH_FRAMES),
|
||||
);
|
||||
const now = new Date();
|
||||
const now = new Date(appClock.now());
|
||||
for (let i = 0; i < perFrame; i++) {
|
||||
if (layerState._denseCursor >= layerState._denseIds.length)
|
||||
layerState._denseCursor = 0;
|
||||
@@ -42,7 +44,7 @@ export function createCatalog({ state: layerState, services, parts, source }) {
|
||||
if (!sat || !point) continue;
|
||||
const pos = parts.orbits.propagatePosition(sat.satrec, now);
|
||||
if (pos) {
|
||||
point.position = Cesium.Cartesian3.fromDegrees(
|
||||
point.position = geo.cartesian3.fromDegrees(
|
||||
pos.longitude,
|
||||
pos.latitude,
|
||||
pos.altitude,
|
||||
@@ -96,7 +98,7 @@ export function createCatalog({ state: layerState, services, parts, source }) {
|
||||
|
||||
const entries = parts.orbits.parseTLE(text);
|
||||
const style = POINT_STYLES.dense;
|
||||
const now = new Date();
|
||||
const now = new Date(appClock.now());
|
||||
let added = 0;
|
||||
|
||||
for (let start = 0; start < entries.length; start += DENSE_CREATE_CHUNK) {
|
||||
@@ -124,7 +126,7 @@ export function createCatalog({ state: layerState, services, parts, source }) {
|
||||
group: 'dense',
|
||||
});
|
||||
const point = layerState._pointCollection.add({
|
||||
position: Cesium.Cartesian3.fromDegrees(
|
||||
position: geo.cartesian3.fromDegrees(
|
||||
pos.longitude,
|
||||
pos.latitude,
|
||||
pos.altitude,
|
||||
@@ -133,7 +135,7 @@ export function createCatalog({ state: layerState, services, parts, source }) {
|
||||
color: style.color,
|
||||
outlineColor: style.outlineColor,
|
||||
outlineWidth: style.outlineWidth,
|
||||
scaleByDistance: new Cesium.NearFarScalar(1e6, 1.5, 2e7, 0.6),
|
||||
scaleByDistance: nearFarScale(1e6, 1.5, 2e7, 0.6),
|
||||
id: noradId,
|
||||
});
|
||||
layerState._points.set(noradId, point);
|
||||
|
||||
Vendored
+5
-1
@@ -1,3 +1,4 @@
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import {
|
||||
tallySatelliteClasses,
|
||||
satelliteClassLabel,
|
||||
@@ -189,7 +190,10 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
if (noradId === null) return null;
|
||||
|
||||
const sat = layerState._catalog.get(noradId);
|
||||
const pos = parts.orbits.propagatePosition(sat.satrec, new Date());
|
||||
const pos = parts.orbits.propagatePosition(
|
||||
sat.satrec,
|
||||
new Date(appClock.now()),
|
||||
);
|
||||
if (!pos) return null;
|
||||
|
||||
return {
|
||||
|
||||
@@ -17,7 +17,15 @@ export function createSatellitesLayer({ services, source }) {
|
||||
throw new TypeError('A satellites source is required');
|
||||
const state = createState({ services });
|
||||
const parts = {};
|
||||
const context = { state, services, parts, source };
|
||||
const getRenderer = () => {
|
||||
if (!state._renderer || state._renderer.view !== state._viewer)
|
||||
state._renderer = services.renderer.create({
|
||||
viewer: state._viewer,
|
||||
engine: state._engine,
|
||||
});
|
||||
return state._renderer;
|
||||
};
|
||||
const context = { state, services, parts, source, getRenderer };
|
||||
parts.controls = createControls(context);
|
||||
parts.catalog = createCatalog(context);
|
||||
parts.labels = createLabels(context);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { twoline2satrec } from 'satellite.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import { nearFarScale } from '../shared/color.js';
|
||||
import { CATALOG_GROUPS, ISS_NORAD, POINT_STYLES } from './policy.js';
|
||||
|
||||
export function createIngestion({
|
||||
@@ -7,6 +9,7 @@ export function createIngestion({
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
const methods = {
|
||||
async update(viewer, { signal = null } = {}) {
|
||||
@@ -78,7 +81,7 @@ export function createIngestion({
|
||||
layerState._pointCollection.removeAll();
|
||||
layerState._points.clear();
|
||||
for (const path of layerState._orbitPaths.values())
|
||||
viewer.scene.primitives.remove(path.primitive);
|
||||
getRenderer().removeOrbit(path.primitive);
|
||||
layerState._orbitPaths.clear();
|
||||
layerState._catalog.clear();
|
||||
// The detection overlay caches one record per satellite and stamps its
|
||||
@@ -93,7 +96,7 @@ export function createIngestion({
|
||||
layerState._denseLoadToken++; // cancel any in-flight dense load against the old catalog
|
||||
parts.labels._syncIssOverlay();
|
||||
|
||||
const now = new Date();
|
||||
const now = new Date(appClock.now());
|
||||
|
||||
// Process all TLE entries in CATALOG_GROUPS order
|
||||
const allEntries = [];
|
||||
@@ -123,7 +126,7 @@ export function createIngestion({
|
||||
const pos = parts.orbits.propagatePosition(satrec, now);
|
||||
if (!pos) continue;
|
||||
|
||||
const cartesian = Cesium.Cartesian3.fromDegrees(
|
||||
const cartesian = geo.cartesian3.fromDegrees(
|
||||
pos.longitude,
|
||||
pos.latitude,
|
||||
pos.altitude,
|
||||
@@ -137,7 +140,7 @@ export function createIngestion({
|
||||
color: style.color,
|
||||
outlineColor: style.outlineColor,
|
||||
outlineWidth: style.outlineWidth,
|
||||
scaleByDistance: new Cesium.NearFarScalar(1e6, 1.5, 2e7, 0.6),
|
||||
scaleByDistance: nearFarScale(1e6, 1.5, 2e7, 0.6),
|
||||
id: noradId,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { isPointerFree } from '../../data/inputOwnership.js';
|
||||
|
||||
export function createInteraction({
|
||||
@@ -6,6 +5,7 @@ export function createInteraction({
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
const { resolvePickId, isOwnedByOtherLayer } = services.picking;
|
||||
|
||||
@@ -25,63 +25,64 @@ export function createInteraction({
|
||||
// new owner controls it). Guarded so our OWN switch (viewer.trackedEntity
|
||||
// briefly undefined mid-_trackSatellite) doesn't self-clear.
|
||||
if (!layerState._trackedEntityChangedRemove) {
|
||||
layerState._trackedEntityChangedRemove =
|
||||
viewer.trackedEntityChanged.addEventListener(() => {
|
||||
layerState._trackedEntityChangedRemove = getRenderer().follow.onChanged(
|
||||
() => {
|
||||
if (!layerState._enabled) return;
|
||||
if (
|
||||
layerState._trackedNorad &&
|
||||
layerState._viewer &&
|
||||
layerState._viewer.trackedEntity &&
|
||||
layerState._viewer.trackedEntity !== layerState._trackedEntity
|
||||
getRenderer().follow.current() &&
|
||||
getRenderer().follow.current() !== layerState._trackedEntity
|
||||
) {
|
||||
parts.tracking._clearTracking(true, {
|
||||
origin:
|
||||
layerState._viewer.trackedEntity?.gevSelectionOrigin ||
|
||||
getRenderer().follow.current()?.gevSelectionOrigin ||
|
||||
'programmatic',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
layerState._clickHandler = new Cesium.ScreenSpaceEventHandler(
|
||||
viewer.scene.canvas,
|
||||
);
|
||||
layerState._clickHandler.setInputAction((click) => {
|
||||
// A tool owns the pointer (src/data/inputOwnership.js): yield the click.
|
||||
if (!isPointerFree()) return;
|
||||
if (!layerState._enabled) return;
|
||||
const picked = viewer.scene.pick(click.position);
|
||||
layerState._clickHandler = getRenderer().listenClick(
|
||||
({ picked }) => {
|
||||
// A tool owns the pointer (src/data/inputOwnership.js): yield the click.
|
||||
if (!isPointerFree()) return;
|
||||
if (!layerState._enabled) return;
|
||||
|
||||
if (picked) {
|
||||
// Clicking tracked entity itself — ignore
|
||||
if (picked.id === layerState._trackedEntity) return;
|
||||
|
||||
// Check if it's a satellite point (id is NORAD catalog number)
|
||||
const prim = picked.primitive;
|
||||
if (prim && prim.id != null) {
|
||||
const noradId = Number(prim.id);
|
||||
if (!isNaN(noradId) && layerState._catalog.has(noradId)) {
|
||||
parts.tracking._cancelPendingTrackingRestore();
|
||||
parts.tracking._trackSatellite(noradId, { origin: 'user' });
|
||||
if (picked) {
|
||||
// Clicking tracked entity itself — ignore
|
||||
if (getRenderer().isTrackedPick(picked, layerState._trackedEntity))
|
||||
return;
|
||||
|
||||
// Check if it's a satellite point (id is NORAD catalog number)
|
||||
const prim = picked.primitive;
|
||||
if (prim && prim.id != null) {
|
||||
const noradId = getRenderer().pickedNorad(picked);
|
||||
if (!isNaN(noradId) && layerState._catalog.has(noradId)) {
|
||||
parts.tracking._cancelPendingTrackingRestore();
|
||||
parts.tracking._trackSatellite(noradId, { origin: 'user' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A pick that belongs to a sibling layer (plane, vessel, station, CCTV
|
||||
// camera…) is not "empty space" — leave OUR tracking (and crucially
|
||||
// viewer.trackedEntity, which that sibling may have JUST set) alone (H2).
|
||||
if (picked) {
|
||||
const pickedId = resolvePickId(picked);
|
||||
if (pickedId && isOwnedByOtherLayer('satellites', pickedId)) return;
|
||||
}
|
||||
// A pick that belongs to a sibling layer (plane, vessel, station, CCTV
|
||||
// camera…) is not "empty space" — leave OUR tracking (and crucially
|
||||
// viewer.trackedEntity, which that sibling may have JUST set) alone (H2).
|
||||
if (picked) {
|
||||
const pickedId = resolvePickId(picked);
|
||||
if (pickedId && isOwnedByOtherLayer('satellites', pickedId)) return;
|
||||
}
|
||||
|
||||
// Clicked empty space — deselect
|
||||
if (layerState._trackedNorad) {
|
||||
parts.tracking._cancelPendingTrackingRestore();
|
||||
parts.tracking._clearTracking(false, { origin: 'user' });
|
||||
}
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
// Clicked empty space — deselect
|
||||
if (layerState._trackedNorad) {
|
||||
parts.tracking._cancelPendingTrackingRestore();
|
||||
parts.tracking._clearTracking(false, { origin: 'user' });
|
||||
}
|
||||
},
|
||||
{ enabled: () => layerState._enabled },
|
||||
);
|
||||
|
||||
document.addEventListener('keydown', _onKeyDown);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { ISS_OVERLAY_SOURCE_ID } from './policy.js';
|
||||
|
||||
export function createLifecycle({
|
||||
@@ -6,6 +5,7 @@ export function createLifecycle({
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
const { clearFocusTarget } = services.focus;
|
||||
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
||||
@@ -14,10 +14,13 @@ export function createLifecycle({
|
||||
const methods = {
|
||||
// Catalog data refresh; propagation remains preRender-owned.
|
||||
|
||||
async init(viewer) {
|
||||
async init(viewer, { engine = null } = {}) {
|
||||
parts.catalog._abortActiveUpdates();
|
||||
clearFocusTarget('satellites');
|
||||
layerState._viewer = viewer;
|
||||
layerState._engine = engine;
|
||||
layerState._renderer?.destroy();
|
||||
layerState._renderer = services.renderer.create({ viewer, engine });
|
||||
layerState._viewer = layerState._renderer.view;
|
||||
layerState._catalog = new Map();
|
||||
layerState._points = new Map();
|
||||
layerState._detectionObjects = new Map();
|
||||
@@ -46,14 +49,13 @@ export function createLifecycle({
|
||||
layerState._catalogRevision++;
|
||||
|
||||
// Point primitives for satellite dots
|
||||
layerState._pointCollection = new Cesium.PointPrimitiveCollection();
|
||||
viewer.scene.primitives.add(layerState._pointCollection);
|
||||
layerState._pointCollection = getRenderer().createPoints();
|
||||
|
||||
parts.interaction._installClickHandler(viewer);
|
||||
|
||||
// Pre-render listener for real-time position updates
|
||||
// (fleet propagation + tracked per-frame dot + orbit ring GMST rotation)
|
||||
layerState._preRenderListener = viewer.scene.preRender.addEventListener(
|
||||
layerState._preRenderListener = getRenderer().onFrame(
|
||||
parts.rendering._preRenderTick,
|
||||
);
|
||||
|
||||
@@ -84,8 +86,8 @@ export function createLifecycle({
|
||||
const norad = Number(pickedId);
|
||||
return Number.isFinite(norad) && layerState._points.has(norad);
|
||||
});
|
||||
if (!layerState._preRenderListener && viewer) {
|
||||
layerState._preRenderListener = viewer.scene.preRender.addEventListener(
|
||||
if (!layerState._preRenderListener && layerState._viewer) {
|
||||
layerState._preRenderListener = getRenderer().onFrame(
|
||||
parts.rendering._preRenderTick,
|
||||
);
|
||||
}
|
||||
@@ -142,12 +144,12 @@ export function createLifecycle({
|
||||
layerState._overlayHost.clearSource(ISS_OVERLAY_SOURCE_ID);
|
||||
layerState._overlayHost.setVisible(ISS_OVERLAY_SOURCE_ID, false);
|
||||
if (layerState._pointCollection) {
|
||||
viewer.scene.primitives.remove(layerState._pointCollection);
|
||||
getRenderer().removePoints(layerState._pointCollection);
|
||||
layerState._pointCollection = null;
|
||||
}
|
||||
// Orbit ring primitives are removed (and destroyed) here
|
||||
for (const path of layerState._orbitPaths.values()) {
|
||||
viewer.scene.primitives.remove(path.primitive);
|
||||
getRenderer().removeOrbit(path.primitive);
|
||||
}
|
||||
layerState._points.clear();
|
||||
layerState._detectionObjects.clear();
|
||||
@@ -171,6 +173,9 @@ export function createLifecycle({
|
||||
status: 'destroyed',
|
||||
failedGroups: [],
|
||||
};
|
||||
layerState._renderer?.destroy();
|
||||
layerState._renderer = null;
|
||||
layerState._engine = null;
|
||||
layerState._viewer = null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import {
|
||||
gstime,
|
||||
@@ -133,7 +134,7 @@ export function createOrbits({ state: layerState, services, parts, source }) {
|
||||
lonDeg,
|
||||
minElevDeg = 10,
|
||||
requireVisible = false,
|
||||
fromMs = Date.now(),
|
||||
fromMs = appClock.now(),
|
||||
}) {
|
||||
return getNextSatellitePass(ISS_NORAD, {
|
||||
latDeg,
|
||||
@@ -163,7 +164,7 @@ export function createOrbits({ state: layerState, services, parts, source }) {
|
||||
lonDeg,
|
||||
minElevDeg = 10,
|
||||
requireVisible = false,
|
||||
fromMs = Date.now(),
|
||||
fromMs = appClock.now(),
|
||||
},
|
||||
) {
|
||||
const id = Number(noradId);
|
||||
@@ -280,7 +281,7 @@ export function createOrbits({ state: layerState, services, parts, source }) {
|
||||
}
|
||||
|
||||
function orbitTrackFromRecord(name, satrec) {
|
||||
const referenceDate = new Date();
|
||||
const referenceDate = new Date(appClock.now());
|
||||
const current = propagatePosition(satrec, referenceDate);
|
||||
if (!current) return null;
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { WHITE, rgbaFromCss } from '../shared/color.js';
|
||||
import { satelliteClassColor } from '../../data/satelliteClass.js';
|
||||
|
||||
/**
|
||||
@@ -71,7 +72,7 @@ export const DENSE_CREATE_CHUNK = 1500;
|
||||
* High orbits (MEO nav / GEO belt) scale this up so the ring stays in frame.
|
||||
*/
|
||||
|
||||
export const TRACK_VIEW_FROM_LEO = new Cesium.Cartesian3(
|
||||
export const TRACK_VIEW_FROM_LEO = geo.cartesian3.create(
|
||||
-450000,
|
||||
-450000,
|
||||
350000,
|
||||
@@ -94,10 +95,9 @@ export const TRACK_VIEW_FROM_HIGH_SCALE = 4;
|
||||
* attribute, so classification costs nothing per frame.
|
||||
*/
|
||||
|
||||
export const POINT_OUTLINE = Cesium.Color.WHITE.withAlpha(0.3);
|
||||
export const POINT_OUTLINE = WHITE.withAlpha(0.3);
|
||||
|
||||
export const _classColor = (group) =>
|
||||
Cesium.Color.fromCssColorString(satelliteClassColor(group));
|
||||
export const _classColor = (group) => rgbaFromCss(satelliteClassColor(group));
|
||||
|
||||
export const POINT_STYLES = {
|
||||
// The ISS keeps its own long-standing red hero styling rather than the
|
||||
@@ -106,7 +106,7 @@ export const POINT_STYLES = {
|
||||
// Its card still reads "STATION · ISS", so the class stays legible.
|
||||
iss: {
|
||||
pixelSize: 12,
|
||||
color: Cesium.Color.fromCssColorString('#ff4444'),
|
||||
color: rgbaFromCss('#ff4444'),
|
||||
outlineColor: POINT_OUTLINE,
|
||||
outlineWidth: 2,
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { satelliteClassLabel } from '../../data/satelliteClass.js';
|
||||
import { ISS_NORAD } from './policy.js';
|
||||
@@ -77,7 +78,7 @@ export function createRecords({ state, parts }) {
|
||||
const limit = Number.isFinite(maxCount)
|
||||
? Math.max(1, Math.floor(maxCount))
|
||||
: 2000;
|
||||
const now = new Date();
|
||||
const now = new Date(appClock.now());
|
||||
const result = [];
|
||||
const emit = (noradId, sat) => {
|
||||
if (result.length >= limit) return false;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Satellite drawing and native follow, extracted unchanged from the layer.
|
||||
import * as Cesium from 'cesium';
|
||||
import { isPointerFree } from '../../../data/inputOwnership.js';
|
||||
|
||||
export function createSatellitesCesiumRenderer(engine, { viewer } = {}) {
|
||||
return {
|
||||
kind: 'cesium',
|
||||
engine,
|
||||
view: viewer,
|
||||
createPoints() {
|
||||
const points = new Cesium.PointPrimitiveCollection();
|
||||
viewer.scene.primitives.add(points);
|
||||
return points;
|
||||
},
|
||||
removePoints(points) {
|
||||
viewer?.scene.primitives.remove(points);
|
||||
},
|
||||
onFrame: (listener) => viewer?.scene.preRender.addEventListener(listener),
|
||||
project: (position, result) =>
|
||||
Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||
viewer.scene,
|
||||
position,
|
||||
result,
|
||||
),
|
||||
createOrbit({ positions, width, color }) {
|
||||
const primitive = new Cesium.Primitive({
|
||||
geometryInstances: new Cesium.GeometryInstance({
|
||||
geometry: new Cesium.PolylineGeometry({
|
||||
positions,
|
||||
width,
|
||||
vertexFormat: Cesium.PolylineColorAppearance.VERTEX_FORMAT,
|
||||
}),
|
||||
attributes: {
|
||||
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
||||
color.withAlpha(0.6),
|
||||
),
|
||||
depthFailColor: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
||||
color.withAlpha(0.35),
|
||||
),
|
||||
},
|
||||
}),
|
||||
appearance: new Cesium.PolylineColorAppearance({ translucent: true }),
|
||||
depthFailAppearance: new Cesium.PolylineColorAppearance({
|
||||
translucent: true,
|
||||
}),
|
||||
asynchronous: false,
|
||||
allowPicking: false,
|
||||
});
|
||||
viewer.scene.primitives.add(primitive);
|
||||
return primitive;
|
||||
},
|
||||
commitOrbit() {},
|
||||
removeOrbit: (primitive) => viewer?.scene.primitives.remove(primitive),
|
||||
createTracked({ position, viewFrom }) {
|
||||
return viewer.entities.add({
|
||||
position: new Cesium.CallbackProperty(position, false),
|
||||
viewFrom,
|
||||
point: {
|
||||
pixelSize: 14,
|
||||
color: Cesium.Color.YELLOW,
|
||||
outlineColor: Cesium.Color.WHITE,
|
||||
outlineWidth: 2,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
});
|
||||
},
|
||||
removeTracked: (entity) => viewer?.entities.remove(entity),
|
||||
follow: {
|
||||
start: (entity) => {
|
||||
viewer.trackedEntity = entity;
|
||||
},
|
||||
release: () => {
|
||||
if (viewer) viewer.trackedEntity = undefined;
|
||||
},
|
||||
current: () => viewer?.trackedEntity,
|
||||
onChanged: (listener) =>
|
||||
viewer.trackedEntityChanged.addEventListener(listener),
|
||||
},
|
||||
listenClick(listener, { enabled = () => true } = {}) {
|
||||
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
||||
handler.setInputAction((click) => {
|
||||
if (!isPointerFree()) return;
|
||||
if (!enabled()) return;
|
||||
listener({
|
||||
picked: viewer.scene.pick(click.position),
|
||||
position: click.position,
|
||||
});
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
return { destroy: () => handler.destroy() };
|
||||
},
|
||||
pickedNorad: (picked) => Number(picked?.primitive?.id),
|
||||
isTrackedPick: (picked, entity) => Boolean(entity && picked?.id === entity),
|
||||
destroy() {},
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import { CYAN } from '../shared/color.js';
|
||||
import { gstime } from 'satellite.js';
|
||||
import { ISS_NORAD, POSITION_UPDATE_MS, RING_ROTATION_MS } from './policy.js';
|
||||
|
||||
@@ -7,6 +9,7 @@ export function createRendering({
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
const {
|
||||
focusNowMs,
|
||||
@@ -42,36 +45,17 @@ export function createRendering({
|
||||
const sat = layerState._catalog.get(noradId);
|
||||
if (!sat || !layerState._viewer) return;
|
||||
|
||||
const bakeDate = new Date();
|
||||
const bakeDate = new Date(appClock.now());
|
||||
const basePositions = parts.orbits.computeOrbitPath(sat.satrec, bakeDate);
|
||||
if (basePositions.length < 2) return;
|
||||
|
||||
const pathColor = color || Cesium.Color.CYAN;
|
||||
const pathColor = color || CYAN;
|
||||
|
||||
const primitive = new Cesium.Primitive({
|
||||
geometryInstances: new Cesium.GeometryInstance({
|
||||
geometry: new Cesium.PolylineGeometry({
|
||||
positions: basePositions,
|
||||
width: noradId === ISS_NORAD ? 2.5 : 2.0,
|
||||
vertexFormat: Cesium.PolylineColorAppearance.VERTEX_FORMAT,
|
||||
}),
|
||||
attributes: {
|
||||
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
||||
pathColor.withAlpha(0.6),
|
||||
),
|
||||
depthFailColor: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
||||
pathColor.withAlpha(0.35),
|
||||
),
|
||||
},
|
||||
}),
|
||||
appearance: new Cesium.PolylineColorAppearance({ translucent: true }),
|
||||
depthFailAppearance: new Cesium.PolylineColorAppearance({
|
||||
translucent: true,
|
||||
}),
|
||||
asynchronous: false, // build this frame — no async-rebuild blink window
|
||||
allowPicking: false, // ring clicks fall through to satellites/deselect
|
||||
const primitive = getRenderer().createOrbit({
|
||||
positions: basePositions,
|
||||
width: noradId === ISS_NORAD ? 2.5 : 2.0,
|
||||
color: pathColor,
|
||||
});
|
||||
layerState._viewer.scene.primitives.add(primitive);
|
||||
|
||||
layerState._orbitPaths.set(noradId, {
|
||||
primitive,
|
||||
@@ -120,6 +104,7 @@ export function createRendering({
|
||||
nowDate,
|
||||
path.primitive.modelMatrix,
|
||||
);
|
||||
getRenderer().commitOrbit(path.primitive);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,8 +115,7 @@ export function createRendering({
|
||||
function _hideOrbitPath(noradId) {
|
||||
const path = layerState._orbitPaths.get(noradId);
|
||||
if (path) {
|
||||
if (layerState._viewer)
|
||||
layerState._viewer.scene.primitives.remove(path.primitive); // remove() destroys
|
||||
if (layerState._viewer) getRenderer().removeOrbit(path.primitive); // remove() destroys
|
||||
layerState._orbitPaths.delete(noradId);
|
||||
}
|
||||
}
|
||||
@@ -143,7 +127,7 @@ export function createRendering({
|
||||
*/
|
||||
|
||||
function _propagateAll() {
|
||||
const now = new Date();
|
||||
const now = new Date(appClock.now());
|
||||
let updated = 0;
|
||||
|
||||
for (const [noradId, sat] of layerState._catalog) {
|
||||
@@ -151,7 +135,7 @@ export function createRendering({
|
||||
const pos = parts.orbits.propagatePosition(sat.satrec, now);
|
||||
if (!pos) continue;
|
||||
|
||||
const cartesian = Cesium.Cartesian3.fromDegrees(
|
||||
const cartesian = geo.cartesian3.fromDegrees(
|
||||
pos.longitude,
|
||||
pos.latitude,
|
||||
pos.altitude,
|
||||
@@ -211,7 +195,7 @@ export function createRendering({
|
||||
layerState._params.showOrbits &&
|
||||
now - layerState._lastRingRotation >= RING_ROTATION_MS
|
||||
) {
|
||||
_updateOrbitPathRotations(new Date(now));
|
||||
_updateOrbitPathRotations(new Date(appClock.now()));
|
||||
layerState._lastRingRotation = now;
|
||||
}
|
||||
}
|
||||
@@ -236,13 +220,9 @@ export function createRendering({
|
||||
previousActiveCount: layerState._activeFocusCount,
|
||||
nowMs,
|
||||
screenPositionFor: (position) =>
|
||||
Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||
scene,
|
||||
position,
|
||||
layerState._scratchFocusScreen,
|
||||
),
|
||||
getRenderer().project(position, layerState._scratchFocusScreen),
|
||||
cameraDistanceFor: (position) =>
|
||||
Cesium.Cartesian3.distance(camera.positionWC, position),
|
||||
geo.cartesian3.distance(camera.positionWC, position),
|
||||
baseColorFor: (noradId) =>
|
||||
parts.controls._pointStyleFor(
|
||||
noradId,
|
||||
|
||||
@@ -45,6 +45,8 @@ export function createState({ services }) {
|
||||
state._denseLoadController = null;
|
||||
|
||||
state._viewer = null;
|
||||
state._renderer = null;
|
||||
state._engine = null;
|
||||
|
||||
state._preRenderListener = null;
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { ISS_NORAD, ISS_OVERLAY_SOURCE_ID } from './policy.js';
|
||||
|
||||
export function createTesting({ state: layerState, services, parts, source }) {
|
||||
export function createTesting({
|
||||
state: layerState,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
/**
|
||||
* Seed a single tracked satellite so tests can invoke the exact production
|
||||
* pre-render callback without constructing a WebGL viewer.
|
||||
@@ -27,6 +33,8 @@ export function createTesting({ state: layerState, services, parts, source }) {
|
||||
// control satellite that must NOT be treated as part of it).
|
||||
neighbours = [],
|
||||
}) {
|
||||
layerState._renderer = null;
|
||||
layerState._engine = null;
|
||||
layerState._viewer = viewer;
|
||||
layerState._catalog = new Map([
|
||||
[noradId, { name, satrec, group: 'stations' }],
|
||||
@@ -168,6 +176,8 @@ export function createTesting({ state: layerState, services, parts, source }) {
|
||||
overlayHost,
|
||||
preservePending = false,
|
||||
}) {
|
||||
layerState._renderer = null;
|
||||
layerState._engine = null;
|
||||
layerState._viewer = viewer;
|
||||
layerState._catalog = new Map([
|
||||
[ISS_NORAD, { name: 'ISS (ZARYA)', satrec, group: 'stations' }],
|
||||
@@ -177,7 +187,7 @@ export function createTesting({ state: layerState, services, parts, source }) {
|
||||
[
|
||||
ISS_NORAD,
|
||||
{
|
||||
primitive: { show: true, modelMatrix: new Cesium.Matrix4() },
|
||||
primitive: { show: true, modelMatrix: geo.matrix4.create() },
|
||||
gmstAtBake: 0,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { constantRgba } from '../shared/color.js';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import { satelliteClassLabel } from '../../data/satelliteClass.js';
|
||||
import {
|
||||
@@ -9,7 +10,15 @@ import {
|
||||
TRACK_VIEW_FROM_LEO,
|
||||
} from './policy.js';
|
||||
|
||||
export function createTracking({ state: layerState, services, parts, source }) {
|
||||
const YELLOW = constantRgba(1, 1, 0);
|
||||
|
||||
export function createTracking({
|
||||
state: layerState,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
const { clearFocusTarget, publishFocusTargetFromCachedPosition } =
|
||||
services.focus;
|
||||
const {
|
||||
@@ -111,9 +120,9 @@ export function createTracking({ state: layerState, services, parts, source }) {
|
||||
parts.rendering._hideOrbitPath(layerState._trackedNorad);
|
||||
}
|
||||
if (layerState._viewer && !skipViewerUntrack)
|
||||
layerState._viewer.trackedEntity = undefined;
|
||||
getRenderer().follow.release();
|
||||
if (layerState._trackedEntity) {
|
||||
layerState._viewer.entities.remove(layerState._trackedEntity);
|
||||
getRenderer().removeTracked(layerState._trackedEntity);
|
||||
layerState._trackedEntity = null;
|
||||
}
|
||||
layerState._trackedNorad = null;
|
||||
@@ -159,7 +168,7 @@ export function createTracking({ state: layerState, services, parts, source }) {
|
||||
);
|
||||
if (!pos) return layerState._trackedFrameGeo; // propagation hiccup — keep last good sample
|
||||
layerState._trackedFrameGeo = pos;
|
||||
Cesium.Cartesian3.fromDegrees(
|
||||
geo.cartesian3.fromDegrees(
|
||||
pos.longitude,
|
||||
pos.latitude,
|
||||
pos.altitude,
|
||||
@@ -410,15 +419,15 @@ export function createTracking({ state: layerState, services, parts, source }) {
|
||||
point.show = false;
|
||||
|
||||
// Show orbital path
|
||||
parts.rendering._showOrbitPath(noradId, Cesium.Color.YELLOW);
|
||||
parts.rendering._showOrbitPath(noradId, YELLOW);
|
||||
|
||||
// Tracked entity position propagates per evaluation through the per-frame
|
||||
// cache (WS-D2) — dot, host readout, and camera share one SGP4 epoch per frame.
|
||||
// Falls back to the point primitive's 1s-throttled position if SGP4 fails.
|
||||
const positionProperty = new Cesium.CallbackProperty(() => {
|
||||
const position = () => {
|
||||
const pos = _getTrackedFramePosition();
|
||||
return pos ? layerState._trackedFrameCartesian : point.position;
|
||||
}, false);
|
||||
};
|
||||
|
||||
const name = sat.name.trim();
|
||||
|
||||
@@ -433,22 +442,15 @@ export function createTracking({ state: layerState, services, parts, source }) {
|
||||
initialPos && initialPos.altitude > HIGH_ORBIT_ALTITUDE_M
|
||||
? TRACK_VIEW_FROM_HIGH_SCALE
|
||||
: 1;
|
||||
const viewFrom = Cesium.Cartesian3.multiplyByScalar(
|
||||
const viewFrom = geo.cartesian3.multiplyByScalar(
|
||||
TRACK_VIEW_FROM_LEO,
|
||||
viewScale,
|
||||
new Cesium.Cartesian3(),
|
||||
geo.cartesian3.create(),
|
||||
);
|
||||
|
||||
layerState._trackedEntity = layerState._viewer.entities.add({
|
||||
position: positionProperty,
|
||||
layerState._trackedEntity = getRenderer().createTracked({
|
||||
position,
|
||||
viewFrom,
|
||||
point: {
|
||||
pixelSize: 14,
|
||||
color: Cesium.Color.YELLOW,
|
||||
outlineColor: Cesium.Color.WHITE,
|
||||
outlineWidth: 2,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
});
|
||||
layerState._trackedEntity.gevSelectionOrigin = origin;
|
||||
layerState._trackedEntity.gevTrackedId = `satellites:${noradId}`;
|
||||
@@ -459,13 +461,13 @@ export function createTracking({ state: layerState, services, parts, source }) {
|
||||
layerId: 'satellites',
|
||||
id: noradId,
|
||||
label: name,
|
||||
position: Cesium.Cartesian3.clone(point.position),
|
||||
position: geo.cartesian3.clone(point.position),
|
||||
origin,
|
||||
});
|
||||
selectTrackedSubjectContext(_contextSubjectMetadata(noradId, initialPos));
|
||||
layerState._contextRefreshedAtMs = Date.now();
|
||||
|
||||
layerState._viewer.trackedEntity = layerState._trackedEntity;
|
||||
getRenderer().follow.start(layerState._trackedEntity);
|
||||
console.log(`[Data:Satellites] Tracking ${name} (NORAD ${noradId})`);
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -311,7 +311,7 @@ test('layers moved onto renderers keep each engine in its render file', (t) => {
|
||||
"export const load = () => import('../layers/flights/render/navara.js');",
|
||||
);
|
||||
// A layer not moved yet still imports cesium anywhere (until its *a task).
|
||||
write('src/layers/satellites/rendering.js', "import 'cesium';");
|
||||
write('src/layers/earthquakes/rendering.js', "import 'cesium';");
|
||||
assert.doesNotThrow(() => checkImportDirections(root));
|
||||
|
||||
write('src/layers/flights/tracking.js', "import 'cesium';");
|
||||
|
||||
Reference in new issue
Block a user