mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
fix(street-level): IC8 review of the Street View provider
Google's Maps Platform terms forbid Street View imagery beside non-Google maps, so the provider declares requiresMapStack: 'photoreal': off Google 3D its chip is dashed with the reason, lookups and opens skip it, and leaving Google 3D closes its panorama. - Ground clicks: only a click on the map itself (scenePick.isSurfacePick: nothing picked or Google 3D tile content) at street zoom opens Street View, and the camera stays put; higher up the hint says to zoom in. scenePick.pickGroundPosition replaces the copy in Directions. - Viewer: poses go out at once and again when the date and address arrive, so an open never hangs on that lookup; one lookup per panorama, seeded by the nearest lookup; a close settles a pending open; failed lookups are asked again; poses follow position changes, not pano_changed. - Loader: the gm_authFailure handler is installed once, so a retried load reports a refusal once. - Viewer host: a prewarm that lands after another provider took the host is released; FIT/FILL availability is set on every open. - Provider: a refusal while switched off reports no error over another photo; SINCE compares against the end of Google's month; messages name the referrer and network causes; FLAT says Street View is 360° only. - The layer row names the key of the providers that are on; the MCP panel CSP admits Street View's tiles, control images and photos. - Tests for each, against fakes that now follow Google's event order; the browser gate restores its state, waits instead of sleeping, aims its panel clicks and corner drags, and asserts the camera stays put. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
22b0009138
commit
7bfcf177ac
35 files changed
+1108
-388
No files matched your search
+150
-104
@@ -628,7 +628,8 @@ async function main() {
|
||||
}
|
||||
await page.setViewport(VIEWPORTS[0]);
|
||||
const expandStrip = async (target = page) => {
|
||||
await target.click(
|
||||
await clickPanelControl(
|
||||
target,
|
||||
'.panel-collapse-btn[data-collapse-target="street-level-panel"]',
|
||||
);
|
||||
await settle(
|
||||
@@ -1774,116 +1775,161 @@ async function main() {
|
||||
`the share link carries ${option}`,
|
||||
);
|
||||
assert.ok((await ui()).coverage.count > 0, 'Mapillary still draws');
|
||||
// Leave it off, so later steps run the same with and without a key.
|
||||
await clickPanelControl(page, googleChip);
|
||||
await uiUntil(
|
||||
(u) => u.providers.find((p) => p.id === 'google')?.on === false,
|
||||
);
|
||||
},
|
||||
);
|
||||
await step(
|
||||
'a click on a street opens Google Street View, captioned and linked, and its view drives the pose',
|
||||
'on Google 3D a click on a street opens Google Street View where it is, captioned and linked, and its view drives the pose',
|
||||
async ({ skip }) => {
|
||||
if (!googleKey) return skip('no Google key');
|
||||
// A Mapillary line under the pointer wins the click: aim at bare
|
||||
// ground with only Street View on.
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.layers
|
||||
.get('street-level')
|
||||
.module.closeViewer(),
|
||||
);
|
||||
await clickPanelControl(page, chip);
|
||||
await uiUntil(
|
||||
(u) => u.providers.find((p) => p.id === 'mapillary')?.on === false,
|
||||
);
|
||||
const street = STREET_VIEW_STREET;
|
||||
await page.evaluate((at) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
v.camera.cancelFlight?.();
|
||||
const C = v.camera.positionCartographic.constructor;
|
||||
v.camera.setView({
|
||||
destination: v.scene.globe.ellipsoid.cartographicToCartesian(
|
||||
C.fromDegrees(at.lon, at.lat, 300),
|
||||
),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
}, street);
|
||||
await sleep(1500);
|
||||
const at = await page.evaluate((point) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const C = v.camera.positionCartographic.constructor;
|
||||
const ground = v.scene.globe.ellipsoid.cartographicToCartesian(
|
||||
C.fromDegrees(point.lon, point.lat, 0),
|
||||
// Google's terms: Street View only beside Google's own map.
|
||||
if (!(await stacks.photoreal())) return skip('no Google 3D');
|
||||
const original = await stacks.active();
|
||||
const closePhoto = () =>
|
||||
page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.layers
|
||||
.get('street-level')
|
||||
.module.closeViewer(),
|
||||
);
|
||||
const canvas = v.scene.cartesianToCanvasCoordinates(ground);
|
||||
const box = v.scene.canvas.getBoundingClientRect();
|
||||
return canvas && { x: box.left + canvas.x, y: box.top + canvas.y };
|
||||
}, street);
|
||||
assert.ok(at, 'the street is on screen');
|
||||
await page.mouse.click(at.x, at.y);
|
||||
await uiUntil(
|
||||
(u) =>
|
||||
Boolean(u.street.error) ||
|
||||
(u.street.providerId === 'google' &&
|
||||
Boolean(u.street.imageId) &&
|
||||
!u.street.loading),
|
||||
null,
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
const opened = (await ui()).street;
|
||||
if (/GOOGLE_MAPS_API_KEY/.test(opened.error || '')) {
|
||||
await clickPanelControl(page, chip);
|
||||
return skip('Google key refused');
|
||||
}
|
||||
assert.equal(opened.error, null);
|
||||
assert.equal(opened.providerId, 'google');
|
||||
const shown = await page.evaluate(() => ({
|
||||
by: document.getElementById('sl-image-by').textContent,
|
||||
when: document.getElementById('sl-image-when').textContent,
|
||||
label: document.getElementById('sl-image-link').textContent,
|
||||
href: document.getElementById('sl-image-link').href,
|
||||
fitFillHidden:
|
||||
document.querySelector('[data-sl-render]').parentElement.hidden,
|
||||
panorama: Boolean(
|
||||
document.querySelector('#sl-viewer .sl-google-panorama'),
|
||||
),
|
||||
mapillaryLeft: Boolean(
|
||||
document.querySelector('#sl-viewer .mapillary-dom'),
|
||||
),
|
||||
}));
|
||||
assert.match(shown.by, /Image by /);
|
||||
assert.match(shown.when, /\d{4}-\d{2}$/, 'dated by month');
|
||||
assert.equal(shown.label, 'STREET VIEW ↗');
|
||||
assert.match(shown.href, /map_action=pano&pano=/);
|
||||
assert.equal(shown.fitFillHidden, true, 'no FIT/FILL for Google');
|
||||
assert.equal(shown.panorama, true);
|
||||
assert.equal(shown.mapillaryLeft, false, 'no viewer left under it');
|
||||
// A real drag turns Google's view, and the pose follows it.
|
||||
const box = await page.$eval(
|
||||
'#sl-viewer .sl-google-panorama',
|
||||
(node) => {
|
||||
const r = node.getBoundingClientRect();
|
||||
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
||||
},
|
||||
);
|
||||
await page.mouse.move(box.x, box.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x - 120, box.y, { steps: 12 });
|
||||
await page.mouse.up();
|
||||
assert.ok(
|
||||
const setOn = async (selector, id, on) => {
|
||||
const now = (await ui()).providers.find((p) => p.id === id)?.on;
|
||||
if (now === on) return;
|
||||
await clickPanelControl(page, selector);
|
||||
await uiUntil(
|
||||
(u, before) =>
|
||||
Number.isFinite(u.street.bearing) &&
|
||||
Math.abs(u.street.bearing - before) > 5,
|
||||
opened.bearing ?? 0,
|
||||
{ timeout: 10_000 },
|
||||
),
|
||||
'the pose bearing follows the drag',
|
||||
);
|
||||
await clickPanelControl(page, '#sl-viewer-close');
|
||||
await uiUntil((u) => u.street.open === false);
|
||||
await clickPanelControl(page, chip);
|
||||
await clickPanelControl(page, googleChip);
|
||||
await uiUntil(
|
||||
(u) =>
|
||||
u.providers.find((p) => p.id === 'google')?.on === false &&
|
||||
u.providers.find((p) => p.id === 'mapillary')?.on === true,
|
||||
);
|
||||
(u, want) =>
|
||||
u.providers.find((p) => p.id === want.id)?.on === want.on,
|
||||
{ id, on },
|
||||
);
|
||||
};
|
||||
await closePhoto();
|
||||
await stacks.set('photoreal');
|
||||
// A Mapillary line under the pointer wins the click: aim at bare
|
||||
// ground with only Street View on (lit first: the last lit chip
|
||||
// would switch the layer off).
|
||||
await setOn(googleChip, 'google', true);
|
||||
await setOn(chip, 'mapillary', false);
|
||||
try {
|
||||
const street = STREET_VIEW_STREET;
|
||||
await page.evaluate((at) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
v.camera.cancelFlight?.();
|
||||
const C = v.camera.positionCartographic.constructor;
|
||||
v.camera.setView({
|
||||
destination: v.scene.globe.ellipsoid.cartographicToCartesian(
|
||||
C.fromDegrees(at.lon, at.lat, 300),
|
||||
),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
}, street);
|
||||
// At street zoom the hint invites the click.
|
||||
assert.ok(
|
||||
await uiUntil(
|
||||
(u) =>
|
||||
u.providers.find((p) => p.id === 'google')?.hint ===
|
||||
'click a street to open Street View',
|
||||
),
|
||||
'the click hint shows at street zoom',
|
||||
);
|
||||
const camera = () =>
|
||||
page.evaluate(() => {
|
||||
const c = window.__godsEyeView.viewer.camera.positionWC;
|
||||
return [c.x, c.y, c.z];
|
||||
});
|
||||
const at = await page.evaluate((point) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const C = v.camera.positionCartographic.constructor;
|
||||
const ground = v.scene.globe.ellipsoid.cartographicToCartesian(
|
||||
C.fromDegrees(point.lon, point.lat, 0),
|
||||
);
|
||||
const canvas = v.scene.cartesianToCanvasCoordinates(ground);
|
||||
const box = v.scene.canvas.getBoundingClientRect();
|
||||
return (
|
||||
canvas && { x: box.left + canvas.x, y: box.top + canvas.y }
|
||||
);
|
||||
}, street);
|
||||
assert.ok(at, 'the street is on screen');
|
||||
const before = await camera();
|
||||
await page.mouse.click(at.x, at.y);
|
||||
await uiUntil(
|
||||
(u) =>
|
||||
Boolean(u.street.error) ||
|
||||
(u.street.providerId === 'google' &&
|
||||
Boolean(u.street.imageId) &&
|
||||
!u.street.loading),
|
||||
null,
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
const opened = (await ui()).street;
|
||||
if (/refused GOOGLE_MAPS_API_KEY/.test(opened.error || ''))
|
||||
return skip('Google key refused');
|
||||
if (/did not answer/.test(opened.error || ''))
|
||||
return skip('Google did not answer');
|
||||
assert.equal(opened.error, null);
|
||||
assert.equal(opened.providerId, 'google');
|
||||
const after = await camera();
|
||||
assert.ok(
|
||||
Math.hypot(...after.map((v, i) => v - before[i])) < 1,
|
||||
'the camera stays where the user clicked',
|
||||
);
|
||||
const shown = await page.evaluate(() => ({
|
||||
by: document.getElementById('sl-image-by').textContent,
|
||||
when: document.getElementById('sl-image-when').textContent,
|
||||
label: document.getElementById('sl-image-link').textContent,
|
||||
href: document.getElementById('sl-image-link').href,
|
||||
fitFillHidden:
|
||||
document.querySelector('[data-sl-render]').parentElement.hidden,
|
||||
panorama: Boolean(
|
||||
document.querySelector('#sl-viewer .sl-google-panorama'),
|
||||
),
|
||||
mapillaryLeft: Boolean(
|
||||
document.querySelector('#sl-viewer .mapillary-dom'),
|
||||
),
|
||||
}));
|
||||
assert.match(shown.by, /Image by /);
|
||||
assert.match(shown.when, /\d{4}-\d{2}$/, 'dated by month');
|
||||
assert.equal(shown.label, 'STREET VIEW ↗');
|
||||
assert.match(shown.href, /map_action=pano&pano=/);
|
||||
assert.equal(shown.fitFillHidden, true, 'no FIT/FILL for Google');
|
||||
assert.equal(shown.panorama, true);
|
||||
assert.equal(shown.mapillaryLeft, false, 'no viewer left under it');
|
||||
// A real drag turns Google's view, and the pose follows it.
|
||||
const box = await page.$eval(
|
||||
'#sl-viewer .sl-google-panorama',
|
||||
(node) => {
|
||||
const r = node.getBoundingClientRect();
|
||||
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
||||
},
|
||||
);
|
||||
await page.mouse.move(box.x, box.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x - 120, box.y, { steps: 12 });
|
||||
await page.mouse.up();
|
||||
assert.ok(
|
||||
await uiUntil(
|
||||
(u, start) =>
|
||||
Number.isFinite(u.street.bearing) &&
|
||||
Math.abs(u.street.bearing - start) > 5,
|
||||
opened.bearing ?? 0,
|
||||
{ timeout: 10_000 },
|
||||
),
|
||||
'the pose bearing follows the drag',
|
||||
);
|
||||
// Leaving Google 3D closes the panorama (Google's terms).
|
||||
await stacks.set('esri-imagery');
|
||||
assert.ok(
|
||||
await uiUntil((u) => u.street.open === false),
|
||||
'the panorama closes off Google 3D',
|
||||
);
|
||||
} finally {
|
||||
await closePhoto();
|
||||
await setOn(chip, 'mapillary', true);
|
||||
await setOn(googleChip, 'google', false);
|
||||
await stacks.set(original);
|
||||
}
|
||||
},
|
||||
);
|
||||
await step(
|
||||
|
||||
Reference in new issue
Block a user