refactor(street-level): drop unused tile and pose fields; keep the sequence error on the sequence

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-08 15:03:37 +04:00
1 parent 7411ebaf1c
commit c78c872f92
10 files changed
+32 -60

No files matched your search

+1 -20
View File
@@ -60,7 +60,6 @@ export function createStreetLevelLayer({
photoViewer || createMapillaryViewer({ source, render: services.render }),
});
parts.openImage = openImage;
parts.reportSequenceError = reportSequenceError;
parts.selection = createSelection(context);
let notifyQueued = false;
@@ -81,24 +80,6 @@ export function createStreetLevelLayer({
}
state.notify = notify;
/** The error a sequence lookup last showed, until it withdraws it. */
let sequenceError = null;
/**
* Show a sequence error, or withdraw it (null). A withdrawal clears only
* that error, never a newer one from the viewer.
*/
function reportSequenceError(message) {
if (message) {
state.street.error = sequenceError = message;
} else {
if (!sequenceError) return;
if (state.street.error === sequenceError) state.street.error = null;
sequenceError = null;
}
notify();
}
/** Coverage waits for the key status: no tile requests without a key. */
async function checkKey() {
let configured;
@@ -172,6 +153,7 @@ export function createStreetLevelLayer({
selectedId: state.sequence.selectedId,
images: state.sequence.images.length,
loading: state.sequence.loading,
error: state.sequence.error,
},
street,
};
@@ -228,7 +210,6 @@ export function createStreetLevelLayer({
state.listeners.clear();
state.viewer = null;
state.initialized = false;
state.destroyed = true;
},
getStats() {
+7 -8
View File
@@ -205,7 +205,7 @@ test('switching Mapillary off clears coverage and closes the image it shows', as
assert.equal(layer.getUIState().providerOn, false);
});
test('a sequence error is withdrawn with the sequence, never the viewer’s own', async (t) => {
test('a sequence error goes with the sequence and leaves the viewer’s own', async (t) => {
const source = fakeMapillarySource({
getSequenceImages: async () => {
throw new Error('Sequence images unavailable');
@@ -213,16 +213,15 @@ test('a sequence error is withdrawn with the sequence, never the viewer’s own'
});
const { layer } = await startLayer(t, { source });
await layer.selectSequence('seq-1');
assert.equal(layer.getUIState().street.error, 'Sequence images unavailable');
layer.clearSequence();
assert.equal(layer.getUIState().street.error, null, 'withdrawn');
// The viewer's own error is not the sequence's to clear.
await layer.selectSequence('seq-2');
layer.attachViewerHost(null);
await layer.openImage('img1');
let ui = layer.getUIState();
assert.equal(ui.sequence.error, 'Sequence images unavailable');
assert.match(ui.street.error, /Open the Street Level panel/);
layer.clearSequence();
assert.match(layer.getUIState().street.error, /Open the Street Level panel/);
ui = layer.getUIState();
assert.equal(ui.sequence.error, null, 'withdrawn with the sequence');
assert.match(ui.street.error, /Open the Street Level panel/, 'kept');
});
test('closing the photo, switching the layer off or destroying it stops the framing flight', async (t) => {
@@ -30,13 +30,8 @@ export function decodeCoverageTile(bytes, address) {
if (!parts.length) continue;
result.sequences.push({
id: String(props.id ?? feature.id ?? `${x}/${y}/${i}`),
imageId: props.image_id != null ? String(props.image_id) : null,
capturedAt: Number(props.captured_at) || 0,
isPano: props.is_pano === true,
onFoot: props.foot === true,
quality: Number.isFinite(props.quality_score)
? props.quality_score
: null,
parts,
});
}
@@ -25,9 +25,6 @@ function normalizeSequenceImage(record) {
compassAngle: Number(record.compass_angle) || 0,
capturedAt: Number(record.captured_at) || 0,
isPano: record.is_pano === true,
altitude: Number.isFinite(record.computed_altitude)
? record.computed_altitude
: null,
};
}
@@ -56,11 +53,6 @@ export function createSequences({ state, source, parts }) {
state.notify?.();
}
/** Show a sequence error, or withdraw it (null); see `reportSequenceError`. */
function reportError(message) {
parts.reportSequenceError?.(message);
}
/** Drawn cone billboards. */
let cones = [];
@@ -151,7 +143,7 @@ export function createSequences({ state, source, parts }) {
state.sequence.loading = false;
state.sequence.images = cached;
renderCones(cached);
reportError(null);
state.sequence.error = null;
notify();
return;
}
@@ -177,13 +169,13 @@ export function createSequences({ state, source, parts }) {
remember(sequenceId, images);
state.sequence.images = images;
renderCones(images);
reportError(null);
state.sequence.error = null;
} catch (error) {
if (!controller.signal.aborted) {
// Nothing to show: drop the highlight, so a click asks again.
parts.coverage.recolorSequence(sequenceId, false);
state.sequence.selectedId = null;
reportError(error?.message || 'Sequence images unavailable');
state.sequence.error = error?.message || 'Sequence images unavailable';
}
} finally {
if (state.sequence.abort === controller) {
@@ -203,7 +195,7 @@ export function createSequences({ state, source, parts }) {
state.sequence.loading = false;
clearCones();
// An error about the sequence goes with it.
reportError(null);
state.sequence.error = null;
requestRender();
notify();
}
@@ -58,7 +58,6 @@ function records(sequenceId, count = 2) {
function setup() {
const source = deferredSource();
const colours = [];
const errors = [];
const state = {
services: {},
viewer: {},
@@ -70,6 +69,7 @@ function setup() {
cache: new Map(),
collection: cones(),
loading: false,
error: null,
abort: null,
},
};
@@ -77,16 +77,15 @@ function setup() {
coverage: {
recolorSequence: (id, selected) => colours.push([id, selected]),
},
reportSequenceError: (message) => errors.push(message),
};
const sequences = createSequences({ state, source, parts });
const drawn = () =>
state.sequence.collection.items.map((cone) => cone.id.split(':').at(-1));
return { state, source, sequences, colours, errors, drawn };
return { state, source, sequences, colours, drawn };
}
test('a failed sequence load leaves no stale cones and can be retried', async () => {
const { state, source, sequences, colours, errors, drawn } = setup();
const { state, source, sequences, colours, drawn } = setup();
sequences.select('A');
source.last('A').resolve(records('A'));
await settle();
@@ -100,7 +99,7 @@ test('a failed sequence load leaves no stale cones and can be retried', async ()
assert.equal(state.sequence.loading, false);
assert.equal(state.sequence.selectedId, null, 'B is not left highlighted');
assert.deepEqual(colours.at(-1), ['B', false]);
assert.deepEqual(errors, [null, 'Sequence images unavailable']);
assert.equal(state.sequence.error, 'Sequence images unavailable');
// Clicking B again asks again.
sequences.select('B');
@@ -109,7 +108,7 @@ test('a failed sequence load leaves no stale cones and can be retried', async ()
await settle();
assert.deepEqual(drawn(), ['B-0', 'B-1']);
assert.equal(state.sequence.selectedId, 'B');
assert.equal(errors.at(-1), null, 'the error is cleared once B loads');
assert.equal(state.sequence.error, null, 'cleared once B loads');
});
test('a late answer for a superseded sequence neither draws nor is cached', async () => {
@@ -151,7 +150,7 @@ test('a cached sequence is re-selected without a lookup', async () => {
});
test('clearSelection cancels the lookup and uncolours the sequence', async () => {
const { state, source, sequences, colours, errors, drawn } = setup();
const { state, source, sequences, colours, drawn } = setup();
sequences.select('A');
sequences.clearSelection();
assert.equal(source.last('A').signal.aborted, true);
@@ -161,7 +160,7 @@ test('clearSelection cancels the lookup and uncolours the sequence', async () =>
]);
assert.equal(state.sequence.selectedId, null);
assert.equal(state.sequence.loading, false);
assert.equal(errors.at(-1), null, 'an error about it goes too');
assert.equal(state.sequence.error, null, 'an error about it goes too');
source.last('A').resolve(records('A'));
await settle();
assert.deepEqual(drawn(), [], 'the late answer draws nothing');
@@ -5,7 +5,6 @@ import { mapillaryImageUrl } from './policy.js';
* @property {string} imageId
* @property {{lon: number, lat: number}} position
* @property {number|null} bearing
* @property {number|null} tilt
* @property {number|null} altitude
* @property {boolean} isPano
* @property {number|null} capturedAt Epoch milliseconds.
@@ -94,7 +93,6 @@ export function createMapillaryViewer({ source, render } = {}) {
...current,
position: { lon: lngLat.lng, lat: lngLat.lat },
bearing: Number.isFinite(pov?.bearing) ? pov.bearing : null,
tilt: Number.isFinite(pov?.tilt) ? pov.tilt : 0,
externalUrl: mapillaryImageUrl(current.imageId),
});
requestRender();
+2 -2
View File
@@ -10,7 +10,6 @@ export function freshStreet() {
imageId: null,
position: null,
bearing: null,
tilt: null,
altitude: null,
isPano: false,
capturedAt: null,
@@ -55,7 +54,6 @@ export function createState({ services }) {
viewer: null,
enabled: false,
initialized: false,
destroyed: false,
listeners: new Set(),
notify: null,
/** The share link's `mapillary` switch: off draws nothing. */
@@ -78,6 +76,8 @@ export function createState({ services }) {
cache: new Map(),
collection: null,
loading: false,
/** Why the selected sequence's images could not load, until it goes. */
error: null,
abort: null,
},
-1
View File
@@ -23,7 +23,6 @@ export function createViewerHost({ state, parts, adapter }) {
imageId: pose.imageId,
position: pose.position ? { ...pose.position } : null,
bearing: Number.isFinite(pose.bearing) ? pose.bearing : null,
tilt: Number.isFinite(pose.tilt) ? pose.tilt : 0,
altitude: Number.isFinite(pose.altitude) ? pose.altitude : null,
isPano: pose.isPano === true,
capturedAt: pose.capturedAt ?? null,
+4 -1
View File
@@ -118,7 +118,10 @@ export function presentStreetLevelPanel(state, { now = Date.now() } = {}) {
controlsDisabled: state.keyRequired === true,
error: keyMissing
? keyHint()
: state.street.error || state.coverage.error || null,
: state.street.error ||
state.sequence.error ||
state.coverage.error ||
null,
filter: { pano: filter.pano, sinceDays: Number(filter.sinceDays) || 0 },
since: presentSince(filter.sinceDays, now),
legend: state.providerOn !== false,
+7 -1
View File
@@ -124,7 +124,7 @@ test('the header pill is the on/off switch and reads OFF, LOADING or ON', () =>
);
});
test('errors from the viewer or the coverage web surface in one alert', () => {
test('errors from the viewer, the sequence or the coverage surface in one alert', () => {
assert.equal(presentStreetLevelPanel(snapshot()).error, null);
assert.equal(
presentStreetLevelPanel(
@@ -132,6 +132,12 @@ test('errors from the viewer or the coverage web surface in one alert', () => {
).error,
'Image could not be opened',
);
assert.equal(
presentStreetLevelPanel(
snapshot({ sequence: { error: 'Sequence images unavailable' } }),
).error,
'Sequence images unavailable',
);
assert.equal(
presentStreetLevelPanel(snapshot({ coverage: { error: 'Tile HTTP 502' } }))
.error,