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:
Sameh Khamis committed 2026-11-08 16:35:23 -08:00
1 parent 9a16fb8cbc
commit b953c3f1dc
8 files changed
+207 -34

No files matched your search

+16 -8
View File
@@ -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,
+3
View File
@@ -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",
+9 -2
View File
@@ -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;
+11 -5
View File
@@ -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) }),
[], [],
+5 -2
View File
@@ -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
+6 -6
View File
@@ -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,
+54 -10
View File
@@ -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,
+103 -1
View File
@@ -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, []);
});