mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
refactor(launches): Cesium rendering behind a renderer interface
This commit is contained in:
1 parent
1839d62160
commit
4baefeb8f9
31 files changed
+1202
-420
No files matched your search
@@ -39,6 +39,7 @@ const RENDERED_LAYER_DIRS = [
|
|||||||
'src/layers/localAdsb/',
|
'src/layers/localAdsb/',
|
||||||
'src/layers/vessels/',
|
'src/layers/vessels/',
|
||||||
'src/layers/satellites/',
|
'src/layers/satellites/',
|
||||||
|
'src/layers/launches/',
|
||||||
'src/layers/aircraft/',
|
'src/layers/aircraft/',
|
||||||
'src/layers/shared/',
|
'src/layers/shared/',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1149,5 +1149,11 @@
|
|||||||
"src/layers/satellites/render/navara.js",
|
"src/layers/satellites/render/navara.js",
|
||||||
"src/layers/satellites/render/navara.test.mjs",
|
"src/layers/satellites/render/navara.test.mjs",
|
||||||
"src/layers/satellites/render/cesium.test.mjs",
|
"src/layers/satellites/render/cesium.test.mjs",
|
||||||
"src/geo/ellipsoidGeodesic.test.mjs"
|
"src/geo/ellipsoidGeodesic.test.mjs",
|
||||||
|
"src/bench/launchFixture.js",
|
||||||
|
"src/layers/launches/replayClock.test.mjs",
|
||||||
|
"scripts/qa-launches.mjs",
|
||||||
|
"src/geo/boundingSphere.test.mjs",
|
||||||
|
"src/layers/launches/render/cesium.js",
|
||||||
|
"src/layers/launches/launchPadFrame.test.mjs"
|
||||||
]
|
]
|
||||||
@@ -529,6 +529,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -695,6 +696,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -819,6 +821,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -888,6 +891,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -948,6 +952,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -994,6 +999,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1039,6 +1045,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1102,6 +1109,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1145,6 +1153,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1167,6 +1176,8 @@
|
|||||||
"src/layers/launches/camera.js",
|
"src/layers/launches/camera.js",
|
||||||
"src/layers/launches/controls.js",
|
"src/layers/launches/controls.js",
|
||||||
"src/layers/launches/index.js",
|
"src/layers/launches/index.js",
|
||||||
|
"src/layers/shared/color.js",
|
||||||
|
"src/layers/launches/render/cesium.js",
|
||||||
"src/layers/launches/ingestion.js",
|
"src/layers/launches/ingestion.js",
|
||||||
"src/layers/launches/launchPad.js",
|
"src/layers/launches/launchPad.js",
|
||||||
"src/layers/launches/lifecycle.js",
|
"src/layers/launches/lifecycle.js",
|
||||||
@@ -1189,6 +1200,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1242,6 +1254,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1286,6 +1299,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1344,6 +1358,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1384,6 +1399,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1442,6 +1458,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1503,6 +1520,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1540,6 +1558,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1590,6 +1609,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -1624,6 +1644,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -2086,6 +2107,7 @@
|
|||||||
"src/layers/launches/camera.js",
|
"src/layers/launches/camera.js",
|
||||||
"src/layers/launches/controls.js",
|
"src/layers/launches/controls.js",
|
||||||
"src/layers/launches/index.js",
|
"src/layers/launches/index.js",
|
||||||
|
"src/layers/launches/render/cesium.js",
|
||||||
"src/layers/launches/ingestion.js",
|
"src/layers/launches/ingestion.js",
|
||||||
"src/layers/launches/launchPad.js",
|
"src/layers/launches/launchPad.js",
|
||||||
"src/layers/launches/lifecycle.js",
|
"src/layers/launches/lifecycle.js",
|
||||||
@@ -2580,6 +2602,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -2901,6 +2924,7 @@
|
|||||||
"src/layers/launches/camera.js",
|
"src/layers/launches/camera.js",
|
||||||
"src/layers/launches/controls.js",
|
"src/layers/launches/controls.js",
|
||||||
"src/layers/launches/index.js",
|
"src/layers/launches/index.js",
|
||||||
|
"src/layers/launches/render/cesium.js",
|
||||||
"src/layers/launches/ingestion.js",
|
"src/layers/launches/ingestion.js",
|
||||||
"src/layers/launches/launchPad.js",
|
"src/layers/launches/launchPad.js",
|
||||||
"src/layers/launches/lifecycle.js",
|
"src/layers/launches/lifecycle.js",
|
||||||
@@ -3192,6 +3216,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -3292,6 +3317,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -3776,6 +3802,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
@@ -3831,6 +3858,7 @@
|
|||||||
"src/geo/ellipsoid.js",
|
"src/geo/ellipsoid.js",
|
||||||
"src/geo/cartographic.js",
|
"src/geo/cartographic.js",
|
||||||
"src/geo/ellipsoidGeodesic.js",
|
"src/geo/ellipsoidGeodesic.js",
|
||||||
|
"src/geo/boundingSphere.js",
|
||||||
"src/geo/cartesian2.js",
|
"src/geo/cartesian2.js",
|
||||||
"src/geo/cartesian3.js",
|
"src/geo/cartesian3.js",
|
||||||
"src/geo/matrix3.js",
|
"src/geo/matrix3.js",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
// military:synthetic[:count] readsb rows, `count` aircraft (1 000); the
|
// military:synthetic[:count] readsb rows, `count` aircraft (1 000); the
|
||||||
// first 25 are also synthetic civil flights
|
// first 25 are also synthetic civil flights
|
||||||
// vessels:fixture 12 000 AIS rows, or those within an area
|
// vessels:fixture 12 000 AIS rows, or those within an area
|
||||||
|
// launches:fixture three LL2 missions; also supplies the satellite dependency
|
||||||
// satellites:fixture 839 core TLEs + 6000 dense points
|
// satellites:fixture 839 core TLEs + 6000 dense points
|
||||||
// local-adsb:fixture the local receivers' answer (server format,
|
// local-adsb:fixture the local receivers' answer (server format,
|
||||||
// /api/local-receivers/aircraft): 40 aircraft
|
// /api/local-receivers/aircraft): 40 aircraft
|
||||||
@@ -24,6 +25,8 @@ import {
|
|||||||
syntheticMilitary,
|
syntheticMilitary,
|
||||||
syntheticVessels,
|
syntheticVessels,
|
||||||
} from '../../bench/w1Feeds.js';
|
} from '../../bench/w1Feeds.js';
|
||||||
|
import { launchFixture } from '../../bench/launchFixture.js';
|
||||||
|
import { createLaunchSource } from '../../layers/launches/source.js';
|
||||||
import { satelliteFixture } from '../../bench/satelliteFixture.js';
|
import { satelliteFixture } from '../../bench/satelliteFixture.js';
|
||||||
import { createSatelliteSource } from '../../layers/satellites/source.js';
|
import { createSatelliteSource } from '../../layers/satellites/source.js';
|
||||||
import { createLocalReceiverFeeds } from '../../layers/localAdsb/feeds.js';
|
import { createLocalReceiverFeeds } from '../../layers/localAdsb/feeds.js';
|
||||||
@@ -33,6 +36,7 @@ const KINDS = Object.freeze({
|
|||||||
military: new Set(['synthetic']),
|
military: new Set(['synthetic']),
|
||||||
vessels: new Set(['fixture']),
|
vessels: new Set(['fixture']),
|
||||||
satellites: new Set(['fixture']),
|
satellites: new Set(['fixture']),
|
||||||
|
launches: new Set(['fixture']),
|
||||||
'local-adsb': new Set(['fixture']),
|
'local-adsb': new Set(['fixture']),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +52,7 @@ export function parseQaFeed(search = '') {
|
|||||||
(count !== null && !(Number.isInteger(count) && count > 0))
|
(count !== null && !(Number.isInteger(count) && count > 0))
|
||||||
)
|
)
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`qaFeed: unknown entry "${item}" (flights:synthetic[:count], military:synthetic[:count], vessels:fixture, satellites:fixture, local-adsb:fixture)`,
|
`qaFeed: unknown entry "${item}" (flights:synthetic[:count], military:synthetic[:count], vessels:fixture, satellites:fixture, launches:fixture, local-adsb:fixture)`,
|
||||||
);
|
);
|
||||||
return { layer, kind, count };
|
return { layer, kind, count };
|
||||||
});
|
});
|
||||||
@@ -64,6 +68,18 @@ const json = (payload) =>
|
|||||||
export function createQaSources(entries, { now = () => Date.now() } = {}) {
|
export function createQaSources(entries, { now = () => Date.now() } = {}) {
|
||||||
const sources = {};
|
const sources = {};
|
||||||
for (const { layer, count } of entries) {
|
for (const { layer, count } of entries) {
|
||||||
|
if (layer === 'launches') {
|
||||||
|
sources.launches = createLaunchSource({
|
||||||
|
fetchImpl: async (url) =>
|
||||||
|
String(url).includes('/celestrak/')
|
||||||
|
? new Response(satelliteFixture('stations'))
|
||||||
|
: json(launchFixture()),
|
||||||
|
});
|
||||||
|
sources.satellites ??= createSatelliteSource({
|
||||||
|
fetchImpl: async (url) =>
|
||||||
|
new Response(satelliteFixture(String(url).split('/').at(-1))),
|
||||||
|
});
|
||||||
|
}
|
||||||
if (layer === 'satellites')
|
if (layer === 'satellites')
|
||||||
sources.satellites = createSatelliteSource({
|
sources.satellites = createSatelliteSource({
|
||||||
fetchImpl: async (url) =>
|
fetchImpl: async (url) =>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesium.js';
|
||||||
// Which engine draws which layer: the layer renderer registry (plan, "layer
|
// Which engine draws which layer: the layer renderer registry (plan, "layer
|
||||||
// renderer interface") and the layer availability API built on it.
|
// renderer interface") and the layer availability API built on it.
|
||||||
//
|
//
|
||||||
@@ -88,6 +89,7 @@ export const layerAvailableOn = (layerId, engineKind) =>
|
|||||||
|
|
||||||
/** Cesium renderers extracted from their layers (render/cesium.js). */
|
/** Cesium renderers extracted from their layers (render/cesium.js). */
|
||||||
export const CESIUM_LAYER_RENDERERS = Object.freeze({
|
export const CESIUM_LAYER_RENDERERS = Object.freeze({
|
||||||
|
'rocket-launches': createLaunchesCesiumRenderer,
|
||||||
satellites: createSatellitesCesiumRenderer,
|
satellites: createSatellitesCesiumRenderer,
|
||||||
flights: createFlightsCesiumRenderer,
|
flights: createFlightsCesiumRenderer,
|
||||||
military: createMilitaryCesiumRenderer,
|
military: createMilitaryCesiumRenderer,
|
||||||
|
|||||||
@@ -56,8 +56,9 @@ test('the default registry: every layer on Cesium, none yet on Navara', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the aircraft, vessels and satellites have extracted Cesium renderers; the others still draw themselves', () => {
|
test('the aircraft, vessels, satellites and launches have extracted Cesium renderers; the others still draw themselves', () => {
|
||||||
assert.deepEqual(Object.keys(CESIUM_LAYER_RENDERERS), [
|
assert.deepEqual(Object.keys(CESIUM_LAYER_RENDERERS), [
|
||||||
|
'rocket-launches',
|
||||||
'satellites',
|
'satellites',
|
||||||
'flights',
|
'flights',
|
||||||
'military',
|
'military',
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
|
import { rendererFor } from './renderers.js';
|
||||||
|
import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesium.js';
|
||||||
import { createRocketLaunchesLayer } from '../../layers/launches/index.js';
|
import { createRocketLaunchesLayer } from '../../layers/launches/index.js';
|
||||||
import * as geometry from '../../celestialRing.js';
|
import * as geometry from '../../celestialRing.js';
|
||||||
import * as overlays from '../../overlays/worldOverlay.js';
|
import * as overlays from '../../overlays/worldOverlay.js';
|
||||||
import * as render from '../../renderGovernor.js';
|
import * as render from '../../renderGovernor.js';
|
||||||
|
|
||||||
/** Construct one layer using the application scene owners and a supplied source. */
|
/** Construct one layer using the application scene owners and a supplied source. */
|
||||||
export function createApplicationLaunches({ source, satellites }) {
|
export function createApplicationLaunches({
|
||||||
|
source,
|
||||||
|
satellites,
|
||||||
|
renderer = {
|
||||||
|
create: ({ engine = null, ...options }) =>
|
||||||
|
engine
|
||||||
|
? rendererFor('rocket-launches', engine, options)
|
||||||
|
: createLaunchesCesiumRenderer(null, options),
|
||||||
|
},
|
||||||
|
}) {
|
||||||
return createRocketLaunchesLayer({
|
return createRocketLaunchesLayer({
|
||||||
source,
|
source,
|
||||||
services: { satellites, geometry, overlays, render },
|
services: { satellites, geometry, overlays, render, renderer },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Deterministic Launch Library 2 records for phase-3 mission/replay acceptance.
|
||||||
|
import { SATELLITE_FIXTURE_EPOCH } from './satelliteFixture.js';
|
||||||
|
export const LAUNCH_FIXTURE_EPOCH = SATELLITE_FIXTURE_EPOCH;
|
||||||
|
export function launchFixture() {
|
||||||
|
const definitions = [
|
||||||
|
[
|
||||||
|
'qa-iss',
|
||||||
|
'QA ISS supply mission',
|
||||||
|
'ISS (ZARYA)',
|
||||||
|
'NASA',
|
||||||
|
-80.604333,
|
||||||
|
28.608389,
|
||||||
|
'Launch Complex 39A',
|
||||||
|
'Low Earth Orbit',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'qa-meo',
|
||||||
|
'QA navigation mission',
|
||||||
|
'QA MEO payload',
|
||||||
|
'Rocket Lab',
|
||||||
|
177.864,
|
||||||
|
-39.262,
|
||||||
|
'Launch Complex 1',
|
||||||
|
'Medium Earth Orbit',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'qa-gto',
|
||||||
|
'QA transfer mission',
|
||||||
|
'QA GTO payload',
|
||||||
|
'SpaceX',
|
||||||
|
-52.768,
|
||||||
|
5.236,
|
||||||
|
'QA Equatorial Pad',
|
||||||
|
'Geostationary Transfer Orbit',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
results: definitions.map(
|
||||||
|
([id, name, payload, provider, lon, lat, pad, orbit], i) => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
net: new Date(LAUNCH_FIXTURE_EPOCH - (i + 1) * 86400000).toISOString(),
|
||||||
|
status: { name: 'Launch Successful' },
|
||||||
|
launch_service_provider: { name: provider },
|
||||||
|
pad: {
|
||||||
|
name: pad,
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lon,
|
||||||
|
location: { name: pad },
|
||||||
|
},
|
||||||
|
mission: {
|
||||||
|
name: payload,
|
||||||
|
description: 'Deterministic QA fixture',
|
||||||
|
orbit: { name: orbit },
|
||||||
|
},
|
||||||
|
timeline: [
|
||||||
|
{ name: 'MECO', relative_time: '00:02:30' },
|
||||||
|
{ name: 'SECO', relative_time: '00:09:00' },
|
||||||
|
{ name: 'Payload deployment', relative_time: '00:10:00' },
|
||||||
|
],
|
||||||
|
rocket: {
|
||||||
|
payloads: [
|
||||||
|
{
|
||||||
|
id: `${id}-payload`,
|
||||||
|
payload: {
|
||||||
|
name: payload,
|
||||||
|
type: { name: 'QA spacecraft' },
|
||||||
|
mass: 500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
launcher_stage:
|
||||||
|
i === 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: 'qa-booster',
|
||||||
|
type: 'First stage',
|
||||||
|
launcher: { serial_number: 'QA-01' },
|
||||||
|
landing: {
|
||||||
|
attempt: true,
|
||||||
|
success: true,
|
||||||
|
type: { name: 'Drone Ship' },
|
||||||
|
landing_location: {
|
||||||
|
name: 'QA Recovery Platform',
|
||||||
|
latitude: 28.32,
|
||||||
|
longitude: -75.12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
trajectory: [],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -124,7 +124,7 @@ test('every scene click handler consults ownership before it picks', () => {
|
|||||||
'src/layers/installations/selection.js',
|
'src/layers/installations/selection.js',
|
||||||
'viewer.scene.pick(click.position)',
|
'viewer.scene.pick(click.position)',
|
||||||
],
|
],
|
||||||
['src/layers/launches/lifecycle.js', 'drillPick(movement.position'],
|
['src/layers/launches/render/cesium.js', 'drillPick(movement.position'],
|
||||||
['src/layers/radio/interaction.js', 'pickedRadioStationAt(click.position)'],
|
['src/layers/radio/interaction.js', 'pickedRadioStationAt(click.position)'],
|
||||||
[
|
[
|
||||||
'src/layers/satellites/render/cesium.js',
|
'src/layers/satellites/render/cesium.js',
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
// Ported from CesiumJS 1.138 (Apache-2.0) — BoundingSphere.js subset.
|
||||||
|
// Exact mission framing: Ritter/box fit and the union of two spheres.
|
||||||
|
import * as Cartesian3 from './cartesian3.js';
|
||||||
|
import { Check } from './check.js';
|
||||||
|
import { DEBUG } from './debug.js';
|
||||||
|
const defined = (value) => value !== null && value !== undefined;
|
||||||
|
export function create(center = Cartesian3.ZERO, radius = 0) {
|
||||||
|
return { center: Cartesian3.clone(center), radius };
|
||||||
|
}
|
||||||
|
export function clone(sphere, result = create()) {
|
||||||
|
Cartesian3.clone(sphere.center, result.center);
|
||||||
|
result.radius = sphere.radius;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const fromPointsXMin = Cartesian3.create();
|
||||||
|
const fromPointsYMin = Cartesian3.create();
|
||||||
|
const fromPointsZMin = Cartesian3.create();
|
||||||
|
const fromPointsXMax = Cartesian3.create();
|
||||||
|
const fromPointsYMax = Cartesian3.create();
|
||||||
|
const fromPointsZMax = Cartesian3.create();
|
||||||
|
const fromPointsCurrentPos = Cartesian3.create();
|
||||||
|
const fromPointsScratch = Cartesian3.create();
|
||||||
|
const fromPointsRitterCenter = Cartesian3.create();
|
||||||
|
const fromPointsMinBoxPt = Cartesian3.create();
|
||||||
|
const fromPointsMaxBoxPt = Cartesian3.create();
|
||||||
|
const fromPointsNaiveCenterScratch = Cartesian3.create();
|
||||||
|
const unionScratch = Cartesian3.create();
|
||||||
|
const unionScratchCenter = Cartesian3.create();
|
||||||
|
export function fromPoints(positions, result) {
|
||||||
|
if (!defined(result)) {
|
||||||
|
result = create();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined(positions) || positions.length === 0) {
|
||||||
|
result.center = Cartesian3.clone(Cartesian3.ZERO, result.center);
|
||||||
|
result.radius = 0.0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPos = Cartesian3.clone(positions[0], fromPointsCurrentPos);
|
||||||
|
|
||||||
|
const xMin = Cartesian3.clone(currentPos, fromPointsXMin);
|
||||||
|
const yMin = Cartesian3.clone(currentPos, fromPointsYMin);
|
||||||
|
const zMin = Cartesian3.clone(currentPos, fromPointsZMin);
|
||||||
|
|
||||||
|
const xMax = Cartesian3.clone(currentPos, fromPointsXMax);
|
||||||
|
const yMax = Cartesian3.clone(currentPos, fromPointsYMax);
|
||||||
|
const zMax = Cartesian3.clone(currentPos, fromPointsZMax);
|
||||||
|
|
||||||
|
const numPositions = positions.length;
|
||||||
|
let i;
|
||||||
|
for (i = 1; i < numPositions; i++) {
|
||||||
|
Cartesian3.clone(positions[i], currentPos);
|
||||||
|
|
||||||
|
const x = currentPos.x;
|
||||||
|
const y = currentPos.y;
|
||||||
|
const z = currentPos.z;
|
||||||
|
|
||||||
|
// Store points containing the the smallest and largest components
|
||||||
|
if (x < xMin.x) {
|
||||||
|
Cartesian3.clone(currentPos, xMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x > xMax.x) {
|
||||||
|
Cartesian3.clone(currentPos, xMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y < yMin.y) {
|
||||||
|
Cartesian3.clone(currentPos, yMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y > yMax.y) {
|
||||||
|
Cartesian3.clone(currentPos, yMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (z < zMin.z) {
|
||||||
|
Cartesian3.clone(currentPos, zMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (z > zMax.z) {
|
||||||
|
Cartesian3.clone(currentPos, zMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute x-, y-, and z-spans (Squared distances b/n each component's min. and max.).
|
||||||
|
const xSpan = Cartesian3.magnitudeSquared(
|
||||||
|
Cartesian3.subtract(xMax, xMin, fromPointsScratch),
|
||||||
|
);
|
||||||
|
const ySpan = Cartesian3.magnitudeSquared(
|
||||||
|
Cartesian3.subtract(yMax, yMin, fromPointsScratch),
|
||||||
|
);
|
||||||
|
const zSpan = Cartesian3.magnitudeSquared(
|
||||||
|
Cartesian3.subtract(zMax, zMin, fromPointsScratch),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set the diameter endpoints to the largest span.
|
||||||
|
let diameter1 = xMin;
|
||||||
|
let diameter2 = xMax;
|
||||||
|
let maxSpan = xSpan;
|
||||||
|
if (ySpan > maxSpan) {
|
||||||
|
maxSpan = ySpan;
|
||||||
|
diameter1 = yMin;
|
||||||
|
diameter2 = yMax;
|
||||||
|
}
|
||||||
|
if (zSpan > maxSpan) {
|
||||||
|
maxSpan = zSpan;
|
||||||
|
diameter1 = zMin;
|
||||||
|
diameter2 = zMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the center of the initial sphere found by Ritter's algorithm
|
||||||
|
const ritterCenter = fromPointsRitterCenter;
|
||||||
|
ritterCenter.x = (diameter1.x + diameter2.x) * 0.5;
|
||||||
|
ritterCenter.y = (diameter1.y + diameter2.y) * 0.5;
|
||||||
|
ritterCenter.z = (diameter1.z + diameter2.z) * 0.5;
|
||||||
|
|
||||||
|
// Calculate the radius of the initial sphere found by Ritter's algorithm
|
||||||
|
let radiusSquared = Cartesian3.magnitudeSquared(
|
||||||
|
Cartesian3.subtract(diameter2, ritterCenter, fromPointsScratch),
|
||||||
|
);
|
||||||
|
let ritterRadius = Math.sqrt(radiusSquared);
|
||||||
|
|
||||||
|
// Find the center of the sphere found using the Naive method.
|
||||||
|
const minBoxPt = fromPointsMinBoxPt;
|
||||||
|
minBoxPt.x = xMin.x;
|
||||||
|
minBoxPt.y = yMin.y;
|
||||||
|
minBoxPt.z = zMin.z;
|
||||||
|
|
||||||
|
const maxBoxPt = fromPointsMaxBoxPt;
|
||||||
|
maxBoxPt.x = xMax.x;
|
||||||
|
maxBoxPt.y = yMax.y;
|
||||||
|
maxBoxPt.z = zMax.z;
|
||||||
|
|
||||||
|
const naiveCenter = Cartesian3.midpoint(
|
||||||
|
minBoxPt,
|
||||||
|
maxBoxPt,
|
||||||
|
fromPointsNaiveCenterScratch,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Begin 2nd pass to find naive radius and modify the ritter sphere.
|
||||||
|
let naiveRadius = 0;
|
||||||
|
for (i = 0; i < numPositions; i++) {
|
||||||
|
Cartesian3.clone(positions[i], currentPos);
|
||||||
|
|
||||||
|
// Find the furthest point from the naive center to calculate the naive radius.
|
||||||
|
const r = Cartesian3.magnitude(
|
||||||
|
Cartesian3.subtract(currentPos, naiveCenter, fromPointsScratch),
|
||||||
|
);
|
||||||
|
if (r > naiveRadius) {
|
||||||
|
naiveRadius = r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make adjustments to the Ritter Sphere to include all points.
|
||||||
|
const oldCenterToPointSquared = Cartesian3.magnitudeSquared(
|
||||||
|
Cartesian3.subtract(currentPos, ritterCenter, fromPointsScratch),
|
||||||
|
);
|
||||||
|
if (oldCenterToPointSquared > radiusSquared) {
|
||||||
|
const oldCenterToPoint = Math.sqrt(oldCenterToPointSquared);
|
||||||
|
// Calculate new radius to include the point that lies outside
|
||||||
|
ritterRadius = (ritterRadius + oldCenterToPoint) * 0.5;
|
||||||
|
radiusSquared = ritterRadius * ritterRadius;
|
||||||
|
// Calculate center of new Ritter sphere
|
||||||
|
const oldToNew = oldCenterToPoint - ritterRadius;
|
||||||
|
ritterCenter.x =
|
||||||
|
(ritterRadius * ritterCenter.x + oldToNew * currentPos.x) /
|
||||||
|
oldCenterToPoint;
|
||||||
|
ritterCenter.y =
|
||||||
|
(ritterRadius * ritterCenter.y + oldToNew * currentPos.y) /
|
||||||
|
oldCenterToPoint;
|
||||||
|
ritterCenter.z =
|
||||||
|
(ritterRadius * ritterCenter.z + oldToNew * currentPos.z) /
|
||||||
|
oldCenterToPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ritterRadius < naiveRadius) {
|
||||||
|
Cartesian3.clone(ritterCenter, result.center);
|
||||||
|
result.radius = ritterRadius;
|
||||||
|
} else {
|
||||||
|
Cartesian3.clone(naiveCenter, result.center);
|
||||||
|
result.radius = naiveRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
export function union(left, right, result) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Check.typeOf.object('left', left);
|
||||||
|
Check.typeOf.object('right', right);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined(result)) {
|
||||||
|
result = create();
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftCenter = left.center;
|
||||||
|
const leftRadius = left.radius;
|
||||||
|
const rightCenter = right.center;
|
||||||
|
const rightRadius = right.radius;
|
||||||
|
|
||||||
|
const toRightCenter = Cartesian3.subtract(
|
||||||
|
rightCenter,
|
||||||
|
leftCenter,
|
||||||
|
unionScratch,
|
||||||
|
);
|
||||||
|
const centerSeparation = Cartesian3.magnitude(toRightCenter);
|
||||||
|
|
||||||
|
if (leftRadius >= centerSeparation + rightRadius) {
|
||||||
|
// Left sphere wins.
|
||||||
|
clone(left, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightRadius >= centerSeparation + leftRadius) {
|
||||||
|
// Right sphere wins.
|
||||||
|
clone(right, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are two tangent points, one on far side of each sphere.
|
||||||
|
const halfDistanceBetweenTangentPoints =
|
||||||
|
(leftRadius + centerSeparation + rightRadius) * 0.5;
|
||||||
|
|
||||||
|
// Compute the center point halfway between the two tangent points.
|
||||||
|
const center = Cartesian3.multiplyByScalar(
|
||||||
|
toRightCenter,
|
||||||
|
(-leftRadius + halfDistanceBetweenTangentPoints) / centerSeparation,
|
||||||
|
unionScratchCenter,
|
||||||
|
);
|
||||||
|
Cartesian3.add(center, leftCenter, center);
|
||||||
|
Cartesian3.clone(center, result.center);
|
||||||
|
result.radius = halfDistanceBetweenTangentPoints;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import * as geo from './index.js';
|
||||||
|
const tuple = (s) => [s.center.x, s.center.y, s.center.z, s.radius];
|
||||||
|
test('mission sphere fit and Earth/orbit union match native Cesium exactly', () => {
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
|
const positions = Array.from({ length: i * 3 }, (_, j) =>
|
||||||
|
geo.cartesian3.fromDegrees(
|
||||||
|
((j * 73 + i * 11) % 360) - 180,
|
||||||
|
((j * 29) % 170) - 85,
|
||||||
|
5e5 + i * 1e6,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const fit = geo.boundingSphere.fromPoints(positions),
|
||||||
|
native = Cesium.BoundingSphere.fromPoints(positions);
|
||||||
|
assert.deepEqual(tuple(fit), tuple(native));
|
||||||
|
const earth = geo.boundingSphere.create(geo.cartesian3.ZERO, 6378137);
|
||||||
|
assert.deepEqual(
|
||||||
|
tuple(geo.boundingSphere.union(earth, fit)),
|
||||||
|
tuple(
|
||||||
|
Cesium.BoundingSphere.union(
|
||||||
|
new Cesium.BoundingSphere(Cesium.Cartesian3.ZERO, 6378137),
|
||||||
|
native,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('union handles containment, in-place results and empty fits', () => {
|
||||||
|
const outer = geo.boundingSphere.create(geo.cartesian3.create(1, 2, 3), 20),
|
||||||
|
inner = geo.boundingSphere.create(geo.cartesian3.create(2, 2, 3), 1);
|
||||||
|
assert.equal(geo.boundingSphere.union(outer, inner, inner), inner);
|
||||||
|
assert.deepEqual(tuple(inner), tuple(outer));
|
||||||
|
assert.deepEqual(tuple(geo.boundingSphere.fromPoints([])), [0, 0, 0, 0]);
|
||||||
|
});
|
||||||
@@ -15,3 +15,4 @@ export * as quaternion from './quaternion.js';
|
|||||||
export * as transforms from './transforms.js';
|
export * as transforms from './transforms.js';
|
||||||
|
|
||||||
export * as ellipsoidGeodesic from './ellipsoidGeodesic.js';
|
export * as ellipsoidGeodesic from './ellipsoidGeodesic.js';
|
||||||
|
export * as boundingSphere from './boundingSphere.js';
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Cesium from 'cesium';
|
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import {
|
import {
|
||||||
REPLAY_ASCENT_CAMERA_OFFSET_RAD,
|
REPLAY_ASCENT_CAMERA_OFFSET_RAD,
|
||||||
@@ -14,7 +13,13 @@ import {
|
|||||||
REPLAY_ORBIT_FRAME_CENTER_BLEND,
|
REPLAY_ORBIT_FRAME_CENTER_BLEND,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
|
||||||
export function createCamera({ state: layerState, services, parts, source }) {
|
export function createCamera({
|
||||||
|
state: layerState,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
source,
|
||||||
|
getRenderer,
|
||||||
|
}) {
|
||||||
function cameraHeadingForPath(path, progress, fallback = Math.PI) {
|
function cameraHeadingForPath(path, progress, fallback = Math.PI) {
|
||||||
if (!path?.length) return fallback;
|
if (!path?.length) return fallback;
|
||||||
const current = parts.paths.samplePath(path, progress);
|
const current = parts.paths.samplePath(path, progress);
|
||||||
@@ -26,7 +31,7 @@ export function createCamera({ state: layerState, services, parts, source }) {
|
|||||||
const nextCartographic =
|
const nextCartographic =
|
||||||
geo.ellipsoid.WGS84.cartesianToCartographic(next);
|
geo.ellipsoid.WGS84.cartesianToCartographic(next);
|
||||||
if (!nextCartographic) continue;
|
if (!nextCartographic) continue;
|
||||||
const geodesic = new Cesium.EllipsoidGeodesic(
|
const geodesic = geo.ellipsoidGeodesic.create(
|
||||||
currentCartographic,
|
currentCartographic,
|
||||||
nextCartographic,
|
nextCartographic,
|
||||||
);
|
);
|
||||||
@@ -308,17 +313,13 @@ export function createCamera({ state: layerState, services, parts, source }) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function replayOrbitFrameSphere(orbitPath = []) {
|
function replayOrbitFrameSphere(orbitPath = []) {
|
||||||
const earth = new Cesium.BoundingSphere(
|
const earth = geo.boundingSphere.create(
|
||||||
geo.cartesian3.ZERO,
|
geo.cartesian3.ZERO,
|
||||||
geo.ellipsoid.WGS84.maximumRadius,
|
geo.ellipsoid.WGS84.maximumRadius,
|
||||||
);
|
);
|
||||||
if (!Array.isArray(orbitPath) || orbitPath.length < 2) return earth;
|
if (!Array.isArray(orbitPath) || orbitPath.length < 2) return earth;
|
||||||
const orbit = Cesium.BoundingSphere.fromPoints(orbitPath);
|
const orbit = geo.boundingSphere.fromPoints(orbitPath);
|
||||||
return Cesium.BoundingSphere.union(
|
return geo.boundingSphere.union(earth, orbit, geo.boundingSphere.create());
|
||||||
earth,
|
|
||||||
orbit,
|
|
||||||
new Cesium.BoundingSphere(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
export function createControls({ state: layerState, services, parts, source }) {
|
export function createControls({ state: layerState, services, parts, source }) {
|
||||||
const methods = {
|
const methods = {
|
||||||
id: 'rocket-launches',
|
id: 'rocket-launches',
|
||||||
@@ -26,7 +27,7 @@ export function createControls({ state: layerState, services, parts, source }) {
|
|||||||
*/
|
*/
|
||||||
getAnalystRecords(maxCount = Infinity) {
|
getAnalystRecords(maxCount = Infinity) {
|
||||||
if (!layerState._enabled) return [];
|
if (!layerState._enabled) return [];
|
||||||
const now = Date.now();
|
const now = appClock.now();
|
||||||
const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
|
const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const launch of layerState._launches || []) {
|
for (const launch of layerState._launches || []) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as geo from '../../geo/index.js';
|
||||||
import { createPolicyHelpers } from './policyHelpers.js';
|
import { createPolicyHelpers } from './policyHelpers.js';
|
||||||
import { createIngestion } from './ingestion.js';
|
import { createIngestion } from './ingestion.js';
|
||||||
import { createOrbitRendering } from './orbitRendering.js';
|
import { createOrbitRendering } from './orbitRendering.js';
|
||||||
@@ -21,7 +22,15 @@ export function createRocketLaunchesLayer({ services, source }) {
|
|||||||
throw new TypeError('A launches source is required');
|
throw new TypeError('A launches source is required');
|
||||||
const state = createState({ services });
|
const state = createState({ services });
|
||||||
const parts = {};
|
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.policyHelpers = createPolicyHelpers(context);
|
parts.policyHelpers = createPolicyHelpers(context);
|
||||||
parts.ingestion = createIngestion(context);
|
parts.ingestion = createIngestion(context);
|
||||||
parts.orbitRendering = createOrbitRendering(context);
|
parts.orbitRendering = createOrbitRendering(context);
|
||||||
@@ -43,6 +52,88 @@ export function createRocketLaunchesLayer({ services, source }) {
|
|||||||
parts.lifecycle.methods,
|
parts.lifecycle.methods,
|
||||||
parts.ingestion.methods,
|
parts.ingestion.methods,
|
||||||
{
|
{
|
||||||
|
qaDrawn() {
|
||||||
|
if (!state._viewer || !state._dataSource) return null;
|
||||||
|
const value = (v) =>
|
||||||
|
typeof v?.getValue === 'function'
|
||||||
|
? v.getValue(state._declutterTime)
|
||||||
|
: v;
|
||||||
|
const plain = (p) => (p ? { x: p.x, y: p.y, z: p.z } : null);
|
||||||
|
const anchors = (positions) =>
|
||||||
|
!positions?.length
|
||||||
|
? []
|
||||||
|
: Array.from({ length: 8 }, (_, i) =>
|
||||||
|
plain(positions[Math.floor((i * (positions.length - 1)) / 8)]),
|
||||||
|
);
|
||||||
|
const track = state._replayTracks.get(state._selectedLaunchId);
|
||||||
|
const frame = track?.getReplayState();
|
||||||
|
return {
|
||||||
|
enabled: state._enabled,
|
||||||
|
selected: state._selectedLaunchId,
|
||||||
|
speed: state._replaySpeed,
|
||||||
|
paused: state._replayPaused,
|
||||||
|
records: state._launches.map((l) => ({
|
||||||
|
id: l.id,
|
||||||
|
name: l.name,
|
||||||
|
longitude: l.lon,
|
||||||
|
latitude: l.lat,
|
||||||
|
orbit: l.orbit?.name,
|
||||||
|
})),
|
||||||
|
points: state._dataSource.entities.values
|
||||||
|
.filter((e) => e.point && e.position)
|
||||||
|
.map((e) => {
|
||||||
|
const color = value(e.point.color);
|
||||||
|
return {
|
||||||
|
id: e.id,
|
||||||
|
position: plain(value(e.position)),
|
||||||
|
show:
|
||||||
|
state._dataSource.show &&
|
||||||
|
e.show &&
|
||||||
|
value(e.point.show) !== false,
|
||||||
|
size: value(e.point.pixelSize),
|
||||||
|
color: color
|
||||||
|
? [color.red, color.green, color.blue, color.alpha]
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
paths: state._dataSource.entities.values
|
||||||
|
.filter((e) => e.polyline)
|
||||||
|
.map((e) => ({
|
||||||
|
id: e.id,
|
||||||
|
show: state._dataSource.show && e.show,
|
||||||
|
anchors: anchors(value(e.polyline.positions)),
|
||||||
|
})),
|
||||||
|
rings: [...state._missionOrbitPrimitives].map(([id, path]) => ({
|
||||||
|
id,
|
||||||
|
show: path.primitive.show,
|
||||||
|
anchors: anchors(path.basePositions).map((p) =>
|
||||||
|
plain(
|
||||||
|
geo.matrix4.multiplyByPoint(
|
||||||
|
path.primitive.modelMatrix,
|
||||||
|
p,
|
||||||
|
geo.cartesian3.create(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
replay: frame
|
||||||
|
? {
|
||||||
|
...frame,
|
||||||
|
active: state._replayCameraLaunchId === state._selectedLaunchId,
|
||||||
|
ascentDurationSec: track.ascentDurationSec,
|
||||||
|
position: plain(
|
||||||
|
parts.paths.samplePath(
|
||||||
|
frame.ascending
|
||||||
|
? track.ascentPath
|
||||||
|
: track.animatedOrbitPath,
|
||||||
|
frame.phaseProgress,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
padVisible: state._launchPadZonePrimitive?.show === true,
|
||||||
|
};
|
||||||
|
},
|
||||||
satelliteParamsForSpaceMissions:
|
satelliteParamsForSpaceMissions:
|
||||||
parts.policyHelpers.satelliteParamsForSpaceMissions,
|
parts.policyHelpers.satelliteParamsForSpaceMissions,
|
||||||
satelliteParamsAfterSpaceMissions:
|
satelliteParamsAfterSpaceMissions:
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as geo from '../../geo/index.js';
|
||||||
import { LAUNCH_PAD_ZONE_RADIUS_M } from './policy.js';
|
import { appClock } from '../../time/clock.js';
|
||||||
|
|
||||||
export function createLaunchPad({
|
export function createLaunchPad({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
services,
|
services,
|
||||||
parts,
|
parts,
|
||||||
source,
|
source,
|
||||||
|
getRenderer,
|
||||||
}) {
|
}) {
|
||||||
function hideLaunchPadZone() {
|
function hideLaunchPadZone() {
|
||||||
if (layerState._launchPadZonePrimitive)
|
if (layerState._launchPadZonePrimitive)
|
||||||
@@ -13,134 +14,60 @@ export function createLaunchPad({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeLaunchPadZonePrimitive() {
|
function removeLaunchPadZonePrimitive() {
|
||||||
if (
|
if (layerState._launchPadZonePrimitive)
|
||||||
layerState._launchPadZonePrimitive &&
|
getRenderer().removePrimitive(layerState._launchPadZonePrimitive);
|
||||||
layerState._viewer?.scene?.primitives
|
|
||||||
) {
|
|
||||||
layerState._viewer.scene.primitives.remove(
|
|
||||||
layerState._launchPadZonePrimitive,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
layerState._launchPadZonePrimitive = null;
|
layerState._launchPadZonePrimitive = null;
|
||||||
layerState._launchPadZoneLaunchId = null;
|
layerState._launchPadZoneLaunchId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLaunchPadZonePrimitive(launch) {
|
function createLaunchPadZonePrimitive(launch) {
|
||||||
removeLaunchPadZonePrimitive();
|
removeLaunchPadZonePrimitive();
|
||||||
const material = new Cesium.Material({
|
layerState._launchPadZonePrimitive = getRenderer().createPad(launch);
|
||||||
fabric: {
|
|
||||||
type: 'GevLaunchPadZone',
|
|
||||||
uniforms: {
|
|
||||||
color: Cesium.Color.fromCssColorString('#22e6e6'),
|
|
||||||
fillAlpha: 0.105,
|
|
||||||
rimAlpha: 0.72,
|
|
||||||
},
|
|
||||||
source: `
|
|
||||||
czm_material czm_getMaterial(czm_materialInput materialInput) {
|
|
||||||
czm_material material = czm_getDefaultMaterial(materialInput);
|
|
||||||
vec2 centered = (materialInput.st - vec2(0.5)) * 2.0;
|
|
||||||
float radius = length(centered);
|
|
||||||
float inside = 1.0 - smoothstep(0.985, 1.0, radius);
|
|
||||||
float rim = smoothstep(0.952, 0.985, radius) * inside;
|
|
||||||
material.diffuse = color.rgb;
|
|
||||||
material.emission = color.rgb * rim * 0.35;
|
|
||||||
material.alpha = color.a * inside * mix(fillAlpha, rimAlpha, rim);
|
|
||||||
return material;
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const geometry = new Cesium.EllipseGeometry({
|
|
||||||
center: Cesium.Cartesian3.fromDegrees(launch.lon, launch.lat),
|
|
||||||
semiMajorAxis: LAUNCH_PAD_ZONE_RADIUS_M,
|
|
||||||
semiMinorAxis: LAUNCH_PAD_ZONE_RADIUS_M,
|
|
||||||
granularity: Cesium.Math.toRadians(0.08),
|
|
||||||
vertexFormat:
|
|
||||||
Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat,
|
|
||||||
});
|
|
||||||
layerState._launchPadZonePrimitive =
|
|
||||||
layerState._viewer.scene.primitives.add(
|
|
||||||
new Cesium.GroundPrimitive({
|
|
||||||
geometryInstances: new Cesium.GeometryInstance({
|
|
||||||
geometry,
|
|
||||||
id: `rocket-pad-zone:${launch.id}`,
|
|
||||||
}),
|
|
||||||
appearance: new Cesium.MaterialAppearance({
|
|
||||||
material,
|
|
||||||
translucent: true,
|
|
||||||
closed: false,
|
|
||||||
faceForward: true,
|
|
||||||
flat: true,
|
|
||||||
// Keep the zone classified onto the photoreal surface, but bias only its
|
|
||||||
// rasterized depth toward the camera. This avoids coplanar fragments
|
|
||||||
// being intermittently buried by the launch-pad mesh at oblique angles
|
|
||||||
// without adding a world-space height that would make the ring float.
|
|
||||||
renderState: {
|
|
||||||
depthTest: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
depthMask: false,
|
|
||||||
polygonOffset: {
|
|
||||||
enabled: true,
|
|
||||||
factor: -1,
|
|
||||||
units: -4,
|
|
||||||
},
|
|
||||||
blending: Cesium.BlendingState.ALPHA_BLEND,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
classificationType: Cesium.ClassificationType.BOTH,
|
|
||||||
asynchronous: true,
|
|
||||||
show: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
layerState._launchPadZoneLaunchId = launch.id;
|
layerState._launchPadZoneLaunchId = launch.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initLaunchPadZonePrimitive() {
|
function initLaunchPadZonePrimitive() {
|
||||||
if (!layerState._viewer || layerState._launchPadZoneRemover) return;
|
if (!layerState._viewer || layerState._launchPadZoneRemover) return;
|
||||||
layerState._launchPadZoneRemover =
|
layerState._launchPadZoneRemover = getRenderer().on('preRender', () => {
|
||||||
layerState._viewer.scene.preRender.addEventListener(() => {
|
if (!layerState._enabled || !layerState._dataSource?.show) {
|
||||||
if (!layerState._enabled || !layerState._dataSource?.show) {
|
hideLaunchPadZone();
|
||||||
hideLaunchPadZone();
|
return;
|
||||||
return;
|
}
|
||||||
}
|
const nowMs = Date.now();
|
||||||
const nowMs = Date.now();
|
if (nowMs - layerState._lastMissionRingRotationMs >= 1000) {
|
||||||
if (nowMs - layerState._lastMissionRingRotationMs >= 1000) {
|
layerState._missionRingDate.setTime(appClock.now());
|
||||||
layerState._missionRingDate.setTime(nowMs);
|
parts.orbitRendering.updateMissionOrbitPrimitiveFrames(
|
||||||
parts.orbitRendering.updateMissionOrbitPrimitiveFrames(
|
layerState._missionRingDate,
|
||||||
layerState._missionRingDate,
|
|
||||||
);
|
|
||||||
layerState._lastMissionRingRotationMs = nowMs;
|
|
||||||
}
|
|
||||||
const launch = layerState._launches.find(
|
|
||||||
(item) => item.id === layerState._selectedLaunchId,
|
|
||||||
);
|
);
|
||||||
const camera = layerState._viewer?.camera;
|
layerState._lastMissionRingRotationMs = nowMs;
|
||||||
if (!launch || !camera) {
|
}
|
||||||
hideLaunchPadZone();
|
const launch = layerState._launches.find(
|
||||||
return;
|
(item) => item.id === layerState._selectedLaunchId,
|
||||||
}
|
);
|
||||||
const launchPosition = Cesium.Cartesian3.fromDegrees(
|
const camera = layerState._viewer?.camera;
|
||||||
launch.lon,
|
if (!launch || !camera) {
|
||||||
launch.lat,
|
hideLaunchPadZone();
|
||||||
);
|
return;
|
||||||
const visible = parts.policyHelpers.launchPadZoneVisible({
|
}
|
||||||
layerActive: Boolean(layerState._dataSource?.show),
|
const launchPosition = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
|
||||||
selectedLaunchId: layerState._selectedLaunchId,
|
const visible = parts.policyHelpers.launchPadZoneVisible({
|
||||||
launchId: launch.id,
|
layerActive: Boolean(layerState._dataSource?.show),
|
||||||
cameraHeightM: camera.positionCartographic?.height,
|
selectedLaunchId: layerState._selectedLaunchId,
|
||||||
cameraDistanceM: Cesium.Cartesian3.distance(
|
launchId: launch.id,
|
||||||
camera.positionWC,
|
cameraHeightM: camera.positionCartographic?.height,
|
||||||
launchPosition,
|
cameraDistanceM: geo.cartesian3.distance(
|
||||||
),
|
camera.positionWC,
|
||||||
});
|
launchPosition,
|
||||||
if (!visible) {
|
),
|
||||||
hideLaunchPadZone();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (layerState._launchPadZoneLaunchId !== launch.id)
|
|
||||||
createLaunchPadZonePrimitive(launch);
|
|
||||||
layerState._launchPadZonePrimitive.show = true;
|
|
||||||
});
|
});
|
||||||
|
if (!visible) {
|
||||||
|
hideLaunchPadZone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (layerState._launchPadZoneLaunchId !== launch.id)
|
||||||
|
createLaunchPadZonePrimitive(launch);
|
||||||
|
layerState._launchPadZonePrimitive.show = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function destroyLaunchPadZonePrimitive() {
|
function destroyLaunchPadZonePrimitive() {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as geo from '../../geo/index.js';
|
||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
|
import { createLaunchPad } from './launchPad.js';
|
||||||
|
test('selected pad frame uses neutral geometry and the physical orbit epoch', () => {
|
||||||
|
const launch = { id: 'qa', lon: -80.6, lat: 28.6 };
|
||||||
|
let tick,
|
||||||
|
created = 0,
|
||||||
|
epoch;
|
||||||
|
const state = {
|
||||||
|
_viewer: {
|
||||||
|
camera: {
|
||||||
|
positionWC: geo.cartesian3.fromDegrees(launch.lon, launch.lat, 500),
|
||||||
|
positionCartographic: { height: 500 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_enabled: true,
|
||||||
|
_dataSource: { show: true },
|
||||||
|
_selectedLaunchId: 'qa',
|
||||||
|
_launches: [launch],
|
||||||
|
_launchPadZoneRemover: null,
|
||||||
|
_lastMissionRingRotationMs: 0,
|
||||||
|
_missionRingDate: new Date(0),
|
||||||
|
_launchPadZonePrimitive: null,
|
||||||
|
_launchPadZoneLaunchId: null,
|
||||||
|
};
|
||||||
|
const renderer = {
|
||||||
|
on: (event, fn) => {
|
||||||
|
assert.equal(event, 'preRender');
|
||||||
|
tick = fn;
|
||||||
|
return () => {};
|
||||||
|
},
|
||||||
|
createPad: () => {
|
||||||
|
created++;
|
||||||
|
return { show: true };
|
||||||
|
},
|
||||||
|
removePrimitive: () => {},
|
||||||
|
};
|
||||||
|
const parts = {
|
||||||
|
orbitRendering: {
|
||||||
|
updateMissionOrbitPrimitiveFrames: (date) => {
|
||||||
|
epoch = date.getTime();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policyHelpers: {
|
||||||
|
launchPadZoneVisible: ({ cameraDistanceM }) => cameraDistanceM < 1000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
appClock.freeze(123000);
|
||||||
|
createLaunchPad({
|
||||||
|
state,
|
||||||
|
services: {},
|
||||||
|
parts,
|
||||||
|
source: {},
|
||||||
|
getRenderer: () => renderer,
|
||||||
|
}).initLaunchPadZonePrimitive();
|
||||||
|
tick();
|
||||||
|
assert.equal(created, 1);
|
||||||
|
assert.equal(state._launchPadZonePrimitive.show, true);
|
||||||
|
assert.equal(epoch, 123000);
|
||||||
|
} finally {
|
||||||
|
appClock.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,61 +1,54 @@
|
|||||||
import * as Cesium from 'cesium';
|
|
||||||
import { isPointerFree } from '../../data/inputOwnership.js';
|
|
||||||
|
|
||||||
export function createLifecycle({
|
export function createLifecycle({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
services,
|
services,
|
||||||
parts,
|
parts,
|
||||||
source,
|
source,
|
||||||
|
getRenderer,
|
||||||
}) {
|
}) {
|
||||||
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
||||||
|
|
||||||
const methods = {
|
const methods = {
|
||||||
init(viewer) {
|
init(viewer, { engine = null } = {}) {
|
||||||
layerState._viewer = viewer;
|
layerState._engine = engine;
|
||||||
|
layerState._renderer = services.renderer.create({ viewer, engine });
|
||||||
|
layerState._viewer = layerState._renderer.view;
|
||||||
|
layerState._declutterOccluder = getRenderer().occluder();
|
||||||
layerState._enabled = false;
|
layerState._enabled = false;
|
||||||
parts.panel.createMissionPanel();
|
parts.panel.createMissionPanel();
|
||||||
parts.overlays.createReplayVehicleOverlay();
|
parts.overlays.createReplayVehicleOverlay();
|
||||||
layerState._dataSource = new Cesium.CustomDataSource('rocket-launches');
|
layerState._dataSource = getRenderer().createDataSource();
|
||||||
layerState._dataSource.show = false;
|
|
||||||
viewer.dataSources.add(layerState._dataSource);
|
|
||||||
layerState._count = 0;
|
layerState._count = 0;
|
||||||
layerState._lastUpdate = null;
|
layerState._lastUpdate = null;
|
||||||
layerState._lastError = null;
|
layerState._lastError = null;
|
||||||
layerState._orbitMatches = 0;
|
layerState._orbitMatches = 0;
|
||||||
parts.overlays.clearMissionOverlaySources();
|
parts.overlays.clearMissionOverlaySources();
|
||||||
layerState._clickHandler = new Cesium.ScreenSpaceEventHandler(
|
const enabled = () => layerState._enabled && layerState._dataSource?.show;
|
||||||
viewer.scene.canvas,
|
layerState._clickHandler = getRenderer().listenClick(
|
||||||
|
layerState._dataSource,
|
||||||
|
(entity) => {
|
||||||
|
const launchId = parts.selection.entityLaunchId(entity);
|
||||||
|
if (!launchId) return;
|
||||||
|
parts.selection.setSelectedMission(launchId);
|
||||||
|
parts.selection.focusMission(
|
||||||
|
layerState._launches.find((l) => l.id === launchId),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ enabled },
|
||||||
);
|
);
|
||||||
layerState._clickHandler.setInputAction((movement) => {
|
layerState._moveHandler = getRenderer().listenMove(
|
||||||
// A tool owns the pointer (src/data/inputOwnership.js): yield the click.
|
layerState._dataSource,
|
||||||
if (!isPointerFree()) return;
|
(entity) => {
|
||||||
if (!layerState._enabled || !layerState._dataSource?.show) return;
|
layerState._viewer.scene.canvas.style.cursor = entity
|
||||||
const entity = viewer.scene
|
? 'pointer'
|
||||||
.drillPick(movement.position, 12)
|
: '';
|
||||||
.map((picked) => picked?.id)
|
},
|
||||||
.find((candidate) => parts.selection.entityLaunchId(candidate));
|
{ enabled },
|
||||||
const launchId = parts.selection.entityLaunchId(entity);
|
|
||||||
if (!launchId) return;
|
|
||||||
parts.selection.setSelectedMission(launchId);
|
|
||||||
parts.selection.focusMission(
|
|
||||||
layerState._launches.find((launch) => launch.id === launchId),
|
|
||||||
);
|
|
||||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
||||||
layerState._moveHandler = new Cesium.ScreenSpaceEventHandler(
|
|
||||||
viewer.scene.canvas,
|
|
||||||
);
|
);
|
||||||
layerState._moveHandler.setInputAction((movement) => {
|
|
||||||
if (!layerState._enabled || !layerState._dataSource?.show) return;
|
|
||||||
const missionEntity = viewer.scene
|
|
||||||
.drillPick(movement.endPosition, 12)
|
|
||||||
.map((picked) => picked?.id)
|
|
||||||
.find((candidate) => parts.selection.entityLaunchId(candidate));
|
|
||||||
viewer.scene.canvas.style.cursor = missionEntity ? 'pointer' : '';
|
|
||||||
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
||||||
// Apply horizon visibility before Cesium traverses and picks the scene.
|
// Apply horizon visibility before Cesium traverses and picks the scene.
|
||||||
// A postRender write is one frame late and can remain visibly stale when
|
// A postRender write is one frame late and can remain visibly stale when
|
||||||
// request-on-demand rendering stops after a globe camera move.
|
// request-on-demand rendering stops after a globe camera move.
|
||||||
layerState._declutterHandler = viewer.scene.preRender.addEventListener(
|
layerState._declutterHandler = getRenderer().on(
|
||||||
|
'preRender',
|
||||||
parts.rendering.updateMissionFrame,
|
parts.rendering.updateMissionFrame,
|
||||||
);
|
);
|
||||||
parts.launchPad.initLaunchPadZonePrimitive();
|
parts.launchPad.initLaunchPadZonePrimitive();
|
||||||
@@ -153,9 +146,8 @@ export function createLifecycle({
|
|||||||
}
|
}
|
||||||
layerState._missionRosterPreviewOwnership = null;
|
layerState._missionRosterPreviewOwnership = null;
|
||||||
layerState._missionRoster = null;
|
layerState._missionRoster = null;
|
||||||
layerState._viewer = null;
|
|
||||||
if (layerState._dataSource)
|
if (layerState._dataSource)
|
||||||
viewer.dataSources.remove(layerState._dataSource, true);
|
getRenderer().removeDataSource(layerState._dataSource);
|
||||||
layerState._dataSource = null;
|
layerState._dataSource = null;
|
||||||
layerState._count = 0;
|
layerState._count = 0;
|
||||||
layerState._orbitMatches = 0;
|
layerState._orbitMatches = 0;
|
||||||
@@ -168,6 +160,10 @@ export function createLifecycle({
|
|||||||
layerState._satelliteStateBeforeMission = null;
|
layerState._satelliteStateBeforeMission = null;
|
||||||
layerState._satelliteActivationPromise = null;
|
layerState._satelliteActivationPromise = null;
|
||||||
layerState._dataManager = null;
|
layerState._dataManager = null;
|
||||||
|
layerState._renderer?.destroy();
|
||||||
|
layerState._renderer = null;
|
||||||
|
layerState._engine = null;
|
||||||
|
layerState._viewer = null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import * as Cesium from 'cesium';
|
import { rgbaFromCss } from '../shared/color.js';
|
||||||
|
import * as geo from '../../geo/index.js';
|
||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
import { WINDOW_DAYS } from './policy.js';
|
import { WINDOW_DAYS } from './policy.js';
|
||||||
|
|
||||||
export function createModel({ state: layerState, services, parts, source }) {
|
export function createModel({
|
||||||
|
state: layerState,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
source,
|
||||||
|
getRenderer,
|
||||||
|
}) {
|
||||||
function finiteCoordinate(value) {
|
function finiteCoordinate(value) {
|
||||||
const number = Number(value);
|
const number = Number(value);
|
||||||
return Number.isFinite(number) ? number : null;
|
return Number.isFinite(number) ? number : null;
|
||||||
@@ -137,23 +145,20 @@ export function createModel({ state: layerState, services, parts, source }) {
|
|||||||
const identity =
|
const identity =
|
||||||
`${launch.provider || ''} ${launch.name || ''} ${launch.missionName || ''}`.toLowerCase();
|
`${launch.provider || ''} ${launch.name || ''} ${launch.missionName || ''}`.toLowerCase();
|
||||||
if (/nasa|national aeronautics/.test(identity))
|
if (/nasa|national aeronautics/.test(identity))
|
||||||
return Cesium.Color.fromCssColorString('#ff9f43');
|
return rgbaFromCss('#ff9f43');
|
||||||
if (/starlink|spacex|space exploration/.test(identity))
|
if (/starlink|spacex|space exploration/.test(identity))
|
||||||
return Cesium.Color.fromCssColorString('#4cc9f0');
|
return rgbaFromCss('#4cc9f0');
|
||||||
if (/rocket lab/.test(identity))
|
if (/rocket lab/.test(identity)) return rgbaFromCss('#7bed9f');
|
||||||
return Cesium.Color.fromCssColorString('#7bed9f');
|
if (/isro|indian space/.test(identity)) return rgbaFromCss('#ff66c4');
|
||||||
if (/isro|indian space/.test(identity))
|
|
||||||
return Cesium.Color.fromCssColorString('#ff66c4');
|
|
||||||
if (/cnsa|china national|long march/.test(identity))
|
if (/cnsa|china national|long march/.test(identity))
|
||||||
return Cesium.Color.fromCssColorString('#ffd166');
|
return rgbaFromCss('#ffd166');
|
||||||
if (/blue origin/.test(identity))
|
if (/blue origin/.test(identity)) return rgbaFromCss('#a78bfa');
|
||||||
return Cesium.Color.fromCssColorString('#a78bfa');
|
|
||||||
if (/ula|united launch alliance/.test(identity))
|
if (/ula|united launch alliance/.test(identity))
|
||||||
return Cesium.Color.fromCssColorString('#f97316');
|
return rgbaFromCss('#f97316');
|
||||||
if (/arianespace|esa|european space/.test(identity))
|
if (/arianespace|esa|european space/.test(identity))
|
||||||
return Cesium.Color.fromCssColorString('#60a5fa');
|
return rgbaFromCss('#60a5fa');
|
||||||
if (launch.provider) return Cesium.Color.fromCssColorString('#c084fc');
|
if (launch.provider) return rgbaFromCss('#c084fc');
|
||||||
return Cesium.Color.fromCssColorString('#22e6e6');
|
return rgbaFromCss('#22e6e6');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,7 +170,7 @@ export function createModel({ state: layerState, services, parts, source }) {
|
|||||||
* @returns {Array<object>}
|
* @returns {Array<object>}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function normalizeRocketLaunches(payload, now = new Date()) {
|
function normalizeRocketLaunches(payload, now = new Date(appClock.now())) {
|
||||||
const launches = Array.isArray(payload) ? payload : payload?.results;
|
const launches = Array.isArray(payload) ? payload : payload?.results;
|
||||||
if (!Array.isArray(launches)) return [];
|
if (!Array.isArray(launches)) return [];
|
||||||
const cutoff = now.getTime() - WINDOW_DAYS * 86400000;
|
const cutoff = now.getTime() - WINDOW_DAYS * 86400000;
|
||||||
|
|||||||
@@ -1,68 +1,12 @@
|
|||||||
import * as Cesium from 'cesium';
|
import { appClock } from '../../time/clock.js';
|
||||||
import {
|
import * as geo from '../../geo/index.js';
|
||||||
MISSION_ORBIT_PATTERN_GROUPS,
|
import { rgbaFromCss } from '../shared/color.js';
|
||||||
MISSION_ORBIT_DASHES_PER_GROUP,
|
|
||||||
} from './policy.js';
|
|
||||||
|
|
||||||
export function createOrbitRendering({
|
export function createOrbitRendering({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
services,
|
services,
|
||||||
parts,
|
getRenderer,
|
||||||
source,
|
|
||||||
}) {
|
}) {
|
||||||
const { orbitFrameModelMatrix } = services.satellites;
|
const { orbitFrameModelMatrix } = services.satellites;
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the selected-orbit tactical material once. Each group begins with
|
|
||||||
* one compact round dot followed by one hundred short dashes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function ensureMissionOrbitPatternRegistered() {
|
|
||||||
if (layerState._missionOrbitPatternRegistered) return;
|
|
||||||
new Cesium.Material({
|
|
||||||
fabric: {
|
|
||||||
type: 'GevMissionOrbitTactical',
|
|
||||||
uniforms: {
|
|
||||||
color: Cesium.Color.CYAN,
|
|
||||||
groupCount: MISSION_ORBIT_PATTERN_GROUPS,
|
|
||||||
dashCount: MISSION_ORBIT_DASHES_PER_GROUP,
|
|
||||||
},
|
|
||||||
source: `
|
|
||||||
czm_material czm_getMaterial(czm_materialInput materialInput) {
|
|
||||||
czm_material material = czm_getDefaultMaterial(materialInput);
|
|
||||||
float groupPosition = fract(materialInput.st.s * groupCount);
|
|
||||||
float markPosition = groupPosition * (dashCount + 1.0);
|
|
||||||
float markIndex = floor(markPosition);
|
|
||||||
float localPosition = fract(markPosition);
|
|
||||||
float centerDistance = abs(localPosition - 0.5);
|
|
||||||
float edge = max(fwidth(localPosition) * 1.35, 0.012);
|
|
||||||
float dashAlong = 1.0 - smoothstep(0.27 - edge, 0.27 + edge, centerDistance);
|
|
||||||
float dashAcross = 1.0 - smoothstep(0.12, 0.24, abs(materialInput.st.t - 0.5));
|
|
||||||
float dash = dashAlong * dashAcross;
|
|
||||||
float dotAlong = (localPosition - 0.5) / 0.32;
|
|
||||||
float dotAcross = (materialInput.st.t - 0.5) / 0.5;
|
|
||||||
float dot = 1.0 - smoothstep(0.78, 1.0, length(vec2(dotAlong, dotAcross)));
|
|
||||||
float isDot = 1.0 - step(0.5, markIndex);
|
|
||||||
float visible = mix(dash, dot, isDot);
|
|
||||||
material.diffuse = color.rgb;
|
|
||||||
material.emission = color.rgb * mix(0.07, 0.65, isDot);
|
|
||||||
material.alpha = color.a * visible * mix(0.58, 1.0, isDot);
|
|
||||||
return material;
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
layerState._missionOrbitPatternRegistered = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMissionOrbitPatternMaterial(color) {
|
|
||||||
ensureMissionOrbitPatternRegistered();
|
|
||||||
return Cesium.Material.fromType('GevMissionOrbitTactical', {
|
|
||||||
color,
|
|
||||||
groupCount: MISSION_ORBIT_PATTERN_GROUPS,
|
|
||||||
dashCount: MISSION_ORBIT_DASHES_PER_GROUP,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function missionOrbitPrimitiveVisible(launchId) {
|
function missionOrbitPrimitiveVisible(launchId) {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
layerState._enabled &&
|
layerState._enabled &&
|
||||||
@@ -81,8 +25,8 @@ export function createOrbitRendering({
|
|||||||
|
|
||||||
function removeMissionOrbitPrimitives() {
|
function removeMissionOrbitPrimitives() {
|
||||||
for (const path of layerState._missionOrbitPrimitives.values()) {
|
for (const path of layerState._missionOrbitPrimitives.values()) {
|
||||||
if (path.primitive && layerState._viewer?.scene?.primitives) {
|
if (path.primitive && layerState._viewer) {
|
||||||
layerState._viewer.scene.primitives.remove(path.primitive);
|
getRenderer().removePrimitive(path.primitive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
layerState._missionOrbitPrimitives.clear();
|
layerState._missionOrbitPrimitives.clear();
|
||||||
@@ -96,8 +40,9 @@ export function createOrbitRendering({
|
|||||||
nowDate,
|
nowDate,
|
||||||
path.primitive.modelMatrix,
|
path.primitive.modelMatrix,
|
||||||
);
|
);
|
||||||
|
getRenderer().commitOrbit(path.primitive);
|
||||||
if (path.labelBakePosition && path.labelPosition) {
|
if (path.labelBakePosition && path.labelPosition) {
|
||||||
Cesium.Matrix4.multiplyByPoint(
|
geo.matrix4.multiplyByPoint(
|
||||||
path.primitive.modelMatrix,
|
path.primitive.modelMatrix,
|
||||||
path.labelBakePosition,
|
path.labelBakePosition,
|
||||||
path.labelPosition,
|
path.labelPosition,
|
||||||
@@ -113,71 +58,30 @@ export function createOrbitRendering({
|
|||||||
!Number.isFinite(satelliteTrack.gmstAtBake)
|
!Number.isFinite(satelliteTrack.gmstAtBake)
|
||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
const collection = new Cesium.PolylineCollection();
|
const collection = getRenderer().createOrbit({
|
||||||
collection.add({
|
|
||||||
positions: orbitPath,
|
positions: orbitPath,
|
||||||
width: 3,
|
width: 3,
|
||||||
material: createMissionOrbitPatternMaterial(
|
color: rgbaFromCss('#22e6e6').withAlpha(0.95),
|
||||||
Cesium.Color.fromCssColorString('#22e6e6').withAlpha(0.95),
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
collection.show = missionOrbitPrimitiveVisible(launch.id);
|
collection.show = missionOrbitPrimitiveVisible(launch.id);
|
||||||
orbitFrameModelMatrix(
|
orbitFrameModelMatrix(
|
||||||
satelliteTrack.gmstAtBake,
|
satelliteTrack.gmstAtBake,
|
||||||
new Date(),
|
new Date(appClock.now()),
|
||||||
collection.modelMatrix,
|
collection.modelMatrix,
|
||||||
);
|
);
|
||||||
layerState._viewer.scene.primitives.add(collection);
|
|
||||||
layerState._missionOrbitPrimitives.set(launch.id, {
|
layerState._missionOrbitPrimitives.set(launch.id, {
|
||||||
primitive: collection,
|
primitive: collection,
|
||||||
|
basePositions: orbitPath,
|
||||||
gmstAtBake: satelliteTrack.gmstAtBake,
|
gmstAtBake: satelliteTrack.gmstAtBake,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MissionOrbitPatternMaterialProperty(color) {
|
|
||||||
ensureMissionOrbitPatternRegistered();
|
|
||||||
this._color = color;
|
|
||||||
this._definitionChanged = new Cesium.Event();
|
|
||||||
}
|
|
||||||
Object.defineProperties(MissionOrbitPatternMaterialProperty.prototype, {
|
|
||||||
isConstant: {
|
|
||||||
get() {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
definitionChanged: {
|
|
||||||
get() {
|
|
||||||
return this._definitionChanged;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
MissionOrbitPatternMaterialProperty.prototype.getType = function getType() {
|
|
||||||
return 'GevMissionOrbitTactical';
|
|
||||||
};
|
|
||||||
MissionOrbitPatternMaterialProperty.prototype.getValue = function getValue(
|
|
||||||
time,
|
|
||||||
result,
|
|
||||||
) {
|
|
||||||
if (!Cesium.defined(result)) result = {};
|
|
||||||
result.color = this._color;
|
|
||||||
result.groupCount = MISSION_ORBIT_PATTERN_GROUPS;
|
|
||||||
result.dashCount = MISSION_ORBIT_DASHES_PER_GROUP;
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
MissionOrbitPatternMaterialProperty.prototype.equals = function equals(
|
|
||||||
other,
|
|
||||||
) {
|
|
||||||
return this === other;
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
ensureMissionOrbitPatternRegistered,
|
|
||||||
createMissionOrbitPatternMaterial,
|
|
||||||
missionOrbitPrimitiveVisible,
|
missionOrbitPrimitiveVisible,
|
||||||
syncMissionOrbitPrimitiveVisibility,
|
syncMissionOrbitPrimitiveVisibility,
|
||||||
removeMissionOrbitPrimitives,
|
removeMissionOrbitPrimitives,
|
||||||
updateMissionOrbitPrimitiveFrames,
|
updateMissionOrbitPrimitiveFrames,
|
||||||
addMissionOrbitPrimitive,
|
addMissionOrbitPrimitive,
|
||||||
MissionOrbitPatternMaterialProperty,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { engineFromViewer } from '../../globe/cesium/engineFromViewer.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import * as Cesium from 'cesium';
|
|
||||||
import {
|
import {
|
||||||
createCyberSonarSampler,
|
createCyberSonarSampler,
|
||||||
isCyberContactSonarActive as isCyberSonarActive,
|
isCyberContactSonarActive as isCyberSonarActive,
|
||||||
@@ -12,7 +11,13 @@ import {
|
|||||||
ROCKET_MISSION_AMBIENT_OVERLAY_SOURCE_OPTIONS,
|
ROCKET_MISSION_AMBIENT_OVERLAY_SOURCE_OPTIONS,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
|
||||||
export function createOverlays({ state: layerState, services, parts, source }) {
|
export function createOverlays({
|
||||||
|
state: layerState,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
source,
|
||||||
|
getRenderer,
|
||||||
|
}) {
|
||||||
/**
|
/**
|
||||||
* Resolve whether a surface mission anchor is safely on the camera-facing
|
* Resolve whether a surface mission anchor is safely on the camera-facing
|
||||||
* side of Earth. The small positive limb margin prevents labels anchored just
|
* side of Earth. The small positive limb margin prevents labels anchored just
|
||||||
@@ -29,25 +34,24 @@ export function createOverlays({ state: layerState, services, parts, source }) {
|
|||||||
limbMargin = 0.012,
|
limbMargin = 0.012,
|
||||||
) {
|
) {
|
||||||
if (!cameraPosition || !markerPosition) return false;
|
if (!cameraPosition || !markerPosition) return false;
|
||||||
const cameraMagnitude = Cesium.Cartesian3.magnitude(cameraPosition);
|
const cameraMagnitude = geo.cartesian3.magnitude(cameraPosition);
|
||||||
if (
|
if (
|
||||||
!Number.isFinite(cameraMagnitude) ||
|
!Number.isFinite(cameraMagnitude) ||
|
||||||
cameraMagnitude <= Cesium.Ellipsoid.WGS84.maximumRadius
|
cameraMagnitude <= geo.ellipsoid.WGS84.maximumRadius
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const cameraDirection = Cesium.Cartesian3.normalize(
|
const cameraDirection = geo.cartesian3.normalize(
|
||||||
cameraPosition,
|
cameraPosition,
|
||||||
new Cesium.Cartesian3(),
|
geo.cartesian3.create(),
|
||||||
);
|
);
|
||||||
const markerDirection = Cesium.Cartesian3.normalize(
|
const markerDirection = geo.cartesian3.normalize(
|
||||||
markerPosition,
|
markerPosition,
|
||||||
new Cesium.Cartesian3(),
|
geo.cartesian3.create(),
|
||||||
);
|
);
|
||||||
const limbThreshold =
|
const limbThreshold = geo.ellipsoid.WGS84.maximumRadius / cameraMagnitude;
|
||||||
Cesium.Ellipsoid.WGS84.maximumRadius / cameraMagnitude;
|
|
||||||
return (
|
return (
|
||||||
Cesium.Cartesian3.dot(cameraDirection, markerDirection) >
|
geo.cartesian3.dot(cameraDirection, markerDirection) >
|
||||||
limbThreshold + limbMargin
|
limbThreshold + limbMargin
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -484,15 +488,7 @@ export function createOverlays({ state: layerState, services, parts, source }) {
|
|||||||
hideReplayVehicleOverlay();
|
hideReplayVehicleOverlay();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// The engine's projector (engineFromViewer), as the world overlay.
|
const project = (point) => getRenderer().project(point);
|
||||||
const projector = engineFromViewer(layerState._viewer)?.projector;
|
|
||||||
const project = (point) =>
|
|
||||||
projector
|
|
||||||
? projector.worldToWindow(point)
|
|
||||||
: Cesium.SceneTransforms.worldToWindowCoordinates(
|
|
||||||
layerState._viewer.scene,
|
|
||||||
point,
|
|
||||||
);
|
|
||||||
const windowPosition = project(position);
|
const windowPosition = project(position);
|
||||||
if (!windowPosition) {
|
if (!windowPosition) {
|
||||||
hideReplayVehicleOverlay();
|
hideReplayVehicleOverlay();
|
||||||
@@ -584,7 +580,7 @@ export function createOverlays({ state: layerState, services, parts, source }) {
|
|||||||
}
|
}
|
||||||
layerState._replayVehicleOverlay.style.setProperty(
|
layerState._replayVehicleOverlay.style.setProperty(
|
||||||
'--mission-replay-rotation',
|
'--mission-replay-rotation',
|
||||||
`${Cesium.Math.toDegrees(vehicleRotation)}deg`,
|
`${geo.math.toDegrees(vehicleRotation)}deg`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mission = shortMissionLabel(launch.name, 22).toUpperCase();
|
const mission = shortMissionLabel(launch.name, 22).toUpperCase();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Cesium from 'cesium';
|
|
||||||
import { appClock } from '../../time/clock.js';
|
import { appClock } from '../../time/clock.js';
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import {
|
import {
|
||||||
@@ -6,7 +5,13 @@ import {
|
|||||||
MISSION_GLOBE_VIEW_RANGE_M,
|
MISSION_GLOBE_VIEW_RANGE_M,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
|
||||||
export function createPanel({ state: layerState, services, parts, source }) {
|
export function createPanel({
|
||||||
|
state: layerState,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
source,
|
||||||
|
getRenderer,
|
||||||
|
}) {
|
||||||
/**
|
/**
|
||||||
* Preserve the user's globe scale for roster previews while avoiding an
|
* Preserve the user's globe scale for roster previews while avoiding an
|
||||||
* accidental surface-level fly-to when the list is opened from a close view.
|
* accidental surface-level fly-to when the list is opened from a close view.
|
||||||
@@ -359,10 +364,7 @@ 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.fromDate(
|
getRenderer().time(appClock.now()),
|
||||||
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,3 @@
|
|||||||
import * as Cesium from 'cesium';
|
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import {
|
import {
|
||||||
SATELLITE_STANDALONE_DEFAULTS,
|
SATELLITE_STANDALONE_DEFAULTS,
|
||||||
@@ -11,6 +10,7 @@ export function createPolicyHelpers({
|
|||||||
services,
|
services,
|
||||||
parts,
|
parts,
|
||||||
source,
|
source,
|
||||||
|
getRenderer,
|
||||||
}) {
|
}) {
|
||||||
/**
|
/**
|
||||||
* Derive the temporary Satellite display mode required by Space Missions.
|
* Derive the temporary Satellite display mode required by Space Missions.
|
||||||
@@ -229,11 +229,11 @@ export function createPolicyHelpers({
|
|||||||
);
|
);
|
||||||
const position = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
|
const position = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
|
||||||
layerState._viewer.camera.flyToBoundingSphere(
|
layerState._viewer.camera.flyToBoundingSphere(
|
||||||
new Cesium.BoundingSphere(position, 0),
|
geo.boundingSphere.create(position, 0),
|
||||||
{
|
{
|
||||||
offset: new Cesium.HeadingPitchRange(0, -geo.math.PI_OVER_TWO, range),
|
offset: getRenderer().range(0, -geo.math.PI_OVER_TWO, range),
|
||||||
duration: 0.8,
|
duration: 0.8,
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
easingFunction: getRenderer().easing,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
// Native mission graphics stay here; shared mission logic uses plain geometry.
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import { isPointerFree } from '../../../data/inputOwnership.js';
|
||||||
|
import {
|
||||||
|
MISSION_ORBIT_PATTERN_GROUPS,
|
||||||
|
MISSION_ORBIT_DASHES_PER_GROUP,
|
||||||
|
LAUNCH_PAD_ZONE_RADIUS_M,
|
||||||
|
} from '../policy.js';
|
||||||
|
export function createLaunchesCesiumRenderer(engine, { viewer } = {}) {
|
||||||
|
let patternRegistered = false;
|
||||||
|
const scratchTime = new Cesium.JulianDate();
|
||||||
|
const color = (value) =>
|
||||||
|
value?.red !== undefined
|
||||||
|
? new Cesium.Color(value.red, value.green, value.blue, value.alpha)
|
||||||
|
: value;
|
||||||
|
function ensureMissionOrbitPatternRegistered() {
|
||||||
|
if (patternRegistered) return;
|
||||||
|
new Cesium.Material({
|
||||||
|
fabric: {
|
||||||
|
type: 'GevMissionOrbitTactical',
|
||||||
|
uniforms: {
|
||||||
|
color: Cesium.Color.CYAN,
|
||||||
|
groupCount: MISSION_ORBIT_PATTERN_GROUPS,
|
||||||
|
dashCount: MISSION_ORBIT_DASHES_PER_GROUP,
|
||||||
|
},
|
||||||
|
source: `
|
||||||
|
czm_material czm_getMaterial(czm_materialInput materialInput) {
|
||||||
|
czm_material material = czm_getDefaultMaterial(materialInput);
|
||||||
|
float groupPosition = fract(materialInput.st.s * groupCount);
|
||||||
|
float markPosition = groupPosition * (dashCount + 1.0);
|
||||||
|
float markIndex = floor(markPosition);
|
||||||
|
float localPosition = fract(markPosition);
|
||||||
|
float centerDistance = abs(localPosition - 0.5);
|
||||||
|
float edge = max(fwidth(localPosition) * 1.35, 0.012);
|
||||||
|
float dashAlong = 1.0 - smoothstep(0.27 - edge, 0.27 + edge, centerDistance);
|
||||||
|
float dashAcross = 1.0 - smoothstep(0.12, 0.24, abs(materialInput.st.t - 0.5));
|
||||||
|
float dash = dashAlong * dashAcross;
|
||||||
|
float dotAlong = (localPosition - 0.5) / 0.32;
|
||||||
|
float dotAcross = (materialInput.st.t - 0.5) / 0.5;
|
||||||
|
float dot = 1.0 - smoothstep(0.78, 1.0, length(vec2(dotAlong, dotAcross)));
|
||||||
|
float isDot = 1.0 - step(0.5, markIndex);
|
||||||
|
float visible = mix(dash, dot, isDot);
|
||||||
|
material.diffuse = color.rgb;
|
||||||
|
material.emission = color.rgb * mix(0.07, 0.65, isDot);
|
||||||
|
material.alpha = color.a * visible * mix(0.58, 1.0, isDot);
|
||||||
|
return material;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
patternRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMissionOrbitPatternMaterial(color) {
|
||||||
|
ensureMissionOrbitPatternRegistered();
|
||||||
|
return Cesium.Material.fromType('GevMissionOrbitTactical', {
|
||||||
|
color,
|
||||||
|
groupCount: MISSION_ORBIT_PATTERN_GROUPS,
|
||||||
|
dashCount: MISSION_ORBIT_DASHES_PER_GROUP,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function MissionOrbitPatternMaterialProperty(color) {
|
||||||
|
ensureMissionOrbitPatternRegistered();
|
||||||
|
this._color = color;
|
||||||
|
this._definitionChanged = new Cesium.Event();
|
||||||
|
}
|
||||||
|
Object.defineProperties(MissionOrbitPatternMaterialProperty.prototype, {
|
||||||
|
isConstant: {
|
||||||
|
get() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
definitionChanged: {
|
||||||
|
get() {
|
||||||
|
return this._definitionChanged;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
MissionOrbitPatternMaterialProperty.prototype.getType = function getType() {
|
||||||
|
return 'GevMissionOrbitTactical';
|
||||||
|
};
|
||||||
|
MissionOrbitPatternMaterialProperty.prototype.getValue = function getValue(
|
||||||
|
time,
|
||||||
|
result,
|
||||||
|
) {
|
||||||
|
if (!Cesium.defined(result)) result = {};
|
||||||
|
result.color = this._color;
|
||||||
|
result.groupCount = MISSION_ORBIT_PATTERN_GROUPS;
|
||||||
|
result.dashCount = MISSION_ORBIT_DASHES_PER_GROUP;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
MissionOrbitPatternMaterialProperty.prototype.equals = function equals(
|
||||||
|
other,
|
||||||
|
) {
|
||||||
|
return this === other;
|
||||||
|
};
|
||||||
|
function createPad(launch) {
|
||||||
|
const material = new Cesium.Material({
|
||||||
|
fabric: {
|
||||||
|
type: 'GevLaunchPadZone',
|
||||||
|
uniforms: {
|
||||||
|
color: Cesium.Color.fromCssColorString('#22e6e6'),
|
||||||
|
fillAlpha: 0.105,
|
||||||
|
rimAlpha: 0.72,
|
||||||
|
},
|
||||||
|
source: `
|
||||||
|
czm_material czm_getMaterial(czm_materialInput materialInput) {
|
||||||
|
czm_material material = czm_getDefaultMaterial(materialInput);
|
||||||
|
vec2 centered = (materialInput.st - vec2(0.5)) * 2.0;
|
||||||
|
float radius = length(centered);
|
||||||
|
float inside = 1.0 - smoothstep(0.985, 1.0, radius);
|
||||||
|
float rim = smoothstep(0.952, 0.985, radius) * inside;
|
||||||
|
material.diffuse = color.rgb;
|
||||||
|
material.emission = color.rgb * rim * 0.35;
|
||||||
|
material.alpha = color.a * inside * mix(fillAlpha, rimAlpha, rim);
|
||||||
|
return material;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const geometry = new Cesium.EllipseGeometry({
|
||||||
|
center: Cesium.Cartesian3.fromDegrees(launch.lon, launch.lat),
|
||||||
|
semiMajorAxis: LAUNCH_PAD_ZONE_RADIUS_M,
|
||||||
|
semiMinorAxis: LAUNCH_PAD_ZONE_RADIUS_M,
|
||||||
|
granularity: Cesium.Math.toRadians(0.08),
|
||||||
|
vertexFormat:
|
||||||
|
Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat,
|
||||||
|
});
|
||||||
|
const primitive = viewer.scene.primitives.add(
|
||||||
|
new Cesium.GroundPrimitive({
|
||||||
|
geometryInstances: new Cesium.GeometryInstance({
|
||||||
|
geometry,
|
||||||
|
id: `rocket-pad-zone:${launch.id}`,
|
||||||
|
}),
|
||||||
|
appearance: new Cesium.MaterialAppearance({
|
||||||
|
material,
|
||||||
|
translucent: true,
|
||||||
|
closed: false,
|
||||||
|
faceForward: true,
|
||||||
|
flat: true,
|
||||||
|
// Keep the zone classified onto the photoreal surface, but bias only its
|
||||||
|
// rasterized depth toward the camera. This avoids coplanar fragments
|
||||||
|
// being intermittently buried by the launch-pad mesh at oblique angles
|
||||||
|
// without adding a world-space height that would make the ring float.
|
||||||
|
renderState: {
|
||||||
|
depthTest: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
depthMask: false,
|
||||||
|
polygonOffset: {
|
||||||
|
enabled: true,
|
||||||
|
factor: -1,
|
||||||
|
units: -4,
|
||||||
|
},
|
||||||
|
blending: Cesium.BlendingState.ALPHA_BLEND,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
classificationType: Cesium.ClassificationType.BOTH,
|
||||||
|
asynchronous: true,
|
||||||
|
show: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return primitive;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'cesium',
|
||||||
|
engine,
|
||||||
|
view: viewer,
|
||||||
|
time(epochMs) {
|
||||||
|
return Cesium.JulianDate.fromDate(new Date(epochMs), scratchTime);
|
||||||
|
},
|
||||||
|
occluder: () =>
|
||||||
|
new Cesium.EllipsoidalOccluder(
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
Cesium.Cartesian3.ZERO,
|
||||||
|
),
|
||||||
|
callback: (fn) => new Cesium.CallbackProperty(fn, false),
|
||||||
|
range: (heading, pitch, range) =>
|
||||||
|
new Cesium.HeadingPitchRange(heading, pitch, range),
|
||||||
|
easing: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||||
|
createDataSource() {
|
||||||
|
const source = new Cesium.CustomDataSource('rocket-launches');
|
||||||
|
source.show = false;
|
||||||
|
viewer.dataSources.add(source);
|
||||||
|
return source;
|
||||||
|
},
|
||||||
|
removeDataSource: (source) => viewer?.dataSources.remove(source, true),
|
||||||
|
addEntity(source, spec) {
|
||||||
|
const options = { ...spec };
|
||||||
|
for (const key of ['point', 'billboard'])
|
||||||
|
if (spec[key])
|
||||||
|
options[key] = {
|
||||||
|
...spec[key],
|
||||||
|
...(spec[key].color ? { color: color(spec[key].color) } : {}),
|
||||||
|
...(spec[key].outlineColor
|
||||||
|
? { outlineColor: color(spec[key].outlineColor) }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
if (spec.polyline) {
|
||||||
|
const { arc, ...line } = spec.polyline;
|
||||||
|
options.polyline = {
|
||||||
|
...line,
|
||||||
|
material: color(line.material),
|
||||||
|
...(arc
|
||||||
|
? {
|
||||||
|
arcType:
|
||||||
|
arc === 'none'
|
||||||
|
? Cesium.ArcType.NONE
|
||||||
|
: Cesium.ArcType.GEODESIC,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return source.entities.add(options);
|
||||||
|
},
|
||||||
|
dash: (options) =>
|
||||||
|
new Cesium.PolylineDashMaterialProperty({
|
||||||
|
...options,
|
||||||
|
color: color(options.color),
|
||||||
|
}),
|
||||||
|
patternMaterial: (value) =>
|
||||||
|
new MissionOrbitPatternMaterialProperty(color(value)),
|
||||||
|
createOrbit({ positions, width, color: rgba }) {
|
||||||
|
const collection = new Cesium.PolylineCollection();
|
||||||
|
collection.add({
|
||||||
|
positions,
|
||||||
|
width,
|
||||||
|
material: createMissionOrbitPatternMaterial(color(rgba)),
|
||||||
|
});
|
||||||
|
viewer.scene.primitives.add(collection);
|
||||||
|
return collection;
|
||||||
|
},
|
||||||
|
commitOrbit() {},
|
||||||
|
removePrimitive: (primitive) => viewer?.scene.primitives.remove(primitive),
|
||||||
|
createPad,
|
||||||
|
project: (position, result) =>
|
||||||
|
Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||||
|
viewer.scene,
|
||||||
|
position,
|
||||||
|
result,
|
||||||
|
),
|
||||||
|
on: (event, listener) => viewer.scene[event].addEventListener(listener),
|
||||||
|
listenClick(source, listener, { enabled = () => true } = {}) {
|
||||||
|
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
||||||
|
handler.setInputAction((movement) => {
|
||||||
|
if (!isPointerFree()) return;
|
||||||
|
if (!enabled()) return;
|
||||||
|
const entity = viewer.scene
|
||||||
|
.drillPick(movement.position, 12)
|
||||||
|
.map((p) => p?.id)
|
||||||
|
.find((e) => e?.id?.startsWith('rocket-'));
|
||||||
|
listener(entity);
|
||||||
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||||
|
return { destroy: () => handler.destroy() };
|
||||||
|
},
|
||||||
|
listenMove(source, listener, { enabled = () => true } = {}) {
|
||||||
|
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
||||||
|
handler.setInputAction((movement) => {
|
||||||
|
if (!enabled()) return;
|
||||||
|
const entity = viewer.scene
|
||||||
|
.drillPick(movement.endPosition, 12)
|
||||||
|
.map((p) => p?.id)
|
||||||
|
.find((e) => e?.id?.startsWith('rocket-'));
|
||||||
|
listener(entity);
|
||||||
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
||||||
|
return { destroy: () => handler.destroy() };
|
||||||
|
},
|
||||||
|
destroy() {},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import { rgbaFromCss } from '../shared/color.js';
|
||||||
|
import * as geo from '../../geo/index.js';
|
||||||
import { appClock } from '../../time/clock.js';
|
import { appClock } from '../../time/clock.js';
|
||||||
import {
|
import {
|
||||||
TRAJECTORY_STAGE_COLORS,
|
TRAJECTORY_STAGE_COLORS,
|
||||||
@@ -12,6 +13,7 @@ export function createRendering({
|
|||||||
services,
|
services,
|
||||||
parts,
|
parts,
|
||||||
source,
|
source,
|
||||||
|
getRenderer,
|
||||||
}) {
|
}) {
|
||||||
const { getSatelliteOrbitTrack, findSatelliteOrbitTrackInTle } =
|
const { getSatelliteOrbitTrack, findSatelliteOrbitTrackInTle } =
|
||||||
services.satellites;
|
services.satellites;
|
||||||
@@ -34,10 +36,8 @@ export function createRendering({
|
|||||||
}
|
}
|
||||||
parts.panel.updateMissionTelemetry();
|
parts.panel.updateMissionTelemetry();
|
||||||
// The app clock's now, as the JulianDate Cesium's entity properties take.
|
// The app clock's now, as the JulianDate Cesium's entity properties take.
|
||||||
const time = Cesium.JulianDate.fromDate(
|
const time = getRenderer().time(appClock.now());
|
||||||
new Date(appClock.now()),
|
layerState._declutterTime = time;
|
||||||
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);
|
||||||
@@ -77,7 +77,7 @@ export function createRendering({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addLaunchEntity(launch, activeTleText = layerState._activeTleText) {
|
function addLaunchEntity(launch, activeTleText = layerState._activeTleText) {
|
||||||
const position = Cesium.Cartesian3.fromDegrees(launch.lon, launch.lat);
|
const position = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
|
||||||
const overlayRecord = {
|
const overlayRecord = {
|
||||||
launch,
|
launch,
|
||||||
anchorPosition: position,
|
anchorPosition: position,
|
||||||
@@ -114,7 +114,7 @@ export function createRendering({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (satelliteTrack) layerState._orbitMatches++;
|
if (satelliteTrack) layerState._orbitMatches++;
|
||||||
const entity = layerState._dataSource.entities.add({
|
const entity = getRenderer().addEntity(layerState._dataSource, {
|
||||||
id: `rocket-launch:${launch.id}`,
|
id: `rocket-launch:${launch.id}`,
|
||||||
position,
|
position,
|
||||||
point: {
|
point: {
|
||||||
@@ -129,8 +129,8 @@ export function createRendering({
|
|||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
show: false,
|
show: false,
|
||||||
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
horizontalOrigin: 0,
|
||||||
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
verticalOrigin: 0,
|
||||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
@@ -147,6 +147,10 @@ export function createRendering({
|
|||||||
noradId: satelliteTrack?.noradId || '',
|
noradId: satelliteTrack?.noradId || '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Publish the same anchor representation held by the active renderer.
|
||||||
|
overlayRecord.anchorPosition = entity.position.getValue(
|
||||||
|
getRenderer().time(appClock.now()),
|
||||||
|
);
|
||||||
const points = launch.trajectory
|
const points = launch.trajectory
|
||||||
.filter(
|
.filter(
|
||||||
(point) =>
|
(point) =>
|
||||||
@@ -161,7 +165,7 @@ export function createRendering({
|
|||||||
point.stageName ||
|
point.stageName ||
|
||||||
'trajectory',
|
'trajectory',
|
||||||
),
|
),
|
||||||
position: Cesium.Cartesian3.fromDegrees(
|
position: geo.cartesian3.fromDegrees(
|
||||||
Number(point.longitude),
|
Number(point.longitude),
|
||||||
Number(point.latitude),
|
Number(point.latitude),
|
||||||
Number(point.altitude || 0),
|
Number(point.altitude || 0),
|
||||||
@@ -177,16 +181,16 @@ export function createRendering({
|
|||||||
});
|
});
|
||||||
segments.forEach((segment, index) => {
|
segments.forEach((segment, index) => {
|
||||||
if (segment.positions.length < 2) return;
|
if (segment.positions.length < 2) return;
|
||||||
layerState._dataSource.entities.add({
|
getRenderer().addEntity(layerState._dataSource, {
|
||||||
id: `rocket-trajectory:${launch.id}:${index}`,
|
id: `rocket-trajectory:${launch.id}:${index}`,
|
||||||
polyline: {
|
polyline: {
|
||||||
positions: parts.paths.surfaceSafePath(segment.positions),
|
positions: parts.paths.surfaceSafePath(segment.positions),
|
||||||
width: 2,
|
width: 2,
|
||||||
material: Cesium.Color.fromCssColorString(
|
material: rgbaFromCss(
|
||||||
TRAJECTORY_STAGE_COLORS[index % TRAJECTORY_STAGE_COLORS.length],
|
TRAJECTORY_STAGE_COLORS[index % TRAJECTORY_STAGE_COLORS.length],
|
||||||
).withAlpha(0.8),
|
).withAlpha(0.8),
|
||||||
clampToGround: false,
|
clampToGround: false,
|
||||||
arcType: Cesium.ArcType.NONE,
|
arc: 'none',
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
launchId: launch.id,
|
launchId: launch.id,
|
||||||
@@ -224,7 +228,7 @@ export function createRendering({
|
|||||||
new Date(insertionEpochMs),
|
new Date(insertionEpochMs),
|
||||||
);
|
);
|
||||||
if (propagatedInsertion) {
|
if (propagatedInsertion) {
|
||||||
insertionReference = Cesium.Cartesian3.fromDegrees(
|
insertionReference = geo.cartesian3.fromDegrees(
|
||||||
propagatedInsertion.longitude,
|
propagatedInsertion.longitude,
|
||||||
propagatedInsertion.latitude,
|
propagatedInsertion.latitude,
|
||||||
propagatedInsertion.altitude,
|
propagatedInsertion.altitude,
|
||||||
@@ -237,7 +241,7 @@ export function createRendering({
|
|||||||
// ascent duration, and join the forward orbit tangent. Using the UTC
|
// ascent duration, and join the forward orbit tangent. Using the UTC
|
||||||
// epoch as phase sent some ascents toward the far side of Earth and
|
// epoch as phase sent some ascents toward the far side of Earth and
|
||||||
// forced a visible 180-degree corrective hook before insertion.
|
// forced a visible 180-degree corrective hook before insertion.
|
||||||
const insertionProgress = Cesium.Math.clamp(
|
const insertionProgress = geo.math.clamp(
|
||||||
insertionDurationSec / orbitPeriodSec,
|
insertionDurationSec / orbitPeriodSec,
|
||||||
1 / 96,
|
1 / 96,
|
||||||
0.16,
|
0.16,
|
||||||
@@ -259,14 +263,14 @@ export function createRendering({
|
|||||||
);
|
);
|
||||||
// Cesium can evaluate CallbackProperty values multiple times while it
|
// Cesium can evaluate CallbackProperty values multiple times while it
|
||||||
// traverses one frame. Camera tracking and the HTML replay overlay must
|
// traverses one frame. Camera tracking and the HTML replay overlay must
|
||||||
// use the exact same replay epoch as those callbacks; sampling Date.now()
|
// use the exact same replay epoch as those callbacks; sampling appClock.now()
|
||||||
// independently made the vehicle advance relative to the camera and then
|
// independently made the vehicle advance relative to the camera and then
|
||||||
// snap back on the next frame.
|
// snap back on the next frame.
|
||||||
let replayStateForFrame = null;
|
let replayStateForFrame = null;
|
||||||
const sampleReplayState = () =>
|
const sampleReplayState = () =>
|
||||||
parts.replay.replayState(
|
parts.replay.replayState(
|
||||||
launch,
|
launch,
|
||||||
layerState._animationStarts.get(launch.id) || Date.now(),
|
layerState._animationStarts.get(launch.id) || appClock.now(),
|
||||||
ascentDurationSec,
|
ascentDurationSec,
|
||||||
REPLAY_ORBIT_DURATION_SEC,
|
REPLAY_ORBIT_DURATION_SEC,
|
||||||
orbitPeriodSec,
|
orbitPeriodSec,
|
||||||
@@ -304,22 +308,22 @@ export function createRendering({
|
|||||||
if (path.length < 2) return;
|
if (path.length < 2) return;
|
||||||
const reentryIndex = parts.paths.atmosphericReentryIndex(path);
|
const reentryIndex = parts.paths.atmosphericReentryIndex(path);
|
||||||
const reentryPath = path.slice(reentryIndex);
|
const reentryPath = path.slice(reentryIndex);
|
||||||
const color = Cesium.Color.fromCssColorString(
|
const color = rgbaFromCss(
|
||||||
TRAJECTORY_STAGE_COLORS[
|
TRAJECTORY_STAGE_COLORS[
|
||||||
(stageIndex + 1) % TRAJECTORY_STAGE_COLORS.length
|
(stageIndex + 1) % TRAJECTORY_STAGE_COLORS.length
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
layerState._dataSource.entities.add({
|
getRenderer().addEntity(layerState._dataSource, {
|
||||||
id: `rocket-reentry-recovery:${launch.id}:${stageIndex}`,
|
id: `rocket-reentry-recovery:${launch.id}:${stageIndex}`,
|
||||||
polyline: {
|
polyline: {
|
||||||
positions: path,
|
positions: path,
|
||||||
width: 2,
|
width: 2,
|
||||||
material: new Cesium.PolylineDashMaterialProperty({
|
material: getRenderer().dash({
|
||||||
color: color.withAlpha(0.82),
|
color: color.withAlpha(0.82),
|
||||||
dashLength: 12,
|
dashLength: 12,
|
||||||
dashPattern: 0xaaaa,
|
dashPattern: 0xaaaa,
|
||||||
}),
|
}),
|
||||||
arcType: Cesium.ArcType.NONE,
|
arc: 'none',
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
launchId: launch.id,
|
launchId: launch.id,
|
||||||
@@ -330,18 +334,17 @@ export function createRendering({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (reentryPath.length > 1) {
|
if (reentryPath.length > 1) {
|
||||||
layerState._dataSource.entities.add({
|
getRenderer().addEntity(layerState._dataSource, {
|
||||||
id: `rocket-reentry:${launch.id}:${stageIndex}`,
|
id: `rocket-reentry:${launch.id}:${stageIndex}`,
|
||||||
polyline: {
|
polyline: {
|
||||||
positions: reentryPath,
|
positions: reentryPath,
|
||||||
width: 2.5,
|
width: 2.5,
|
||||||
material: new Cesium.PolylineDashMaterialProperty({
|
material: getRenderer().dash({
|
||||||
color:
|
color: rgbaFromCss('#ffd166').withAlpha(0.9),
|
||||||
Cesium.Color.fromCssColorString('#ffd166').withAlpha(0.9),
|
|
||||||
dashLength: 8,
|
dashLength: 8,
|
||||||
dashPattern: 0xf0f0,
|
dashPattern: 0xf0f0,
|
||||||
}),
|
}),
|
||||||
arcType: Cesium.ArcType.NONE,
|
arc: 'none',
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
launchId: launch.id,
|
launchId: launch.id,
|
||||||
@@ -363,7 +366,7 @@ export function createRendering({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
layerState._dataSource.entities.add({
|
getRenderer().addEntity(layerState._dataSource, {
|
||||||
id: `rocket-recovery-end:${launch.id}:${stageIndex}`,
|
id: `rocket-recovery-end:${launch.id}:${stageIndex}`,
|
||||||
position: path.at(-1),
|
position: path.at(-1),
|
||||||
point: {
|
point: {
|
||||||
@@ -394,12 +397,12 @@ export function createRendering({
|
|||||||
lastOverlayWindowPosition: null,
|
lastOverlayWindowPosition: null,
|
||||||
lastOverlayMode: null,
|
lastOverlayMode: null,
|
||||||
});
|
});
|
||||||
let livePosition = Cesium.Cartesian3.fromDegrees(
|
let livePosition = geo.cartesian3.fromDegrees(
|
||||||
orbitCurrent.longitude,
|
orbitCurrent.longitude,
|
||||||
orbitCurrent.latitude,
|
orbitCurrent.latitude,
|
||||||
orbitCurrent.altitude,
|
orbitCurrent.altitude,
|
||||||
);
|
);
|
||||||
const liveTime = new Date();
|
const liveTime = new Date(appClock.now());
|
||||||
const liveTelemetry = { speedMps: null };
|
const liveTelemetry = { speedMps: null };
|
||||||
layerState._satelliteTelemetry.set(launch.id, liveTelemetry);
|
layerState._satelliteTelemetry.set(launch.id, liveTelemetry);
|
||||||
let liveFrameNumber = -1;
|
let liveFrameNumber = -1;
|
||||||
@@ -410,13 +413,13 @@ export function createRendering({
|
|||||||
layerState._viewer?.scene?.frameState?.frameNumber ?? -1;
|
layerState._viewer?.scene?.frameState?.frameNumber ?? -1;
|
||||||
if (frameNumber !== -1 && frameNumber === liveFrameNumber) return;
|
if (frameNumber !== -1 && frameNumber === liveFrameNumber) return;
|
||||||
liveFrameNumber = frameNumber;
|
liveFrameNumber = frameNumber;
|
||||||
const nowMs = Date.now();
|
const nowMs = appClock.now();
|
||||||
liveTime.setTime(nowMs);
|
liveTime.setTime(nowMs);
|
||||||
if (satelliteTrack) {
|
if (satelliteTrack) {
|
||||||
const propagated =
|
const propagated =
|
||||||
satelliteTrack.positionAt?.(liveTime) || satelliteTrack.current;
|
satelliteTrack.positionAt?.(liveTime) || satelliteTrack.current;
|
||||||
if (propagated) {
|
if (propagated) {
|
||||||
Cesium.Cartesian3.fromDegrees(
|
geo.cartesian3.fromDegrees(
|
||||||
propagated.longitude,
|
propagated.longitude,
|
||||||
propagated.latitude,
|
propagated.latitude,
|
||||||
propagated.altitude,
|
propagated.altitude,
|
||||||
@@ -438,17 +441,15 @@ export function createRendering({
|
|||||||
) || livePosition;
|
) || livePosition;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
layerState._dataSource.entities.add({
|
getRenderer().addEntity(layerState._dataSource, {
|
||||||
id: `rocket-satellite:${launch.id}`,
|
id: `rocket-satellite:${launch.id}`,
|
||||||
position: new Cesium.CallbackProperty(() => {
|
position: getRenderer().callback(() => {
|
||||||
updateLiveState();
|
updateLiveState();
|
||||||
return livePosition;
|
return livePosition;
|
||||||
}, false),
|
}, false),
|
||||||
point: {
|
point: {
|
||||||
pixelSize: 6,
|
pixelSize: 6,
|
||||||
color: Cesium.Color.fromCssColorString(
|
color: rgbaFromCss(satelliteTrack ? '#7bed9f' : '#ffd166'),
|
||||||
satelliteTrack ? '#7bed9f' : '#ffd166',
|
|
||||||
),
|
|
||||||
outlineWidth: 0,
|
outlineWidth: 0,
|
||||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||||
},
|
},
|
||||||
@@ -488,18 +489,17 @@ export function createRendering({
|
|||||||
satelliteTrack,
|
satelliteTrack,
|
||||||
);
|
);
|
||||||
if (!primitiveOrbitAdded) {
|
if (!primitiveOrbitAdded) {
|
||||||
layerState._dataSource.entities.add({
|
getRenderer().addEntity(layerState._dataSource, {
|
||||||
id: `rocket-orbit:${launch.id}`,
|
id: `rocket-orbit:${launch.id}`,
|
||||||
polyline: {
|
polyline: {
|
||||||
positions: orbitPath,
|
positions: orbitPath,
|
||||||
// The 3 px canvas gives the periodic dot room to read as round; the
|
// The 3 px canvas gives the periodic dot room to read as round; the
|
||||||
// shader masks the ten intervening dashes down to a thin center stroke.
|
// shader masks the ten intervening dashes down to a thin center stroke.
|
||||||
width: 3,
|
width: 3,
|
||||||
material:
|
material: getRenderer().patternMaterial(
|
||||||
new parts.orbitRendering.MissionOrbitPatternMaterialProperty(
|
rgbaFromCss('#c084fc').withAlpha(0.95),
|
||||||
Cesium.Color.fromCssColorString('#c084fc').withAlpha(0.95),
|
),
|
||||||
),
|
arc: 'none',
|
||||||
arcType: Cesium.ArcType.NONE,
|
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
launchId: launch.id,
|
launchId: launch.id,
|
||||||
@@ -514,10 +514,10 @@ export function createRendering({
|
|||||||
const primitivePath = layerState._missionOrbitPrimitives.get(launch.id);
|
const primitivePath = layerState._missionOrbitPrimitives.get(launch.id);
|
||||||
if (primitivePath) {
|
if (primitivePath) {
|
||||||
primitivePath.labelBakePosition = orbitLabelBakePosition;
|
primitivePath.labelBakePosition = orbitLabelBakePosition;
|
||||||
primitivePath.labelPosition = Cesium.Matrix4.multiplyByPoint(
|
primitivePath.labelPosition = geo.matrix4.multiplyByPoint(
|
||||||
primitivePath.primitive.modelMatrix,
|
primitivePath.primitive.modelMatrix,
|
||||||
orbitLabelBakePosition,
|
orbitLabelBakePosition,
|
||||||
new Cesium.Cartesian3(),
|
geo.cartesian3.create(),
|
||||||
);
|
);
|
||||||
orbitLabelPosition = () => primitivePath.labelPosition;
|
orbitLabelPosition = () => primitivePath.labelPosition;
|
||||||
}
|
}
|
||||||
@@ -531,13 +531,13 @@ export function createRendering({
|
|||||||
gapPx: 8,
|
gapPx: 8,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const replayPosition = new Cesium.CallbackProperty(() => {
|
const replayPosition = getRenderer().callback(() => {
|
||||||
const state = getReplayState();
|
const state = getReplayState();
|
||||||
return state.ascending
|
return state.ascending
|
||||||
? parts.paths.samplePath(ascentPath, state.phaseProgress)
|
? parts.paths.samplePath(ascentPath, state.phaseProgress)
|
||||||
: parts.paths.samplePath(animatedOrbitPath, state.phaseProgress);
|
: parts.paths.samplePath(animatedOrbitPath, state.phaseProgress);
|
||||||
}, false);
|
}, false);
|
||||||
layerState._dataSource.entities.add({
|
getRenderer().addEntity(layerState._dataSource, {
|
||||||
id: `rocket-vehicle:${launch.id}`,
|
id: `rocket-vehicle:${launch.id}`,
|
||||||
position: replayPosition,
|
position: replayPosition,
|
||||||
properties: {
|
properties: {
|
||||||
@@ -549,7 +549,7 @@ export function createRendering({
|
|||||||
: 'Approximate trajectory animation',
|
: 'Approximate trajectory animation',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
layerState._dataSource.entities.add({
|
getRenderer().addEntity(layerState._dataSource, {
|
||||||
id: `rocket-transfer:${launch.id}`,
|
id: `rocket-transfer:${launch.id}`,
|
||||||
polyline: {
|
polyline: {
|
||||||
positions: ascentPath,
|
positions: ascentPath,
|
||||||
@@ -557,9 +557,9 @@ export function createRendering({
|
|||||||
// The animated vehicle uses Cartesian interpolation between these
|
// The animated vehicle uses Cartesian interpolation between these
|
||||||
// exact samples. Prevent Cesium from replacing each segment with a
|
// exact samples. Prevent Cesium from replacing each segment with a
|
||||||
// geodesic arc, which would visually separate the dot from the line.
|
// geodesic arc, which would visually separate the dot from the line.
|
||||||
arcType: Cesium.ArcType.NONE,
|
arc: 'none',
|
||||||
material: new Cesium.PolylineDashMaterialProperty({
|
material: getRenderer().dash({
|
||||||
color: Cesium.Color.fromCssColorString('#7bed9f').withAlpha(0.8),
|
color: rgbaFromCss('#7bed9f').withAlpha(0.8),
|
||||||
dashLength: 24,
|
dashLength: 24,
|
||||||
dashPattern: 0xf0f0,
|
dashPattern: 0xf0f0,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
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 {
|
||||||
REPLAY_ASCENT_FALLBACK_SEC,
|
REPLAY_ASCENT_FALLBACK_SEC,
|
||||||
@@ -14,7 +14,13 @@ import {
|
|||||||
REPLAY_INITIAL_RANGE_M,
|
REPLAY_INITIAL_RANGE_M,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
|
||||||
export function createReplay({ state: layerState, services, parts, source }) {
|
export function createReplay({
|
||||||
|
state: layerState,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
source,
|
||||||
|
getRenderer,
|
||||||
|
}) {
|
||||||
/**
|
/**
|
||||||
* Rotate an upright screen-space rocket so its nose follows a projected path.
|
* Rotate an upright screen-space rocket so its nose follows a projected path.
|
||||||
* @param {{x: number, y: number}} from Current screen point.
|
* @param {{x: number, y: number}} from Current screen point.
|
||||||
@@ -125,7 +131,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
orbitDurationSec,
|
orbitDurationSec,
|
||||||
orbitPeriodSec,
|
orbitPeriodSec,
|
||||||
speed = 1,
|
speed = 1,
|
||||||
nowMs = Date.now(),
|
nowMs = appClock.now(),
|
||||||
preCountdownDurationSec = 0,
|
preCountdownDurationSec = 0,
|
||||||
loop = true,
|
loop = true,
|
||||||
) {
|
) {
|
||||||
@@ -136,7 +142,8 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
Number(preCountdownDurationSec) || 0,
|
Number(preCountdownDurationSec) || 0,
|
||||||
);
|
);
|
||||||
const preCountdownActive =
|
const preCountdownActive =
|
||||||
preCountdownDuration > 0 && realSecondsSinceStart < -preCountdownDuration;
|
preCountdownDuration > 0 &&
|
||||||
|
realSecondsSinceStart < -REPLAY_COUNTDOWN_DURATION_SEC;
|
||||||
const countdownActive = realSecondsSinceStart < 0 && !preCountdownActive;
|
const countdownActive = realSecondsSinceStart < 0 && !preCountdownActive;
|
||||||
const countdownSeconds = countdownActive
|
const countdownSeconds = countdownActive
|
||||||
? Math.ceil(-realSecondsSinceStart)
|
? Math.ceil(-realSecondsSinceStart)
|
||||||
@@ -273,7 +280,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
const now =
|
const now =
|
||||||
layerState._replayPaused && Number.isFinite(layerState._replayPausedAtMs)
|
layerState._replayPaused && Number.isFinite(layerState._replayPausedAtMs)
|
||||||
? layerState._replayPausedAtMs
|
? layerState._replayPausedAtMs
|
||||||
: Date.now();
|
: appClock.now();
|
||||||
for (const [launchId, startedAt] of layerState._animationStarts) {
|
for (const [launchId, startedAt] of layerState._animationStarts) {
|
||||||
if (!Number.isFinite(startedAt)) continue;
|
if (!Number.isFinite(startedAt)) continue;
|
||||||
const elapsedMissionMs = (now - startedAt) * previousSpeed;
|
const elapsedMissionMs = (now - startedAt) * previousSpeed;
|
||||||
@@ -294,13 +301,13 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
) {
|
) {
|
||||||
return layerState._replayPausedAtMs;
|
return layerState._replayPausedAtMs;
|
||||||
}
|
}
|
||||||
return Date.now();
|
return appClock.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
function pauseMissionReplay() {
|
function pauseMissionReplay() {
|
||||||
if (!layerState._replayCameraLaunchId || layerState._replayPaused)
|
if (!layerState._replayCameraLaunchId || layerState._replayPaused)
|
||||||
return false;
|
return false;
|
||||||
layerState._replayPausedAtMs = Date.now();
|
layerState._replayPausedAtMs = appClock.now();
|
||||||
layerState._replayPaused = true;
|
layerState._replayPaused = true;
|
||||||
syncReplayButton();
|
syncReplayButton();
|
||||||
return true;
|
return true;
|
||||||
@@ -314,7 +321,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const resumedAt = Date.now();
|
const resumedAt = appClock.now();
|
||||||
const startedAt = layerState._animationStarts.get(
|
const startedAt = layerState._animationStarts.get(
|
||||||
layerState._replayCameraLaunchId,
|
layerState._replayCameraLaunchId,
|
||||||
);
|
);
|
||||||
@@ -341,12 +348,12 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
layerState._replayCameraRemover = null;
|
layerState._replayCameraRemover = null;
|
||||||
layerState._viewer?.camera?.cancelFlight();
|
layerState._viewer?.camera?.cancelFlight();
|
||||||
if (layerState._viewer?.camera)
|
if (layerState._viewer?.camera)
|
||||||
layerState._viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
layerState._viewer.camera.lookAtTransform(geo.matrix4.IDENTITY);
|
||||||
layerState._replayCameraLaunchId = null;
|
layerState._replayCameraLaunchId = null;
|
||||||
layerState._replayPaused = false;
|
layerState._replayPaused = false;
|
||||||
layerState._replayPausedAtMs = null;
|
layerState._replayPausedAtMs = null;
|
||||||
if (stoppedLaunchId)
|
if (stoppedLaunchId)
|
||||||
layerState._animationStarts.set(stoppedLaunchId, Date.now());
|
layerState._animationStarts.set(stoppedLaunchId, appClock.now());
|
||||||
syncReplayButton();
|
syncReplayButton();
|
||||||
parts.overlays.syncMissionOverlayEntries();
|
parts.overlays.syncMissionOverlayEntries();
|
||||||
}
|
}
|
||||||
@@ -381,7 +388,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
layerState._viewer.camera.cancelFlight();
|
layerState._viewer.camera.cancelFlight();
|
||||||
layerState._animationStarts.set(
|
layerState._animationStarts.set(
|
||||||
launchId,
|
launchId,
|
||||||
Date.now() +
|
appClock.now() +
|
||||||
(REPLAY_TILE_SETTLE_DELAY_SEC + REPLAY_COUNTDOWN_DURATION_SEC) * 1000,
|
(REPLAY_TILE_SETTLE_DELAY_SEC + REPLAY_COUNTDOWN_DURATION_SEC) * 1000,
|
||||||
);
|
);
|
||||||
let cameraReady = false;
|
let cameraReady = false;
|
||||||
@@ -423,7 +430,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
orbitBlend,
|
orbitBlend,
|
||||||
);
|
);
|
||||||
if (state.ascending) track.orbitCameraWorldFrame = false;
|
if (state.ascending) track.orbitCameraWorldFrame = false;
|
||||||
const nowMs = performance.now();
|
const nowMs = appClock.now();
|
||||||
const frameDurationMs = Number.isFinite(lastCameraUpdateMs)
|
const frameDurationMs = Number.isFinite(lastCameraUpdateMs)
|
||||||
? geo.math.clamp(nowMs - lastCameraUpdateMs, 4, 50)
|
? geo.math.clamp(nowMs - lastCameraUpdateMs, 4, 50)
|
||||||
: 1000 / 60;
|
: 1000 / 60;
|
||||||
@@ -458,7 +465,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
if (state.ascending) {
|
if (state.ascending) {
|
||||||
layerState._viewer.camera.lookAt(
|
layerState._viewer.camera.lookAt(
|
||||||
cameraTarget,
|
cameraTarget,
|
||||||
new Cesium.HeadingPitchRange(
|
getRenderer().range(
|
||||||
track.lastCameraHeading,
|
track.lastCameraHeading,
|
||||||
cameraView.pitch,
|
cameraView.pitch,
|
||||||
cameraRange,
|
cameraRange,
|
||||||
@@ -488,9 +495,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
);
|
);
|
||||||
if (orbitPose) {
|
if (orbitPose) {
|
||||||
if (!track.orbitCameraWorldFrame) {
|
if (!track.orbitCameraWorldFrame) {
|
||||||
layerState._viewer.camera.lookAtTransform(
|
layerState._viewer.camera.lookAtTransform(geo.matrix4.IDENTITY);
|
||||||
Cesium.Matrix4.IDENTITY,
|
|
||||||
);
|
|
||||||
track.orbitCameraWorldFrame = true;
|
track.orbitCameraWorldFrame = true;
|
||||||
}
|
}
|
||||||
layerState._viewer.camera.setView({
|
layerState._viewer.camera.setView({
|
||||||
@@ -511,15 +516,15 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
layerState._viewer.camera.flyToBoundingSphere(
|
layerState._viewer.camera.flyToBoundingSphere(
|
||||||
new Cesium.BoundingSphere(track.ascentPath[0], 0),
|
geo.boundingSphere.create(track.ascentPath[0], 0),
|
||||||
{
|
{
|
||||||
offset: new Cesium.HeadingPitchRange(
|
offset: getRenderer().range(
|
||||||
initialHeading,
|
initialHeading,
|
||||||
geo.math.toRadians(-20),
|
geo.math.toRadians(-20),
|
||||||
REPLAY_INITIAL_RANGE_M,
|
REPLAY_INITIAL_RANGE_M,
|
||||||
),
|
),
|
||||||
duration: 1.4,
|
duration: 1.4,
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
easingFunction: getRenderer().easing,
|
||||||
complete: () => {
|
complete: () => {
|
||||||
cameraReady = true;
|
cameraReady = true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { appClock } from '../../time/clock.js';
|
||||||
|
import { createReplay } from './replay.js';
|
||||||
|
function setup() {
|
||||||
|
const state = {
|
||||||
|
_missionPanel: null,
|
||||||
|
_replayCameraLaunchId: 'qa',
|
||||||
|
_replayPaused: false,
|
||||||
|
_replayPausedAtMs: null,
|
||||||
|
_replaySpeed: 1,
|
||||||
|
_animationStarts: new Map([['qa', 1000]]),
|
||||||
|
};
|
||||||
|
const parts = { paths: { orbitInsertionOffsetSeconds: () => 600 } };
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
replay: createReplay({ state, services: {}, parts, source: {} }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
test('five seconds of tile preparation are followed by the full ten-second countdown', () => {
|
||||||
|
const { replay } = setup(),
|
||||||
|
launch = { launchTime: '2026-10-15T12:00:00Z' };
|
||||||
|
try {
|
||||||
|
appClock.freeze(100000);
|
||||||
|
assert.equal(
|
||||||
|
replay.replayState(launch, 115000, 12, 28, 5400, 4, undefined, 5)
|
||||||
|
.preCountdownActive,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
appClock.freeze(105000);
|
||||||
|
const countdown = replay.replayState(
|
||||||
|
launch,
|
||||||
|
115000,
|
||||||
|
12,
|
||||||
|
28,
|
||||||
|
5400,
|
||||||
|
4,
|
||||||
|
undefined,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
assert.equal(countdown.preCountdownActive, false);
|
||||||
|
assert.equal(countdown.countdownSeconds, 10);
|
||||||
|
appClock.freeze(114000);
|
||||||
|
assert.equal(
|
||||||
|
replay.replayState(launch, 115000, 12, 28, 5400, 4, undefined, 5)
|
||||||
|
.countdownSeconds,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
appClock.freeze(116000);
|
||||||
|
assert.equal(
|
||||||
|
replay.replayState(launch, 115000, 12, 28, 5400, 4, undefined, 5)
|
||||||
|
.elapsedSinceStart,
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
appClock.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('pause, resume and speed changes use app time without a wall-clock jump', () => {
|
||||||
|
const { state, replay } = setup();
|
||||||
|
try {
|
||||||
|
appClock.freeze(4000);
|
||||||
|
assert.equal(replay.pauseMissionReplay(), true);
|
||||||
|
appClock.freeze(9500);
|
||||||
|
assert.equal(replay.replayClockNow('qa'), 4000);
|
||||||
|
assert.equal(replay.resumeMissionReplay(), true);
|
||||||
|
assert.equal(state._animationStarts.get('qa'), 6500);
|
||||||
|
replay.setReplaySpeed(4);
|
||||||
|
assert.equal(state._animationStarts.get('qa'), 8750);
|
||||||
|
} finally {
|
||||||
|
appClock.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Cesium from 'cesium';
|
|
||||||
import { appClock } from '../../time/clock.js';
|
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';
|
||||||
@@ -8,6 +7,7 @@ export function createSelection({
|
|||||||
services,
|
services,
|
||||||
parts,
|
parts,
|
||||||
source,
|
source,
|
||||||
|
getRenderer,
|
||||||
}) {
|
}) {
|
||||||
const { getKeyholeGeometry } = services.geometry;
|
const { getKeyholeGeometry } = services.geometry;
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export function createSelection({
|
|||||||
roll: 0,
|
roll: 0,
|
||||||
},
|
},
|
||||||
duration,
|
duration,
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
easingFunction: getRenderer().easing,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ export function createSelection({
|
|||||||
stopMissionZoomAnchor();
|
stopMissionZoomAnchor();
|
||||||
layerState._selectedLaunchId = launchId;
|
layerState._selectedLaunchId = launchId;
|
||||||
layerState._explicitSelection = Boolean(launchId && isolate);
|
layerState._explicitSelection = Boolean(launchId && isolate);
|
||||||
if (launchId) layerState._animationStarts.set(launchId, Date.now());
|
if (launchId) layerState._animationStarts.set(launchId, appClock.now());
|
||||||
if (!launchId && layerState._viewer) {
|
if (!launchId && layerState._viewer) {
|
||||||
stopMissionZoomAnchor();
|
stopMissionZoomAnchor();
|
||||||
layerState._viewer.selectedEntity = undefined;
|
layerState._viewer.selectedEntity = undefined;
|
||||||
@@ -93,7 +93,7 @@ export function createSelection({
|
|||||||
layerState._missionZoomAnchorRemover = null;
|
layerState._missionZoomAnchorRemover = null;
|
||||||
layerState._missionZoomAnchorId = null;
|
layerState._missionZoomAnchorId = null;
|
||||||
if (layerState._viewer?.camera && !layerState._replayCameraLaunchId) {
|
if (layerState._viewer?.camera && !layerState._replayCameraLaunchId) {
|
||||||
layerState._viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
layerState._viewer.camera.lookAtTransform(geo.matrix4.IDENTITY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,11 +109,7 @@ export function createSelection({
|
|||||||
layerState._missionZoomAnchorId = launch.id;
|
layerState._missionZoomAnchorId = launch.id;
|
||||||
layerState._viewer.camera.lookAt(
|
layerState._viewer.camera.lookAt(
|
||||||
target,
|
target,
|
||||||
new Cesium.HeadingPitchRange(
|
getRenderer().range(0, parts.camera.missionZoomPitch(range), range),
|
||||||
0,
|
|
||||||
parts.camera.missionZoomPitch(range),
|
|
||||||
range,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +123,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.fromDate(new Date(appClock.now())),
|
getRenderer().time(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;
|
||||||
@@ -202,15 +198,15 @@ export function createSelection({
|
|||||||
`rocket-launch:${launch.id}`,
|
`rocket-launch:${launch.id}`,
|
||||||
);
|
);
|
||||||
layerState._viewer.camera.flyToBoundingSphere(
|
layerState._viewer.camera.flyToBoundingSphere(
|
||||||
new Cesium.BoundingSphere(launchPosition, 0),
|
geo.boundingSphere.create(launchPosition, 0),
|
||||||
{
|
{
|
||||||
offset: new Cesium.HeadingPitchRange(
|
offset: getRenderer().range(
|
||||||
0,
|
0,
|
||||||
geo.math.toRadians(-42),
|
geo.math.toRadians(-42),
|
||||||
MISSION_FOCUS_RANGE_M,
|
MISSION_FOCUS_RANGE_M,
|
||||||
),
|
),
|
||||||
duration: 1.4,
|
duration: 1.4,
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
easingFunction: getRenderer().easing,
|
||||||
complete: () => {
|
complete: () => {
|
||||||
if (layerState._selectedLaunchId === launch.id) {
|
if (layerState._selectedLaunchId === launch.id) {
|
||||||
startMissionZoomAnchor(launch, MISSION_FOCUS_RANGE_M);
|
startMissionZoomAnchor(launch, MISSION_FOCUS_RANGE_M);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Cesium from 'cesium';
|
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
|
|
||||||
export function createState({ services }) {
|
export function createState({ services }) {
|
||||||
@@ -122,17 +121,13 @@ export function createState({ services }) {
|
|||||||
|
|
||||||
state._missionOrbitPrimitives = new Map();
|
state._missionOrbitPrimitives = new Map();
|
||||||
|
|
||||||
state._declutterTime = new Cesium.JulianDate();
|
state._renderer = null;
|
||||||
|
state._engine = null;
|
||||||
state._declutterOccluder = new Cesium.EllipsoidalOccluder(
|
state._declutterTime = null;
|
||||||
Cesium.Ellipsoid.WGS84,
|
state._declutterOccluder = null;
|
||||||
geo.cartesian3.create(),
|
|
||||||
);
|
|
||||||
|
|
||||||
state._missionRingDate = new Date(0);
|
state._missionRingDate = new Date(0);
|
||||||
|
|
||||||
state._missionOrbitPatternRegistered = false;
|
|
||||||
|
|
||||||
state._pathDistanceCache = new WeakMap();
|
state._pathDistanceCache = new WeakMap();
|
||||||
state._sourceController = new AbortController();
|
state._sourceController = new AbortController();
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ export class Rgba {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Cesium's CSS form, including its float-to-byte conversion. */
|
||||||
|
toCssColorString() {
|
||||||
|
const byte = (v) => (v === 1 ? 255 : (v * 256) | 0);
|
||||||
|
const rgb = `${byte(this.red)},${byte(this.green)},${byte(this.blue)}`;
|
||||||
|
return this.alpha === 1 ? `rgb(${rgb})` : `rgba(${rgb},${this.alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
/** `[red, green, blue, alpha]`, 0..1. */
|
/** `[red, green, blue, alpha]`, 0..1. */
|
||||||
rgba() {
|
rgba() {
|
||||||
return [this.red, this.green, this.blue, this.alpha];
|
return [this.red, this.green, this.blue, this.alpha];
|
||||||
|
|||||||
@@ -75,3 +75,16 @@ test('Cesium billboards and models take an Rgba as their colour', () => {
|
|||||||
assert.notEqual(billboard.color, CYAN, 'the billboard keeps its own copy');
|
assert.notEqual(billboard.color, CYAN, 'the billboard keeps its own copy');
|
||||||
collection.destroy();
|
collection.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('CSS serialization keeps native byte rounding and alpha formatting', async () => {
|
||||||
|
const { constantRgba } = await import('./color.js');
|
||||||
|
for (const values of [
|
||||||
|
[0.1, 0.5, 1, 1],
|
||||||
|
[1, 0, 0.3, 0.42],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
])
|
||||||
|
assert.equal(
|
||||||
|
constantRgba(...values).toCssColorString(),
|
||||||
|
new Cesium.Color(...values).toCssColorString(),
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user