mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
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:
commit
eb802408e4
59 files changed
+4009
-199
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 () => {
|
||||
|
||||
Reference in new issue
Block a user