mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
fix(cockpit): no sensor swath past the limb, no strip for a GEO view or a SAR; the TLE age reads from the world-clock time shown; pre-loads cancel on exit, retry when they fail, start over on a new base map, wait while the clock is away from now and reach the frame edges
This commit is contained in:
1 parent
9a16fb8cbc
commit
b953c3f1dc
8 files changed
+207
-34
No files matched your search
+16
-8
@@ -1656,12 +1656,15 @@ is confirmed also gets 16×, 32×, 64× and 128×, each only while the view at
|
|||||||
the satellite's live altitude and the window's shape is still at least as
|
the satellite's live altitude and the window's shape is still at least as
|
||||||
wide as that swath (`orbitalMaxZoom` in `src/cockpitOrbital.js`); when the
|
wide as that swath (`orbitalMaxZoom` in `src/cockpitOrbital.js`); when the
|
||||||
cap falls below the zoom shown, the view steps back to it. Unconfirmed
|
cap falls below the zoom shown, the view steps back to it. Unconfirmed
|
||||||
swaths add no steps. The sensor's swath is drawn to scale as a strip along
|
swaths add no steps, but are still drawn, read out and offered to SENSOR.
|
||||||
the ground track with a SENSOR SWATH readout, and a SENSOR button zooms
|
The sensor's swath is drawn to scale as a strip along the ground track with
|
||||||
to exactly that width when it lies between 1× and the cap. Tilted off nadir
|
a SENSOR SWATH readout, and a SENSOR button zooms to exactly that width when
|
||||||
|
it lies between 1× and the cap. No strip is drawn for a geostationary view
|
||||||
|
(no ground track; its sensors image areas, not strips) or a SAR (it images
|
||||||
|
to one side of nadir, not under it); the readout stays. Tilted off nadir
|
||||||
the strip stays (view and swath widen alike), the readout scales as the swath
|
the strip stays (view and swath widen alike), the readout scales as the swath
|
||||||
readout does (`SENSOR SWATH ≈ 191 KM`), and SENSOR, a nadir framing, is
|
readout does (`SENSOR SWATH ≈ 191 KM`), and SENSOR, a nadir framing, is
|
||||||
hidden. A swath is confirmed only on the operator's, an agency's (ESA's
|
hidden; tilted past the Earth's limb neither the sensor nor the swath reads. A swath is confirmed only on the operator's, an agency's (ESA's
|
||||||
eoPortal included) or an academic page for its platform. From
|
eoPortal included) or an academic page for its platform. From
|
||||||
16×, with flat imagery on the globe (not Google 3D tiles) and the view at
|
16×, with flat imagery on the globe (not Google 3D tiles) and the view at
|
||||||
nadir, every 2 s the view asks the base imagery layer for the tiles it will
|
nadir, every 2 s the view asks the base imagery layer for the tiles it will
|
||||||
@@ -1675,9 +1678,13 @@ error, about one to two screen pixels per texel). It asks for at most 12
|
|||||||
tiles at a time through Cesium's request scheduler, throttled per server and
|
tiles at a time through Cesium's request scheduler, throttled per server and
|
||||||
at the lowest priority, so a request is declined while the imagery server's
|
at the lowest priority, so a request is declined while the imagery server's
|
||||||
slots are taken by the tiles on screen and is the first dropped when the
|
slots are taken by the tiles on screen and is the first dropped when the
|
||||||
scheduler is full (a declined tile is asked for again later); it remembers the last 512 tiles asked for and does not ask
|
scheduler is full (a declined or failed tile is asked for again later); it remembers the last 512 tiles asked for and does not ask
|
||||||
for those again. While tilted off nadir the view looks away from the track,
|
for those again, starts over on another base map, and cancels the requests
|
||||||
so nothing is pre-loaded. The arrows tilt the view off nadir in 5° steps
|
still pending on exit. Across the track it samples the centre line, a third
|
||||||
|
out and the frame's edges. While tilted off nadir the view looks away from
|
||||||
|
the track, and with the world clock more than a minute from now (paused or
|
||||||
|
played back) it will not pass where the look-ahead points, so nothing is
|
||||||
|
pre-loaded. The arrows tilt the view off nadir in 5° steps
|
||||||
along the frame's axes (Up/Down tilt forward/aft along the ground track,
|
along the frame's axes (Up/Down tilt forward/aft along the ground track,
|
||||||
Left/Right across it, each axis to ±45°; Shift takes no larger step); Home,
|
Left/Right across it, each axis to ±45°; Shift takes no larger step); Home,
|
||||||
`0` or NADIR return to nadir. While tilted the panel shows the total angle
|
`0` or NADIR return to nadir. While tilted the panel shows the total angle
|
||||||
@@ -1714,7 +1721,8 @@ again. The readout card puts
|
|||||||
the owner country's flag (an SVG from `flag-icons`, fetched only for the shown
|
the owner country's flag (an SVG from `flag-icons`, fetched only for the shown
|
||||||
country) or a text badge for an organisation before the name, the operator
|
country) or a text badge for an organisation before the name, the operator
|
||||||
under it, and a NORAD · COSPAR · TLE-epoch line, ending with the element
|
under it, and a NORAD · COSPAR · TLE-epoch line, ending with the element
|
||||||
set's age (`<1 H OLD`, then whole hours, then whole days), whose TLE button
|
set's age (`<1 H OLD`, then whole hours, then whole days: the gap, either
|
||||||
|
side, between the epoch and the world-clock time the satellite is shown at), whose TLE button
|
||||||
unfolds the two element lines; each row is left out when the satellites layer does not
|
unfolds the two element lines; each row is left out when the satellites layer does not
|
||||||
know it. The
|
know it. The
|
||||||
aircraft and bridge instruments, tapes, compass, radar, look controls,
|
aircraft and bridge instruments, tapes, compass, radar, look controls,
|
||||||
|
|||||||
@@ -331,6 +331,9 @@
|
|||||||
"src/ui/styles/cockpitBridge.css",
|
"src/ui/styles/cockpitBridge.css",
|
||||||
"src/ui/styles/cockpitRadar.css",
|
"src/ui/styles/cockpitRadar.css",
|
||||||
"src/ui/styles/cockpitOrbital.css",
|
"src/ui/styles/cockpitOrbital.css",
|
||||||
|
"src/layers/satellites/sensors.js",
|
||||||
|
"src/layers/satellites/sensors.test.mjs",
|
||||||
|
"src/data/imagingSensors.js",
|
||||||
"src/ui/styles/command-dock-compact.css",
|
"src/ui/styles/command-dock-compact.css",
|
||||||
"src/app/stateChannel.js",
|
"src/app/stateChannel.js",
|
||||||
"src/app/stateChannel.test.mjs",
|
"src/app/stateChannel.test.mjs",
|
||||||
|
|||||||
@@ -585,9 +585,16 @@ export const ORBITAL_PREFETCH_LEAD_S = Object.freeze([5, 10, 15, 20]);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Where across the track it looks, as fractions of the view's width: the
|
* Where across the track it looks, as fractions of the view's width: the
|
||||||
* centre line and a third out each side, so a tile row spans the frame.
|
* centre line, a third out and the frame's edges each side, so a row of
|
||||||
|
* tiles a fifth of the view wide or more spans the frame.
|
||||||
*/
|
*/
|
||||||
export const ORBITAL_PREFETCH_ACROSS = Object.freeze([0, -1 / 3, 1 / 3]);
|
export const ORBITAL_PREFETCH_ACROSS = Object.freeze([
|
||||||
|
0,
|
||||||
|
-1 / 3,
|
||||||
|
1 / 3,
|
||||||
|
-1 / 2,
|
||||||
|
1 / 2,
|
||||||
|
]);
|
||||||
|
|
||||||
/** At most this many tile requests per look-ahead. */
|
/** At most this many tile requests per look-ahead. */
|
||||||
export const ORBITAL_PREFETCH_MAX_TILES = 12;
|
export const ORBITAL_PREFETCH_MAX_TILES = 12;
|
||||||
|
|||||||
@@ -461,15 +461,17 @@ test('the points ahead run along the ground track at ground speed', () => {
|
|||||||
assert.ok(Math.abs(km - groundKmPerS * seconds) < 0.5, `${seconds} s`);
|
assert.ok(Math.abs(km - groundKmPerS * seconds) < 0.5, `${seconds} s`);
|
||||||
// North-east of the start, on a 45° track.
|
// North-east of the start, on a 45° track.
|
||||||
assert.ok(centre.lat > PASS.latitude && centre.lon > PASS.longitude);
|
assert.ok(centre.lat > PASS.latitude && centre.lon > PASS.longitude);
|
||||||
// The side points sit a third of the view off the centre line.
|
// The side points sit their fraction of the view off the centre line.
|
||||||
for (const side of points.slice(i * across + 1, (i + 1) * across))
|
points.slice(i * across + 1, (i + 1) * across).forEach((side, k) => {
|
||||||
assert.ok(Math.abs(greatCircleKm(centre, side) - 20 / 3) < 0.05);
|
const offKm = 20 * Math.abs(ORBITAL_PREFETCH_ACROSS[k + 1]);
|
||||||
|
assert.ok(Math.abs(greatCircleKm(centre, side) - offKm) < 0.05);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
assert.deepEqual(groundTrackAhead({ ...PASS, groundTrackDeg: null }, 20), []);
|
assert.deepEqual(groundTrackAhead({ ...PASS, groundTrackDeg: null }, 20), []);
|
||||||
assert.deepEqual(groundTrackAhead(PASS, null), []);
|
assert.deepEqual(groundTrackAhead(PASS, null), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the tile level is the one Cesium draws for the view", () => {
|
test('the tile level is the one Cesium draws for the view', () => {
|
||||||
const mercator = new Cesium.WebMercatorTilingScheme();
|
const mercator = new Cesium.WebMercatorTilingScheme();
|
||||||
// Worked case: 10 m per CSS pixel at the equator. Cesium refines the
|
// Worked case: 10 m per CSS pixel at the equator. Cesium refines the
|
||||||
// ellipsoid terrain until a tile's error is at most 2 px, 20 m: level 12,
|
// ellipsoid terrain until a tile's error is at most 2 px, 20 m: level 12,
|
||||||
@@ -566,7 +568,11 @@ test('the tiles ahead are the ones the points fall in, bounded and new', () => {
|
|||||||
assert.ok(tiles.every((tile) => tile.x >= here.x && tile.y <= here.y));
|
assert.ok(tiles.every((tile) => tile.x >= here.x && tile.y <= here.y));
|
||||||
// Bounded, and a tile already asked for is skipped.
|
// Bounded, and a tile already asked for is skipped.
|
||||||
assert.equal(tilesForPoints(mercator, points, 15, { maxTiles: 3 }).length, 3);
|
assert.equal(tilesForPoints(mercator, points, 15, { maxTiles: 3 }).length, 3);
|
||||||
const asked = new Set(tiles.map((tile) => tile.key));
|
const asked = new Set(
|
||||||
|
tilesForPoints(mercator, points, 13, { maxTiles: Infinity }).map(
|
||||||
|
(tile) => tile.key,
|
||||||
|
),
|
||||||
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
tilesForPoints(mercator, points, 13, { skip: (key) => asked.has(key) }),
|
tilesForPoints(mercator, points, 13, { skip: (key) => asked.has(key) }),
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ function freezeOwner(owner) {
|
|||||||
* @param {number} noradId NORAD catalog number.
|
* @param {number} noradId NORAD catalog number.
|
||||||
* @param {{name?: string, group?: string}|null} sat Catalog entry.
|
* @param {{name?: string, group?: string}|null} sat Catalog entry.
|
||||||
* @param {{latitude: number, longitude: number, altitude: number,
|
* @param {{latitude: number, longitude: number, altitude: number,
|
||||||
* speedMps?: number|null, groundTrackDeg?: number|null}|null} fix Per-frame
|
* speedMps?: number|null, groundTrackDeg?: number|null, atMs?: number}|null}
|
||||||
* SGP4 sample (orbits.js propagatePosition).
|
* fix Per-frame SGP4 sample (orbits.js propagatePosition), at `atMs`.
|
||||||
* @returns {object|null} Subject info, or null without a usable fix.
|
* @returns {object|null} Subject info, or null without a usable fix.
|
||||||
*/
|
*/
|
||||||
export function satelliteCockpitInfo(noradId, sat, fix) {
|
export function satelliteCockpitInfo(noradId, sat, fix) {
|
||||||
@@ -122,6 +122,9 @@ export function satelliteCockpitInfo(noradId, sat, fix) {
|
|||||||
altitudeM: altitude,
|
altitudeM: altitude,
|
||||||
// SGP4's TEME velocity: the inertial orbital speed, not ground speed.
|
// SGP4's TEME velocity: the inertial orbital speed, not ground speed.
|
||||||
velocityMps: Number.isFinite(fix.speedMps) ? fix.speedMps : null,
|
velocityMps: Number.isFinite(fix.speedMps) ? fix.speedMps : null,
|
||||||
|
// The time the position is at (epoch ms), the satellites' world-clock
|
||||||
|
// time; null when the sample does not say.
|
||||||
|
atMs: Number.isFinite(fix.atMs) ? fix.atMs : null,
|
||||||
// Degrees clockwise from north; null for a geostationary satellite.
|
// Degrees clockwise from north; null for a geostationary satellite.
|
||||||
groundTrackDeg: Number.isFinite(fix.groundTrackDeg)
|
groundTrackDeg: Number.isFinite(fix.groundTrackDeg)
|
||||||
? fix.groundTrackDeg
|
? fix.groundTrackDeg
|
||||||
|
|||||||
@@ -155,13 +155,13 @@ export function createTracking({ state: layerState, services, parts, source }) {
|
|||||||
frameNumber !== layerState._trackedFrameNumber ||
|
frameNumber !== layerState._trackedFrameNumber ||
|
||||||
layerState._trackedFrameGeo === null
|
layerState._trackedFrameGeo === null
|
||||||
) {
|
) {
|
||||||
const pos = parts.orbits.propagatePosition(
|
const atMs = layerState._trackedFrameNowForTest
|
||||||
sat.satrec,
|
? layerState._trackedFrameNowForTest()
|
||||||
layerState._trackedFrameNowForTest
|
: layerState.now();
|
||||||
? new Date(layerState._trackedFrameNowForTest())
|
const pos = parts.orbits.propagatePosition(sat.satrec, new Date(atMs));
|
||||||
: new Date(layerState.now()),
|
|
||||||
);
|
|
||||||
if (!pos) return layerState._trackedFrameGeo; // propagation hiccup — keep last good sample
|
if (!pos) return layerState._trackedFrameGeo; // propagation hiccup — keep last good sample
|
||||||
|
// The time the sample is at: the world clock's, not the wall clock's.
|
||||||
|
pos.atMs = atMs;
|
||||||
layerState._trackedFrameGeo = pos;
|
layerState._trackedFrameGeo = pos;
|
||||||
Cesium.Cartesian3.fromDegrees(
|
Cesium.Cartesian3.fromDegrees(
|
||||||
pos.longitude,
|
pos.longitude,
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ export function beginOrbital() {
|
|||||||
// looked ahead, and the tiles already asked for (oldest first).
|
// looked ahead, and the tiles already asked for (oldest first).
|
||||||
prefetchMs: -Infinity,
|
prefetchMs: -Infinity,
|
||||||
prefetched: new Set(),
|
prefetched: new Set(),
|
||||||
|
// The provider those tiles came from, and the requests still in flight.
|
||||||
|
prefetchProvider: null,
|
||||||
|
prefetchRequests: new Set(),
|
||||||
// A zoom step in flight: the magnification it started from and when.
|
// A zoom step in flight: the magnification it started from and when.
|
||||||
transition: null,
|
transition: null,
|
||||||
previousUp: null,
|
previousUp: null,
|
||||||
@@ -119,6 +122,9 @@ export function endOrbital() {
|
|||||||
if (!state) return;
|
if (!state) return;
|
||||||
this.orbital = null;
|
this.orbital = null;
|
||||||
state.placeTracker?.reset();
|
state.placeTracker?.reset();
|
||||||
|
// Pre-loads still queued or in flight are for a view no longer shown.
|
||||||
|
for (const request of state.prefetchRequests) request.cancel?.();
|
||||||
|
state.prefetchRequests.clear();
|
||||||
const frustum = this.viewer.camera?.frustum;
|
const frustum = this.viewer.camera?.frustum;
|
||||||
if (frustum && state.savedFov !== null) frustum.fov = state.savedFov;
|
if (frustum && state.savedFov !== null) frustum.fov = state.savedFov;
|
||||||
// Back to the map the operator had, unless they picked another meanwhile.
|
// Back to the map the operator had, unless they picked another meanwhile.
|
||||||
@@ -356,6 +362,9 @@ export function updateOrbital(nowMs) {
|
|||||||
/** Tiles remembered as asked for in one orbital session. */
|
/** Tiles remembered as asked for in one orbital session. */
|
||||||
const PREFETCH_MEMORY = 512;
|
const PREFETCH_MEMORY = 512;
|
||||||
|
|
||||||
|
/** How far the shown time may be from now for the look-ahead to run, ms. */
|
||||||
|
const PREFETCH_LIVE_TOLERANCE_MS = 60_000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* From 16x the view is 10 to 40 km wide while the ground track moves about
|
* From 16x the view is 10 to 40 km wide while the ground track moves about
|
||||||
* 7 km/s, so the imagery under it arrives late. Every
|
* 7 km/s, so the imagery under it arrives late. Every
|
||||||
@@ -367,9 +376,11 @@ const PREFETCH_MEMORY = 512;
|
|||||||
* imagery server's slots are taken by the tiles on screen or the scheduler
|
* imagery server's slots are taken by the tiles on screen or the scheduler
|
||||||
* is full, and a declined tile is asked again on a later look-ahead. At most
|
* is full, and a declined tile is asked again on a later look-ahead. At most
|
||||||
* ORBITAL_PREFETCH_MAX_TILES a time; the last PREFETCH_MEMORY tiles asked
|
* ORBITAL_PREFETCH_MAX_TILES a time; the last PREFETCH_MEMORY tiles asked
|
||||||
* for are not asked again. Nothing below 16x, while the view is tilted off
|
* for are not asked again unless their request failed, and a new base map
|
||||||
* nadir (it no longer looks at the track), after the exit, or with the globe
|
* starts the memory over. Exit cancels the requests still pending. Nothing
|
||||||
* hidden (Google 3D tiles drawn instead).
|
* below 16x, while the view is tilted off nadir (it no longer looks at the
|
||||||
|
* track), with the world clock away from now (paused, played back), after
|
||||||
|
* the exit, or with the globe hidden (Google 3D tiles drawn instead).
|
||||||
* @returns {number} The tiles asked for.
|
* @returns {number} The tiles asked for.
|
||||||
*/
|
*/
|
||||||
export function prefetchImageryAhead(info, nowMs) {
|
export function prefetchImageryAhead(info, nowMs) {
|
||||||
@@ -379,12 +390,24 @@ export function prefetchImageryAhead(info, nowMs) {
|
|||||||
if (lookIsOffset(state.lookTarget, 0) || lookIsOffset(state.lookView, 0))
|
if (lookIsOffset(state.lookTarget, 0) || lookIsOffset(state.lookView, 0))
|
||||||
return 0;
|
return 0;
|
||||||
if (nowMs - state.prefetchMs < ORBITAL_PREFETCH_INTERVAL_MS) return 0;
|
if (nowMs - state.prefetchMs < ORBITAL_PREFETCH_INTERVAL_MS) return 0;
|
||||||
|
// The look-ahead runs at live speed: with the world clock paused, played
|
||||||
|
// back or moved, the view will not pass where it points.
|
||||||
|
if (
|
||||||
|
Number.isFinite(info.atMs) &&
|
||||||
|
Math.abs(info.atMs - Date.now()) > PREFETCH_LIVE_TOLERANCE_MS
|
||||||
|
)
|
||||||
|
return 0;
|
||||||
state.prefetchMs = nowMs;
|
state.prefetchMs = nowMs;
|
||||||
try {
|
try {
|
||||||
const globe = this.viewer.scene?.globe;
|
const globe = this.viewer.scene?.globe;
|
||||||
if (!globe || globe.show === false) return 0;
|
if (!globe || globe.show === false) return 0;
|
||||||
const provider = baseImageryProvider(this.viewer);
|
const provider = baseImageryProvider(this.viewer);
|
||||||
if (!provider) return 0;
|
if (!provider) return 0;
|
||||||
|
// Another base map: its tiles have not been asked for.
|
||||||
|
if (provider !== state.prefetchProvider) {
|
||||||
|
state.prefetched.clear();
|
||||||
|
state.prefetchProvider = provider;
|
||||||
|
}
|
||||||
const fov = fovForZoom(orbitalBaseFovRad(info.altitudeM), state.zoom);
|
const fov = fovForZoom(orbitalBaseFovRad(info.altitudeM), state.zoom);
|
||||||
const viewKm = footprintWidthKm(
|
const viewKm = footprintWidthKm(
|
||||||
info.altitudeM,
|
info.altitudeM,
|
||||||
@@ -427,6 +450,7 @@ export function prefetchImageryAhead(info, nowMs) {
|
|||||||
let asked = 0;
|
let asked = 0;
|
||||||
for (const tile of tiles) {
|
for (const tile of tiles) {
|
||||||
let pending;
|
let pending;
|
||||||
|
let request;
|
||||||
try {
|
try {
|
||||||
// The tiles on screen keep their slots: Cesium's scheduler declines
|
// The tiles on screen keep their slots: Cesium's scheduler declines
|
||||||
// this request while the imagery server's slots are taken (shared
|
// this request while the imagery server's slots are taken (shared
|
||||||
@@ -437,19 +461,28 @@ export function prefetchImageryAhead(info, nowMs) {
|
|||||||
tile.x,
|
tile.x,
|
||||||
tile.y,
|
tile.y,
|
||||||
tile.level,
|
tile.level,
|
||||||
new Cesium.Request({
|
(request = new Cesium.Request({
|
||||||
throttle: true,
|
throttle: true,
|
||||||
throttleByServer: true,
|
throttleByServer: true,
|
||||||
priority: Number.MAX_VALUE,
|
priority: Number.MAX_VALUE,
|
||||||
type: Cesium.RequestType.IMAGERY,
|
type: Cesium.RequestType.IMAGERY,
|
||||||
}),
|
})),
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// A provider that declines now (throttled) is asked again later.
|
// A provider that declines now (throttled) is asked again later.
|
||||||
if (!pending) continue;
|
if (!pending) continue;
|
||||||
Promise.resolve(pending).catch(() => {});
|
const requests = state.prefetchRequests;
|
||||||
|
requests.add(request);
|
||||||
|
Promise.resolve(pending).then(
|
||||||
|
() => requests.delete(request),
|
||||||
|
() => {
|
||||||
|
requests.delete(request);
|
||||||
|
// Failed or dropped by the scheduler: asked again later.
|
||||||
|
memory.delete(tile.key);
|
||||||
|
},
|
||||||
|
);
|
||||||
memory.add(tile.key);
|
memory.add(tile.key);
|
||||||
if (memory.size > PREFETCH_MEMORY)
|
if (memory.size > PREFETCH_MEMORY)
|
||||||
memory.delete(memory.values().next().value);
|
memory.delete(memory.values().next().value);
|
||||||
@@ -602,15 +635,21 @@ export function publishOrbitalView(info, transition = IDLE_TRANSITION) {
|
|||||||
footprintKm: offset ? null : footprintKm,
|
footprintKm: offset ? null : footprintKm,
|
||||||
// The imaging sensor's swath, drawn to scale; null without one. The
|
// The imaging sensor's swath, drawn to scale; null without one. The
|
||||||
// SENSOR step's zoom is null when the swath lies outside 1x..maxZoom.
|
// SENSOR step's zoom is null when the swath lies outside 1x..maxZoom.
|
||||||
|
// Looking past the limb there is no ground for it to cover: no sensor.
|
||||||
sensor:
|
sensor:
|
||||||
swathKm === null
|
swathKm === null || !seesEarth
|
||||||
? null
|
? null
|
||||||
: {
|
: {
|
||||||
label: info.sensor?.label ?? null,
|
label: info.sensor?.label ?? null,
|
||||||
swathKm,
|
swathKm,
|
||||||
// Off nadir the view and the swath widen alike: the strip
|
// Off nadir the view and the swath widen alike: the strip
|
||||||
// keeps its share of the frame.
|
// keeps its share of the frame. No strip where it would
|
||||||
stripFraction: sensorStripFraction(swathKm, footprintKm),
|
// mislead: a geostationary view has no ground track to run
|
||||||
|
// along, and a SAR images to one side of nadir, not under it.
|
||||||
|
stripFraction:
|
||||||
|
trackUp && info.sensor?.type !== 'SAR'
|
||||||
|
? sensorStripFraction(swathKm, footprintKm)
|
||||||
|
: null,
|
||||||
zoom: sensorSwathZoom(info.altitudeM, swathKm, aspect, maxZoom),
|
zoom: sensorSwathZoom(info.altitudeM, swathKm, aspect, maxZoom),
|
||||||
},
|
},
|
||||||
transition: { ...transition },
|
transition: { ...transition },
|
||||||
@@ -634,8 +673,13 @@ export function publishOrbitalView(info, transition = IDLE_TRANSITION) {
|
|||||||
cosparId: info.cosparId ?? null,
|
cosparId: info.cosparId ?? null,
|
||||||
tle: info.tle ?? null,
|
tle: info.tle ?? null,
|
||||||
// Read on the HUD's cadence, so the age stays current without a timer.
|
// Read on the HUD's cadence, so the age stays current without a timer.
|
||||||
|
// The gap between the TLE's epoch and the time the satellite is shown
|
||||||
|
// at (the world clock's), either side: SGP4 drifts both ways.
|
||||||
tleAgeMs: Number.isFinite(info.tle?.epochMs)
|
tleAgeMs: Number.isFinite(info.tle?.epochMs)
|
||||||
? Date.now() - info.tle.epochMs
|
? Math.abs(
|
||||||
|
(Number.isFinite(info.atMs) ? info.atMs : Date.now()) -
|
||||||
|
info.tle.epochMs,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
owner: info.owner ?? null,
|
owner: info.owner ?? null,
|
||||||
operator: info.operator ?? null,
|
operator: info.operator ?? null,
|
||||||
|
|||||||
@@ -1018,9 +1018,11 @@ const WORLDVIEW = Object.freeze({
|
|||||||
id: '40115',
|
id: '40115',
|
||||||
noradId: 40115,
|
noradId: 40115,
|
||||||
name: 'WORLDVIEW-3 (WV-3)',
|
name: 'WORLDVIEW-3 (WV-3)',
|
||||||
classKey: 'visual',
|
satelliteClass: 'imaging',
|
||||||
classLabel: 'IMAGING',
|
classLabel: 'IMAGING',
|
||||||
altitudeM: 617_000,
|
altitudeM: 617_000,
|
||||||
|
// A sun-synchronous pass: the strip runs along this track.
|
||||||
|
groundTrackDeg: 190,
|
||||||
sensor: Object.freeze({
|
sensor: Object.freeze({
|
||||||
label: 'WorldView-3',
|
label: 'WorldView-3',
|
||||||
narrowestSwathKm: 13.1,
|
narrowestSwathKm: 13.1,
|
||||||
@@ -1453,3 +1455,103 @@ test('the pre-load is throttled, at the level Cesium draws, and waits out a tilt
|
|||||||
});
|
});
|
||||||
assert.equal(Number(requests[0].split('/')[0]), expected);
|
assert.equal(Number(requests[0].split('/')[0]), expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the TLE age is measured from the time the satellite is shown at', async (t) => {
|
||||||
|
const DAY = 86_400_000;
|
||||||
|
const { store } = await orbitAt(t, {
|
||||||
|
satellite: {
|
||||||
|
...ISS,
|
||||||
|
// The world clock thirty days back: before the element set's epoch.
|
||||||
|
atMs: START_MS - 30 * DAY,
|
||||||
|
tle: { line1: '1 25544U', line2: '2 25544', epochMs: START_MS - 2 * DAY },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.ok(Math.abs(store.orbital.tleAgeMs / DAY - 28) < 1e-6);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** A geostationary imager with a 1000 km sensor swath. */
|
||||||
|
const GEO_IMAGER = Object.freeze({
|
||||||
|
...ISS,
|
||||||
|
id: '41866',
|
||||||
|
noradId: 41866,
|
||||||
|
name: 'GEO IMAGER',
|
||||||
|
satelliteClass: 'imaging',
|
||||||
|
classLabel: 'IMAGING',
|
||||||
|
altitudeM: 35_786_000,
|
||||||
|
sensor: Object.freeze({
|
||||||
|
label: 'GEO imager',
|
||||||
|
narrowestSwathKm: 1000,
|
||||||
|
swathConfirmed: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
test('looking past the limb shows no sensor swath; a GEO view draws no strip', async (t) => {
|
||||||
|
const { cockpit, store } = await orbitAt(t, { satellite: GEO_IMAGER });
|
||||||
|
assert.ok(store.orbital.sensor, 'the sensor reads at nadir');
|
||||||
|
assert.equal(
|
||||||
|
store.orbital.sensor.stripFraction,
|
||||||
|
null,
|
||||||
|
'no ground track, no strip',
|
||||||
|
);
|
||||||
|
for (let step = 0; step < 3; step += 1) press(cockpit, 'ArrowRight');
|
||||||
|
await settle();
|
||||||
|
assert.equal(store.orbital.look.swathKm, null);
|
||||||
|
assert.equal(store.orbital.sensor, null, 'nothing below to sense');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a SAR swath reads but draws no strip under nadir', async (t) => {
|
||||||
|
const { store } = await orbitAt(t, {
|
||||||
|
satellite: {
|
||||||
|
...WORLDVIEW,
|
||||||
|
sensor: { ...WORLDVIEW.sensor, type: 'SAR', label: 'SAR' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.ok(store.orbital.sensor.swathKm > 0);
|
||||||
|
assert.equal(store.orbital.sensor.stripFraction, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('exit cancels pending pre-loads, and a failed one is asked again', async (t) => {
|
||||||
|
const { cockpit, viewer, store, clock } = await orbitAt(t, {
|
||||||
|
satellite: WORLDVIEW_ON_TRACK,
|
||||||
|
});
|
||||||
|
const asked = [];
|
||||||
|
let fail = true;
|
||||||
|
const pending = [];
|
||||||
|
imageryGlobe(viewer, {
|
||||||
|
requestImage: (x, y, level, request) => {
|
||||||
|
asked.push(`${level}/${x}/${y}`);
|
||||||
|
if (fail) return Promise.reject(new Error('dropped'));
|
||||||
|
pending.push(request);
|
||||||
|
return new Promise(() => {});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (let i = 0; i < 4; i += 1) press(cockpit, '+');
|
||||||
|
assert.equal(store.orbital.zoom, 16);
|
||||||
|
clock.advance(ORBITAL_PREFETCH_INTERVAL_MS);
|
||||||
|
cockpit.update();
|
||||||
|
await settle();
|
||||||
|
const firstAsk = asked.slice();
|
||||||
|
assert.ok(firstAsk.length > 0);
|
||||||
|
fail = false;
|
||||||
|
clock.advance(ORBITAL_PREFETCH_INTERVAL_MS);
|
||||||
|
cockpit.update();
|
||||||
|
assert.deepEqual(asked.slice(firstAsk.length), firstAsk, 'asked again');
|
||||||
|
assert.ok(pending.length > 0);
|
||||||
|
cockpit.exit();
|
||||||
|
assert.ok(
|
||||||
|
pending.every((request) => request.cancelled === true),
|
||||||
|
'cancelled on exit',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the pre-load waits while the world clock is away from now', async (t) => {
|
||||||
|
const { cockpit, viewer, store, clock } = await orbitAt(t, {
|
||||||
|
satellite: { ...WORLDVIEW_ON_TRACK, atMs: START_MS - 3_600_000 },
|
||||||
|
});
|
||||||
|
const { requests } = imageryGlobe(viewer);
|
||||||
|
for (let i = 0; i < 4; i += 1) press(cockpit, '+');
|
||||||
|
assert.equal(store.orbital.zoom, 16);
|
||||||
|
clock.advance(ORBITAL_PREFETCH_INTERVAL_MS);
|
||||||
|
cockpit.update();
|
||||||
|
assert.deepEqual(requests, []);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user