Files
gods-eye-view/src/layers/streetLevel/index.test.mjs
T
German KhokhlovandClaude Opus 5.5 37a96072fa 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]>
2026-10-07 22:19:24 +04:00

431 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import test from 'node:test';
import * as Cesium from 'cesium';
import { createStreetLevelLayer } from './index.js';
import { MAPILLARY_CREDIT_HTML } from './providers/mapillary/policy.js';
import {
fakeMapillarySource,
fakePhotoViewer,
rayCamera,
} from '../../testSupport/streetLevelFakes.mjs';
/**
* A stand-in Cesium viewer the layer can be enabled on: a canvas for the
* click handler, camera events, a primitive list and a credit display. With
* `view`, the camera looks straight down on Sacramento from 900 m, so
* coverage asks for tiles.
*/
function fakeViewer({ view = false } = {}) {
const credits = [];
/** Cesium's flight bookkeeping: a new flight cancels the current one. */
let flight = null;
const flights = { started: 0, cancelled: 0 };
const stopFlight = () => {
const current = flight;
flight = null;
current?.cancel?.();
return Boolean(current);
};
const canvas = Object.assign(new EventTarget(), {
style: {},
// Keep Cesium's handler on the canvas; there is no real document here.
disableRootEvents: true,
onwheel: null,
...(view ? { clientWidth: 100, clientHeight: 100 } : {}),
});
const camera = Object.assign(
view
? rayCamera({
lon: -121.4944,
lat: 38.5816,
altitude: 900,
pitch: -90,
width: 100,
height: 100,
})
: {},
{
changed: new Cesium.Event(),
moveStart: new Cesium.Event(),
moveEnd: new Cesium.Event(),
flyToBoundingSphere(sphere, options = {}) {
stopFlight();
flight = options;
flights.started++;
},
cancelFlight() {
if (stopFlight()) flights.cancelled++;
},
},
);
return {
credits,
flights,
scene: {
canvas,
primitives: { add: (p) => p, remove() {} },
groundPrimitives: { add: (p) => p, remove() {} },
// Enough of a scene for the position marker to clamp to the ground.
frameState: { mode: Cesium.SceneMode.SCENE3D },
updateHeight: () => () => {},
getHeight: () => undefined,
},
camera,
creditDisplay: {
addStaticCredit: (credit) => credits.push(credit),
removeStaticCredit: (credit) =>
credits.splice(credits.indexOf(credit), 1),
},
};
}
/**
* A layer over `source` and `photoViewer`, initialised on a stand-in viewer
* and (unless `enable` is false) enabled.
*/
async function startLayer(
t,
{
source = fakeMapillarySource(),
photoViewer = fakePhotoViewer(),
view = false,
enable = true,
} = {},
) {
const saved = globalThis.document;
// A pose moves the position marker, whose glyph is drawn on a canvas.
const drawing = new Proxy({}, { get: () => () => ({ addColorStop() {} }) });
globalThis.document = Object.assign(new EventTarget(), {
createElement: () => ({ getContext: () => drawing }),
});
// Skip Cesium's one-time terrain table download for draped lines.
t.mock.method(
Cesium.GroundPolylinePrimitive,
'initializeTerrainHeights',
async () => {},
);
const viewer = fakeViewer({ view });
const layer = createStreetLevelLayer({ source, photoViewer });
layer.init(viewer);
if (enable) layer.enable(viewer);
t.after(() => {
layer.destroy();
globalThis.document = saved;
});
await settle();
return { layer, viewer, source, photoViewer };
}
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
function deferred() {
let resolve;
let reject;
const promise = new Promise((done, fail) => {
resolve = done;
reject = fail;
});
return { promise, resolve, reject };
}
/** A photo viewer reporting a pose for every image it opens. */
const posingViewer = () =>
fakePhotoViewer({ pose: { position: { lon: -121.49, lat: 38.58 } } });
/** Nearest-image lookups that answer when the test says, with their signals. */
function slowLookups() {
const lookups = [];
const nearestImages = (query, { signal } = {}) => {
const answer = deferred();
lookups.push({ query, signal, answer });
return answer.promise.then((id) => [{ id, is_pano: false }]);
};
return { lookups, source: fakeMapillarySource({ nearestImages }) };
}
test('the layer is Mapillary under the codec keys it has always had', () => {
const layer = createStreetLevelLayer({ source: fakeMapillarySource() });
assert.equal(layer.id, 'street-level');
assert.equal(layer.requiresKeyId, 'mapillary');
assert.deepEqual(layer.getParams(), {
mapillary: true,
pano: 'all',
sinceDays: 0,
});
assert.throws(() => createStreetLevelLayer({ source: {} }), /source/);
});
test('the CC BY-SA credit shows while Mapillary is on and goes with it', async (t) => {
const { layer, viewer } = await startLayer(t);
const shown = () => viewer.credits.map((credit) => credit.html);
assert.deepEqual(shown(), [MAPILLARY_CREDIT_HTML], 'shown on enable');
assert.match(shown()[0], /CC BY-SA 4\.0/);
layer.setParams({ mapillary: false });
assert.deepEqual(shown(), [], 'hidden with the switch');
assert.deepEqual(layer.getUIState().legend, [], 'no legend either');
layer.setParams({ mapillary: true });
assert.deepEqual(shown(), [MAPILLARY_CREDIT_HTML]);
layer.disable();
assert.deepEqual(shown(), [], 'the credit goes with the layer');
});
test('coverage asks for no tiles before the key status is known, and none without a key', async (t) => {
for (const configured of [false, true]) {
const status = deferred();
const source = fakeMapillarySource({ getStatus: () => status.promise });
const { layer } = await startLayer(t, { source, view: true });
assert.equal(source.calls.tiles.length, 0, 'nothing before the status');
status.resolve({ configured });
await settle();
if (configured) assert.ok(source.calls.tiles.length > 0, 'then coverage');
else {
assert.equal(
source.calls.tiles.length,
0,
'a key-less install never asks',
);
assert.deepEqual(
[layer.getStats().loadingLabel, layer.getStats().error],
['KEY REQUIRED', 'KEY REQUIRED'],
);
assert.equal(layer.getUIState().keyRequired, true);
assert.equal(layer.getUIState().keyRejected, false);
}
layer.destroy();
}
});
test('a rejected key gates the layer as KEY REJECTED until it goes off', async (t) => {
const rejected = Object.assign(new Error('Mapillary rejected the token'), {
keyRejected: true,
});
const source = fakeMapillarySource({
getTile: async () => {
throw rejected;
},
});
const { layer, viewer } = await startLayer(t, { source, view: true });
await settle();
const stats = layer.getStats();
assert.equal(stats.keyRequired, true);
assert.equal(stats.loadingLabel, 'KEY REJECTED');
assert.match(stats.error, /rejected MAPILLARY_CLIENT_TOKEN/);
const ui = layer.getUIState();
assert.deepEqual([ui.keyRequired, ui.keyRejected], [true, true]);
layer.disable();
layer.enable(viewer);
assert.equal(layer.getUIState().keyRejected, false, 'asked again once on');
});
test('switching Mapillary off clears coverage and closes the image it shows', async (t) => {
const { layer, photoViewer } = await startLayer(t);
layer.attachViewerHost({});
assert.equal(await layer.openImage('img1'), true);
assert.deepEqual(photoViewer.calls.open, ['img1']);
assert.equal(layer.getUIState().street.open, true);
layer.setParams({ mapillary: false });
assert.equal(photoViewer.calls.unmount, 1, 'its viewer is released');
assert.equal(layer.getUIState().street.open, false);
assert.equal(layer.getUIState().providerOn, false);
});
test('openNearest without the panel reports it instead of loading forever', async (t) => {
const source = fakeMapillarySource({
nearestImages: async () => [{ id: 1 }],
});
const { layer, photoViewer } = await startLayer(t, { source });
assert.equal(await layer.openNearest({ lat: 38.58, lon: -121.49 }), false);
const { street } = layer.getUIState();
assert.equal(street.loading, false);
assert.match(street.error, /Open the Street Level panel/);
assert.equal(photoViewer.calls.mount, 0);
});
test('openNearest opens nothing once the layer or its switch went off during the lookup', async (t) => {
for (const off of [
(layer) => layer.disable(),
(layer) => layer.setParams({ mapillary: false }),
]) {
const { lookups, source } = slowLookups();
const { layer, photoViewer } = await startLayer(t, { source });
layer.attachViewerHost({});
const opening = layer.openNearest({ lat: 38.58, lon: -121.49 });
off(layer);
lookups[0].answer.resolve('img1');
assert.equal(await opening, false);
assert.equal(lookups[0].signal.aborted, true, 'the request was aborted');
assert.equal(photoViewer.calls.mount, 0, 'no viewer was stood up');
const { street } = layer.getUIState();
assert.deepEqual(
[street.open, street.loading, street.error],
[false, false, null],
);
layer.destroy();
}
});
test('a sequence error is withdrawn with the sequence, never the viewer’s own', async (t) => {
const source = fakeMapillarySource({
getSequenceImages: async () => {
throw new Error('Sequence images unavailable');
},
});
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');
layer.clearSequence();
assert.match(layer.getUIState().street.error, /Open the Street Level panel/);
});
test('closing the photo stops the globe flying to it', async (t) => {
const { layer, viewer } = await startLayer(t, {
photoViewer: posingViewer(),
});
layer.attachViewerHost({});
assert.equal(await layer.openImage('img1'), true);
assert.equal(viewer.flights.started, 1, 'the photo is framed');
layer.closeViewer();
assert.equal(viewer.flights.cancelled, 1);
});
test('switching the layer off or destroying it stops the framing flight', async (t) => {
const { layer, viewer } = await startLayer(t, {
photoViewer: posingViewer(),
});
layer.attachViewerHost({});
await layer.openImage('img1');
layer.disable();
assert.equal(viewer.flights.cancelled, 1, 'layer off');
layer.enable(viewer);
await layer.openImage('img2');
layer.destroy();
assert.equal(viewer.flights.cancelled, 2, 'destroyed');
});
test('closing the photo leaves a newer navigation flight alone', async (t) => {
const { layer, viewer } = await startLayer(t, {
photoViewer: posingViewer(),
});
layer.attachViewerHost({});
await layer.openImage('img1');
assert.equal(viewer.flights.started, 1, 'the photo is being framed');
// A search result flies the globe elsewhere before the framing lands.
viewer.camera.flyToBoundingSphere(null, {});
layer.closeViewer();
assert.equal(viewer.flights.cancelled, 0, 'the search flight keeps going');
});
test('an older nearest lookup that answers late cannot replace a newer one', async (t) => {
const { lookups, source } = slowLookups();
const { layer, photoViewer } = await startLayer(t, { source });
layer.attachViewerHost({});
const older = layer.openNearest({ lat: 38.58, lon: -121.49 });
const newer = layer.openNearest({ lat: 38.59, lon: -121.48 });
lookups[1].answer.resolve('newer');
assert.equal(await newer, true);
lookups[0].answer.resolve('older');
assert.equal(await older, false);
assert.deepEqual(photoViewer.calls.open, ['newer']);
assert.equal(lookups[0].signal?.aborted, true, 'its request was aborted');
assert.equal(lookups[1].signal?.aborted, false);
});
test('an image picked during a nearest lookup wins over its late answer', async (t) => {
const { lookups, source } = slowLookups();
const { layer, photoViewer } = await startLayer(t, { source });
layer.attachViewerHost({});
const lookup = layer.openNearest({ lat: 38.58, lon: -121.49 });
// A cone click opens its image directly.
assert.equal(await layer.openImage('picked'), true);
lookups[0].answer.resolve('nearest');
assert.equal(await lookup, false);
assert.deepEqual(photoViewer.calls.open, ['picked']);
assert.equal(lookups[0].signal?.aborted, true);
});
test('closing the viewer or switching the layer off retires a nearest lookup', async (t) => {
const { lookups, source } = slowLookups();
const { layer, viewer, photoViewer } = await startLayer(t, { source });
layer.attachViewerHost({});
const closed = layer.openNearest({ lat: 38.58, lon: -121.49 });
layer.closeViewer();
// The aborted fetch rejects, as fetch does; that is not the user's error.
lookups[0].answer.reject(new DOMException('aborted', 'AbortError'));
assert.equal(await closed, false);
let { street } = layer.getUIState();
assert.deepEqual(
[street.open, street.loading, street.error],
[false, false, null],
);
assert.equal(lookups[0].signal?.aborted, true, 'closing aborts the request');
const disabled = layer.openNearest({ lat: 38.58, lon: -121.49 });
layer.disable();
layer.enable(viewer);
lookups[1].answer.resolve('img1');
assert.equal(await disabled, false, 'nor does it open once back on');
assert.deepEqual(photoViewer.calls.open, []);
({ street } = layer.getUIState());
assert.deepEqual(
[street.open, street.loading, street.error],
[false, false, null],
);
assert.equal(lookups[1].signal?.aborted, true, 'layer off aborts it too');
});
test('setParams takes "any date" (0 days) and the switch over the current values (share-link defaults)', async (t) => {
const { layer } = await startLayer(t);
layer.setParams({ pano: 'flat', sinceDays: 365 });
assert.equal(layer.getParams().sinceDays, 365);
layer.setParams({ sinceDays: 0 });
assert.deepEqual(layer.getParams(), {
mapillary: true,
pano: 'flat',
sinceDays: 0,
});
layer.setParams({ mapillary: false });
assert.equal(layer.getParams().mapillary, false);
});
test('setParams ignores unknown keys and malformed values', async (t) => {
const { layer } = await startLayer(t);
layer.setParams({ pano: 'pano', sinceDays: 730 });
layer.setParams({ mapillary: '0', kartaview: true, pano: 'weird', x: 1 });
layer.setParams({ sinceDays: -3 });
layer.setParams(null);
assert.deepEqual(layer.getParams(), {
mapillary: true,
pano: 'pano',
sinceDays: 730,
});
});
test('switching Mapillary on while the layer is off draws nothing (M01)', async (t) => {
const { layer, viewer, source, photoViewer } = await startLayer(t, {
view: true,
enable: false,
});
layer.attachViewerHost({});
layer.setParams({ mapillary: false });
layer.setParams({ mapillary: true });
await settle();
assert.equal(source.calls.tiles.length, 0, 'no coverage drawn');
assert.deepEqual(viewer.credits, [], 'no credit shown');
assert.equal(photoViewer.calls.mount, 0, 'no viewer stood up');
assert.equal(layer.getUIState().providerOn, true, 'the switch is kept');
// Enabling the layer is what draws it.
layer.enable(viewer);
await settle();
assert.ok(source.calls.tiles.length > 0);
assert.equal(viewer.credits.length, 1);
});