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

+4 -9
View File
@@ -180,15 +180,10 @@ export class StreetLevelControls {
async _toggleEnabled() {
const enabled = this.actions.isEnabled?.() === true;
// The pill is the only switch: switching on also brings back a provider
// a share link or a tool turned off, or the layer would draw nothing.
const dark = enabled
? []
: (this._state?.providers || []).filter((entry) => entry.on === false);
if (dark.length)
this._setParams(
Object.fromEntries(dark.map((entry) => [entry.id, true])),
);
// The pill is the only switch: switching on also brings back Mapillary
// if a share link or a tool turned it off, or the layer would draw nothing.
if (!enabled && this._state?.providerOn === false)
this._setParams({ mapillary: true });
try {
await this.actions.setEnabled?.(!enabled);
} catch (error) {
+15 -37
View File
@@ -9,7 +9,7 @@ import {
REGISTERED_LAYER_IDS,
encodeLayerStateParams,
} from '../data/layerState.js';
import { fakeStreetLevelProvider } from '../testSupport/streetLevelFakes.mjs';
import { fakeMapillarySource } from '../testSupport/streetLevelFakes.mjs';
/* ── A small DOM: just what the panel controls touch ───────────────────── */
@@ -215,12 +215,7 @@ async function withDom(fn, options) {
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
/* ── A Street Level layer with one stand-in provider ───────────────────── */
function standInProvider() {
const def = fakeStreetLevelProvider({ html: 'Mapillary' });
return { filters: def.filters, def };
}
/* ── A Street Level layer over a stand-in Mapillary source ─────────────── */
/** Every other registered layer, as a module with no options. */
function plainLayer(id) {
@@ -254,8 +249,7 @@ const memoryStorage = () => {
/** The production path: controls → dataManager → layer → durable layer state. */
async function productionPanel(dom) {
const provider = standInProvider();
const layer = createStreetLevelLayer({ providers: [provider.def] });
const layer = createStreetLevelLayer({ source: fakeMapillarySource() });
const manager = new DataLayerManager({});
for (const id of REGISTERED_LAYER_IDS)
manager.register(id === 'street-level' ? layer : plainLayer(id));
@@ -288,7 +282,7 @@ async function productionPanel(dom) {
encodeLayerStateParams(params, coordinator.getDurableState());
return (params.get('lo') || '').split('_');
};
return { provider, layer, manager, coordinator, controls, share };
return { layer, manager, coordinator, controls, share };
}
const TOKEN = LAYER_STATE_REGISTRY.find(
@@ -297,11 +291,14 @@ const TOKEN = LAYER_STATE_REGISTRY.find(
test('a 360° click reaches the share link through the data manager', () =>
withDom(async (dom) => {
const { provider, coordinator, controls, share } =
await productionPanel(dom);
const { layer, coordinator, controls, share } = await productionPanel(dom);
dom.root.querySelectorAll('[data-sl-pano]')[1].click(); // 360°
await settle();
assert.equal(provider.filters.at(-1).pano, 'pano', 'the layer applied it');
assert.equal(
layer.getUIState().filter.pano,
'pano',
'the layer applied it',
);
assert.equal(
coordinator.getDurableState().options['street-level'].pano,
'pano',
@@ -362,35 +359,20 @@ function uiState({
} = {}) {
return {
enabled,
providerOn: on,
keyRequired,
keyRejected: false,
filter: { pano, sinceDays: 0 },
providers: [
{
id: 'mapillary',
name: 'Mapillary',
label: 'MAPILLARY',
color: '#05cb63',
on,
configured: !keyRequired,
keyRequired,
requiresKeyId: 'mapillary',
loading: false,
count: 3,
hint: '',
error: null,
},
],
coverage: { count: 3, loading: false, hint: '', error: null },
legend: legend || [
{ key: 'mapillary', label: 'Mapillary', color: '#05cb63' },
{ key: 'selected', label: 'Selected', color: '#00d4ff' },
],
sequence: { providerId: null, selectedId: null, images: 0, loading: false },
sequence: { selectedId: null, images: 0, loading: false },
street: {
open,
loading: false,
renderMode,
providerId: open ? 'mapillary' : null,
imageId: open ? 'img-1' : null,
error: null,
},
@@ -422,18 +404,14 @@ function stubPanel(dom, state, extraActions = {}) {
return { layer, calls, controls };
}
test('the header pill switches the layer, bringing back a provider switched off elsewhere', () =>
test('the header pill switches the layer, bringing back Mapillary switched off elsewhere', () =>
withDom(async (dom) => {
const status = dom.root.querySelector('#sl-status');
const on = stubPanel(dom, uiState());
status.click();
await settle();
assert.deepEqual(on.calls.setEnabled, [false]);
assert.deepEqual(
on.calls.setParams,
[],
'switching off keeps the provider',
);
assert.deepEqual(on.calls.setParams, [], 'switching off keeps the switch');
on.controls.destroy();
const dark = stubPanel(dom, uiState({ enabled: false, on: false }));
status.click();
+6 -9
View File
@@ -67,13 +67,10 @@ function presentStatus(state) {
: { text: 'OFF', tone: '', pressed, title };
}
/** How to add the missing key, for the provider that needs one. */
function keyHint(state) {
const provider = (state.providers || []).find(
(entry) => entry.keyRequired === true && entry.requiresKeyId,
);
const requirement = provider && keySetupRequirement(provider.requiresKeyId);
return requirement ? `${provider.name}: ${requirement}` : null;
/** How to add the missing Mapillary key. */
function keyHint() {
const requirement = keySetupRequirement('mapillary');
return requirement ? `Mapillary: ${requirement}` : null;
}
function presentViewer(state) {
@@ -90,7 +87,7 @@ function presentViewer(state) {
captionLeft: street.creator ? `Image by ${street.creator}` : '',
captionRight: right.join(' · '),
link: street.externalUrl || null,
linkLabel: street.providerLabel ? `${street.providerLabel} ↗` : '',
linkLabel: street.externalUrl ? 'MAPILLARY ↗' : '',
};
}
@@ -115,7 +112,7 @@ export function presentStreetLevelPanel(state, { now = Date.now() } = {}) {
status: presentStatus(state),
controlsDisabled: state.keyRequired === true,
error: keyMissing
? keyHint(state)
? keyHint()
: state.street.error || state.coverage.error || null,
filter: { pano: filter.pano, sinceDays: Number(filter.sinceDays) || 0 },
since: presentSince(filter.sinceDays, now),
+5 -16
View File
@@ -5,29 +5,23 @@ import {
SINCE_STOPS,
sinceStopIndex,
} from './streetLevelPresentation.js';
import { providerSnapshot } from '../testSupport/streetLevelFakes.mjs';
const provider = providerSnapshot;
function snapshot(overrides = {}) {
const base = {
enabled: false,
providerOn: true,
keyRequired: false,
keyRejected: false,
filter: { pano: 'all', sinceDays: 0 },
providers: [provider()],
coverage: { loading: false, count: 0, hint: '', error: null },
legend: [
{ key: 'mapillary', label: 'Mapillary', color: '#05cb63' },
{ key: 'selected', label: 'Selected', color: '#00d4ff' },
],
sequence: { providerId: null, selectedId: null, images: 0, loading: false },
sequence: { selectedId: null, images: 0, loading: false },
street: {
open: false,
loading: false,
error: null,
providerId: null,
providerName: null,
providerLabel: null,
imageId: null,
position: null,
bearing: null,
@@ -70,7 +64,6 @@ test('KEY REQUIRED says how to add the key instead of a raw error code', () => {
enabled: true,
keyRequired: true,
coverage: { error: 'no_key' },
providers: [provider({ keyRequired: true, error: 'no_key' })],
}),
);
assert.equal(view.status.text, 'KEY REQUIRED');
@@ -86,7 +79,6 @@ test('a key Mapillary rejected reads KEY REJECTED and names the fix', () => {
keyRequired: true,
keyRejected: true,
coverage: { error },
providers: [provider({ keyRequired: true, keyRejected: true, error })],
}),
);
assert.equal(view.status.text, 'KEY REJECTED');
@@ -96,7 +88,7 @@ test('a key Mapillary rejected reads KEY REJECTED and names the fix', () => {
});
test('a key problem outranks loading on the pill: KEY REJECTED, then KEY REQUIRED (M65)', () => {
// A keyless provider can still report loading (its status check, a retry):
// A keyless layer can still report loading (its status check, a retry):
// the pill must say what is wrong, not that something is on its way.
const loading = { enabled: true, coverage: { loading: true } };
assert.equal(
@@ -221,15 +213,12 @@ test('the meta line never mixes the visible-sequence count with the selected seq
assert.equal(hinted.meta, 'Point the camera at the globe');
});
test('viewer caption reads "Image by" left, date right, and links to the provider', () => {
test('viewer caption reads "Image by" left, date right, and links to Mapillary', () => {
const view = presentStreetLevelPanel(
snapshot({
enabled: true,
street: {
open: true,
providerId: 'mapillary',
providerName: 'Mapillary',
providerLabel: 'MAPILLARY',
imageId: '1814275685699406',
creator: 'mapfool',
capturedAt: Date.UTC(2023, 9, 8),