mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-10 21:32:23 +00:00
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:
1 parent
b953c3f1dc
commit
4396ba4506
5 files changed
+119
-18
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
Reference in new issue
Block a user