mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Cover drone airspace, OSM places, sea marks and ski pistes on both engines in the strict acceptance
This commit is contained in:
1 parent
e1d3a588fc
commit
8bea1419f6
2 files changed
+301
No files matched your search
@@ -297,6 +297,11 @@ const runs = [
|
|||||||
],
|
],
|
||||||
['local-adsb-engine-switch', 'local-adsb-engine-switch', ['local-adsb']],
|
['local-adsb-engine-switch', 'local-adsb-engine-switch', ['local-adsb']],
|
||||||
['ghostmaps-switch', 'ghostmaps-switch', ['s2-ghostmaps']],
|
['ghostmaps-switch', 'ghostmaps-switch', ['s2-ghostmaps']],
|
||||||
|
[
|
||||||
|
'open-layers-switch',
|
||||||
|
'open-layers-switch',
|
||||||
|
['drone-airspace', 'osm-places', 'sea-marks', 'ski-pistes'],
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'weather-engine-switch',
|
'weather-engine-switch',
|
||||||
'weather-engine-switch',
|
'weather-engine-switch',
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createStrict, localPreview } 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({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||||
|
const p = await b.newPage();
|
||||||
|
await p.setViewport({ width: 1024, height: 768 });
|
||||||
|
const errors = [];
|
||||||
|
p.on('pageerror', (e) => {
|
||||||
|
errors.push(e.message);
|
||||||
|
console.log('error', e.message.split('?')[0]);
|
||||||
|
});
|
||||||
|
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 ready = async (kind) => {
|
||||||
|
await p.waitForFunction(
|
||||||
|
(k) =>
|
||||||
|
window.__godsEyeView?.engine?.kind === k &&
|
||||||
|
!document.querySelector('#globe-engine')?.disabled &&
|
||||||
|
!document.querySelector('.engine-switch-frame') &&
|
||||||
|
document.querySelector('#loading-screen.hidden'),
|
||||||
|
{ timeout: 90000 },
|
||||||
|
kind,
|
||||||
|
);
|
||||||
|
await p.evaluate(
|
||||||
|
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
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 ready('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.pick(p, kind);
|
||||||
|
await ready(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;
|
||||||
|
const drawn =
|
||||||
|
d.engine.kind === 'navara'
|
||||||
|
? (d.engine.movers.list().find((set) => set.id === 'osm-places')
|
||||||
|
?.count ?? 0)
|
||||||
|
: (d.viewer.dataSources.getByName('osm-places')[0]?.entities.values
|
||||||
|
.length ?? 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, []);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user