refactor(street-level): one provider, no orbit overview, no prewarm, simpler tile choice

The layer now owns its Mapillary parts directly: the provider registry,
pick router, multi-provider UI summary, params codec helper and provider
context are gone. createStreetLevelLayer({ source }) builds coverage,
sequences, the nearest-image lookup and the MapillaryJS viewer itself; a
single providerOn switch keeps the share link's `mapillary` option (off
draws nothing), and getParams/setParams keep { mapillary, pano, sinceDays }.

No z0-5 overview points: above 60 km nothing is drawn and the panel says to
zoom in; the proxy now serves only coverage z11-14. Tiles come from the
visible ground within range of the camera, ranked from the ground under it
(no line-of-sight ranking or near-range padding). MapillaryJS loads on the
first photo; viewer prewarm is removed. Key status, a rejected key and a
rate limit share one `blocked` state on coverage.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-07 22:19:24 +04:00
1 parent 0f41fb04cd
commit 37a96072fa
51 files changed
+1240 -2659

No files matched your search

+35 -97
View File
@@ -33,27 +33,16 @@ const { createMapillaryViewer } =
/**
* A viewer host over `adapter`. `marker` records where the position marker
* was put (null once cleared), `framing` counts framing calls and
* `selected` every sequence the provider was asked to select.
* `selected` every sequence the layer was asked to select.
*/
function harness(adapter) {
const state = {
enabled: true,
notify() {},
providers: new Map(),
street: { host: {}, renderMode: 'letterbox' },
sequence: { selectedId: null },
};
const def = { id: 'mapillary', name: 'Mapillary', label: 'MAPILLARY' };
const selected = [];
let selectedId = null;
const instance = {
viewer: adapter,
selectSequence(id) {
selected.push(id);
selectedId = id;
},
sequenceStats: () => ({ selectedId }),
};
state.providers.set('mapillary', { def, instance });
const framing = { begun: 0, started: 0, cancelled: 0 };
const marker = [];
const parts = {
@@ -67,17 +56,23 @@ function harness(adapter) {
frame: (ticket) => ticket && framing.started++,
cancel: () => framing.cancelled++,
},
sequences: {
select(id) {
selected.push(id);
state.sequence.selectedId = id;
},
},
};
return {
state,
framing,
marker,
selected,
/** Esc on the map: the provider drops its selected sequence. */
/** Esc on the map: the selected sequence is dropped. */
clearSequence() {
selectedId = null;
state.sequence.selectedId = null;
},
host: createViewerHost({ state, parts }),
host: createViewerHost({ state, parts, adapter }),
};
}
@@ -102,7 +97,6 @@ function fakeAdapter({ failMounts = 0, gate = null, openGate = null } = {}) {
/** A pose event from the library, as MapillaryJS fires them. */
emitPose(id, overrides = {}) {
emit?.({
providerId: 'mapillary',
imageId: id,
sequenceId: `seq-${id}`,
position: { lon: 1, lat: 2 },
@@ -139,10 +133,10 @@ function fakeAdapter({ failMounts = 0, gate = null, openGate = null } = {}) {
test('a failed mount is retried on the next open instead of being cached', async () => {
const adapter = fakeAdapter({ failMounts: 1 });
const { state, host } = harness(adapter);
await host.open('mapillary', 'a');
await host.open('a');
assert.equal(state.street.error, 'library failed to load');
assert.equal(adapter.calls.listeners, 0, 'the pose listener was released');
await host.open('mapillary', 'b');
await host.open('b');
assert.equal(state.street.error, null);
assert.equal(adapter.calls.mount, 2);
assert.deepEqual(adapter.calls.open, ['b']);
@@ -153,7 +147,7 @@ test('a failed mount is retried on the next open instead of being cached', async
test('concurrent opens share one mount and one pose listener', async () => {
const adapter = fakeAdapter();
const { host } = harness(adapter);
await Promise.all([host.open('mapillary', 'a'), host.open('mapillary', 'b')]);
await Promise.all([host.open('a'), host.open('b')]);
assert.equal(adapter.calls.mount, 1);
assert.equal(adapter.calls.listeners, 1);
});
@@ -161,62 +155,22 @@ test('concurrent opens share one mount and one pose listener', async () => {
test('unmounting while a mount is in flight does not leave it active', async () => {
const adapter = fakeAdapter();
const { state, host } = harness(adapter);
const opening = host.open('mapillary', 'a');
const opening = host.open('a');
host.unmount();
await opening;
assert.equal(adapter.calls.listeners, 0);
assert.equal(adapter.calls.unmount, 1);
assert.equal(adapter.calls.unmount, 2, 'layer off, then the late mount');
assert.equal(state.street.open, false);
// Not left active: the next open mounts again.
await host.open('mapillary', 'b');
await host.open('b');
assert.equal(adapter.calls.mount, 2);
assert.equal(state.street.imageId, 'b');
});
test('a prewarmed viewer is released when the layer goes off, or at once if it went off mid-load', async () => {
const adapter = fakeAdapter();
let release = null;
adapter.prewarm = () =>
new Promise((resolve) => {
release = resolve;
});
const { state, host } = harness(adapter);
const entry = state.providers.get('mapillary');
entry.on = true;
// Warmed, then the layer is switched off: unmount releases the warm viewer.
const warming = host.prewarm([entry]);
release();
await warming;
assert.equal(adapter.calls.unmount, 0);
host.unmount();
assert.equal(adapter.calls.unmount, 1, 'the WebGL viewer is destroyed');
// Switched off while the library was still loading: released on arrival.
const late = host.prewarm([entry]);
state.enabled = false;
release();
await late;
assert.equal(adapter.calls.unmount, 2);
});
test('switching one provider off releases only its viewer', async () => {
const adapter = fakeAdapter();
adapter.prewarm = async () => {};
const { state, host } = harness(adapter);
const entry = state.providers.get('mapillary');
entry.on = true;
await host.prewarm([entry]);
host.unmount('panoramax');
assert.equal(adapter.calls.unmount, 0, 'another provider leaves it alone');
host.unmount('mapillary');
assert.equal(adapter.calls.unmount, 1);
});
test('a pose that arrives after the photo was closed does not bring it back', async () => {
const adapter = fakeAdapter();
const { state, host } = harness(adapter);
await host.open('mapillary', 'a');
await host.open('a');
host.close();
const before = { ...state.street };
// The library's `image` event for a photo that was still loading.
@@ -239,16 +193,16 @@ test('switching the layer off and on during a cold first open keeps the shared v
const gate = deferred();
const adapter = fakeAdapter({ gate });
const { state, host } = harness(adapter);
const first = host.open('mapillary', 'a'); // the library is still loading
const first = host.open('a'); // the library is still loading
host.unmount(); // layer off …
const second = host.open('mapillary', 'b'); // … on again, and a new click
const second = host.open('b'); // … on again, and a new click
gate.resolve();
await Promise.all([first, second]);
assert.equal(adapter.calls.unmount, 0, 'the outdated mount left it alone');
assert.equal(adapter.calls.unmount, 1, 'only the layer going off');
assert.equal(adapter.calls.listeners, 1);
assert.equal(state.street.error, null);
assert.equal(state.street.imageId, 'b');
await host.open('mapillary', 'c');
await host.open('c');
assert.equal(state.street.error, null, 'later opens are not stuck');
assert.equal(state.street.imageId, 'c');
});
@@ -258,7 +212,7 @@ const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
test('every pose the viewer reports moves the marker; only the open frames the camera', async () => {
const adapter = fakeAdapter();
const { host, marker, framing } = harness(adapter);
await host.open('mapillary', 'a');
await host.open('a');
assert.deepEqual(marker, [{ position: { lon: 1, lat: 2 }, bearing: 90 }]);
// The user walks on: the marker goes along, the globe camera stays.
adapter.emitPose('b', { position: { lon: 3, lat: 4 }, bearing: 180 });
@@ -276,7 +230,7 @@ test('a pose on another sequence selects it, but not while the image still loads
const openGate = deferred();
const adapter = fakeAdapter({ openGate });
const { state, host, selected, clearSequence } = harness(adapter);
const opening = host.open('mapillary', 'a');
const opening = host.open('a');
await settle();
assert.equal(state.street.loading, true, 'the image is downloading');
assert.equal(state.street.sequenceId, 'seq-a', 'its pose is in');
@@ -299,7 +253,7 @@ test('a pose on another sequence selects it, but not while the image still loads
test('closing the photo clears the marker', async () => {
const adapter = fakeAdapter();
const { host, marker } = harness(adapter);
await host.open('mapillary', 'a');
await host.open('a');
assert.notEqual(marker.at(-1), null);
host.close();
assert.equal(marker.at(-1), null);
@@ -309,7 +263,7 @@ test('an open still mounting when the photo is closed does nothing when the moun
const gate = deferred();
const adapter = fakeAdapter({ gate });
const { state, host, framing, selected } = harness(adapter);
const opening = host.open('mapillary', 'a'); // MapillaryJS still loading
const opening = host.open('a'); // MapillaryJS still loading
await settle();
host.close();
gate.resolve();
@@ -322,7 +276,7 @@ test('an open still mounting when the photo is closed does nothing when the moun
[false, false, null],
);
// The mounted viewer serves the next open.
assert.equal(await host.open('mapillary', 'b'), true);
assert.equal(await host.open('b'), true);
assert.equal(state.street.imageId, 'b');
});
@@ -330,7 +284,7 @@ test('an image still opening when the photo is closed is neither framed nor sele
const openGate = deferred();
const adapter = fakeAdapter({ openGate });
const { state, host, framing, selected } = harness(adapter);
const opening = host.open('mapillary', 'a');
const opening = host.open('a');
await settle();
assert.deepEqual(adapter.calls.open, ['a'], 'the image is downloading');
host.close();
@@ -408,16 +362,16 @@ test('the Mapillary viewer survives the layer going off and on during its first
const { gate, viewers } = fakeLibrary();
const adapter = createMapillaryViewer({ source: { token: 't' } });
const { state, host } = harness(adapter);
const first = host.open('mapillary', 'img1');
const first = host.open('img1');
await Promise.resolve();
host.unmount();
const second = host.open('mapillary', 'img2');
const second = host.open('img2');
await Promise.resolve();
gate.resolve();
await Promise.all([first, second]);
assert.equal(state.street.error, null);
assert.equal(state.street.imageId, 'img2');
await host.open('mapillary', 'img3');
await host.open('img3');
assert.equal(state.street.error, null, 'later opens are not stuck');
assert.equal(state.street.imageId, 'img3');
assert.equal(viewers.live, 1);
@@ -439,20 +393,6 @@ test('a Mapillary viewer unmounted mid-download is never built; the next mount b
assert.equal(viewers.live, 0, 'nothing left holding a WebGL context');
});
test('a prewarm that gave up when the layer went off and on mid-download is followed by one that builds', async () => {
const { gate, viewers } = fakeLibrary();
const adapter = createMapillaryViewer({ source: { token: 't' } });
const element = {};
const first = adapter.prewarm(element);
adapter.unmount(); // layer off while MapillaryJS downloads
const second = adapter.prewarm(element); // layer on again
gate.resolve();
await Promise.all([first, second]);
assert.equal(viewers.created, 1, 'the second prewarm built the viewer');
assert.equal(viewers.live, 1);
adapter.unmount();
});
test('the Mapillary viewer leaves resizing to the panel, so a hidden one never asks for z=NaN', async () => {
const { gate, viewers } = fakeLibrary();
gate.resolve();
@@ -467,7 +407,7 @@ test('closing the photo stops sequence playback in the hidden viewer', async ()
gate.resolve();
const adapter = createMapillaryViewer({ source: { token: 't' } });
const { host } = harness(adapter);
await host.open('mapillary', 'img1');
await host.open('img1');
const [viewer] = viewers.instances;
assert.equal(viewer.playback.stops, 0);
host.close();
@@ -478,13 +418,11 @@ test('closing the photo stops sequence playback in the hidden viewer', async ()
test('closing the photo or switching the layer off stops its framing flight', async () => {
const adapter = fakeAdapter();
const { framing, host } = harness(adapter);
await host.open('mapillary', 'a');
await host.open('a');
assert.equal(framing.started, 1, 'the open framed the photo');
host.close();
assert.equal(framing.cancelled, 1, 'closed: the globe stops flying to it');
await host.open('mapillary', 'b');
host.unmount('panoramax');
assert.equal(framing.cancelled, 1, 'another provider leaves it alone');
await host.open('b');
host.unmount();
assert.equal(framing.cancelled, 2, 'layer off: likewise');
});
@@ -557,7 +495,7 @@ test('a photo asks for the camera when it starts opening and frames with that ti
const openGate = deferred();
const adapter = fakeAdapter({ openGate });
const { host, framing } = harness(adapter);
const opening = host.open('mapillary', 'a');
const opening = host.open('a');
assert.equal(framing.begun, 1, 'claimed before the download');
await settle();
assert.equal(framing.started, 0);