refactor(street-level): drop openNearest; QA opens a photo by clicking a line then a cone

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-08 15:01:42 +04:00
1 parent 07a239fd82
commit 94049c5667
15 files changed
+61 -439

No files matched your search

+1 -1
View File
@@ -338,7 +338,7 @@ _The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED
**Setup.** At [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers), register an application with **Read** access and copy its **Client Token** (`MLY|…`). Paste it into **POWER UP → Mapillary**, or set `MAPILLARY_CLIENT_TOKEN` in `.env` and restart. Without it the panel reads **KEY REQUIRED**.
**Use.** Turn on **Street Level** under DATA LAYERS → Cameras, or with the pill in the STREET LEVEL panel header. Zoom in to street level (below 60 km) for the green coverage lines; from orbit the panel says to zoom in. The **IMAGERY** (ALL / 360° / FLAT) and **SINCE** filters narrow the lines, cones and nearest-photo lookups. Click a coverage line for its image cones and a cone to open the photo. Above the photo: **EXPAND** (full screen; Esc leaves) and **FIT / FILL**. The caption links to the image on mapillary.com, and the globe credits "© Mapillary contributors, CC BY-SA 4.0" while the layer is on.
**Use.** Turn on **Street Level** under DATA LAYERS → Cameras, or with the pill in the STREET LEVEL panel header. Zoom in to street level (below 60 km) for the green coverage lines; from orbit the panel says to zoom in. The **IMAGERY** (ALL / 360° / FLAT) and **SINCE** filters narrow the lines and cones. Click a coverage line for its image cones and a cone to open the photo. Above the photo: **EXPAND** (full screen; Esc leaves) and **FIT / FILL**. The caption links to the image on mapillary.com, and the globe credits "© Mapillary contributors, CC BY-SA 4.0" while the layer is on.
Share links carry the Mapillary switch and the filters (`0.m.0` Mapillary off, `0.p.p` panoramas only, `0.s.365` the last year).
@@ -27,15 +27,6 @@ export const THUMB_HOST = 'qa-fixture.mapillary.com';
const METRES_PER_DEG_LAT = 110_540;
/** Ground distance in metres, as the app's metresBetween. */
export function metresApart(a, b) {
const lat = (((a.lat + b.lat) / 2) * Math.PI) / 180;
return Math.hypot(
(b.lon - a.lon) * 111_320 * Math.cos(lat),
(b.lat - a.lat) * METRES_PER_DEG_LAT,
);
}
/**
* Photos along PHOTO_LINE, south to north; every third one is a panorama.
* @returns {Array<{id: string, lon: number, lat: number, isPano: boolean, capturedAt: number, compassAngle: number}>}
@@ -181,18 +172,6 @@ export function answerMapillaryRequest({ method, url, images }) {
: [];
return { ...json(200, { data }), known: true };
}
if (q.has('lat') && q.has('lng')) {
const at = { lat: Number(q.get('lat')), lon: Number(q.get('lng')) };
const radius = Math.min(50, Number(q.get('radius')) || 50);
const limit = Number(q.get('limit')) || 10;
// In no particular order, as the real API answers.
const data = photos
.filter((image) => metresApart(at, image) <= radius)
.reverse()
.slice(0, limit)
.map((image) => withFields(imageRecord(image), fields));
return { ...json(200, { data }), known: true };
}
if (q.has('s2')) return { ...json(200, { data: [] }), known: true };
return {
...json(400, { error: 'unsupported images query' }),
+51 -6
View File
@@ -264,6 +264,35 @@ async function assertKeyRequired(page) {
assert.match(shown.hint, /MAPILLARY_CLIENT_TOKEN/, 'names the key to add');
}
/**
* Client coordinates of the first thing drawn whose pick id starts with
* `prefix`, scanning `box` ({x, y, w, h} in canvas pixels; default: a row
* across the middle) every `step` px; null when nothing matches.
*/
const findPick = (page, prefix, { box = null, step = 3 } = {}) =>
page.evaluate(
(want, area, stride) => {
const { scene } = window.__godsEyeView.viewer;
const rect = scene.canvas.getBoundingClientRect();
const { x, y, w, h } = area || {
x: 0,
y: Math.round(rect.height / 2),
w: rect.width,
h: 1,
};
for (let py = y; py < y + h; py += stride)
for (let px = x; px < x + w; px += stride) {
const id = scene.pick({ x: px, y: py })?.id;
if (typeof id === 'string' && id.startsWith(want))
return { x: rect.left + px, y: rect.top + py, local: { x: px, y: py } };
}
return null;
},
prefix,
box,
step,
);
/** Park the camera over the photo line; a late startup flight can move it. */
async function park(page) {
for (let attempt = 0; attempt < 5; attempt++) {
@@ -519,14 +548,30 @@ async function main() {
await park(page);
await waitForCoverage(page);
await step(
'the viewer loads and the nearest photo opens with a caption',
'a line selects its sequence, and a cone opens its photo with a caption',
async () => {
await page.evaluate(() => {
// Fire and forget: the open can outlive one CDP call.
void window.__godsEyeView.dataManager.layers
.get('street-level')
.module.openNearest();
// Fixtures answer lookups for the photo sequence only.
const line = await findPick(
page,
fixtures ? `mly:seq:${PHOTO_SEQUENCE_ID}` : 'mly:seq:',
);
assert.ok(line, 'a sequence line across the middle of the view');
await page.mouse.click(line.x, line.y);
await uiUntil(
page,
(u) => u.sequence.selectedId && !u.sequence.loading,
);
const cone = await findPick(page, 'mly:img:', {
box: {
x: Math.max(0, line.local.x - 150),
y: Math.max(0, line.local.y - 150),
w: 300,
h: 300,
},
step: 8,
});
assert.ok(cone, 'an image cone near the selected line');
await page.mouse.click(cone.x, cone.y);
await uiUntil(
page,
(u) => (u.street.imageId && !u.street.loading) || u.street.error,
+2 -51
View File
@@ -1,22 +1,19 @@
import { createState, isActive } from './state.js';
import { createState } from './state.js';
import { createMarker } from './marker.js';
import { createCameraFraming } from './cameraFraming.js';
import { createCredit } from './credits.js';
import { createSelection } from './selection.js';
import { createViewerHost } from './viewerHost.js';
import { normalizeFilter, resolveFilter, sameFilter } from './filter.js';
import { viewCentre } from './view.js';
import { normalizeFilter, sameFilter } from './filter.js';
import { COLORS, STREET_LEVEL_LAYER_ID } from './policy.js';
import { createCoverage } from './providers/mapillary/coverage.js';
import { createSequences } from './providers/mapillary/sequences.js';
import { createMapillaryViewer } from './providers/mapillary/viewer.js';
import { nearestImageId } from './providers/mapillary/nearest.js';
import {
COLORS as MAPILLARY_COLORS,
MAPILLARY_CREDIT_HTML,
MAPILLARY_KEY_ID,
MAPILLARY_SOURCE_METHODS,
NEAREST_RADIUS_M,
} from './providers/mapillary/policy.js';
export { STREET_LEVEL_LAYER_ID } from './policy.js';
@@ -124,7 +121,6 @@ export function createStreetLevelLayer({
/** Close the photo, clear lines and cones, forget refusals. */
function stopDrawing() {
abortNearest();
parts.viewerHost.unmount();
parts.coverage.detach();
parts.coverage.clear();
@@ -145,16 +141,7 @@ export function createStreetLevelLayer({
notify();
}
/** The nearest-image lookup in flight; any newer user action aborts it. */
let nearestLookup = null;
function abortNearest() {
nearestLookup?.abort();
nearestLookup = null;
}
function openImage(imageId) {
abortNearest();
return parts.viewerHost.open(imageId);
}
@@ -319,44 +306,8 @@ export function createStreetLevelLayer({
parts.viewerHost.attach(element);
},
openImage,
/** Open the nearest image around a point (default: the view centre). */
async openNearest(point) {
if (!isActive(state)) return false;
const view = point || viewCentre(state.viewer);
if (!Number.isFinite(view?.lat) || !Number.isFinite(view?.lon))
return false;
abortNearest();
const lookup = new AbortController();
nearestLookup = lookup;
state.street.loading = true;
state.street.error = null;
notify();
let imageId = null;
let failure = null;
try {
imageId = await nearestImageId(
source,
{ lat: view.lat, lon: view.lon },
resolveFilter(state.filter),
{ signal: lookup.signal },
);
} catch (error) {
failure = error;
}
// Overtaken, closed or switched off: whatever did that owns the panel.
if (lookup.signal.aborted) return false;
nearestLookup = null;
if (imageId) return openImage(imageId);
state.street.error =
failure?.message ||
`No street-level imagery within ${NEAREST_RADIUS_M} m of the view centre`;
state.street.loading = false;
notify();
return false;
},
/** Close the image and deselect its sequence on the map. */
closeViewer() {
abortNearest();
parts.viewerHost.close();
parts.sequences.clearSelection();
},
-105
View File
@@ -132,17 +132,6 @@ function deferred() {
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 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);
@@ -217,41 +206,6 @@ test('switching Mapillary off clears coverage and closes the image it shows', as
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 () => {
@@ -292,65 +246,6 @@ test('closing the photo, switching the layer off or destroying it stops the fram
assert.equal(viewer.flights.cancelled, 3, 'destroyed');
});
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) => {
assert.throws(() => createStreetLevelLayer({ source: {} }), /source/);
const { layer } = await startLayer(t);
@@ -1,31 +1,8 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { nearestImageId } from './nearest.js';
import { mapillaryImageUrl } from './policy.js';
import { sequenceIdFromPick } from './coverage.js';
import { thinImages } from './sequences.js';
import { resolveFilter } from '../../filter.js';
const ALL = Object.freeze({ pano: 'all', sinceMs: null });
/** A source answering nearest-image lookups with `nearest`, recording options. */
function fakeSource(nearest = []) {
const requests = [];
return {
requests,
nearestImages: async (query, options) => {
requests.push({ query, options });
return nearest;
},
};
}
const at = (id, lon, lat) => ({
id,
is_pano: false,
captured_at: 10,
geometry: { type: 'Point', coordinates: [lon, lat] },
});
test('image deep links match the mapillary.com share format', () => {
assert.equal(
@@ -35,101 +12,12 @@ test('image deep links match the mapillary.com share format', () => {
assert.equal(mapillaryImageUrl(' '), 'https://www.mapillary.com/app/');
});
test('nearestImageId honours the imagery filter and hands the signal to the source', async () => {
const source = fakeSource([
{ id: 1, is_pano: false, captured_at: 10 },
{ id: 2, is_pano: true, captured_at: 20 },
]);
const { signal } = new AbortController();
assert.equal(
await nearestImageId(source, { lat: 1, lon: 2 }, ALL, { signal }),
'1',
);
assert.equal(source.requests[0].options.signal, signal);
assert.equal(source.requests[0].query.radius, 50);
assert.equal(
await nearestImageId(
source,
{ lat: 1, lon: 2 },
{
pano: 'pano',
sinceMs: null,
},
),
'2',
);
assert.equal(
await nearestImageId(
source,
{ lat: 1, lon: 2 },
{
pano: 'all',
sinceMs: 100,
},
),
null,
);
});
test('any part of a multi-part sequence picks the whole sequence', () => {
test('a sequence line pick names its sequence; other picks do not', () => {
assert.equal(sequenceIdFromPick('mly:seq:abc'), 'abc');
assert.equal(sequenceIdFromPick('mly:seq:abc~2'), 'abc');
assert.equal(sequenceIdFromPick('mly:img:abc'), null);
assert.equal(sequenceIdFromPick(null), null);
});
test('nearestImageId picks the closest image, not the first the API returned', async () => {
const source = fakeSource([
at('far', 2.0004, 1), // ~45 m east
at('near', 2.00005, 1), // ~5 m east
at('mid', 2, 1.0002), // ~22 m north
]);
assert.equal(await nearestImageId(source, { lat: 1, lon: 2 }, ALL), 'near');
});
test('a "since N days" window follows the clock in a long-open tab', async (t) => {
t.mock.timers.enable({ apis: ['Date'], now: Date.UTC(2026, 9, 1) });
const day = 86_400_000;
const capturedAt = Date.now() - day / 2;
const source = fakeSource([
{ id: 'recent', is_pano: false, captured_at: capturedAt },
]);
// The layer resolves the stored filter on every lookup.
const lookup = () =>
nearestImageId(
source,
{ lat: 1, lon: 2 },
resolveFilter({ pano: 'all', sinceDays: 1 }),
);
assert.equal(await lookup(), 'recent');
t.mock.timers.tick(2 * day);
assert.equal(
await lookup(),
null,
'two days on, the image is older than the window',
);
});
test('nearestImageId measures the short way round the date line', async () => {
const east = fakeSource([
at('same-side', 179.9995, 0), // ~45 m west, on the camera's side
at('across', -179.9999, 0), // ~11 m east, across ±180°
]);
assert.equal(
await nearestImageId(east, { lat: 0, lon: 179.9999 }, ALL),
'across',
);
// And from the other side.
const west = fakeSource([
at('same-side', -179.9995, 0),
at('across', 179.9999, 0),
]);
assert.equal(
await nearestImageId(west, { lat: 0, lon: -179.9999 }, ALL),
'across',
);
});
test('cone thinning across the date line drops images metres apart', () => {
const image = (id, lon) => ({ id, lon, lat: 0 });
// A sequence driving east over ±180° with images ~1 m apart, then ~11 m on.
@@ -1,38 +0,0 @@
import { passesImageryFilter } from '../../filter.js';
import { metresBetween } from '../../view.js';
import { NEAREST_LIMIT, NEAREST_RADIUS_M } from './policy.js';
/**
* Id of the nearest image around a point that passes the resolved imagery
* filter, or null. An aborted `signal` gives up (the source may reject).
*/
export async function nearestImageId(
source,
{ lat, lon },
filter,
{ signal } = {},
) {
const images = await source.nearestImages(
{ lat, lon, radius: NEAREST_RADIUS_M, limit: NEAREST_LIMIT },
{ signal },
);
// The API returns the images in the radius in no particular order.
const metres = (record) => {
const [lon2, lat2] = (record.computed_geometry || record.geometry)
?.coordinates || [NaN, NaN];
const d = metresBetween({ lon, lat }, { lon: lon2, lat: lat2 });
return Number.isFinite(d) ? d : Infinity;
};
const hit = [...images]
.sort((a, b) => metres(a) - metres(b))
.find((record) =>
passesImageryFilter(
{
isPano: record.is_pano === true,
capturedAt: Number(record.captured_at) || 0,
},
filter,
),
);
return hit ? String(hit.id) : null;
}
@@ -9,7 +9,6 @@ export const MAPILLARY_SOURCE_METHODS = Object.freeze([
'getStatus',
'getTile',
'getSequenceImages',
'nearestImages',
]);
/** Stable id prefixes for picked primitives; every one starts with `mly:`. */
@@ -48,11 +47,6 @@ export const SEQUENCE_IMAGES_LIMIT = 2000;
export const IMAGE_CONE_SIZE_PX = 26;
export const IMAGE_CONE_MIN_SPACING_M = 3;
/** Nearest-image search radius in metres (the graph API caps it at 50 m). */
export const NEAREST_RADIUS_M = 50;
/** Images asked for in a nearest-image search. */
export const NEAREST_LIMIT = 8;
/** On-globe credit: Mapillary imagery is CC BY-SA 4.0 and needs attribution. */
export const MAPILLARY_CREDIT_HTML =
'Street Level: imagery © <a href="https://www.mapillary.com" target="_blank" rel="noopener">Mapillary</a> contributors, CC BY-SA 4.0';
@@ -64,8 +58,5 @@ export function mapillaryImageUrl(imageId) {
return `https://www.mapillary.com/app/?pKey=${encodeURIComponent(id)}&focus=photo`;
}
/** Fields requested from the graph API. */
export const IMAGE_FIELDS =
'id,captured_at,compass_angle,computed_compass_angle,geometry,computed_geometry,computed_altitude,is_pano,sequence,thumb_256_url,creator,quality_score';
export const SEQUENCE_IMAGE_FIELDS =
'id,captured_at,compass_angle,geometry,is_pano,computed_altitude';
/** Fields requested for a sequence's images. */
export const SEQUENCE_IMAGE_FIELDS = 'id,captured_at,compass_angle,geometry,is_pano';
@@ -1,8 +1,5 @@
import {
IMAGE_FIELDS,
MAPILLARY_GRAPH_HOST,
NEAREST_LIMIT,
NEAREST_RADIUS_M,
SEQUENCE_IMAGE_FIELDS,
SEQUENCE_IMAGES_LIMIT,
} from './policy.js';
@@ -126,23 +123,5 @@ export function createMapillarySource({
);
return Array.isArray(payload?.data) ? payload.data : [];
},
async nearestImages(
{ lat, lon, radius = NEAREST_RADIUS_M, limit = NEAREST_LIMIT },
{ signal } = {},
) {
const payload = await graph(
'images',
{
lat: String(lat),
lng: String(lon),
radius: String(Math.min(50, Math.max(1, radius))),
limit: String(Math.min(100, Math.max(1, limit))),
fields: IMAGE_FIELDS,
},
{ signal },
);
return Array.isArray(payload?.data) ? payload.data : [];
},
};
}
@@ -62,21 +62,6 @@ test('graph lookups call graph.mapillary.com with the client token and requested
assert.equal(url.searchParams.get('access_token'), 'MLY|1|abc');
assert.equal(url.searchParams.get('sequence_ids'), 'seq-1');
assert.match(url.searchParams.get('fields'), /geometry/);
const nearest = await source.nearestImages({
lat: 38.5,
lon: -121.5,
radius: 500,
limit: 500,
});
assert.equal(nearest.length, 1);
const nearestUrl = new URL(calls[1].url);
assert.equal(
nearestUrl.searchParams.get('radius'),
'50',
'radius is clamped to the API maximum',
);
assert.equal(nearestUrl.searchParams.get('limit'), '100');
});
test('graph calls without a token fail fast as keyRequired without a request', async () => {
-14
View File
@@ -153,20 +153,6 @@ export function visibleBbox(
];
}
/** Centre of the visible ground, or the camera's own footprint. */
export function viewCentre(viewer) {
if (!viewer) return null;
const bbox = visibleBbox(viewer);
if (bbox) {
// Across the date line (west > east) the middle is on the far side of 0°.
const span = bbox[2] - bbox[0] + (bbox[0] > bbox[2] ? 360 : 0);
let lon = bbox[0] + span / 2;
if (lon > 180) lon -= 360;
return { lat: (bbox[1] + bbox[3]) / 2, lon };
}
return cameraNadir(viewer);
}
/** The point on the ground under the camera, as {lon, lat} degrees. */
export function cameraNadir(viewer) {
const carto = viewer?.camera?.positionCartographic;
-6
View File
@@ -6,7 +6,6 @@ import {
createHorizonCull,
groundUnderCamera,
metresBetween,
viewCentre,
viewFocus,
visibleBbox,
} from './view.js';
@@ -89,16 +88,11 @@ test('a view across the date line is a narrow box with west > east', () => {
const bbox = visibleBbox(viewer);
assert.ok(bbox[0] > 177 && bbox[0] < 179, `west ${bbox[0]}`);
assert.ok(bbox[2] < -177 && bbox[2] > -179, `east ${bbox[2]}`);
const centre = viewCentre(viewer);
assert.ok(Math.abs(Math.abs(centre.lon) - 180) < 0.5, `centre ${centre.lon}`);
});
test('an ordinary view keeps west < east', () => {
const bbox = visibleBbox(gridViewer([10, 11, 12], [50, 51]));
assert.ok(bbox[0] < bbox[2]);
assert.ok(
Math.abs(viewCentre(gridViewer([10, 11, 12], [50, 51])).lon - 11) < 1e-6,
);
});
/** A pinhole viewer `agl` m above ground `ground` m up, looking north. */
-2
View File
@@ -78,8 +78,6 @@ export function createViewerHost({ state, parts, adapter }) {
async function open(imageId) {
if (!imageId) return false;
if (!state.street.host) {
// A nearest-image lookup may have set it; nothing is loading now.
state.street.loading = false;
state.street.error = 'Open the Street Level panel to view imagery';
notify();
return false;
-1
View File
@@ -18,7 +18,6 @@ export function fakeMapillarySource(overrides = {}) {
return new Uint8Array(0);
},
getSequenceImages: async () => [],
nearestImages: async () => [],
...overrides,
};
}
+4 -34
View File
@@ -4,7 +4,6 @@ import { fixtureTile } from '../../scripts/qa-street-level.mjs';
import {
answerMapillaryRequest,
describeCall,
metresApart,
PHOTO_LINE,
PHOTO_SEQUENCE_ID,
photoImages,
@@ -73,16 +72,14 @@ test('the photo sequence runs through the parked view, clipped to each tile it c
);
});
test('the fixture photos: one sequence, 360° and flat, close enough for any nearest lookup on the line', () => {
test('the fixture photos: one sequence, 360° and flat, spaced past the cone thinning', () => {
const images = photoImages(Date.UTC(2026, 9, 1));
assert.ok(images.length >= 3);
assert.ok(images.some((image) => image.isPano));
assert.ok(images.some((image) => !image.isPano));
for (let i = 1; i < images.length; i++) {
const gap = metresApart(images[i - 1], images[i]);
// Above the cones' 3 m thinning, and at most 50 m apart: any point on
// the line has a photo within the Graph API's 50 m radius.
assert.ok(gap > 3 && gap < 50, `gap ${gap}`);
const gap = (images[i].lat - images[i - 1].lat) * 110_540;
assert.ok(gap > 3, `gap ${gap}`);
}
assert.equal(new Set(images.map((image) => image.id)).size, images.length);
});
@@ -97,35 +94,8 @@ const answer = (url, method = 'GET') => {
};
};
test('the Graph fixtures answer the provider’s nearest and sequence lookups', () => {
test('the Graph fixtures answer the provider’s sequence lookups', () => {
const images = photoImages();
const middle = images[Math.floor(images.length / 2)];
const near = answer(
graph('images', {
lat: String(middle.lat + 0.0001),
lng: String(middle.lon),
radius: '50',
limit: '8',
fields: 'id,geometry,is_pano,captured_at,sequence',
}),
);
assert.equal(near.status, 200);
assert.ok(near.json.data.length > 0);
assert.ok(near.json.data.some((record) => record.id === middle.id));
for (const record of near.json.data) {
assert.deepEqual(Object.keys(record).sort(), [
'captured_at',
'geometry',
'id',
'is_pano',
'sequence',
]);
assert.equal(record.sequence, PHOTO_SEQUENCE_ID);
}
const far = answer(
graph('images', { lat: '0', lng: '0', radius: '50', fields: 'id' }),
);
assert.deepEqual(far.json.data, [], 'nothing outside the radius');
const sequence = answer(
graph('images', {
sequence_ids: PHOTO_SEQUENCE_ID,