mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge remote-tracking branch 'origin/main' into refactor/realtime-owners
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
a6f9c3d82a
8 files changed
+348
-21
No files matched your search
@@ -702,6 +702,7 @@
|
||||
"src/layers/transit/viewport.js",
|
||||
"src/layers/transit/lifecycle.test.mjs",
|
||||
"scripts/qa-transit.mjs",
|
||||
"scripts/qa-transit-heading.mjs",
|
||||
"src/tooling/transitProvider.test.mjs",
|
||||
"src/app/layers/transit.js",
|
||||
"src/data/transitIcons.js",
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
/** Prove parked and moving headings in the real scene, without live feeds. */
|
||||
export async function runTransitHeadingRegression(base, check) {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath:
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
args: ['--no-sandbox', '--disable-background-timer-throttling'],
|
||||
});
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 800 });
|
||||
await page.goto(
|
||||
`${base}/?welcome=0#lat=42.3601&lon=-71.0589&alt=1200&heading=0&pitch=-90`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.dataManager?.layers?.has('transit'),
|
||||
{ timeout: 90000 },
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 6000));
|
||||
await page.evaluate(async () => {
|
||||
const app = window.__godsEyeView;
|
||||
await app.dataManager.setEnabled('transit', true, { source: 'qa' });
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
layer._loadTransitFleetForTest(4, { lat: 42.3601, lon: -71.0589 }, 100);
|
||||
const state = layer._transitStateForTest(),
|
||||
parts = layer._transitPartsForTest();
|
||||
const { destroyTrack } = await import('/src/data/contactPlayback.js');
|
||||
const {
|
||||
initializePlayback,
|
||||
recordFix,
|
||||
updatePlayback,
|
||||
attachFloorToPlace,
|
||||
} = await import('/src/layers/transit/movement.js');
|
||||
// The first three are parked: held east, reported east only, unknown.
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const entry = state._vehicles.get(`mbta:qa-${i}`);
|
||||
state._moving.delete(entry);
|
||||
destroyTrack(entry.track);
|
||||
initializePlayback(entry, state._historyBudget);
|
||||
recordFix(entry, {
|
||||
t: Date.now() - 30000,
|
||||
lat: entry.record.lat,
|
||||
lon: entry.record.lon,
|
||||
});
|
||||
attachFloorToPlace(entry, entry.record, 20);
|
||||
updatePlayback(entry, Date.now(), performance.now());
|
||||
entry.courseDeg = i === 0 ? 90 : null;
|
||||
entry.record.bearing = i === 1 ? 90 : null;
|
||||
entry.displayPaths.clear();
|
||||
parts.rendering.placeSample(entry);
|
||||
parts.rendering.schedulePlayback(entry);
|
||||
}
|
||||
parts.rendering.syncRenderHold();
|
||||
window.__headingErrors = [];
|
||||
app.viewer.scene.renderError.addEventListener((scene, error) =>
|
||||
window.__headingErrors.push(error.message),
|
||||
);
|
||||
});
|
||||
const pose = async (heading) => {
|
||||
await page.evaluate((heading) => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
const C = camera.positionCartographic.constructor;
|
||||
camera.setView({
|
||||
destination: C.toCartesian(C.fromDegrees(-71.0589, 42.3601, 1200)),
|
||||
orientation: { heading, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
}, heading);
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
};
|
||||
const sample = () =>
|
||||
page.evaluate(async () => {
|
||||
const app = window.__godsEyeView,
|
||||
scene = app.viewer.scene;
|
||||
const state = app.dataManager.layers
|
||||
.get('transit')
|
||||
.module._transitStateForTest();
|
||||
const mul = (m, v) =>
|
||||
[0, 1, 2, 3].map(
|
||||
(r) =>
|
||||
m[r] * v[0] +
|
||||
m[r + 4] * v[1] +
|
||||
m[r + 8] * v[2] +
|
||||
m[r + 12] * v[3],
|
||||
);
|
||||
const project = (p) => {
|
||||
const clip = mul(
|
||||
scene.camera.frustum.projectionMatrix,
|
||||
mul(scene.camera.viewMatrix, [p.x, p.y, p.z, 1]),
|
||||
);
|
||||
return {
|
||||
x: ((clip[0] / clip[3] + 1) * scene.canvas.clientWidth) / 2,
|
||||
y: ((1 - clip[1] / clip[3]) * scene.canvas.clientHeight) / 2,
|
||||
};
|
||||
};
|
||||
return [0, 1, 2, 3].map((i) => {
|
||||
const entry = state._vehicles.get(`mbta:qa-${i}`);
|
||||
const p = project(entry.marker.position);
|
||||
return {
|
||||
rotation: entry.marker.rotation,
|
||||
course: entry.courseDeg,
|
||||
moving: state._moving.has(entry),
|
||||
visible: entry.marker.show,
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
};
|
||||
});
|
||||
});
|
||||
await pose(0);
|
||||
const before = await sample();
|
||||
await pose(Math.PI / 2);
|
||||
const after = await sample();
|
||||
const deltaDeg = (a, b) =>
|
||||
(Math.atan2(Math.sin(b - a), Math.cos(b - a)) * 180) / Math.PI;
|
||||
for (const [i, label] of ['held course', 'reported bearing'].entries()) {
|
||||
const delta = deltaDeg(before[i].rotation, after[i].rotation);
|
||||
check(
|
||||
`parked ${label} reprojects through a 90 degree camera orbit`,
|
||||
before[i].visible &&
|
||||
after[i].visible &&
|
||||
!before[i].moving &&
|
||||
!after[i].moving &&
|
||||
Math.abs(delta - 90) <= 5,
|
||||
JSON.stringify({ before: before[i], after: after[i], delta }),
|
||||
);
|
||||
}
|
||||
check(
|
||||
'unknown parked course remains screen-up',
|
||||
before[2].rotation === 0 && after[2].rotation === 0,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
const moved = (await sample())[3];
|
||||
const dx = moved.x - after[3].x,
|
||||
dy = moved.y - after[3].y;
|
||||
const travelRotation = Math.atan2(-dx, -dy);
|
||||
const error = Math.abs(deltaDeg(travelRotation, moved.rotation));
|
||||
check(
|
||||
'moving vehicle still points along its rendered travel after orbit',
|
||||
moved.visible && moved.moving && Math.hypot(dx, dy) > 0.5 && error <= 5,
|
||||
JSON.stringify({
|
||||
pixels: Math.hypot(dx, dy),
|
||||
error,
|
||||
after: after[3],
|
||||
moved,
|
||||
}),
|
||||
);
|
||||
check(
|
||||
'heading regression has no scene render errors',
|
||||
(await page.evaluate(() => window.__headingErrors)).length === 0,
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { runTransitHeadingRegression } from './qa-transit-heading.mjs';
|
||||
import {
|
||||
boundPageEvaluations,
|
||||
reportTransitVisibility,
|
||||
@@ -79,6 +80,18 @@ const check = (
|
||||
failures += 1;
|
||||
} else if (!passed) failures += 1;
|
||||
};
|
||||
// A focused deterministic regression can run independently of live-feed QA.
|
||||
if (process.argv.includes('--heading-only')) {
|
||||
try {
|
||||
await runTransitHeadingRegression(BASE, check);
|
||||
} catch (error) {
|
||||
check('heading regression completed', false, error.stack || error.message);
|
||||
}
|
||||
console.log(`HEADING: ${failures ? 'FAIL' : 'PASS'} (${failures} failed)`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
}
|
||||
if (runs('heading')) await runTransitHeadingRegression(BASE, check);
|
||||
|
||||
const renderingSource = await readFile(
|
||||
new URL('../src/layers/transit/rendering.js', import.meta.url),
|
||||
'utf8',
|
||||
|
||||
Reference in new issue
Block a user