mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Read QA drawing evidence through the globe contract
Port the layer probes in twenty-six browser scripts to engine primitive lists, drawn rows and layer diagnostics. Preserve Cesium as the reference and keep the supported Navara paths on the same observations. This changes QA observation only, with no application behaviour change. Review the interrupted infrastructure group and retain its readiness, drape, selection, label and floor checks. Match both installation outline buffers. Frame each CCTV city before reading its visible coverage. Read Radio colours and selected identities from actual mover writes. Retain transit's GPU upload budgets using engine-owned line primitives rather than substituting rebuild counts. Keep native inspection where a check specifically measures GPU uploads, loaded meshes or ground readiness. The preceding combined B/C commit owns all shared manifests, docs, locale catalogs and the coupling baseline; this commit changes only its own twenty-six QA scripts. Coupling remains unchanged at 344 Cesium renderer lines, 1294 Navara renderer lines, 9 Cesium import files, 1026 viewer lines, 2 Navara kind checks, 0 engine sniff calls, 9 extension reads, 1 ground pass, 3 SDK loads, 4 screen-handler files and 42 engine-name lines. The full unit suite and the boundary, format and coupling checks pass. Browser validation remains to be run. Browser checks to run GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4291 node scripts/qa-alpr-journey.mjs http://localhost:4291 node scripts/qa-alpr-journey.mjs 'http://localhost:4291/?engine=navara' node scripts/qa-cables-overlay.mjs --url http://localhost:4291 node scripts/qa-cctv-v2.mjs --url http://localhost:4291 --fixture --terrain-globe node scripts/qa-context-arrivals.mjs http://localhost:4291 node scripts/qa-directions.mjs --url http://localhost:4291 QA_BASE_URL=http://localhost:4291 node scripts/qa-draw-tool.mjs QA_BASE_URL=http://localhost:4291 node scripts/qa-floor-hold.mjs node scripts/qa-ghostmaps.mjs --url http://localhost:4291 node scripts/qa-gnss.mjs --url http://localhost:4291 node scripts/qa-height-datum.mjs --url http://localhost:4291 node scripts/qa-infra-lod.mjs --url http://localhost:4291 node scripts/qa-installation-navigation.mjs http://localhost:4291 node scripts/qa-military-names.mjs http://localhost:4291 --no-video node scripts/qa-overpass-offload.mjs --url http://localhost:4291 node scripts/qa-radio.mjs --url http://localhost:4291 node scripts/qa-traffic-motion.mjs http://localhost:4291 --mode=osm QA_BASE_URL=http://localhost:4291 node scripts/qa-transit-recovery.mjs QA_BASE_URL=http://localhost:4291 node scripts/qa-transit.mjs node scripts/qa-weather-teardown.mjs --url http://localhost:4291 --headless for engine in cesium navara; do node scripts/qa-enrich-ambient.mjs --url http://localhost:4291 --engine=$engine QA_BASE_URL=http://localhost:4291 node scripts/qa-floor-verify.mjs --engine=$engine node scripts/qa-heading-b3.mjs --url http://localhost:4291 --engine=$engine node scripts/qa-overlay-baseline.mjs --url http://localhost:4291 --engine=$engine node scripts/qa-sprites-b5.mjs --url http://localhost:4291 --engine=$engine node scripts/qa-vessel-datum.mjs --url 'http://localhost:4291/?qaFeed=vessels:fixture' --engine $engine done The military-names driver exercises installation-polish; the transit driver exercises transit-scenes. Use only the keyless server. Cases that require photoreal tiles or unavailable providers remain unverified.
This commit is contained in:
1 parent
97c4dc76ce
commit
3907b7084e
26 files changed
+1429
-1075
No files matched your search
+93
-58
@@ -8,7 +8,8 @@
|
||||
*
|
||||
* (i) LIVE — enabling the layer loads the default products (Common
|
||||
* Intelligence Picture and the 30-day kinetic pull) from the live
|
||||
* repository through /api/ghostmaps, one data source per product,
|
||||
* repository through /api/ghostmaps, one drawn edition per product
|
||||
* (its globe-contract sets),
|
||||
* matching the proxy's catalog commit; the row shows one chip per
|
||||
* catalog product with the defaults active, and the page holds an
|
||||
* open event stream to /api/ghostmaps/events.
|
||||
@@ -17,12 +18,12 @@
|
||||
* and clicking empty ground clears it.
|
||||
* (iii) PUSH (synthetic) — the event stream is replaced in the page and a
|
||||
* catalog naming a new edition of one product is pushed: only that
|
||||
* product reloads (a new data source replaces the old one, no
|
||||
* duplicates), the other keeps its data source. A catalog without a
|
||||
* product reloads (a new edition replaces the old one, no
|
||||
* duplicates), the other keeps its edition. A catalog without a
|
||||
* product removes it.
|
||||
* (iv) CHIPS — clicking an active product chip removes that product,
|
||||
* clicking it again restores it; the choice survives a reload.
|
||||
* (v) DISABLE — every GhostMaps data source leaves the viewer.
|
||||
* (v) DISABLE — every GhostMaps set leaves the globe.
|
||||
* (vi) VIEWPORT — at a 390x844 phone viewport the open row and its chips
|
||||
* fit the panel width with no horizontal overflow.
|
||||
* (vii) FAR SIDE — pins skip the depth test, so they are horizon-culled:
|
||||
@@ -78,15 +79,63 @@ async function boot(page) {
|
||||
await page.keyboard.press('Escape');
|
||||
}
|
||||
|
||||
/** GhostMaps data sources on the viewer: name → entity count. */
|
||||
/**
|
||||
* GhostMaps product editions on the globe contract: one row per drawn
|
||||
* edition, as one data source per product used to be. The display
|
||||
* (render/display.js) gives each edition three sets named
|
||||
* `${prefix}${productId}:${edition}:{polygons,lines,pins}`; `count` sums their
|
||||
* items.
|
||||
*/
|
||||
function ghostSources(prefix) {
|
||||
const { dataSources } = window.__godsEyeView.viewer;
|
||||
const out = [];
|
||||
for (let i = 0; i < dataSources.length; i++) {
|
||||
const ds = dataSources.get(i);
|
||||
if (ds.name?.startsWith(prefix))
|
||||
out.push({ name: ds.name, count: ds.entities.values.length });
|
||||
const { engine } = window.__godsEyeView;
|
||||
const editions = new Map();
|
||||
for (const set of [...engine.ground.list(), ...engine.movers.list()]) {
|
||||
if (!set.id.startsWith(prefix)) continue;
|
||||
const [productId, edition] = set.id.slice(prefix.length).split(':');
|
||||
const key = `${productId}:${edition}`;
|
||||
const row = editions.get(key) || {
|
||||
name: `${prefix}${productId}`,
|
||||
edition: Number(edition),
|
||||
count: 0,
|
||||
};
|
||||
row.count += set.count;
|
||||
editions.set(key, row);
|
||||
}
|
||||
return [...editions.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* The drawn placemarks of the GhostMaps sprite sets (`...:pins`), from the
|
||||
* Cesium engine's own billboards for those sets (the contract has no
|
||||
* per-instance read-back): { id, show, position } each.
|
||||
*/
|
||||
function ghostPins(prefix) {
|
||||
const gev = window.__godsEyeView;
|
||||
const counts = new Map(
|
||||
gev.engine.movers.list().map((set) => [set.id, set.count]),
|
||||
);
|
||||
const out = [];
|
||||
const walk = (collection, depth) => {
|
||||
for (let i = 0; i < (collection?.length || 0); i++) {
|
||||
const primitive = collection.get(i);
|
||||
const setId = primitive?.gevMoverSet;
|
||||
if (typeof setId === 'string') {
|
||||
if (!setId.startsWith(prefix) || !setId.endsWith(':pins')) continue;
|
||||
const count = Math.min(counts.get(setId) ?? 0, primitive.length);
|
||||
for (let j = 0; j < count; j++) {
|
||||
const billboard = primitive.get(j);
|
||||
out.push({
|
||||
set: setId,
|
||||
id: String(billboard.id),
|
||||
show: billboard.show,
|
||||
position: billboard.position,
|
||||
});
|
||||
}
|
||||
} else if (depth < 3 && typeof primitive?.get === 'function')
|
||||
walk(primitive, depth + 1);
|
||||
}
|
||||
};
|
||||
walk(gev.viewer.scene.primitives, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -127,6 +176,9 @@ const stats = (page) =>
|
||||
);
|
||||
const sources = (page) =>
|
||||
page.evaluate(`(${ghostSources})(${JSON.stringify(`${LAYER_ID}:`)})`);
|
||||
// Installs ghostPins() in the page (the evaluate callbacks below call it).
|
||||
const installPins = (page) =>
|
||||
page.evaluate(`window.__qaGhostPins = ${ghostPins}`);
|
||||
|
||||
async function showRow(page) {
|
||||
await page.evaluate((id) => {
|
||||
@@ -298,14 +350,12 @@ async function main() {
|
||||
);
|
||||
|
||||
// ---- (ii) PICK ----------------------------------------------------------
|
||||
await installPins(page);
|
||||
const target = await page.evaluate((id) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const ds = [...Array(gev.viewer.dataSources.length).keys()]
|
||||
.map((i) => gev.viewer.dataSources.get(i))
|
||||
.find((item) => item.name === `${id}:cip`);
|
||||
const entity = ds.entities.values.find((e) => e.billboard);
|
||||
const pin = window.__qaGhostPins(`${id}:cip:`)[0];
|
||||
const carto = gev.viewer.scene.globe.ellipsoid.cartesianToCartographic(
|
||||
entity.position.getValue(gev.viewer.clock.currentTime),
|
||||
pin.position,
|
||||
);
|
||||
try {
|
||||
gev.viewer.camera.cancelFlight();
|
||||
@@ -321,7 +371,7 @@ async function main() {
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
gev.viewer.scene.requestRender?.();
|
||||
return entity.id;
|
||||
return pin.id;
|
||||
}, LAYER_ID);
|
||||
await page
|
||||
.waitForFunction(
|
||||
@@ -333,24 +383,22 @@ async function main() {
|
||||
await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const { viewer: v, engine } = window.__godsEyeView;
|
||||
v.scene.requestRender?.();
|
||||
const c = v.scene.canvas;
|
||||
return v.scene
|
||||
return engine.picker
|
||||
.drillPick({ x: c.clientWidth / 2, y: c.clientHeight / 2 }, 8)
|
||||
.some((p) =>
|
||||
String(p?.id?.id ?? p?.id ?? '').startsWith('ghostmaps:cip:'),
|
||||
);
|
||||
.some((p) => String(p?.id ?? '').startsWith('ghostmaps:cip:'));
|
||||
},
|
||||
{ timeout: 15000, polling: 500 },
|
||||
)
|
||||
.catch(() => {});
|
||||
const probe = await page.evaluate(() => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const { viewer: v, engine } = window.__godsEyeView;
|
||||
const c = v.scene.canvas;
|
||||
const box = c.getBoundingClientRect();
|
||||
const centre = { x: c.clientWidth / 2, y: c.clientHeight / 2 };
|
||||
const picked = v.scene.drillPick(centre, 8);
|
||||
const picked = engine.picker.drillPick(centre, 8);
|
||||
// Empty ground: the first point below the marker (the card is drawn
|
||||
// above it) that picks nothing at all; markers can be dense in a city.
|
||||
let empty = null;
|
||||
@@ -360,14 +408,14 @@ async function main() {
|
||||
x: centre.x + r * Math.cos((a * Math.PI) / 4),
|
||||
y: centre.y + r * Math.sin((a * Math.PI) / 4),
|
||||
};
|
||||
if (!v.scene.pick(p)) {
|
||||
if (engine.picker.pick(p)?.id == null) {
|
||||
empty = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
empty ||= { x: centre.x + 400, y: centre.y + 300 };
|
||||
return {
|
||||
ids: picked.map((p) => String(p?.id?.id ?? p?.id ?? '')),
|
||||
ids: picked.map((p) => String(p?.id ?? '')),
|
||||
centre: { x: centre.x + box.left, y: centre.y + box.top },
|
||||
empty: { x: empty.x + box.left, y: empty.y + box.top },
|
||||
};
|
||||
@@ -462,23 +510,18 @@ async function main() {
|
||||
const norm = (v) => Math.hypot(v.x, v.y, v.z);
|
||||
let far = 0;
|
||||
let nearShown = 0;
|
||||
for (let i = 0; i < gev.viewer.dataSources.length; i++) {
|
||||
const ds = gev.viewer.dataSources.get(i);
|
||||
if (!ds.name?.startsWith(`${id}:`)) continue;
|
||||
for (const e of ds.entities.values) {
|
||||
if (!e.billboard) continue;
|
||||
const p = e.position.getValue(gev.viewer.clock.currentTime);
|
||||
// Angle from the camera's nadir to the pin, against the
|
||||
// geometric horizon angle; the cull's 1 km margin shows pins up
|
||||
// to about 1° past it, so "clearly behind" is beyond 2°.
|
||||
const angle = Math.acos(
|
||||
(p.x * c.x + p.y * c.y + p.z * c.z) / (norm(p) * norm(c)),
|
||||
);
|
||||
const horizon = Math.acos(norm(p) / norm(c));
|
||||
const margin = (2 * Math.PI) / 180;
|
||||
if (e.show && angle > horizon + margin) far++;
|
||||
if (e.show && angle < horizon - margin) nearShown++;
|
||||
}
|
||||
for (const e of window.__qaGhostPins(`${id}:`)) {
|
||||
const p = e.position;
|
||||
// Angle from the camera's nadir to the pin, against the
|
||||
// geometric horizon angle; the cull's 1 km margin shows pins up
|
||||
// to about 1° past it, so "clearly behind" is beyond 2°.
|
||||
const angle = Math.acos(
|
||||
(p.x * c.x + p.y * c.y + p.z * c.z) / (norm(p) * norm(c)),
|
||||
);
|
||||
const horizon = Math.acos(norm(p) / norm(c));
|
||||
const margin = (2 * Math.PI) / 180;
|
||||
if (e.show && angle > horizon + margin) far++;
|
||||
if (e.show && angle < horizon - margin) nearShown++;
|
||||
}
|
||||
return { far, nearShown };
|
||||
},
|
||||
@@ -605,21 +648,13 @@ async function main() {
|
||||
});
|
||||
await boot(push);
|
||||
await enableAndSettle(push);
|
||||
const identity = () =>
|
||||
push.evaluate((id) => {
|
||||
const { dataSources } = window.__godsEyeView.viewer;
|
||||
window.__qaIds ||= new WeakMap();
|
||||
window.__qaNext ||= 1;
|
||||
const out = {};
|
||||
for (let i = 0; i < dataSources.length; i++) {
|
||||
const ds = dataSources.get(i);
|
||||
if (!ds.name?.startsWith(`${id}:`)) continue;
|
||||
if (!window.__qaIds.has(ds))
|
||||
window.__qaIds.set(ds, window.__qaNext++);
|
||||
out[ds.name] = window.__qaIds.get(ds);
|
||||
}
|
||||
return out;
|
||||
}, LAYER_ID);
|
||||
// Each product's drawn edition (its sets' sequence number): a reload
|
||||
// draws a new edition in place of the old one.
|
||||
const identity = async () => {
|
||||
const out = {};
|
||||
for (const row of await sources(push)) out[row.name] = row.edition;
|
||||
return out;
|
||||
};
|
||||
const first = await identity();
|
||||
const pushed = Date.now();
|
||||
await push.evaluate(async (id) => {
|
||||
|
||||
Reference in new issue
Block a user