fix(street-level): review fixes for show_street_level and sl= links

P1: on a phone the Street Level panel sits in the Cameras tab sheet, so the
action and sl= links opened photos nobody could see and still reported
success. A photo that opens, and a link that arms, now open that sheet
(revealPanel). In cockpit, clean view, recording or scene playback the action
stops at a new `view` stage before anything switches or loads.

P2s:
- Mapillary's nearest search asks for 100 photos, the API's most. It cuts
  the radius's photos at the limit in no particular order, so 8 left the
  distance sort and the imagery filter a few arbitrary photos.
- The MCP globe panel opens a billed photo only for a result issued in the
  last five minutes (show_in_gods_eye_view stamps issued_at); a host that
  mounts the panel again hands back the old result, which only arms.
- OPEN uses an armed link's point only while the view centre is within 1 km
  of it; a user who moved on opens what the view shows.
- show_in_gods_eye_view measures a camera's altitude from the terrain under
  it before taking its look-at point, so high cities no longer put the
  photos kilometres too far ahead.
- The armed-link toast is translated, and the new view-stage message too,
  in all 11 catalogs.
- Tests: a turn superseded during the geocode, flight or map switch changes
  nothing; an unknown key state lets a cold sl= link through; a wide tilted
  area still opens photos at its centre.
This commit is contained in:
Said Turksever committed 2026-11-03 15:30:48 +00:00
1 parent 8ce01cf48e
commit 0ec57057fa
30 files changed
+473 -65

No files matched your search

+38 -28
View File
@@ -540,6 +540,7 @@ test('a failing link step does not take the next one with it', async () => {
test('a plain link never auto-loads a billed photo: it arms the panel and says so; an embedded view opens', async () => {
const toasts = [];
const panels = [];
const revealed = [];
const { calls, run } = recorder({
show_street_level: () => ({ ok: true, armed: true }),
});
@@ -549,6 +550,7 @@ test('a plain link never auto-loads a billed photo: it arms the panel and says s
_showToast: (text) => toasts.push(text),
setPanelCollapsed: (id, collapsed, options) =>
panels.push([id, collapsed, options]),
revealPanel: (id) => revealed.push(id),
},
viewer: fakeViewer(),
dataManager: fakeLayers(new Set()),
@@ -569,6 +571,7 @@ test('a plain link never auto-loads a billed photo: it arms the panel and says s
[['street-level-panel', false, { explicit: true }]],
'the panel opens so OPEN is in view',
);
assert.deepEqual(revealed, ['street-level-panel'], 'its phone sheet too');
// Adding ?embed=1 to a published link does not lift the gate: the page is
// still a plain top-level load.
const framedToasts = [];
@@ -599,38 +602,45 @@ test('a plain link never auto-loads a billed photo: it arms the panel and says s
assert.equal(framed.calls[0][1].autoOpen, false);
// In a frame the rail that holds OPEN is hidden: no nudge, no toast.
assert.deepEqual([framedToasts, framedPanels], [[], []]);
// Only the panel page's own link (inline embed) is the client's request.
// Only the panel page's own link for a result issued just now is the
// client's request: a host that mounts the panel again hands back an old
// result, and that one only arms, like any link.
globalThis.GEV_EMBED_INLINE = true;
try {
const inline = recorder({ show_street_level: () => ({ ok: true }) });
const inlineWindow = new EventTarget();
inlineWindow.document = { body: { classList: { add() {} } } };
inlineWindow.postMessage = () => {};
// The inline panel keeps rendering from a timer; give it inert clocks.
Object.assign(inlineWindow, {
requestAnimationFrame: () => 1,
cancelAnimationFrame() {},
setTimeout: () => 1,
clearTimeout() {},
setInterval: () => 1,
clearInterval() {},
});
const stop = installViews({
shell: { initialRestorePromise: Promise.resolve(), setCleanView() {} },
viewer: fakeViewer(),
dataManager: fakeLayers(new Set()),
run: inline.run,
location: {
search: '',
hash: '#v=2&lat=1&lon=2&map=photoreal&sl=google,38.57986,-121.49372',
},
windowRef: inlineWindow,
});
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal('autoOpen' in inline.calls[0][1], false);
stop();
for (const fresh of [true, false]) {
globalThis.GEV_EMBED_FRESH = fresh;
const inline = recorder({ show_street_level: () => ({ ok: true }) });
const inlineWindow = new EventTarget();
inlineWindow.document = { body: { classList: { add() {} } } };
inlineWindow.postMessage = () => {};
// The inline panel keeps rendering from a timer; give it inert clocks.
Object.assign(inlineWindow, {
requestAnimationFrame: () => 1,
cancelAnimationFrame() {},
setTimeout: () => 1,
clearTimeout() {},
setInterval: () => 1,
clearInterval() {},
});
const stop = installViews({
shell: { initialRestorePromise: Promise.resolve(), setCleanView() {} },
viewer: fakeViewer(),
dataManager: fakeLayers(new Set()),
run: inline.run,
location: {
search: '',
hash: '#v=2&lat=1&lon=2&map=photoreal&sl=google,38.57986,-121.49372',
},
windowRef: inlineWindow,
});
await new Promise((resolve) => setTimeout(resolve, 0));
if (fresh) assert.equal('autoOpen' in inline.calls[0][1], false);
else assert.equal(inline.calls[0][1].autoOpen, false, 'a remount arms');
stop();
}
} finally {
delete globalThis.GEV_EMBED_INLINE;
delete globalThis.GEV_EMBED_FRESH;
}
// applyView (a posted view) opens too.
const posted = recorder();