mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(street-level): sequence loads, playback and nearest-photo edge cases
From gekh's review of #768, each bug with a test that fails before: - P2: a failed sequence load clears the previous sequence's cones, resets the selection and can be retried; a second click while loading starts no second lookup. sequences.js had no tests: sequences.test.mjs now covers failure and retry, a superseded late answer, the cache and clearSelection. - P2: closing a photo stops MapillaryJS sequence playback, which kept the hidden viewer stepping and downloading images. - openNearest without the panel reports it instead of loading forever, and stops if the layer (or its provider) went off during the lookup; the mesh sampler's misses are bounded and forgotten with its samples; the sequence count is the union of ids across tiles; a provider withdraws only its own error, so a sequence error cannot mask KEY REJECTED; hover stays on (throttled) under an orbiting camera and re-picks when it stops; "since N days" follows the clock in a long-open tab. - A prewarm waits for one in flight and then builds if it gave up. - New coverage for the layer core (credits, KEY REJECTED vs REQUIRED, switching a provider off), the refresh wiring of the tilted-view fix and the draped-to-cast swap completion. - Cleanup: loading is derived from pending; freshStreet(); tile ranking computes distances once and shares one path for the date line; shared metresBetween / whenIdle / redrawTile; one NEAREST_RADIUS_M and one source method list; unused contract fields, exports, getImage and the decoder's image branch removed; PROVIDER_COLORS lists only Mapillary. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
48276450ec
commit
5f98b871e8
31 files changed
+1129
-470
No files matched your search
@@ -1,7 +1,13 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { decodeCoverageTile } from './decode.js';
|
||||
import { passesImageryFilter } from '../../filter.js';
|
||||
import { groundUnderCamera, viewFocus, visibleBbox } from '../../view.js';
|
||||
import {
|
||||
groundUnderCamera,
|
||||
metresBetween,
|
||||
viewFocus,
|
||||
visibleBbox,
|
||||
whenIdle,
|
||||
} from '../../view.js';
|
||||
import { densifyLine, MESH_DENSIFY_DEG } from '../../groundCast.js';
|
||||
import { MESH_CELL_DEG } from '../../meshSampler.js';
|
||||
import {
|
||||
@@ -27,7 +33,7 @@ import {
|
||||
} from './policy.js';
|
||||
|
||||
/** Draped lines stay at most this long while a tile's cast lines build. */
|
||||
const SWAP_MAX_WAIT_MS = 4000;
|
||||
export const SWAP_MAX_WAIT_MS = 4000;
|
||||
/** Tiles touched by new mesh samples are redrawn at most this often. */
|
||||
const REMESH_INTERVAL_MS = 1500;
|
||||
/** Gap between redrawing one dirty tile and the next. */
|
||||
@@ -104,8 +110,9 @@ export function createCoverage({ state, source }) {
|
||||
return `${z}/${x}/${y}`;
|
||||
}
|
||||
|
||||
/** The imagery filter, resolved now: "since N days" moves with the clock. */
|
||||
function filter() {
|
||||
return state.filter;
|
||||
return state.context.getFilter();
|
||||
}
|
||||
|
||||
function terrainMode() {
|
||||
@@ -122,9 +129,10 @@ export function createCoverage({ state, source }) {
|
||||
*/
|
||||
function drawnSequences(entry) {
|
||||
const drawn = [];
|
||||
const current = filter();
|
||||
for (const sequence of entry.sequenceList) {
|
||||
if (drawn.length >= PER_TILE_SEQUENCE_CAP) break;
|
||||
if (passesImageryFilter(sequence, filter())) drawn.push(sequence);
|
||||
if (passesImageryFilter(sequence, current)) drawn.push(sequence);
|
||||
}
|
||||
return drawn;
|
||||
}
|
||||
@@ -232,15 +240,22 @@ export function createCoverage({ state, source }) {
|
||||
}
|
||||
// A remesh may have cleared the flag while this was in flight.
|
||||
entry.castRequested = true;
|
||||
// Keep the draped lines until the cast ones are built, so nothing blinks.
|
||||
const previous = entry.primitives;
|
||||
entry.primitives = [];
|
||||
attachPrimitive(entry);
|
||||
removeWhenReady(entry, previous);
|
||||
redrawTile(entry);
|
||||
requestMesh(entry);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild a tile's lines (cast, or with new mesh heights), keeping the old
|
||||
* ones on screen until the new ones are built, so nothing blinks.
|
||||
*/
|
||||
function redrawTile(entry) {
|
||||
const previous = entry.primitives;
|
||||
entry.primitives = [];
|
||||
attachPrimitive(entry);
|
||||
removeWhenReady(entry, previous);
|
||||
}
|
||||
|
||||
/** The mesh cells under a tile's drawn lines, built once per filter. */
|
||||
function meshCells(entry) {
|
||||
if (entry.meshCells) return entry.meshCells;
|
||||
@@ -302,25 +317,16 @@ export function createCoverage({ state, source }) {
|
||||
if (!entry) return;
|
||||
state.coverage.remeshDirty.delete(entry);
|
||||
if (attached.has(entry)) {
|
||||
const previous = entry.primitives;
|
||||
entry.primitives = [];
|
||||
attachPrimitive(entry);
|
||||
removeWhenReady(entry, previous);
|
||||
redrawTile(entry);
|
||||
requestRender();
|
||||
}
|
||||
if (state.coverage.remeshDirty.size)
|
||||
state.coverage.remeshTimer = setTimeout(
|
||||
() => idleTask(remesh),
|
||||
() => whenIdle(remesh, 500),
|
||||
REMESH_STAGGER_MS,
|
||||
);
|
||||
}
|
||||
|
||||
function idleTask(task) {
|
||||
if (typeof globalThis.requestIdleCallback === 'function')
|
||||
globalThis.requestIdleCallback(() => task(), { timeout: 500 });
|
||||
else task();
|
||||
}
|
||||
|
||||
state.coverage.remeshDirty = new Set();
|
||||
state.context.meshSampler?.onSampled(onMeshSampled);
|
||||
|
||||
@@ -367,8 +373,9 @@ export function createCoverage({ state, source }) {
|
||||
0.85,
|
||||
);
|
||||
const drawn = [];
|
||||
const current = filter();
|
||||
for (const point of points) {
|
||||
if (!passesImageryFilter(point, filter())) continue;
|
||||
if (!passesImageryFilter(point, current)) continue;
|
||||
drawn.push(
|
||||
collection.add({
|
||||
position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat),
|
||||
@@ -513,7 +520,6 @@ export function createCoverage({ state, source }) {
|
||||
return;
|
||||
const controller = new AbortController();
|
||||
state.coverage.pending.set(key, controller);
|
||||
state.coverage.loading++;
|
||||
notify();
|
||||
try {
|
||||
// Fetch and the one-time terrain-height table load run side by side.
|
||||
@@ -578,10 +584,9 @@ export function createCoverage({ state, source }) {
|
||||
if (error?.retryAfterSec) holdFor(error.retryAfterSec);
|
||||
} finally {
|
||||
// Only the request that still owns the key settles it: a superseded one
|
||||
// must not drop a newer request's entry or its loading count.
|
||||
// must not drop a newer request's entry (which is what counts as loading).
|
||||
if (state.coverage.pending.get(key) === controller) {
|
||||
state.coverage.pending.delete(key);
|
||||
state.coverage.loading = Math.max(0, state.coverage.loading - 1);
|
||||
if (!state.coverage.pending.size) purgeStale();
|
||||
}
|
||||
notify();
|
||||
@@ -632,7 +637,6 @@ export function createCoverage({ state, source }) {
|
||||
for (const controller of state.coverage.pending.values())
|
||||
controller.abort();
|
||||
state.coverage.pending.clear();
|
||||
state.coverage.loading = 0;
|
||||
for (const [key, entry] of state.coverage.tiles) {
|
||||
cancelCast(entry);
|
||||
const previous = state.coverage.stale.get(key);
|
||||
@@ -654,10 +658,7 @@ export function createCoverage({ state, source }) {
|
||||
*/
|
||||
function isTilted({ nadir, ahead }, height) {
|
||||
if (!ahead) return true;
|
||||
const dLat = (ahead.lat - nadir.lat) * 111_320;
|
||||
const dLon =
|
||||
(ahead.lon - nadir.lon) * 111_320 * Math.cos((nadir.lat * Math.PI) / 180);
|
||||
return Math.hypot(dLat, dLon) > Math.max(1, height);
|
||||
return metresBetween(nadir, ahead) > Math.max(1, height);
|
||||
}
|
||||
|
||||
/** Recompute the tile set for the current camera and reconcile primitives. */
|
||||
@@ -723,10 +724,9 @@ export function createCoverage({ state, source }) {
|
||||
for (const [key, controller] of [...state.coverage.pending])
|
||||
if (!wanted.has(key)) {
|
||||
// The aborted request no longer owns its key, so its `finally` will
|
||||
// not settle the count: settle it here.
|
||||
// not settle it: settle it here.
|
||||
controller.abort();
|
||||
state.coverage.pending.delete(key);
|
||||
state.coverage.loading = Math.max(0, state.coverage.loading - 1);
|
||||
}
|
||||
for (const tile of tiles) loadTile(tile, kind);
|
||||
if (!state.coverage.pending.size) purgeStale();
|
||||
@@ -769,7 +769,6 @@ export function createCoverage({ state, source }) {
|
||||
for (const controller of state.coverage.pending.values())
|
||||
controller.abort();
|
||||
state.coverage.pending.clear();
|
||||
state.coverage.loading = 0;
|
||||
for (const key of [...state.coverage.tiles.keys()]) removeTile(key);
|
||||
purgeStale();
|
||||
state.coverage.zoom = null;
|
||||
@@ -807,15 +806,6 @@ export function createCoverage({ state, source }) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
/** Look a sequence up across loaded tiles. */
|
||||
function findSequence(id) {
|
||||
for (const entry of state.coverage.tiles.values()) {
|
||||
const hit = entry.sequences.get(id);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Recolour every part of a sequence in one ready primitive. */
|
||||
function recolorInstances(primitive, sequence, selected) {
|
||||
const value = sequenceColor({ selected });
|
||||
@@ -893,10 +883,18 @@ export function createCoverage({ state, source }) {
|
||||
state.coverage.stopSelectionWatch = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sequences (or overview points) drawn. A sequence that crosses a tile
|
||||
* edge is in both tiles' lists, so sequences count once by id.
|
||||
*/
|
||||
function sequenceCount() {
|
||||
let count = 0;
|
||||
for (const entry of state.coverage.tiles.values()) count += entry.count;
|
||||
return count;
|
||||
const ids = new Set();
|
||||
let points = 0;
|
||||
for (const entry of state.coverage.tiles.values())
|
||||
if (entry.kind === 'sequence')
|
||||
for (const id of entry.sequences.keys()) ids.add(id);
|
||||
else points += entry.count;
|
||||
return ids.size + points;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -907,7 +905,6 @@ export function createCoverage({ state, source }) {
|
||||
resetErrors,
|
||||
rebuild,
|
||||
setSurface,
|
||||
findSequence,
|
||||
recolorSequence,
|
||||
sequenceCount,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user