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:
Said TurkseverandClaude Opus 5.5 committed 2026-10-05 18:35:44 +01:00
1 parent 48276450ec
commit 5f98b871e8
31 files changed
+1129 -470

No files matched your search

+16 -27
View File
@@ -1,3 +1,5 @@
import { freshStreet } from './state.js';
/**
* Own the one viewer element in the panel and whichever provider's viewer
* adapter is mounted in it. Poses the adapter emits become `state.street`,
@@ -107,16 +109,20 @@ export function createViewerHost({ state, parts }) {
}
/**
* Open an image from one provider. Returns once the first pose is in;
* later poses stream through `state.street` as the user navigates.
* Open an image from one provider. Resolves once the first pose is in
* (true) or the open failed or was overtaken (false); later poses stream
* through `state.street` as the user navigates.
* @returns {Promise<boolean>}
*/
async function open(providerId, imageId, { frame = true } = {}) {
const entry = providerEntry(providerId);
if (!entry || !imageId) return;
if (!entry || !imageId) return false;
if (!state.street.host) {
// A nearest-image lookup may have set it; nothing is loading now.
state.street.loading = false;
state.street.error = 'Open the Street Level panel to view imagery';
notify();
return;
return false;
}
const seq = ++openSeq;
const current = () => seq === openSeq;
@@ -131,9 +137,9 @@ export function createViewerHost({ state, parts }) {
notify();
try {
const adapter = await mount(entry);
if (!current()) return;
if (!current()) return false;
await adapter.open(String(imageId));
if (!current()) return;
if (!current()) return false;
if (frame && !state.street.follow) parts.follow.lookAtPosition();
} catch (error) {
if (current())
@@ -142,32 +148,16 @@ export function createViewerHost({ state, parts }) {
if (current()) state.street.loading = false;
notify();
}
if (current() && !state.street.error) selectCurrentSequence();
if (!current() || state.street.error) return false;
selectCurrentSequence();
return true;
}
/** Close the image; the mounted adapter stays warm for the next open. */
function close() {
openSeq++;
active?.adapter.close();
Object.assign(state.street, {
open: false,
follow: false,
providerId: null,
providerName: null,
providerLabel: null,
imageId: null,
position: null,
bearing: null,
tilt: null,
altitude: null,
isPano: false,
capturedAt: null,
sequenceId: null,
creator: null,
externalUrl: null,
loading: false,
error: null,
});
Object.assign(state.street, freshStreet());
parts.marker.clear();
notify();
}
@@ -248,6 +238,5 @@ export function createViewerHost({ state, parts }) {
setRenderMode,
prewarm,
unmount,
activeProviderId: () => active?.id || null,
};
}