Files
gods-eye-view/scripts/qa-open-layers-switch.mjs
T
German Khokhlov 6dc643182e Count OSM Places on the movers set on both engines in the open layers scenario
OSM Places draws through one display on every engine now, so on Cesium its
points are a movers set too, not a data source's entities.
2026-11-07 04:36:31 +04:00

279 lines
9.3 KiB
JavaScript

import sharp from 'sharp';
import assert from 'node:assert/strict';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
// Drone airspace, OSM places, sea marks and ski pistes on both engines, over
// Navara → Cesium → Navara: each draws over the view centre, and the switch
// keeps the same layer models with their data (no refetch, the places and
// the overlays still on).
//
// node scripts/qa-open-layers-switch.mjs http://127.0.0.1:4208
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0';
base.hash =
'v=2&lat=38.9000&lon=-77.0300&alt=30000&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
// One prohibited zone over the view centre (P-56's place, enlarged).
const RING = [
[-77.06, 38.88],
[-77.0, 38.88],
[-77.0, 38.92],
[-77.06, 38.92],
[-77.06, 38.88],
];
const PLACES = [
{ id: 'node/1', name: 'QA Clinic', lat: 38.9, lon: -77.03, tags: {} },
{ id: 'node/2', name: 'QA Hospital', lat: 38.905, lon: -77.02, tags: {} },
{ id: 'node/3', name: '', lat: 38.895, lon: -77.04, tags: {} },
];
// The overlays' tiles: half-transparent tints, one per overlay, so the
// view centre takes the colour of whichever is on.
const tint = (r, g, b) =>
sharp({
create: {
width: 256,
height: 256,
channels: 4,
background: { r, g, b, alpha: 0.6 },
},
})
.png()
.toBuffer();
const TILES = {
'sea-marks': await tint(255, 0, 255),
'ski-pistes': await tint(0, 255, 0),
};
const tileRoute = (pattern, id) => ({
pattern,
respond: () => ({ status: 200, contentType: 'image/png', body: TILES[id] }),
});
const LAYERS = ['drone-airspace', 'osm-places', 'sea-marks', 'ski-pistes'];
const strict = createStrict({
origin: base.origin,
layers: LAYERS,
routes: [
tileRoute('https://tiles.openseamap.org/*', 'sea-marks'),
tileRoute('https://www.opensnowmap.org/*', 'ski-pistes'),
],
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b);
await p.evaluateOnNewDocument((ring) => {
const fetch = window.fetch.bind(window);
window.qaZoneReads = 0;
window.fetch = (input, options) => {
const { pathname } = new URL(input.url ?? input, document.baseURI);
if (pathname === '/api/drone-airspace') {
window.qaZoneReads++;
return Promise.resolve(
Response.json({
rows: [
{
stableId: '310',
name: 'QA Zone',
typeCode: 'P',
type: 'Prohibited',
lowerAltitude: 'SFC',
upperAltitude: '18,000 ft MSL',
timesOfUse: 'CONTINUOUS',
agency: null,
city: 'WASHINGTON',
state: 'DC',
polygons: [[ring]],
},
],
stale: false,
fetchedAt: Date.now(),
}),
);
}
// OSM Places asks whether an Overpass is configured.
if (pathname === '/api/overpass/status')
return Promise.resolve(Response.json({ configured: true }));
return fetch(input, options);
};
}, RING);
await p.goto(base.href);
const setEnabled = (id, on) =>
p.evaluate(
(layerId, show) =>
window.__godsEyeView.dataManager.setEnabled(layerId, show, {
origin: 'user',
}),
id,
on,
);
// The view centre once the globe has settled and drawn a frame.
const sample = async () => {
await p.waitForFunction(
() => {
const d = window.__godsEyeView;
if (!d?.engine) return false;
d.engine.render.request('qa-open-settle');
if (!d.engine.surface.tilesLoaded()) {
window.__qaSettledSince = null;
return false;
}
window.__qaSettledSince ??= performance.now();
return performance.now() - window.__qaSettledSince > 1200;
},
{ polling: 100, timeout: 30000 },
);
await p.evaluate(() => {
window.__qaSettledSince = null;
return new Promise((resolve) => {
const { engine } = window.__godsEyeView;
const remove = engine.frames.postRender.addEventListener(() => {
remove();
requestAnimationFrame(() => resolve());
});
engine.render.request('qa-open');
});
});
// stats() reads the whole input, so the patch is cut out first.
const patch = await sharp(await p.screenshot())
.extract({ left: 492, top: 369, width: 40, height: 30 })
.removeAlpha()
.png()
.toBuffer();
const { channels } = await sharp(patch).stats();
return channels.map((channel) => channel.mean);
};
// Until the layer shows over `off` by `test`, for at most 20 s (the
// engines draw clamped polygons and imagery a few frames after their data).
const shows = async (off, test) => {
const deadline = Date.now() + 20000;
let on = await sample();
while (!test(on, off) && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 500));
on = await sample();
}
return on;
};
const redder = (on, off) => on[0] - on[2] > off[0] - off[2] + 5;
const magenta = (on, off) =>
on[0] + on[2] - 2 * on[1] > off[0] + off[2] - 2 * off[1] + 10;
const greener = (on, off) =>
2 * on[1] - on[0] - on[2] > 2 * off[1] - off[0] - off[2] + 10;
await waitForEngine(p, 'navara');
await p.evaluate(() => {
const { dataManager } = window.__godsEyeView;
window.qaModels = Object.fromEntries(
['drone-airspace', 'osm-places', 'sea-marks', 'ski-pistes'].map((id) => [
id,
dataManager.layers.get(id).module,
]),
);
});
for (const [leg, kind] of ['navara', 'cesium', 'navara'].entries()) {
const readsBeforeSwitch = await p.evaluate(() => window.qaZoneReads);
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.switchTo(p, kind);
const same = await p.evaluate(() =>
Object.entries(window.qaModels).every(
([id, model]) =>
window.__godsEyeView.dataManager.layers.get(id).module === model,
),
);
await strict.check(p, LAYERS, 'same layer models after the switch', same);
// The zone stayed on across the switch: the next globe draws the rows
// it already had, with no new request.
const kept = await p.evaluate(() => ({
reads: window.qaZoneReads,
records: window.qaModels['drone-airspace'].getAnalystRecords().length,
}));
assert.deepEqual(
kept,
{ reads: readsBeforeSwitch, records: 1 },
`${kind}: the switch keeps the zone rows without a refetch`,
);
}
// Drone airspace: the zone tints the centre red; after a switch it is
// drawn from the rows already fetched.
await setEnabled('drone-airspace', false);
const zoneOff = await sample();
await setEnabled('drone-airspace', true);
if (leg === 0)
await p.evaluate(() => window.qaModels['drone-airspace'].update());
const zoneOn = await shows(zoneOff, redder);
assert.ok(redder(zoneOn, zoneOff), `${kind} must draw the zone`);
const zone = await p.evaluate(() => ({
reads: window.qaZoneReads,
records: window.qaModels['drone-airspace'].getAnalystRecords(),
}));
assert.equal(zone.records.length, 1);
await strict.passed(p, 'drone-airspace', 'zone drawn over the centre', {
off: zoneOff,
on: zoneOn,
reads: zone.reads,
});
console.log('PASS', kind, 'drone airspace zone');
// The zone stays on for the switch; the overlays sample without it.
// OSM places: the results stay with the model across the switch, and
// the engine draws a point for each.
if (leg === 0)
await p.evaluate((places) => {
window.qaModels['osm-places'].setResults(places, {
kind: 'clinic',
label: 'Clinics',
area: 'Washington',
});
}, PLACES);
await setEnabled('osm-places', true);
const places = await p.evaluate(() => {
const d = window.__godsEyeView;
// Both engines draw the places as one movers point set.
const drawn =
d.engine.movers.list().find((set) => set.id === 'osm-places')?.count ??
0;
return {
records: window.qaModels['osm-places'].getAnalystRecords().length,
drawn,
};
});
assert.deepEqual(places, { records: 3, drawn: 3 }, `${kind} places`);
await strict.passed(
p,
'osm-places',
'three places drawn as points',
places,
);
console.log('PASS', kind, 'OSM places', JSON.stringify(places));
await setEnabled('osm-places', false);
// The overlays tint the centre with their tile; each on alone.
await setEnabled('drone-airspace', false);
for (const [id, test] of [
['sea-marks', magenta],
['ski-pistes', greener],
]) {
const off = await sample();
await setEnabled(id, true);
const on = await shows(off, test);
assert.ok(test(on, off), `${kind} must drape ${id}`);
const stats = await p.evaluate(
(layerId) => window.qaModels[layerId].getStats(),
id,
);
assert.equal(stats.error, null);
await strict.passed(p, id, 'overlay tiles draped over the centre', {
off,
on,
});
console.log('PASS', kind, id);
await setEnabled(id, false);
}
await setEnabled('drone-airspace', true);
}
assert.deepEqual(errors, []);
});