mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
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:
1 parent
0f41fb04cd
commit
37a96072fa
51 files changed
+1240
-2659
No files matched your search
@@ -3,14 +3,19 @@ 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 { createMapillaryProvider } from './providers/mapillary/index.js';
|
||||
import { fakeStreetLevelProvider as fakeProvider } from '../../testSupport/streetLevelFakes.mjs';
|
||||
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.
|
||||
* 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() {
|
||||
function fakeViewer({ view = false } = {}) {
|
||||
const credits = [];
|
||||
/** Cesium's flight bookkeeping: a new flight cancels the current one. */
|
||||
let flight = null;
|
||||
@@ -26,19 +31,20 @@ function fakeViewer() {
|
||||
// Keep Cesium's handler on the canvas; there is no real document here.
|
||||
disableRootEvents: true,
|
||||
onwheel: null,
|
||||
...(view ? { clientWidth: 100, clientHeight: 100 } : {}),
|
||||
});
|
||||
return {
|
||||
credits,
|
||||
flights,
|
||||
scene: {
|
||||
canvas,
|
||||
primitives: { 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: {
|
||||
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(),
|
||||
@@ -51,6 +57,20 @@ function fakeViewer() {
|
||||
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) =>
|
||||
@@ -59,24 +79,41 @@ function fakeViewer() {
|
||||
};
|
||||
}
|
||||
|
||||
/** A layer over `providers`, initialised and enabled on a stand-in viewer. */
|
||||
async function enabledLayer(t, providers = [fakeProvider()]) {
|
||||
/**
|
||||
* 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 }),
|
||||
});
|
||||
const viewer = fakeViewer();
|
||||
const layer = createStreetLevelLayer({ providers });
|
||||
// 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);
|
||||
layer.enable(viewer);
|
||||
if (enable) layer.enable(viewer);
|
||||
t.after(() => {
|
||||
layer.destroy();
|
||||
globalThis.document = saved;
|
||||
});
|
||||
await settle();
|
||||
return { layer, viewer };
|
||||
return { layer, viewer, source, photoViewer };
|
||||
}
|
||||
|
||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
@@ -91,180 +128,194 @@ function deferred() {
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
/** A provider whose viewer reports a pose for every image it opens. */
|
||||
function posingProvider() {
|
||||
const provider = fakeProvider();
|
||||
const create = provider.create;
|
||||
provider.create = (context) => {
|
||||
const instance = create(context);
|
||||
let emit = null;
|
||||
instance.viewer.onPose = (listener) => {
|
||||
emit = listener;
|
||||
return () => {
|
||||
emit = null;
|
||||
};
|
||||
};
|
||||
instance.viewer.open = async (imageId) => {
|
||||
provider.calls.open.push(imageId);
|
||||
emit?.({
|
||||
providerId: 'mapillary',
|
||||
imageId,
|
||||
position: { lon: -121.49, lat: 38.58 },
|
||||
bearing: 90,
|
||||
});
|
||||
};
|
||||
return instance;
|
||||
};
|
||||
return provider;
|
||||
}
|
||||
/** A photo viewer reporting a pose for every image it opens. */
|
||||
const posingViewer = () =>
|
||||
fakePhotoViewer({ pose: { position: { lon: -121.49, lat: 38.58 } } });
|
||||
|
||||
/** nearestImage lookups that answer when the test says, with their signals. */
|
||||
/** Nearest-image lookups that answer when the test says, with their signals. */
|
||||
function slowLookups() {
|
||||
const lookups = [];
|
||||
const nearestImage = (point, { signal } = {}) => {
|
||||
const nearestImages = (query, { signal } = {}) => {
|
||||
const answer = deferred();
|
||||
lookups.push({ point, signal, answer });
|
||||
return answer.promise;
|
||||
lookups.push({ query, signal, answer });
|
||||
return answer.promise.then((id) => [{ id, is_pano: false }]);
|
||||
};
|
||||
return { lookups, nearestImage };
|
||||
return { lookups, source: fakeMapillarySource({ nearestImages }) };
|
||||
}
|
||||
|
||||
test('the CC BY-SA credit shows while a provider is on and goes with it', async (t) => {
|
||||
const { layer, viewer } = await enabledLayer(t);
|
||||
const shown = () => viewer.credits.map((credit) => credit.html);
|
||||
assert.deepEqual(shown(), [MAPILLARY_CREDIT_HTML], 'shown on activate');
|
||||
assert.match(shown()[0], /CC BY-SA 4\.0/);
|
||||
layer.setProviderEnabled('mapillary', false);
|
||||
assert.deepEqual(shown(), [], 'hidden with the provider');
|
||||
layer.setProviderEnabled('mapillary', true);
|
||||
assert.deepEqual(shown(), [MAPILLARY_CREDIT_HTML]);
|
||||
layer.disable();
|
||||
assert.deepEqual(shown(), [], 'every credit goes with the layer');
|
||||
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('getStats tells a rejected key from a missing one', async (t) => {
|
||||
const provider = fakeProvider();
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
provider.stats.loading = true;
|
||||
assert.equal(layer.getStats().loadingLabel, 'loading coverage...');
|
||||
assert.equal(layer.getStats().keyRequired, false);
|
||||
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');
|
||||
});
|
||||
|
||||
provider.stats.keyRequired = true;
|
||||
assert.deepEqual(
|
||||
[layer.getStats().loadingLabel, layer.getStats().error],
|
||||
['KEY REQUIRED', 'KEY REQUIRED'],
|
||||
);
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
// A rejected key gates the provider exactly like a missing one.
|
||||
provider.stats.keyRequired = false;
|
||||
provider.stats.keyRejected = true;
|
||||
provider.stats.error = 'Mapillary rejected MAPILLARY_CLIENT_TOKEN';
|
||||
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.equal(stats.error, 'Mapillary rejected MAPILLARY_CLIENT_TOKEN');
|
||||
assert.match(stats.error, /rejected MAPILLARY_CLIENT_TOKEN/);
|
||||
const ui = layer.getUIState();
|
||||
assert.equal(ui.keyRequired, true);
|
||||
assert.equal(ui.keyRejected, true);
|
||||
assert.equal(ui.providers[0].keyRequired, true);
|
||||
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 a provider off deactivates it and closes the image it shows', async (t) => {
|
||||
const provider = fakeProvider();
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
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('mapillary', 'img1'), true);
|
||||
assert.deepEqual(provider.calls.open, ['img1']);
|
||||
assert.equal(await layer.openImage('img1'), true);
|
||||
assert.deepEqual(photoViewer.calls.open, ['img1']);
|
||||
assert.equal(layer.getUIState().street.open, true);
|
||||
layer.setProviderEnabled('mapillary', false);
|
||||
assert.equal(provider.calls.deactivate, 1);
|
||||
assert.equal(provider.calls.unmount, 1, 'its viewer is released');
|
||||
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().street.providerId, null);
|
||||
assert.equal(layer.getUIState().providerOn, false);
|
||||
});
|
||||
|
||||
test('openNearest without the panel reports it instead of loading forever', async (t) => {
|
||||
const provider = fakeProvider({ nearestImage: async () => 'img1' });
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
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(provider.calls.mount, 0);
|
||||
assert.equal(photoViewer.calls.mount, 0);
|
||||
});
|
||||
|
||||
test('openNearest opens nothing once the layer went off during the lookup', async (t) => {
|
||||
const answer = deferred();
|
||||
const provider = fakeProvider({ nearestImage: () => answer.promise });
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
layer.attachViewerHost({});
|
||||
const opening = layer.openNearest({ lat: 38.58, lon: -121.49 });
|
||||
layer.disable();
|
||||
answer.resolve('img1');
|
||||
assert.equal(await opening, false);
|
||||
assert.equal(provider.calls.mount, 0, 'no viewer was stood up');
|
||||
const { street } = layer.getUIState();
|
||||
assert.equal(street.open, false);
|
||||
assert.equal(street.loading, false);
|
||||
assert.equal(street.error, null);
|
||||
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('openNearest skips a provider switched off during its lookup', async (t) => {
|
||||
const answer = deferred();
|
||||
const provider = fakeProvider({ nearestImage: () => answer.promise });
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
layer.attachViewerHost({});
|
||||
const opening = layer.openNearest({ lat: 38.58, lon: -121.49 });
|
||||
layer.setProviderEnabled('mapillary', false);
|
||||
answer.resolve('img1');
|
||||
assert.equal(await opening, false);
|
||||
assert.deepEqual(provider.calls.open, []);
|
||||
assert.equal(layer.getUIState().street.loading, false);
|
||||
});
|
||||
|
||||
test('a provider withdraws only the error it reported', async (t) => {
|
||||
const provider = fakeProvider();
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
const { actions } = provider.context();
|
||||
actions.reportError('Sequence images unavailable');
|
||||
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');
|
||||
actions.reportError(null);
|
||||
layer.clearSequence();
|
||||
assert.equal(layer.getUIState().street.error, null, 'withdrawn');
|
||||
|
||||
// The viewer's own error is not the provider's to clear.
|
||||
// The viewer's own error is not the sequence's to clear.
|
||||
await layer.selectSequence('seq-2');
|
||||
layer.attachViewerHost(null);
|
||||
actions.reportError('Sequence images unavailable');
|
||||
await layer.openImage('mapillary', 'img1');
|
||||
actions.reportError(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 enabledLayer(t, [posingProvider()]);
|
||||
const { layer, viewer } = await startLayer(t, {
|
||||
photoViewer: posingViewer(),
|
||||
});
|
||||
layer.attachViewerHost({});
|
||||
assert.equal(await layer.openImage('mapillary', 'img1'), true);
|
||||
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 enabledLayer(t, [posingProvider()]);
|
||||
const { layer, viewer } = await startLayer(t, {
|
||||
photoViewer: posingViewer(),
|
||||
});
|
||||
layer.attachViewerHost({});
|
||||
await layer.openImage('mapillary', 'img1');
|
||||
await layer.openImage('img1');
|
||||
layer.disable();
|
||||
assert.equal(viewer.flights.cancelled, 1, 'layer off');
|
||||
|
||||
layer.enable(viewer);
|
||||
await layer.openImage('mapillary', 'img2');
|
||||
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 enabledLayer(t, [posingProvider()]);
|
||||
const { layer, viewer } = await startLayer(t, {
|
||||
photoViewer: posingViewer(),
|
||||
});
|
||||
layer.attachViewerHost({});
|
||||
await layer.openImage('mapillary', 'img1');
|
||||
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, {});
|
||||
@@ -273,9 +324,8 @@ test('closing the photo leaves a newer navigation flight alone', async (t) => {
|
||||
});
|
||||
|
||||
test('an older nearest lookup that answers late cannot replace a newer one', async (t) => {
|
||||
const { lookups, nearestImage } = slowLookups();
|
||||
const provider = fakeProvider({ nearestImage });
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
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 });
|
||||
@@ -283,29 +333,27 @@ test('an older nearest lookup that answers late cannot replace a newer one', asy
|
||||
assert.equal(await newer, true);
|
||||
lookups[0].answer.resolve('older');
|
||||
assert.equal(await older, false);
|
||||
assert.deepEqual(provider.calls.open, ['newer']);
|
||||
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, nearestImage } = slowLookups();
|
||||
const provider = fakeProvider({ nearestImage });
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
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 goes through the provider's openImage action.
|
||||
assert.equal(await provider.context().actions.openImage('picked'), true);
|
||||
// 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(provider.calls.open, ['picked']);
|
||||
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, nearestImage } = slowLookups();
|
||||
const provider = fakeProvider({ nearestImage });
|
||||
const { layer, viewer } = await enabledLayer(t, [provider]);
|
||||
const { lookups, source } = slowLookups();
|
||||
const { layer, viewer, photoViewer } = await startLayer(t, { source });
|
||||
layer.attachViewerHost({});
|
||||
|
||||
const closed = layer.openNearest({ lat: 38.58, lon: -121.49 });
|
||||
@@ -325,7 +373,7 @@ test('closing the viewer or switching the layer off retires a nearest lookup', a
|
||||
layer.enable(viewer);
|
||||
lookups[1].answer.resolve('img1');
|
||||
assert.equal(await disabled, false, 'nor does it open once back on');
|
||||
assert.deepEqual(provider.calls.open, []);
|
||||
assert.deepEqual(photoViewer.calls.open, []);
|
||||
({ street } = layer.getUIState());
|
||||
assert.deepEqual(
|
||||
[street.open, street.loading, street.error],
|
||||
@@ -334,36 +382,8 @@ test('closing the viewer or switching the layer off retires a nearest lookup', a
|
||||
assert.equal(lookups[1].signal?.aborted, true, 'layer off aborts it too');
|
||||
});
|
||||
|
||||
test('the Mapillary provider hands the lookup signal to its source', async () => {
|
||||
const requests = [];
|
||||
const source = {
|
||||
hasToken: () => true,
|
||||
getStatus: async () => ({ configured: true }),
|
||||
getTile: async () => new Uint8Array(0),
|
||||
getSequenceImages: async () => [],
|
||||
nearestImages: async (query, options) => {
|
||||
requests.push(options);
|
||||
return [{ id: 7, is_pano: false, captured_at: 10 }];
|
||||
},
|
||||
};
|
||||
const instance = createMapillaryProvider({ source }).create({
|
||||
services: {},
|
||||
getFilter: () => ({ pano: 'all', sinceMs: null }),
|
||||
isActive: () => true,
|
||||
notify() {},
|
||||
actions: { openImage() {}, reportError() {} },
|
||||
});
|
||||
const { signal } = new AbortController();
|
||||
assert.equal(
|
||||
await instance.nearestImage({ lat: 1, lon: 2 }, { signal }),
|
||||
'7',
|
||||
);
|
||||
assert.equal(requests[0]?.signal, signal);
|
||||
});
|
||||
|
||||
test('setParams takes "any date" (0 days) and a provider switch over the current values (share-link defaults)', async (t) => {
|
||||
const provider = fakeProvider();
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
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 });
|
||||
@@ -372,54 +392,39 @@ test('setParams takes "any date" (0 days) and a provider switch over the current
|
||||
pano: 'flat',
|
||||
sinceDays: 0,
|
||||
});
|
||||
assert.equal(
|
||||
provider.filters.at(-1).sinceMs,
|
||||
null,
|
||||
'providers drop the cut-off',
|
||||
);
|
||||
layer.setParams({ mapillary: false });
|
||||
assert.equal(layer.getParams().mapillary, false);
|
||||
});
|
||||
|
||||
/** A layer initialised on a stand-in viewer but never enabled. */
|
||||
function initialisedLayer(t, providers) {
|
||||
const saved = globalThis.document;
|
||||
const drawing = new Proxy({}, { get: () => () => ({ addColorStop() {} }) });
|
||||
globalThis.document = Object.assign(new EventTarget(), {
|
||||
createElement: () => ({ getContext: () => drawing }),
|
||||
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,
|
||||
});
|
||||
const viewer = fakeViewer();
|
||||
const layer = createStreetLevelLayer({ providers });
|
||||
layer.init(viewer);
|
||||
t.after(() => {
|
||||
layer.destroy();
|
||||
globalThis.document = saved;
|
||||
});
|
||||
return { layer, viewer };
|
||||
}
|
||||
});
|
||||
|
||||
test('switching a provider on while the layer is off activates nothing (M01)', async (t) => {
|
||||
const provider = fakeProvider();
|
||||
const prewarmed = [];
|
||||
const create = provider.create;
|
||||
provider.create = (context) => {
|
||||
const instance = create(context);
|
||||
instance.viewer.prewarm = async (host) => prewarmed.push(host);
|
||||
return instance;
|
||||
};
|
||||
const { layer, viewer } = initialisedLayer(t, [provider]);
|
||||
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.setProviderEnabled('mapillary', false);
|
||||
layer.setProviderEnabled('mapillary', true);
|
||||
layer.setParams({ mapillary: false });
|
||||
layer.setParams({ mapillary: true });
|
||||
await new Promise((resolve) => setTimeout(resolve, 40)); // past whenIdle
|
||||
assert.equal(provider.calls.activate, 0, 'no coverage drawn');
|
||||
await settle();
|
||||
assert.equal(source.calls.tiles.length, 0, 'no coverage drawn');
|
||||
assert.deepEqual(viewer.credits, [], 'no credit shown');
|
||||
assert.deepEqual(prewarmed, [], 'no viewer stood up');
|
||||
assert.equal(layer.getUIState().providers[0].on, true, 'the switch is kept');
|
||||
// Enabling the layer is what activates it, once.
|
||||
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);
|
||||
assert.equal(provider.calls.activate, 1);
|
||||
await settle();
|
||||
assert.ok(source.calls.tiles.length > 0);
|
||||
assert.equal(viewer.credits.length, 1);
|
||||
});
|
||||
Reference in new issue
Block a user