mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
- src/geo drops the ported debug argument checks and the tests that bitExact already covers; package boundaries accept a directory entry, so src/geo/ is listed once per group. - Cesium no longer carries movers and lines nobody draws with: they are optional engine capabilities. The fake engine and the Cesium camera use the shared cameraMath; unused contract tables, clock.ticked, the 'finest' surface detail and Navara click statistics are gone. - ALPR keeps one overlay core and one presentation model for both engines, bikeshare one station bookkeeping, the Navara ground effects one definition helper; image atlases, view centres, mover banks and colours are shared. - The legacy switch route, the clock offset and rate controls and other unused exports are removed. - The acceptance runs the Navara draw, geo import and scope checks; scenarios use the harness switch and page helpers; the main chunk check keeps the size budget only and runs once in CI.
361 lines
13 KiB
JavaScript
361 lines
13 KiB
JavaScript
import { trafficFixtureResponse } from './traffic-fixtures.mjs';
|
|
import sharp from 'sharp';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
createStrict,
|
|
localPreview,
|
|
openPage,
|
|
waitForEngine,
|
|
} from './qa-strict-harness.mjs';
|
|
import { STRICT_TILE_HOSTS } from './qa-map-policy.mjs';
|
|
|
|
const base = localPreview(process.argv[2]);
|
|
base.search = '?engine=navara&welcome=0&look=cinematic';
|
|
base.hash =
|
|
'v=2&lat=30.2670&lon=-97.7440&alt=1800&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
|
|
// The scenario answers the Sentinel-2 host itself (a fixed dark ground) and
|
|
// the OpenFreeMap road tiles from its fixtures; the disk cache keeps the
|
|
// other tile hosts.
|
|
// The fixed fixture gives 1 093 vehicles and 35 congestion corridors with
|
|
// flow and 618 simulated vehicles without it; the bands allow 5%.
|
|
const BAND = process.argv.includes('--simulation')
|
|
? { vehicles: [587, 649], corridors: [0, 0] }
|
|
: { vehicles: [1038, 1148], corridors: [33, 37] };
|
|
const strict = createStrict({
|
|
origin: base.origin,
|
|
layers: ['traffic'],
|
|
fixtures: ['https://tiles.maps.eox.at/*', 'https://tiles.openfreemap.org/*'],
|
|
tileHosts: STRICT_TILE_HOSTS.filter((host) => host !== 'tiles.maps.eox.at'),
|
|
});
|
|
await strict.run({}, async (b) => {
|
|
const { page: p, errors } = await openPage(b, { shaderErrors: true });
|
|
|
|
const keyless = process.argv.includes('--simulation');
|
|
const reads = new Map();
|
|
// A uniform dark Sentinel-2 tile: live imagery is bright and blurry at this
|
|
// altitude, so white 2 px vehicles on it gave anything from 14 to 300
|
|
// changed pixels. A fixed dark ground makes every drawn vehicle visible.
|
|
const ground = await sharp({
|
|
create: {
|
|
width: 256,
|
|
height: 256,
|
|
channels: 3,
|
|
background: { r: 28, g: 32, b: 30 },
|
|
},
|
|
})
|
|
.jpeg()
|
|
.toBuffer();
|
|
await p.setRequestInterception(true);
|
|
p.on('request', async (req) => {
|
|
const u = new URL(req.url());
|
|
if (u.hostname === 'tiles.maps.eox.at')
|
|
return req.respond({
|
|
status: 200,
|
|
contentType: 'image/jpeg',
|
|
headers: { 'access-control-allow-origin': '*' },
|
|
body: ground,
|
|
});
|
|
if (
|
|
u.hostname === 'tiles.openfreemap.org' ||
|
|
u.pathname.startsWith('/api/tomtom/')
|
|
) {
|
|
reads.set(u.href, (reads.get(u.href) || 0) + 1);
|
|
if (keyless && u.pathname === '/api/tomtom/status')
|
|
return req.respond({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: '{"hasKey":false}',
|
|
});
|
|
const response = trafficFixtureResponse(req);
|
|
if (response) return req.respond(response);
|
|
throw Error('unrecognized traffic URL: ' + u.pathname);
|
|
}
|
|
return req.continue();
|
|
});
|
|
await p.goto(base.href);
|
|
await waitForEngine(p, 'navara');
|
|
await p.evaluate(async () => {
|
|
const { tileset, dataManager } = window.__godsEyeView;
|
|
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
|
|
throw Error('Keyless preview required');
|
|
window.qaTraffic = dataManager.layers.get('traffic').module;
|
|
window.qaTraffic.setParams({ jamViz: 'both' });
|
|
await dataManager.setEnabled('traffic', true, { origin: 'user' });
|
|
});
|
|
const waitTraffic = async () => {
|
|
try {
|
|
// Vehicles, and with flow the congestion corridors, which the layer
|
|
// publishes again once the next globe has attached.
|
|
await p.waitForFunction(
|
|
(keyless) => {
|
|
const stats = window.qaTraffic.getStats();
|
|
return stats.count >= 10 && (keyless || stats.heatLines > 0);
|
|
},
|
|
{ timeout: 40000 },
|
|
keyless,
|
|
);
|
|
} catch (e) {
|
|
console.log(
|
|
'no traffic',
|
|
await p.evaluate(() => window.qaTraffic.getStats()),
|
|
);
|
|
throw e;
|
|
}
|
|
};
|
|
await waitTraffic();
|
|
// The view compared before and after the switches: a central window free of
|
|
// panels and status toasts.
|
|
const region = { left: 310, top: 160, right: 810, bottom: 660 };
|
|
// Each vehicle in the window with its window position and its height above
|
|
// the terrain Navara draws there. A vehicle below that terrain is hidden by
|
|
// the depth test, so it cannot be expected in the picture.
|
|
const vehiclesInView = () =>
|
|
p.evaluate((r) => {
|
|
const { engine } = window.__godsEyeView;
|
|
const a = 6378137,
|
|
f = 1 / 298.257223563,
|
|
e2 = f * (2 - f);
|
|
const rows = [];
|
|
for (const o of window.qaTraffic.getDetectableObjects()) {
|
|
const w = engine.projector.worldToWindow(o.position);
|
|
if (!w || w.x < r.left || w.y < r.top || w.x >= r.right) continue;
|
|
if (w.y >= r.bottom) continue;
|
|
const { x, y, z } = o.position;
|
|
const longitude = Math.atan2(y, x),
|
|
ring = Math.hypot(x, y);
|
|
let latitude = Math.atan2(z, ring * (1 - e2)),
|
|
height = 0;
|
|
for (let i = 0; i < 6; i++) {
|
|
const n = a / Math.sqrt(1 - e2 * Math.sin(latitude) ** 2);
|
|
height = ring / Math.cos(latitude) - n;
|
|
latitude = Math.atan2(z, ring * (1 - (e2 * n) / (n + height)));
|
|
}
|
|
const ground = engine.surface.terrainHeight({
|
|
longitude,
|
|
latitude,
|
|
height: 0,
|
|
});
|
|
rows.push({
|
|
id: o.id,
|
|
x: w.x,
|
|
y: w.y,
|
|
clearance: Number.isFinite(ground) ? height - ground : null,
|
|
});
|
|
}
|
|
return rows;
|
|
}, region);
|
|
const settled = async () => {
|
|
const deadline = Date.now() + 30000;
|
|
for (;;) {
|
|
const loading = await p.evaluate(
|
|
() => window.qaTraffic.getStats().loading,
|
|
);
|
|
const rows = await vehiclesInView();
|
|
if (!loading && rows.length >= 12) return rows;
|
|
if (Date.now() > deadline)
|
|
throw Error(
|
|
'traffic did not settle with 12 vehicles in view: ' +
|
|
JSON.stringify({ loading, inView: rows.length }),
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
}
|
|
};
|
|
// The population once roads, heights and dots have settled. Source counts
|
|
// can be ready before the renderer has drawn terrain, so the camera and the
|
|
// attached road heights get the same delay before and after the switches.
|
|
const settledPopulation = async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 2200));
|
|
await settled();
|
|
return p.evaluate(() => window.qaTraffic.getStats().count);
|
|
};
|
|
const population = await settledPopulation();
|
|
const buriedBefore = (await vehiclesInView()).filter(
|
|
(v) => v.clearance != null && v.clearance <= -1,
|
|
).length;
|
|
const ids = await p.evaluate(() =>
|
|
window.qaTraffic.getDetectableObjects().map((p) => p.id),
|
|
);
|
|
const first = await p.evaluate(
|
|
() => window.qaTraffic.getStats().motion.rebuilds,
|
|
);
|
|
for (const kind of ['navara', 'cesium', 'navara']) {
|
|
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
|
await strict.switchTo(p, kind);
|
|
}
|
|
await waitTraffic();
|
|
const check = await p.evaluate(() => {
|
|
const { dataManager } = window.__godsEyeView;
|
|
const mod = dataManager.layers.get('traffic').module;
|
|
return {
|
|
same: mod === window.qaTraffic,
|
|
...mod.getStats(),
|
|
params: mod.getParams(),
|
|
ids: mod.getDetectableObjects().map((p) => p.id),
|
|
};
|
|
});
|
|
console.log(
|
|
kind,
|
|
check.mode,
|
|
check.count,
|
|
check.flowBuckets,
|
|
check.heatLines,
|
|
check.motion,
|
|
);
|
|
assert.equal(check.same, true);
|
|
assert.equal(check.params.jamViz, 'both');
|
|
assert.equal(check.error, null);
|
|
assert.equal(
|
|
check.motion.rebuilds,
|
|
first,
|
|
'traffic population was cleared',
|
|
);
|
|
assert.ok(
|
|
check.ids.some((id) => ids.includes(id)),
|
|
'original moving vehicles survived',
|
|
);
|
|
if (!keyless)
|
|
assert.ok(check.heatLines > 0, 'congestion corridors are published');
|
|
if (keyless) assert.equal(check.flowBuckets.sim, check.count);
|
|
else assert.ok(check.flowBuckets.slow + check.flowBuckets.jam > 0);
|
|
await strict.check(
|
|
p,
|
|
'traffic',
|
|
keyless
|
|
? `same module, parameters and vehicle ids; ${BAND.vehicles.join('-')} simulated vehicles, no flow`
|
|
: `same module, parameters and vehicle ids; ${BAND.vehicles.join('-')} vehicles with flow, ${BAND.corridors.join('-')} congestion corridors`,
|
|
check.count >= BAND.vehicles[0] &&
|
|
check.count <= BAND.vehicles[1] &&
|
|
check.heatLines >= BAND.corridors[0] &&
|
|
check.heatLines <= BAND.corridors[1],
|
|
{ count: check.count, corridors: check.heatLines },
|
|
);
|
|
}
|
|
if (keyless)
|
|
assert.ok(![...reads.keys()].some((u) => u.includes('/tomtom/flow/')));
|
|
|
|
// Compare the same view with the layer shown and hidden, vehicle by
|
|
// vehicle.
|
|
const raw = async () =>
|
|
sharp(await p.screenshot())
|
|
.removeAlpha()
|
|
.raw()
|
|
.toBuffer({ resolveWithObject: true });
|
|
const settledCount = await settledPopulation();
|
|
const before = await vehiclesInView();
|
|
const shown = await raw();
|
|
const after = new Map((await vehiclesInView()).map((v) => [v.id, v]));
|
|
// Read again once the comparison is taken: the population used to fall
|
|
// seconds after the return, once the incoming globe republished the roads.
|
|
const finalCount = await p.evaluate(() => window.qaTraffic.getStats().count);
|
|
await p.evaluate(() =>
|
|
window.__godsEyeView.dataManager.setEnabled('traffic', false, {
|
|
origin: 'user',
|
|
}),
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
const hidden = await raw();
|
|
const { width, height } = shown.info;
|
|
const scale = width / 1024;
|
|
const changed = (x, y) => {
|
|
const i = (y * width + x) * 3;
|
|
let d = 0;
|
|
for (let c = 0; c < 3; c++)
|
|
d += Math.abs(shown.data[i + c] - hidden.data[i + c]);
|
|
return d > 90;
|
|
};
|
|
const near = new Set();
|
|
const visible = { drawn: 0, missing: [] };
|
|
let buried = 0,
|
|
unclear = 0;
|
|
for (const v of before) {
|
|
const later = after.get(v.id);
|
|
if (!later) continue;
|
|
// The screenshot falls between the two projections.
|
|
const cx = (v.x + later.x) / 2,
|
|
cy = (v.y + later.y) / 2;
|
|
let hit = false;
|
|
for (let dy = -8; dy <= 8; dy++)
|
|
for (let dx = -8; dx <= 8; dx++) {
|
|
const x = Math.round((cx + dx) * scale),
|
|
y = Math.round((cy + dy) * scale);
|
|
if (x < 0 || y < 0 || x >= width || y >= height) continue;
|
|
near.add(y * width + x);
|
|
if (Math.abs(dx) <= 4 && Math.abs(dy) <= 4 && changed(x, y)) hit = true;
|
|
}
|
|
if (v.clearance == null || Math.abs(v.clearance) < 1) unclear++;
|
|
else if (v.clearance < 0) buried++;
|
|
else if (hit) visible.drawn++;
|
|
else visible.missing.push(v);
|
|
}
|
|
let stray = 0,
|
|
area = 0;
|
|
for (let y = Math.round(region.top * scale); y < region.bottom * scale; y++)
|
|
for (
|
|
let x = Math.round(region.left * scale);
|
|
x < region.right * scale;
|
|
x++
|
|
) {
|
|
area++;
|
|
if (!near.has(y * width + x) && changed(x, y)) stray++;
|
|
}
|
|
const aboveGround = visible.drawn + visible.missing.length;
|
|
console.log(
|
|
'vehicles in view',
|
|
JSON.stringify({
|
|
aboveGround,
|
|
drawn: visible.drawn,
|
|
belowTerrain: buried,
|
|
withinOneMetre: unclear,
|
|
strayChangedPixels: stray,
|
|
area,
|
|
}),
|
|
);
|
|
if (visible.missing.length)
|
|
console.log('not drawn', JSON.stringify(visible.missing.slice(0, 10)));
|
|
if (buried)
|
|
console.log(
|
|
'vehicles below the drawn terrain, hidden by the depth test:',
|
|
buried,
|
|
);
|
|
// The checks after each switch run before the incoming globe republishes
|
|
// the roads; these compare what the population settles to.
|
|
console.log(
|
|
'population before the switches',
|
|
population,
|
|
'settled after the round trip',
|
|
settledCount,
|
|
'after the comparison',
|
|
finalCount,
|
|
'below the drawn terrain before the switches',
|
|
buriedBefore,
|
|
);
|
|
for (const count of [settledCount, finalCount])
|
|
assert.ok(
|
|
count >= 0.8 * population && count <= 1.25 * population,
|
|
`the population after the round trip (${count}) stays within 80-125% of the ${population} before it`,
|
|
);
|
|
assert.ok(aboveGround >= 12, 'at least 12 vehicles above ground in view');
|
|
await strict.check(
|
|
p,
|
|
'traffic',
|
|
'after the round trip the population stays at 80-125% and at least 90% of the vehicles above ground in view are drawn where the layer places them',
|
|
visible.drawn >= 0.9 * aboveGround &&
|
|
[settledCount, finalCount].every(
|
|
(count) => count >= 0.8 * population && count <= 1.25 * population,
|
|
),
|
|
{ population, settledCount, finalCount, aboveGround, drawn: visible.drawn },
|
|
);
|
|
assert.ok(
|
|
visible.drawn >= 0.9 * aboveGround,
|
|
'at least 90% of vehicles above the ground are drawn where the layer places them',
|
|
);
|
|
// Live mode adds congestion lines along roads; only the simulation must
|
|
// change nothing but the vehicles.
|
|
if (keyless)
|
|
assert.ok(stray < 0.002 * area, 'hiding the layer changes only vehicles');
|
|
assert.deepEqual(errors, []);
|
|
console.log(
|
|
'PASS kept the traffic module, parameters, flow and vehicle identities across both globes; vehicles in view are drawn where the layer places them',
|
|
);
|
|
});
|