Merge upstream/main: Street View rework meets i18n

Upstream's Street View rework (second imagery provider, one source at a
time, OPEN STREET VIEW button, key-alternative guidance) rewrote exactly
the street-level regions this branch localizes.

Resolved as upstream logic + re-applied localization:

- keySetupCore: new unlock copy kept as msg() data; added
  keySetupAlternativeKeyNames() returning machine values (registry key
  names) so key-setup core stays English while presentation localizes.
- New src/ui/keyGuidance.js: localized key-guidance composer shared by
  layerPanel and streetLevelPresentation (requirement sentence plus the
  "or light X to use your Y key" alternatives tail, placeholders only).
- streetLevelPresentation: keyGuidance()/presentNearest()/presentMeta
  relocalized (t/tn + formatNumber instead of toLocaleString);
  renderModes and month-precision dates kept from upstream.
- layer-panels.html: upstream row ids, sl-meta-row and the new button
  kept, data-i18n hooks re-applied (incl. OPEN STREET VIEW fallback).
- es/ru catalogs: 13 new keys, stale unlock key replaced in place.

npm run i18n: complete for es and ru. npm test: 6345/6347; the one
failure (devFreshDotenv provenance) also fails on plain upstream/main
and on the pre-merge tip here - this machine leaks OPENAI_API_KEY into
spawned children with a replaced env, so it is environmental.
This commit is contained in:
Meir Michanie committed 2026-10-13 18:33:44 +02:00
commit eb802408e4
59 files changed
+4009 -199

No files matched your search

