fix(cockpit): the pre-load serves every lead's centre line before the frame edges, starts a fresh memory and cancels the old map's requests on a new base map, and retries only what the scheduler dropped; the sensor waits for the eased view to reach the Earth

This commit is contained in:
Sameh Khamis committed 2026-11-08 16:43:59 -08:00
1 parent b953c3f1dc
commit 4396ba4506
5 files changed
+119 -18

No files matched your search

+3 -3
View File
@@ -1678,9 +1678,9 @@ 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
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
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, starts over on another base map, and cancels the requests
still pending on exit. Across the track it samples the centre line, a third
scheduler is full (a declined or dropped tile is asked for again later, one that failed to load is not); it remembers the last 512 tiles asked for and does not ask
for those again, starts over on another base map (cancelling the old map's
pending requests), and cancels the requests 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
+19
View File
@@ -596,6 +596,25 @@ export const ORBITAL_PREFETCH_ACROSS = Object.freeze([
1 / 2,
]);
/**
* The points ahead, regrouped by their place across the track: every lead's
* centre point first, then each lead's next pair out, so the tile cap trims
* the frame's edges before it trims the far leads.
* @param {Array<{lat: number, lon: number}>} points From `groundTrackAhead`,
* lead by lead.
* @param {number} [across] Points per lead.
* @returns {Array<{lat: number, lon: number}>}
*/
export function centreLinesFirst(
points,
across = ORBITAL_PREFETCH_ACROSS.length,
) {
const out = [];
for (let k = 0; k < across; k += 1)
for (let i = k; i < points.length; i += across) out.push(points[i]);
return out;
}
/** At most this many tile requests per look-ahead. */
export const ORBITAL_PREFETCH_MAX_TILES = 12;
+24
View File
@@ -15,6 +15,7 @@ import {
nextOrbitalZoom,
offNadirAngleDeg,
offNadirCentreMeetsEarth,
centreLinesFirst,
offNadirSwathScale,
orbitalMaxZoom,
sensorStripFraction,
@@ -592,3 +593,26 @@ test('a view tilted past the limb no longer meets the Earth', () => {
assert.equal(offNadirCentreMeetsEarth(420_000, { eastDeg: 80 }), false);
assert.equal(offNadirCentreMeetsEarth(NaN, {}), false);
});
test('the centre line of every lead comes before the frame edges', () => {
const points = groundTrackAhead(PASS, 20);
const across = ORBITAL_PREFETCH_ACROSS.length;
const ordered = centreLinesFirst(points);
assert.equal(ordered.length, points.length);
const leads = ORBITAL_PREFETCH_LEAD_S.length;
assert.deepEqual(
ordered.slice(0, leads),
points.filter((_, i) => i % across === 0),
);
// The 12-tile cap at a level of small tiles still reaches the far lead.
const mercator = new Cesium.WebMercatorTilingScheme();
const tiles = tilesForPoints(mercator, ordered, 17);
const far = mercator.positionToTileXY(
Cesium.Cartographic.fromDegrees(
ordered[leads - 1].lon,
ordered[leads - 1].lat,
),
17,
);
assert.ok(tiles.some((tile) => tile.x === far.x && tile.y === far.y));
});
+22 -13
View File
@@ -4,6 +4,7 @@ import { formatNumber, msg, t } from '../i18n/index.js';
import {
ORBITAL_DEFAULT_ZOOM,
ORBITAL_PREFETCH_INTERVAL_MS,
centreLinesFirst,
ORBITAL_PREFETCH_MAX_TILES,
ORBITAL_PREFETCH_MIN_ZOOM,
ORBITAL_SETTLE_BLUR_PX,
@@ -404,8 +405,12 @@ export function prefetchImageryAhead(info, nowMs) {
const provider = baseImageryProvider(this.viewer);
if (!provider) return 0;
// Another base map: its tiles have not been asked for.
// A fresh set, so a late failure from the old map's requests cannot
// drop a tile the new one has fetched.
if (provider !== state.prefetchProvider) {
state.prefetched.clear();
for (const request of state.prefetchRequests) request.cancel?.();
state.prefetchRequests = new Set();
state.prefetched = new Set();
state.prefetchProvider = provider;
}
const fov = fovForZoom(orbitalBaseFovRad(info.altitudeM), state.zoom);
@@ -440,7 +445,7 @@ export function prefetchImageryAhead(info, nowMs) {
const memory = state.prefetched;
const tiles = tilesForPoints(
provider.tilingScheme,
groundTrackAhead(info, viewKm),
centreLinesFirst(groundTrackAhead(info, viewKm)),
level,
{
maxTiles: ORBITAL_PREFETCH_MAX_TILES,
@@ -477,10 +482,12 @@ export function prefetchImageryAhead(info, nowMs) {
requests.add(request);
Promise.resolve(pending).then(
() => requests.delete(request),
() => {
(error) => {
requests.delete(request);
// Failed or dropped by the scheduler: asked again later.
memory.delete(tile.key);
// Dropped by the scheduler (rejected with no error): asked again
// later. A tile that failed to load stays remembered, so one
// missing from the map is not asked for every look-ahead.
if (error === undefined) memory.delete(tile.key);
},
);
memory.add(tile.key);
@@ -612,16 +619,18 @@ export function publishOrbitalView(info, transition = IDLE_TRANSITION) {
// republishes on its cadence, so the swath appears once the view arrives.
// Exact angles here, not the whole degrees the readout rounds to.
const target = clampOrbitalLook(state.lookTarget);
// The eased view is tested even with the target at nadir: on the way back
// from past the limb it still looks into space.
const seesEarth =
!offset ||
(offNadirCentreMeetsEarth(info.altitudeM, {
northDeg: target.pitchDeg,
eastDeg: target.yawDeg,
}) &&
(!offset ||
offNadirCentreMeetsEarth(info.altitudeM, {
northDeg: state.lookView?.pitchDeg ?? 0,
eastDeg: state.lookView?.yawDeg ?? 0,
}));
northDeg: target.pitchDeg,
eastDeg: target.yawDeg,
})) &&
offNadirCentreMeetsEarth(info.altitudeM, {
northDeg: state.lookView?.pitchDeg ?? 0,
eastDeg: state.lookView?.yawDeg ?? 0,
});
this.publishOrbital({
name: info.name,
classLabel: info.classLabel || '',
+51 -2
View File
@@ -1510,7 +1510,7 @@ test('a SAR swath reads but draws no strip under nadir', async (t) => {
assert.equal(store.orbital.sensor.stripFraction, null);
});
test('exit cancels pending pre-loads, and a failed one is asked again', async (t) => {
test('exit cancels pending pre-loads, and a dropped one is asked again', async (t) => {
const { cockpit, viewer, store, clock } = await orbitAt(t, {
satellite: WORLDVIEW_ON_TRACK,
});
@@ -1520,7 +1520,8 @@ test('exit cancels pending pre-loads, and a failed one is asked again', async (t
imageryGlobe(viewer, {
requestImage: (x, y, level, request) => {
asked.push(`${level}/${x}/${y}`);
if (fail) return Promise.reject(new Error('dropped'));
// Cesium's scheduler drops a request by rejecting with no error.
if (fail) return Promise.reject(undefined);
pending.push(request);
return new Promise(() => {});
},
@@ -1555,3 +1556,51 @@ test('the pre-load waits while the world clock is away from now', async (t) => {
cockpit.update();
assert.deepEqual(requests, []);
});
test('a tile that fails to load is not asked again', async (t) => {
const { cockpit, viewer, store, clock } = await orbitAt(t, {
satellite: WORLDVIEW_ON_TRACK,
});
const asked = [];
imageryGlobe(viewer, {
requestImage: (x, y, level) => {
asked.push(`${level}/${x}/${y}`);
return Promise.reject(new Error('404'));
},
});
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 first = asked.length;
assert.ok(first > 0);
clock.advance(ORBITAL_PREFETCH_INTERVAL_MS);
cockpit.update();
assert.equal(new Set(asked).size, asked.length, 'none asked twice');
});
test('back to nadir from past the limb, the sensor waits for the view', async (t) => {
const { cockpit, store, clock } = await orbitAt(t, {
satellite: GEO_IMAGER,
reducedMotion: false,
});
for (let step = 0; step < 3; step += 1) press(cockpit, 'ArrowRight');
for (let i = 0; i < 30; i += 1) {
clock.advance(100);
cockpit.update();
}
await settle();
assert.equal(store.orbital.sensor, null, 'looking into space');
press(cockpit, 'Home');
clock.advance(16);
cockpit.update();
await settle();
assert.equal(store.orbital.sensor, null, 'still easing back');
for (let i = 0; i < 30; i += 1) {
clock.advance(100);
cockpit.update();
}
await settle();
assert.ok(store.orbital.sensor, 'back on the Earth');
});