+11
View File
@@ -897,6 +897,17 @@
"src/layers/streetLevel/selection.js",
"src/layers/streetLevel/providers/mapillary/viewer.js",
"src/layers/streetLevel/providers/mapillary/source.js",
"src/layers/streetLevel/groundClick.js",
"src/layers/streetLevel/providers/google/index.js",
"src/layers/streetLevel/providers/google/policy.js",
"src/layers/streetLevel/providers/google/viewer.js",
"src/layers/streetLevel/providers/google/mapsLoader.js",
"src/layers/streetLevel/groundClick.test.mjs",
"src/app/layers/streetLevel.test.mjs",
"src/layers/streetLevel/providers/google/provider.test.mjs",
"src/layers/streetLevel/providers/google/viewer.test.mjs",
"src/layers/streetLevel/providers/google/mapsLoader.test.mjs",
"src/testSupport/googleStreetViewFakes.mjs",
"server/providers/mapillary.js",
"server/providers/mapillary/routes.js",
"server/providers/mapillary/constants.js",
+17 -1
View File
@@ -1286,7 +1286,8 @@
"exports": [
"./layers/street-level",
"./layers/street-level/mapillary",
"./layers/street-level/mapillary/source"
"./layers/street-level/mapillary/source",
"./layers/street-level/google"
],
"modules": [
"src/layers/streetLevel/index.js",
@@ -1315,6 +1316,11 @@
"src/layers/streetLevel/tileMath.js",
"src/layers/streetLevel/selection.js",
"src/layers/streetLevel/providers/mapillary/viewer.js",
"src/layers/streetLevel/groundClick.js",
"src/layers/streetLevel/providers/google/index.js",
"src/layers/streetLevel/providers/google/policy.js",
"src/layers/streetLevel/providers/google/viewer.js",
"src/layers/streetLevel/providers/google/mapsLoader.js",
"src/layers/streetLevel/providers/mapillary/source.js",
"scripts/qa-street-level.mjs",
"src/i18n/index.js",
@@ -2290,6 +2296,11 @@
"src/layers/streetLevel/tileMath.js",
"src/layers/streetLevel/selection.js",
"src/layers/streetLevel/providers/mapillary/viewer.js",
"src/layers/streetLevel/groundClick.js",
"src/layers/streetLevel/providers/google/index.js",
"src/layers/streetLevel/providers/google/policy.js",
"src/layers/streetLevel/providers/google/viewer.js",
"src/layers/streetLevel/providers/google/mapsLoader.js",
"src/layers/streetLevel/providers/mapillary/source.js",
"src/app/layers/streetLevel.js",
"src/layers/alpr/tileRecords.js",
@@ -2820,6 +2831,11 @@
"src/layers/streetLevel/tileMath.js",
"src/layers/streetLevel/selection.js",
"src/layers/streetLevel/providers/mapillary/viewer.js",
"src/layers/streetLevel/groundClick.js",
"src/layers/streetLevel/providers/google/index.js",
"src/layers/streetLevel/providers/google/policy.js",
"src/layers/streetLevel/providers/google/viewer.js",
"src/layers/streetLevel/providers/google/mapsLoader.js",
"src/layers/streetLevel/providers/mapillary/source.js",
"src/app/layers/streetLevel.js",
"src/layers/alpr/tileRecords.js",
+28 -3
View File
@@ -381,11 +381,36 @@ async function main() {
);
await nextFrames(page);
};
await drag(handlePoint(await readPanel(), 'se'), 0, 200);
const chosen = await readPanel();
// Grow it from the corner once the restored window holds still, with the
// pointer really on the handle (a press beside it selects page text).
let chosen = await readPanel();
const tried = [];
for (
let attempt = 0;
attempt < 3 && chosen.height <= MIN_SIZE.height + 150;
attempt++
) {
await settle();
const box = await readPanel();
const corner = handlePoint(box, 'se');
const hit = await page.evaluate(({ x, y }) => {
const node = document.elementFromPoint(x, y);
return {
dir: node?.dataset?.dir || null,
target: node
? `${node.tagName.toLowerCase()}${node.id ? `#${node.id}` : ''}.${[...node.classList].join('.')}`
: 'nothing',
};
}, corner);
tried.push(
`${Math.round(corner.x)},${Math.round(corner.y)} on ${hit.target} (box ${Math.round(box.left)},${Math.round(box.top)} ${Math.round(box.width)}x${Math.round(box.height)})`,
);
if (hit.dir === 'se') await drag(corner, 0, 200);
chosen = await readPanel();
}
assert.ok(
chosen.height > MIN_SIZE.height + 150,
`the window grew to ${chosen.height}px`,
`the window grew to ${chosen.height}px; tried ${tried.join('; ')}`,
);
await setCollapsed(true);
const strip = await readPanel();
+317 -12
View File
@@ -49,18 +49,27 @@ export const VIEWPORTS = Object.freeze([
]);
/** Provider chips the panel must show, in order. */
export const EXPECTED_PROVIDERS = Object.freeze(['mapillary']);
export const EXPECTED_PROVIDERS = Object.freeze(['mapillary', 'google']);
/**
* Skips no setup can avoid: CI forks have no Google key, and live runs cannot
* stage fixture-only cases. `no Mapillary key` is not accepted: CI's dummy
* token runs the keyed flow, so a keyless strict run means that wiring broke.
* Skips no setup can avoid: CI forks have no Google key (so neither Google 3D
* nor Street View), and live runs cannot stage fixture-only cases. `no
* Mapillary key` is not accepted: CI's dummy token runs the keyed flow, so a
* keyless strict run means that wiring broke. Nor is `Google key refused`: a
* key that is set must work.
*/
export const STRICT_ALLOWED_SKIPS = Object.freeze([
'no Google 3D',
'no Google key',
'fixtures only',
]);
/** A Sacramento street with Google Street View (10th St, by the Capitol). */
export const STREET_VIEW_STREET = Object.freeze({
lon: -121.49372,
lat: 38.57986,
});
export function strictViolations(skips) {
return skips.filter((skip) => !STRICT_ALLOWED_SKIPS.includes(skip.reason));
}
@@ -619,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"]',
);
assert.ok(
@@ -649,14 +659,30 @@ async function main() {
),
].map((chip) => chip.dataset.chipId),
);
assert.deepEqual(chips, EXPECTED_PROVIDERS);
// One swatch per source plus "Selected"; each chip wears its colour.
assert.equal(
await page.evaluate(
() => document.querySelectorAll('#sl-legend li').length,
),
EXPECTED_PROVIDERS.length + 1,
// Every registered provider usable on this map, in order (Street View
// only on Google 3D).
const listed = await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('street-level')
.module.getUIState()
.providers.map((provider) => provider.id),
);
assert.deepEqual(chips, listed);
assert.deepEqual(
EXPECTED_PROVIDERS.filter((id) => listed.includes(id)),
listed,
);
assert.ok(listed.includes('mapillary'));
// One swatch per switched-on source plus "Selected" (Street View
// starts off); each chip wears its colour.
const legend = await page.evaluate(() => ({
swatches: document.querySelectorAll('#sl-legend li').length,
on: window.__godsEyeView.dataManager.layers
.get('street-level')
.module.getUIState()
.providers.filter((provider) => provider.on).length,
}));
assert.equal(legend.swatches, legend.on + 1);
assert.equal(
await page.$eval('[data-chip-id="mapillary"]', (chip) =>
chip.style.getPropertyValue('--chip-color'),
@@ -1721,6 +1747,285 @@ async function main() {
);
},
);
const googleChip = '#sl-provider-chips [data-chip-id="google"]';
const googleKey = await page.evaluate(() =>
Boolean(window.__GOOGLE_MAPS_API_KEY__),
);
await step(
'Street View starts off, appears only on the Google 3D map, and travels in the share link',
async () => {
const module = () =>
window.__godsEyeView.dataManager.layers.get('street-level').module;
assert.equal(
await page.evaluate(`(${module})().getParams().google`),
false,
'off until lit: panoramas are billed',
);
const setStreetView = (on) =>
page.evaluate(
(value) =>
window.__godsEyeView.dataManager.setLayerParams(
'street-level',
{ google: value },
{ origin: 'user' },
),
on,
);
const shown = () =>
page.evaluate(() => ({
chip: Boolean(
document.querySelector(
'#sl-provider-chips [data-chip-id="google"]',
),
),
legend: [...document.querySelectorAll('#sl-legend li')].some(
(item) => /Street View/.test(item.textContent),
),
credit: document.body.innerHTML.includes('Street View imagery'),
button: !document.getElementById('sl-nearest-btn').hidden,
}));
const original = await stacks.active();
// Off Google 3D (CI's only map) nothing of Street View shows, even
// switched on: Google's terms keep it off other maps.
if (original === 'photoreal') await stacks.set('esri-imagery');
await setStreetView(true);
await uiUntil((u) => u.providers.every((p) => p.id !== 'google'));
assert.ok(
await settle(
page,
() => !document.body.innerHTML.includes('Street View imagery'),
),
'no Google credit on another map',
);
assert.deepEqual(await shown(), {
chip: false,
legend: false,
credit: false,
button: false,
});
// Street View rides the shared provider-switch field (r).
const option = `${STREET_LEVEL_TOKEN}.r.google-1`;
assert.ok(
await settle(
page,
(token) =>
(new URLSearchParams(location.hash.slice(1)).get('lo') || '')
.split('_')
.includes(token),
option,
// The link is written once the view rests for 500 ms; under a
// software renderer with a photo open that can take a while.
{ timeout: 30_000 },
),
`the share link keeps the choice: ${option}`,
);
if (await stacks.photoreal()) {
await stacks.set('photoreal');
await uiUntil((u) => u.providers.some((p) => p.id === 'google'));
const chipNow = await page.$eval(googleChip, (node) => ({
className: node.className,
title: node.title,
color: node.style.getPropertyValue('--chip-color'),
}));
assert.equal(chipNow.color, '#4285f4');
if (googleKey) assert.match(chipNow.className, /chip-active/);
else {
assert.match(chipNow.className, /chip-error/);
assert.match(chipNow.title, /GOOGLE_MAPS_API_KEY/);
}
assert.ok(
await settle(page, () =>
document.body.innerHTML.includes('Street View imagery'),
),
'the Google credit shows on Google 3D',
);
}
// Back to Mapillary (one source at a time), as the next steps expect.
await page.evaluate(() =>
window.__godsEyeView.dataManager.setLayerParams(
'street-level',
{ mapillary: true, google: false },
{ origin: 'user' },
),
);
await stacks.set(original);
await uiUntil(
(u) => u.providers.find((p) => p.id === 'google')?.on !== true,
);
},
);
await step(
'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');
// 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 setOn = async (selector, id, on) => {
const now = (await ui()).providers.find((p) => p.id === id)?.on;
if (now === on) return;
await clickPanelControl(page, selector);
assert.ok(
await uiUntil(
(u, want) =>
u.providers.find((p) => p.id === want.id)?.on === want.on,
{ id, on },
),
`the ${id} chip switched ${on ? 'on' : 'off'}`,
);
};
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, 'GOOGLE 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',
);
// The keyboard path: OPEN STREET VIEW at the view centre.
await closePhoto();
await uiUntil((u) => u.street.open === false);
await clickPanelControl(page, '#sl-nearest-btn');
assert.ok(
await uiUntil(
(u) =>
u.street.providerId === 'google' &&
Boolean(u.street.imageId) &&
!u.street.loading,
null,
{ timeout: 30_000 },
),
'OPEN STREET VIEW opens the panorama at the view centre',
);
// 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();
// Off Google 3D the Street View chip is hidden: return to it
// before switching the chip, or the cleanup throws and hides
// the step's own result.
await stacks.set('photoreal');
await setOn(chip, 'mapillary', true);
await setOn(googleChip, 'google', false);
await stacks.set(original);
}
},
);
await step(
'a reload with the layer on keeps a collapsed panel collapsed, and the share link carries its ui token',
async () => {