Merge upstream/main into feat/changelog-fragments

One add/add conflict, in scripts/format-scope.json: upstream's twelve new
entries and this branch's two. Both kept — 866 entries on main, 868 here,
none dropped.

Gates on the result: npm test 5397 passing, build, test:track 109/109,
changelog:check, format:check, check:boundaries, layer-token:check.
This commit is contained in:
daikaginza committed 2026-10-02 16:29:33 +07:00
commit 1d409adf91
201 files changed
+26668 -1976

No files matched your search

+759
View File
@@ -0,0 +1,759 @@
#!/usr/bin/env node
/**
* Build the bundled administrative-boundary packs read by
* `src/data/adminBoundaries.js`:
*
* src/data/local_data/natural_earth/states_provinces.json
* Natural Earth 10m admin-1 states and provinces (public domain).
* src/data/local_data/us_census_counties/counties.json
* US Census Bureau cartographic boundary counties (public domain).
*
* Both sources are pinned by URL and SHA-256, so a rerun reproduces the packs
* byte for byte. Downloads are cached outside the repository (default: the
* system temp directory) and are never committed.
*
* Usage:
* node scripts/build-admin-packs.mjs [--cache <dir>] [--only countries|admin1|counties]
*
* The provenance READMEs beside each pack record these parameters.
*/
import { createHash } from 'node:crypto';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { inflateRawSync } from 'node:zlib';
import { normalizeAdminName } from '../src/data/adminBoundaries.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const NE_COMMIT = 'ca96624a56bd078437bca8184e78163e5039ad19';
const NE_BASE = `https://raw.githubusercontent.com/nvkelso/natural-earth-vector/${NE_COMMIT}/geojson`;
export const SOURCES = Object.freeze({
countries: {
url: `${NE_BASE}/ne_10m_admin_0_countries.geojson`,
sha256: '239eec57ac17f100a11e2536cffc56752c318b50ae765b0918ff7aab4ce8f255',
},
mapUnits: {
url: `${NE_BASE}/ne_10m_admin_0_map_units.geojson`,
sha256: '57da82be755f4afccd8f3b14251bb2752f5df1395f47d2d86f817470c4a48862',
},
admin1: {
url: `${NE_BASE}/ne_10m_admin_1_states_provinces.geojson`,
sha256: '22d0e3ad85eb3e27f17cabf8ba2d50e554fbc27a87796ff891d958185da62fb5',
},
places: {
url: `${NE_BASE}/ne_10m_populated_places_simple.geojson`,
sha256: 'fd3fa867a320cbd5c5b6bb5bc550afeec2939fb2cef688e508007282a55ac42f',
},
counties: {
url: 'https://www2.census.gov/geo/tiger/GENZ2025/shp/cb_2025_us_county_5m.zip',
sha256: 'faec522080681e79be5be435c981009a77891206ff8a7f1d142f3bf5da9ebd74',
},
});
/**
* Pack parameters. The Douglas-Peucker tolerance scales with the unit's size
* (`toleranceFactor` × √area, clamped to `[minToleranceDeg, maxToleranceDeg]`)
* because a unit is viewed at a zoom that fits it: Texas keeps ~2 km detail,
* Bavaria ~1 km, Travis County ~150 m. Parts smaller than `minPartKm2` or than
* `minPartShare` of the largest part are dropped (the largest part always
* stays); holes under `minHoleKm2` are dropped.
*/
export const PARAMS = Object.freeze({
countries: {
toleranceFactor: 0.004,
minToleranceDeg: 0.005,
maxToleranceDeg: 0.02,
decimals: 3,
minPartKm2: 20,
minPartShare: 0.00003,
minHoleKm2: 20,
},
admin1: {
toleranceFactor: 0.004,
minToleranceDeg: 0.005,
maxToleranceDeg: 0.02,
decimals: 3,
minPartKm2: 20,
minPartShare: 0.0003,
minHoleKm2: 20,
},
counties: {
toleranceFactor: 0.003,
minToleranceDeg: 0.0005,
maxToleranceDeg: 0.01,
decimals: 4,
minPartKm2: 1,
minPartShare: 0.001,
minHoleKm2: 1,
},
// A bare admin-1 name is marked ambiguous — the resolver then lets a
// geocoder decide — when it is also a country, the name of a city of at
// least `insideMinPopulation` inside the unit's box ("New York", "Madrid"),
// or the name of a city of at least `elsewhereMinPopulation` outside it
// ("Santa Fe", "Victoria"). A unit no larger than `cityStateMaxKm2` whose
// own city lies inside is the city itself (Berlin, Vienna, Paris): it is
// marked `cityState` and may be named alone however small.
ambiguity: {
insideMinPopulation: 200_000,
elsewhereMinPopulation: 50_000,
cityStateMaxKm2: 1_000,
},
});
const args = process.argv.slice(2);
const cacheDir = path.resolve(
args.includes('--cache')
? args[args.indexOf('--cache') + 1]
: path.join(os.tmpdir(), 'gev-admin-packs'),
);
const only = args.includes('--only') ? args[args.indexOf('--only') + 1] : null;
async function fetchPinned(name) {
const { url, sha256 } = SOURCES[name];
const file = path.join(cacheDir, path.basename(new URL(url).pathname));
let bytes = await readFile(file).catch(() => null);
if (!bytes) {
console.log(`Downloading ${url}`);
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
bytes = Buffer.from(await response.arrayBuffer());
await mkdir(cacheDir, { recursive: true });
await writeFile(file, bytes);
}
const digest = createHash('sha256').update(bytes).digest('hex');
if (digest !== sha256)
throw new Error(
`${name}: SHA-256 ${digest} does not match the pinned ${sha256}`,
);
return { bytes, digest };
}
// ── geometry ────────────────────────────────────────────────────────────
const EARTH_RADIUS_KM = 6371;
const toRad = (d) => (d * Math.PI) / 180;
/** Spherical ring area (km²); matches src/data/naturalEarthRegions.js. */
function ringAreaKm2(ring) {
const n = ring.length;
if (n < 3) return 0;
let sum = 0;
for (let i = 0; i < n; i++) {
const [lon1, lat1] = ring[i];
const [lon2, lat2] = ring[(i + 1) % n];
sum +=
toRad(lon2 - lon1) * (2 + Math.sin(toRad(lat1)) + Math.sin(toRad(lat2)));
}
return Math.abs((sum * EARTH_RADIUS_KM * EARTH_RADIUS_KM) / 2);
}
/** Signed planar (shoelace) area: positive counter-clockwise in lon/lat. */
function signedArea(ring) {
let sum = 0;
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++)
sum += ring[j][0] * ring[i][1] - ring[i][0] * ring[j][1];
return sum / 2;
}
function segDist(p, a, b) {
let [x, y] = p;
const [x1, y1] = a;
const [x2, y2] = b;
const dx = x2 - x1;
const dy = y2 - y1;
if (dx !== 0 || dy !== 0) {
const t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy);
if (t > 1) {
x -= x2;
y -= y2;
return Math.hypot(x, y);
}
if (t > 0) {
x -= x1 + dx * t;
y -= y1 + dy * t;
return Math.hypot(x, y);
}
}
return Math.hypot(x - x1, y - y1);
}
/** Iterative Douglas-Peucker over an open point list. */
function douglasPeucker(points, tolerance) {
if (points.length <= 2) return points.slice();
const keep = new Uint8Array(points.length);
keep[0] = keep[points.length - 1] = 1;
const stack = [[0, points.length - 1]];
while (stack.length) {
const [first, last] = stack.pop();
let maxDist = 0;
let index = -1;
for (let i = first + 1; i < last; i++) {
const d = segDist(points[i], points[first], points[last]);
if (d > maxDist) {
maxDist = d;
index = i;
}
}
if (maxDist > tolerance && index !== -1) {
keep[index] = 1;
stack.push([first, index], [index, last]);
}
}
return points.filter((_, i) => keep[i]);
}
/**
* Simplify a closed ring to an OPEN rounded ring. The ring is split at its
* farthest vertex from the start so Douglas-Peucker has two anchored halves
* (a closed ring has no chord to measure against). Returns null when fewer
* than four distinct vertices survive.
*/
function simplifyRing(ring, tolerance, decimals) {
let open = ring.slice();
const first = open[0];
const last = open[open.length - 1];
if (open.length > 1 && first[0] === last[0] && first[1] === last[1])
open = open.slice(0, -1);
if (open.length < 4) return null;
let far = 1;
let farDist = -1;
for (let i = 1; i < open.length; i++) {
const d = Math.hypot(open[i][0] - first[0], open[i][1] - first[1]);
if (d > farDist) {
farDist = d;
far = i;
}
}
const a = douglasPeucker(open.slice(0, far + 1), tolerance);
const b = douglasPeucker([...open.slice(far), first], tolerance);
const simplified = [...a, ...b.slice(1, -1)];
const factor = 10 ** decimals;
const out = [];
for (const [lon, lat] of simplified) {
const p = [
Math.round(lon * factor) / factor,
Math.round(lat * factor) / factor,
];
const prev = out[out.length - 1];
if (prev && prev[0] === p[0] && prev[1] === p[1]) continue;
out.push(p);
}
while (
out.length > 1 &&
out[0][0] === out[out.length - 1][0] &&
out[0][1] === out[out.length - 1][1]
)
out.pop();
return out.length >= 4 ? out : null;
}
/**
* Simplify polygons ([outer, ...holes][]): drop parts and holes under the
* area floors (the largest part always stays), orient outers
* counter-clockwise and holes clockwise, sort parts by area descending.
*/
function simplifyPolygons(polygons, params, tolerance, decimals) {
const parts = [];
for (const poly of polygons) {
const outer = simplifyRing(poly[0], tolerance, decimals);
if (!outer) continue;
const area = ringAreaKm2(outer);
if (signedArea(outer) < 0) outer.reverse();
const holes = [];
for (const hole of poly.slice(1)) {
const ring = simplifyRing(hole, tolerance, decimals);
if (!ring || ringAreaKm2(ring) < params.minHoleKm2) continue;
if (signedArea(ring) > 0) ring.reverse();
holes.push(ring);
}
parts.push({ rings: [outer, ...holes], area });
}
parts.sort((x, y) => y.area - x.area);
const floor = Math.max(
params.minPartKm2,
(parts[0]?.area || 0) * params.minPartShare,
);
return parts
.filter((part, index) => index === 0 || part.area >= floor)
.map((part) => part.rings);
}
/**
* Simplify one unit at its size-scaled tolerance. A unit too small to
* survive the pack precision (a Maldives atoll) is kept at one more decimal
* and a fifth of the minimum tolerance, or failing that unsimplified at two
* more decimals; the caller records `d`.
*/
function simplifyUnit(polygons, params) {
const sourceArea = polygons.reduce(
(sum, poly) => sum + ringAreaKm2(poly[0]),
0,
);
const tolerance = Math.min(
params.maxToleranceDeg,
Math.max(
params.minToleranceDeg,
(params.toleranceFactor * Math.sqrt(sourceArea)) / 111.32,
),
);
const coarse = simplifyPolygons(polygons, params, tolerance, params.decimals);
if (coarse.length) return { polygons: coarse, decimals: params.decimals };
const fine = simplifyPolygons(
polygons,
params,
params.minToleranceDeg / 5,
params.decimals + 1,
);
if (fine.length) return { polygons: fine, decimals: params.decimals + 1 };
// Smaller still (Vatican City): the source ring as drawn.
const exact = simplifyPolygons(polygons, params, 0, params.decimals + 2);
return { polygons: exact, decimals: params.decimals + 2 };
}
/**
* Encode a rounded open ring as integers in units of 10^-decimals degrees:
* the first vertex absolute, every later vertex as a delta from the previous
* one — [lon0, lat0, dLon1, dLat1, …]. Decoded by `decodeRing` in
* src/data/adminBoundaries.js.
*/
function encodeRing(ring, decimals) {
const factor = 10 ** decimals;
const out = [];
let px = 0;
let py = 0;
for (const [lon, lat] of ring) {
const x = Math.round(lon * factor);
const y = Math.round(lat * factor);
out.push(x - px, y - py);
px = x;
py = y;
}
return out;
}
function encodeUnit({ polygons, decimals }) {
return polygons.map((poly) => poly.map((ring) => encodeRing(ring, decimals)));
}
function geojsonPolygons(geometry) {
if (geometry?.type === 'Polygon') return [geometry.coordinates];
if (geometry?.type === 'MultiPolygon') return geometry.coordinates;
return [];
}
function bboxOf(polygons) {
let w = Infinity;
let s = Infinity;
let e = -Infinity;
let n = -Infinity;
for (const poly of polygons)
for (const [lon, lat] of poly[0]) {
if (lon < w) w = lon;
if (lon > e) e = lon;
if (lat < s) s = lat;
if (lat > n) n = lat;
}
return [w, s, e, n];
}
// ── zip / shapefile / dbf ───────────────────────────────────────────────
/** Read named entries from a zip archive (stored or deflated). */
function unzip(buffer) {
let eocd = buffer.length - 22;
while (eocd >= 0 && buffer.readUInt32LE(eocd) !== 0x06054b50) eocd--;
if (eocd < 0) throw new Error('zip: end of central directory not found');
const count = buffer.readUInt16LE(eocd + 10);
let offset = buffer.readUInt32LE(eocd + 16);
const files = new Map();
for (let i = 0; i < count; i++) {
if (buffer.readUInt32LE(offset) !== 0x02014b50)
throw new Error('zip: bad central directory entry');
const method = buffer.readUInt16LE(offset + 10);
const compressed = buffer.readUInt32LE(offset + 20);
const nameLength = buffer.readUInt16LE(offset + 28);
const extraLength = buffer.readUInt16LE(offset + 30);
const commentLength = buffer.readUInt16LE(offset + 32);
const local = buffer.readUInt32LE(offset + 42);
const name = buffer.toString('utf8', offset + 46, offset + 46 + nameLength);
const dataStart =
local +
30 +
buffer.readUInt16LE(local + 26) +
buffer.readUInt16LE(local + 28);
const data = buffer.subarray(dataStart, dataStart + compressed);
if (method === 0) files.set(name, data);
else if (method === 8) files.set(name, inflateRawSync(data));
else throw new Error(`zip: unsupported method ${method} for ${name}`);
offset += 46 + nameLength + extraLength + commentLength;
}
return files;
}
/** Polygon records (shape type 5) as arrays of closed [lon, lat] rings. */
function readShp(buffer) {
const shapes = [];
let offset = 100;
while (offset < buffer.length) {
const length = buffer.readInt32BE(offset + 4) * 2;
const content = offset + 8;
const type = buffer.readInt32LE(content);
const rings = [];
if (type === 5) {
const numParts = buffer.readInt32LE(content + 36);
const numPoints = buffer.readInt32LE(content + 40);
const partsAt = content + 44;
const pointsAt = partsAt + numParts * 4;
for (let p = 0; p < numParts; p++) {
const start = buffer.readInt32LE(partsAt + p * 4);
const end =
p + 1 < numParts
? buffer.readInt32LE(partsAt + (p + 1) * 4)
: numPoints;
const ring = [];
for (let i = start; i < end; i++)
ring.push([
buffer.readDoubleLE(pointsAt + i * 16),
buffer.readDoubleLE(pointsAt + i * 16 + 8),
]);
rings.push(ring);
}
} else if (type !== 0) {
throw new Error(`shp: unsupported shape type ${type}`);
}
shapes.push(rings);
offset = content + length;
}
return shapes;
}
/** dBASE records as plain objects (character and numeric fields). */
function readDbf(buffer) {
const count = buffer.readUInt32LE(4);
const headerLength = buffer.readUInt16LE(8);
const recordLength = buffer.readUInt16LE(10);
const fields = [];
for (let at = 32; buffer[at] !== 0x0d; at += 32) {
fields.push({
name: buffer.toString('latin1', at, at + 11).replace(/\0.*$/, ''),
length: buffer[at + 16],
});
}
const records = [];
for (let r = 0; r < count; r++) {
let at = headerLength + r * recordLength + 1;
const record = {};
for (const field of fields) {
record[field.name] = buffer
.toString('utf8', at, at + field.length)
.trim();
at += field.length;
}
records.push(record);
}
return records;
}
/** Group shapefile rings into polygons: clockwise outers, holes assigned by containment. */
function shpPolygons(rings) {
const outers = [];
const holes = [];
for (const ring of rings) {
if (ring.length < 4) continue;
(signedArea(ring) < 0 ? outers : holes).push(ring);
}
const polygons = outers.map((outer) => [outer]);
for (const hole of holes) {
const [x, y] = hole[0];
const owner = polygons.find(([outer]) => pointInRing(outer, x, y));
if (owner) owner.push(hole);
else polygons.push([hole.slice().reverse()]);
}
return polygons;
}
function pointInRing(ring, x, y) {
let inside = false;
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
const [xi, yi] = ring[i];
const [xj, yj] = ring[j];
if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi)
inside = !inside;
}
return inside;
}
// ── packs ────────────────────────────────────────────────────────────────
const LATIN = /^[\p{Script=Latin}\p{N}\s.,'’()\-/&]+$/u;
function uniqueNames(values, exclude) {
const seen = new Set(exclude.map(normalizeAdminName));
const out = [];
for (const value of values) {
const text = String(value || '').trim();
const key = normalizeAdminName(text);
if (!text || !key || seen.has(key) || !LATIN.test(text)) continue;
seen.add(key);
out.push(text);
}
return out;
}
async function buildCountries() {
const source = await fetchPinned('countries');
const mapUnits = await fetchPinned('mapUnits');
const params = PARAMS.countries;
const features = [];
for (const { properties: p, geometry } of JSON.parse(source.bytes).features) {
const unit = simplifyUnit(geojsonPolygons(geometry), params);
if (!unit.polygons.length) continue;
const name = p.ADMIN || p.NAME_EN || p.NAME;
features.push({
name,
alt: uniqueNames(
[p.NAME, p.NAME_LONG, p.NAME_EN, p.FORMAL_EN, p.BRK_NAME].filter(
Boolean,
),
[name],
),
country: name,
iso: p.ADM0_A3,
iso2: p.ISO_A2_EH || p.ISO_A2,
type: 'Country',
...(unit.decimals !== params.decimals ? { d: unit.decimals } : {}),
polygons: encodeUnit(unit),
});
}
for (const { properties: p, geometry } of JSON.parse(mapUnits.bytes)
.features) {
if (p.ADM0_A3 !== 'GBR') continue;
const unit = simplifyUnit(geojsonPolygons(geometry), params);
features.push({
name: p.GEOUNIT,
country: p.ADMIN,
iso: p.GU_A3,
iso2: 'GB',
type: 'Constituent country',
...(unit.decimals !== params.decimals ? { d: unit.decimals } : {}),
polygons: encodeUnit(unit),
});
}
features.sort((a, b) => a.name.localeCompare(b.name));
return {
meta: {
title: 'Natural Earth admin-0 countries',
mapUnits: SOURCES.mapUnits,
source: 'ne_10m_admin_0_countries',
url: SOURCES.countries.url,
sha256: source.digest,
commit: NE_COMMIT,
license:
'Public domain (https://www.naturalearthdata.com/about/terms-of-use/)',
decimals: params.decimals,
curation: params,
script: 'scripts/build-admin-packs.mjs',
},
features,
};
}
async function buildAdmin1() {
const params = PARAMS.admin1;
const source = await fetchPinned('admin1');
const places = await fetchPinned('places');
const collection = JSON.parse(source.bytes.toString('utf8'));
const { ambiguity } = PARAMS;
const cities = JSON.parse(places.bytes.toString('utf8'))
.features.map((f) => f.properties)
.filter((p) => p.pop_max >= ambiguity.elsewhereMinPopulation)
.map((p) => ({
keys: new Set([p.name, p.nameascii].map(normalizeAdminName)),
lon: p.longitude,
lat: p.latitude,
population: p.pop_max,
country: p.adm0name,
}));
const countryNames = new Set();
for (const { properties: p } of collection.features)
for (const name of [p.admin, p.geonunit])
if (name) countryNames.add(normalizeAdminName(name));
const features = [];
for (const { properties: p, geometry } of collection.features) {
const name = String(p.name || '').trim();
if (!name) continue;
const unit = simplifyUnit(geojsonPolygons(geometry), params);
if (!unit.polygons.length) continue;
const nameEn =
p.name_en && normalizeAdminName(p.name_en) !== normalizeAdminName(name)
? String(p.name_en).trim()
: null;
const alt = uniqueNames(
String(p.name_alt || '').split('|'),
[name, nameEn].filter(Boolean),
);
const bbox = bboxOf(unit.polygons);
const areaKm2 = unit.polygons.reduce(
(sum, poly) => sum + ringAreaKm2(poly[0]),
0,
);
const keys = new Set(
[name, nameEn, ...alt].filter(Boolean).map(normalizeAdminName),
);
const amb = [];
const ambAbroad = [];
let cityState = false;
const inBox = (c) =>
c.lon >= bbox[0] &&
c.lon <= bbox[2] &&
c.lat >= bbox[1] &&
c.lat <= bbox[3];
for (const key of keys) {
const namesakes = cities.filter((c) => c.keys.has(key));
const ownCity = namesakes.some(
(c) => inBox(c) && c.population >= ambiguity.insideMinPopulation,
);
const cityInside = ownCity && areaKm2 > ambiguity.cityStateMaxKm2;
if (ownCity && !cityInside) cityState = true;
const elsewhere = namesakes.filter((c) => !inBox(c));
if (
countryNames.has(key) ||
cityInside ||
elsewhere.some((c) => c.country === p.admin)
)
amb.push(key);
// Only abroad: "Victoria, Australia" settles it; "Victoria" alone not.
else if (elsewhere.length) ambAbroad.push(key);
}
const postal = String(p.postal || '').trim();
features.push({
name,
...(nameEn ? { nameEn } : {}),
...(alt.length ? { alt } : {}),
...(/^[A-Z]{2}$/.test(postal) ? { postal } : {}),
type: p.type_en || p.type || null,
...(Number.isFinite(p.min_label) ? { rank: p.min_label } : {}),
country: p.admin,
iso2: p.iso_a2 && p.iso_a2 !== '-99' ? p.iso_a2 : null,
iso: p.iso_3166_2 && !p.iso_3166_2.includes('~') ? p.iso_3166_2 : null,
...(amb.length ? { amb: amb.sort() } : {}),
...(ambAbroad.length ? { ambAbroad: ambAbroad.sort() } : {}),
...(cityState ? { cityState } : {}),
...(Number.isFinite(p.longitude) && Number.isFinite(p.latitude)
? {
label: [
Math.round(p.longitude * 1000) / 1000,
Math.round(p.latitude * 1000) / 1000,
],
}
: {}),
...(unit.decimals !== params.decimals ? { d: unit.decimals } : {}),
polygons: encodeUnit(unit),
});
}
features.sort(
(a, b) =>
a.country.localeCompare(b.country) ||
a.name.localeCompare(b.name) ||
(a.iso || '').localeCompare(b.iso || ''),
);
return {
meta: {
title: 'Natural Earth admin-1 states and provinces',
source: 'ne_10m_admin_1_states_provinces',
url: SOURCES.admin1.url,
sha256: source.digest,
ambiguitySource: SOURCES.places.url,
ambiguitySha256: places.digest,
commit: NE_COMMIT,
license:
'Public domain (https://www.naturalearthdata.com/about/terms-of-use/)',
schema:
'features[]: name, nameEn?, alt?[], postal?, type, rank? (min_label: the zoom at which Natural Earth labels the unit; lower is more prominent), country, iso2, iso (ISO 3166-2), amb?[] (normalized names that are also a country or a city — alone or with the country named), ambAbroad?[] (names shared only with a city in another country — ambiguous alone, settled by naming the country), cityState? (the unit is its own city), label? [lon, lat] label point, d? (decimals when not meta.decimals), polygons[][ring] (first ring outer, rest holes; each ring open and encoded as integers in 10^-d degrees, first vertex absolute then [dLon, dLat] deltas)',
decimals: params.decimals,
curation: { ...params, ambiguity },
script: 'scripts/build-admin-packs.mjs',
},
features,
};
}
async function buildCounties() {
const params = PARAMS.counties;
const source = await fetchPinned('counties');
const files = unzip(source.bytes);
const base = path.basename(new URL(SOURCES.counties.url).pathname, '.zip');
const shapes = readShp(files.get(`${base}.shp`));
const records = readDbf(files.get(`${base}.dbf`));
if (shapes.length !== records.length)
throw new Error('counties: shp/dbf record counts differ');
const features = [];
records.forEach((record, index) => {
const unit = simplifyUnit(shpPolygons(shapes[index]), params);
if (!unit.polygons.length) return;
const lsad = record.NAMELSAD.startsWith(record.NAME)
? record.NAMELSAD.slice(record.NAME.length).trim()
: '';
features.push({
name: record.NAME,
full: record.NAMELSAD,
...(lsad ? { lsad } : {}),
state: record.STATE_NAME,
st: record.STUSPS,
geoid: record.GEOID,
...(unit.decimals !== params.decimals ? { d: unit.decimals } : {}),
polygons: encodeUnit(unit),
});
});
features.sort((a, b) => a.geoid.localeCompare(b.geoid));
return {
meta: {
title: 'US Census Bureau cartographic boundary counties',
source: base,
url: SOURCES.counties.url,
sha256: source.digest,
license: 'Public domain (U.S. Government work, 17 U.S.C. § 105)',
schema:
'features[]: name, full (NAMELSAD), lsad? (County, Parish, Borough, …), state, st (USPS), geoid, d? (decimals when not meta.decimals), polygons[][ring] (first ring outer, rest holes; each ring open and encoded as integers in 10^-d degrees, first vertex absolute then [dLon, dLat] deltas)',
decimals: params.decimals,
curation: params,
script: 'scripts/build-admin-packs.mjs',
},
features,
};
}
async function writePack(relative, pack) {
const file = path.join(ROOT, relative);
await mkdir(path.dirname(file), { recursive: true });
const text = JSON.stringify(pack) + '\n';
await writeFile(file, text);
const parts = pack.features.reduce((n, f) => n + f.polygons.length, 0);
const vertices = pack.features.reduce(
(n, f) => n + f.polygons.flat().reduce((m, ring) => m + ring.length / 2, 0),
0,
);
console.log(
`${relative}: ${pack.features.length} features, ${parts} parts, ${vertices} vertices, ${Buffer.byteLength(text)} bytes`,
);
}
if (!only || only === 'countries')
await writePack(
'src/data/local_data/natural_earth/countries.json',
await buildCountries(),
);
if (!only || only === 'admin1')
await writePack(
'src/data/local_data/natural_earth/states_provinces.json',
await buildAdmin1(),
);
if (!only || only === 'counties')
await writePack(
'src/data/local_data/us_census_counties/counties.json',
await buildCounties(),
);
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Maintainer-only Overture military-name pack build (DuckDB 1.5.5)."""
import argparse
import gzip
import hashlib
import json
import math
from pathlib import Path
import re
ROOT = Path(__file__).resolve().parent.parent
RELEASE = '2026-09-23.1'
CLASSES = ['military_land', 'airfield', 'naval_base', 'range', 'barracks', 'base', 'training_area']
PACK = ROOT / 'src/data/local_data/osm_military_names'
def assemble(rows):
records = []
snapshots = set()
for row in rows:
source = next(s for s in row['sources'] if s['dataset'] == 'OpenStreetMap')
match = re.fullmatch(r'([nwr])(\d+)@\d+', source['record_id'])
if not match:
raise ValueError('Invalid typed OSM identity')
ident = match[1] + match[2]
bbox = [row['bbox'][k] for k in ['xmin', 'ymin', 'xmax', 'ymax']]
lon, lat, area = row['lon'], row['lat'], row['area']
if not all(math.isfinite(v) for v in [lon, lat, area, *bbox]):
raise ValueError('Invalid geometry measurement')
if not bbox[0] - 1e-7 <= lon <= bbox[2] + 1e-7 or not bbox[1] - 1e-7 <= lat <= bbox[3] + 1e-7:
raise ValueError('Label outside source bounds')
kind = row['military'] if row['military'] in CLASSES else 'military_land'
records.append([ident, row['name'], round(lon, 5), round(lat, 5),
*[round(v, 4) for v in bbox], CLASSES.index(kind), max(1, round(abs(area)))])
snapshots.add(source['version'][:10])
records.sort(key=lambda r: (r[0][0], int(r[0][1:])))
if len(set(r[0] for r in records)) != len(records):
raise ValueError('Duplicate OSM identity')
return {'release': RELEASE, 'snapshots': sorted(snapshots), 'classes': CLASSES, 'records': records}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--cache', type=Path, required=True)
parser.add_argument('--extensions', type=Path)
parser.add_argument('--verify', action='store_true')
args = parser.parse_args()
args.cache.mkdir(parents=True, exist_ok=True)
cached = args.cache / ('military-names-' + RELEASE + '.json')
if not cached.exists():
import duckdb
if duckdb.__version__ != '1.5.5':
raise RuntimeError('Use duckdb==1.5.5')
config = {'threads': '8', 'memory_limit': '3GB', 'temp_directory': str(args.cache / 'temp')}
if args.extensions:
config['extension_directory'] = str(args.extensions)
connection = duckdb.connect(config=config)
if not args.extensions:
connection.execute('INSTALL httpfs; INSTALL spatial;')
connection.execute('LOAD httpfs; LOAD spatial; SET geometry_always_xy=true; SET enable_progress_bar=false;')
files = json.loads((ROOT / 'scripts/military-names-files.json').read_text())
if not all('/' + RELEASE + '/' in url for url in files):
raise ValueError('Unexpected source release')
cursor = connection.execute((ROOT / 'scripts/military-names.sql').read_text(), [files])
fields = [d[0] for d in cursor.description]
rows = [dict(zip(fields, row)) for row in cursor.fetchall()]
cached.write_text(json.dumps(rows, ensure_ascii=False, separators=(',', ':')))
pack = assemble(json.loads(cached.read_text()))
raw = json.dumps(pack, ensure_ascii=False, separators=(',', ':')).encode()
digest = hashlib.sha256(raw).hexdigest()
if args.verify:
expected = (PACK / 'names.sha256').read_text().split()[0]
if digest != expected:
raise ValueError(f'Output SHA-256 mismatch: {digest}')
else:
PACK.mkdir(parents=True, exist_ok=True)
(PACK / 'names.json').write_bytes(raw)
(PACK / 'names.sha256').write_text(digest + ' names.json\n')
print(json.dumps({'records': len(pack['records']), 'rawBytes': len(raw),
'gzipBytes': len(gzip.compress(raw, compresslevel=9, mtime=0)), 'sha256': digest}))
if __name__ == '__main__':
main()
+20
View File
@@ -26,11 +26,17 @@
"src/annotations/drawMode.test.mjs",
"src/annotations/drawTool.js",
"src/annotations/drawTool.test.mjs",
"src/annotations/adminOutline.test.mjs",
"src/annotations/regionRing.test.mjs",
"src/data/adminBoundaries.test.mjs",
"scripts/build-admin-packs.mjs",
"scripts/qa-admin-outlines.mjs",
"src/data/inputOwnership.test.mjs",
"docs/CODE-BOUNDARIES.md",
"src/main.js",
"docs/APPLICATION.md",
"scripts/qa-application.mjs",
"scripts/qa-terrain-429.mjs",
"src/app/application.js",
"src/app/application.test.mjs",
"src/app/mapLifecycle.test.mjs",
@@ -581,6 +587,8 @@
"src/maps/imagery.js",
"src/maps/registry.js",
"src/maps/terrain.js",
"src/maps/terrainRetry.js",
"src/maps/terrainRetry.test.mjs",
"src/mapStackController.js",
"src/mapStartup.js",
"src/mapStackChips.test.mjs",
@@ -845,6 +853,18 @@
"src/ui/recentImagery.test.mjs",
"src/layers/recentImagery/testDoubles.mjs",
"scripts/qa-recent-imagery.mjs",
"scripts/qa-overpass-offload.mjs",
"src/overpassOffload.test.mjs",
"src/sources/openFreeMap.test.mjs",
"src/sources/vectorTiles.test.mjs",
"src/sources/militaryTileGeometry.test.mjs",
"src/layers/traffic/surface.test.mjs",
"src/layers/traffic/roadModes.test.mjs",
"scripts/qa-traffic-motion.mjs",
"src/layers/traffic/motion.test.mjs",
"scripts/qa-journey-recorder.mjs",
"scripts/qa-alpr-journey.mjs",
"scripts/qa-context-arrivals.mjs",
"scripts/assemble-changelog.mjs",
"src/tooling/changelogFragments.test.mjs"
]
+66
View File
@@ -0,0 +1,66 @@
[
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00000-18dcb239-f29e-5e57-b36f-4bd46c12fb86-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00001-4a0ec645-f2e2-5fd1-aeaf-a2210290fe76-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00002-0bfb9ec4-5947-583a-9b20-5b39a6635e7d-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00003-0aa711cc-b475-5914-9670-7295640f184f-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00004-9a1af7ec-89dd-5aa1-a419-d6325719c330-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00005-e85508c1-3f80-5883-8539-35302da710f3-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00006-184886ec-67b4-5a65-852f-f26f757aaeea-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00007-adbd870a-4116-50a2-9554-57064af19a18-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00008-6362555a-8b0c-5589-a6fe-b71ddca82083-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00009-19bb41f3-6c3b-579a-adae-8f69030c37c7-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00010-0e372a9c-a60d-5b06-89e2-866989d612dd-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00011-2ae04990-7b00-5def-bf72-12b2fdc13b48-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00012-dcfe8859-26a4-544b-ad61-dfbc7e7f272a-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00013-a369ed98-5e3b-5bda-94c9-460f06e8746b-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00014-d03d3cde-6ae3-5938-abe9-b2018f7ab0e3-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00015-57a736a1-7d58-5851-877a-2fca7de484ba-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00016-141b8ad5-f6d4-5f5e-8353-450a53188e27-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00017-14133e77-297b-57f3-b028-c5133bb56578-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00018-65485e6e-8939-51ab-8566-47dd3887b6c3-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00019-64fb52b8-5ba4-5f00-860f-4fe71e1c9f3a-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00020-0feea924-6adb-5118-9a2b-e0a0d3ef6963-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00021-a9115b0c-a202-5468-bfae-0a1e6f953be3-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00022-440ee3b6-f6ae-5bf9-b5d1-cc81fcb0a430-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00023-53a4d462-62b9-589c-9650-f7c9652354e7-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00024-54089af7-32f4-5ad8-8970-cea7350cbb20-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00025-59cc63a7-e7da-58e2-8d06-1df15f3a0636-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00026-ba0e8b28-756c-5b5e-aa01-0939a15e45c8-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00027-8237ebb3-2fcb-50d7-884b-81c8e81bda1e-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00028-6fb209d3-9fbe-5155-9983-896b1bb9e84e-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00029-e86227ec-f5f7-51c6-b537-a10334796cc4-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00030-a2779b0d-003a-5f5d-812c-02eb4a5db71f-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00031-554320d6-a62d-5be7-80b0-d32fb67d87ca-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00032-a49d2756-469a-571f-86ef-be60690cfaea-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00033-0b36146b-6963-5133-8a67-ced0bbb37d6a-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00034-b676aef2-1e90-5966-8531-40f42c5d01d6-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00035-c9b04728-fc59-50f2-bfa8-5f8692ea731e-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00036-22a0fbd7-fd24-5832-ac6c-85e82b56ced6-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00037-d157ddb7-8962-515b-9ec3-d54333fcf836-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00038-39d77c44-55f2-5792-83e1-cac6416d0630-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00039-f6871cf2-753b-53f1-aacf-645ea035826d-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00040-3d142e7a-4189-5e50-9f68-0f77e9a51047-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00041-9e6d1cdd-f130-5089-9d0f-703626552100-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00042-4cefe8fe-4966-5e1e-9cf5-19fd90384db8-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00043-65360671-af63-58dd-9784-bceb07abe219-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00044-b423bff1-dd2b-52c4-90f6-cd1605e4d83f-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00045-1f4d97be-99fc-50de-b9cb-f3624c9a1817-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00046-32a9756a-e3a0-5980-a8d9-88dd475f25b9-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00047-cf80d668-e9f1-5f69-8e21-0a73f1732e5f-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00048-8d932283-4f3f-533d-895b-684faaa9cc34-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00049-996168f3-0c3a-5b58-8444-168c7d859a04-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00050-f9c3c526-3ea5-5dab-a46d-05c623c3e905-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00051-72b4d98d-4b3b-5b3b-a565-62c206d4fa29-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00052-7385fa90-2fa9-53c8-9ed4-96d5703f3a37-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00053-96804709-94fa-56d2-bafd-69b2cbce3881-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00054-520c3c50-82f8-5f3d-97b5-fa8f5f4e65e7-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00055-8742f073-69cd-579e-bf00-b16b6e6a7669-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00056-58083e97-7020-5522-ac19-d8a5b4dcbc98-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00057-bf201c73-1fb3-5dbc-8cec-d344d999820e-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00058-9e1561c3-ba15-528b-8db3-62ce04b1570b-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00059-dd8cd7ca-410f-54f8-9b50-66beb004d640-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00060-203c7177-ec4a-55c2-8003-d828b144a6d6-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00061-0da76969-7fb1-56b9-bb83-4e40f3acea8d-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00062-13344b87-db6c-5415-a48e-afd95a928a85-c000.zstd.parquet",
"https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-09-23.1/theme=base/type=land_use/part-00063-a61ab643-210f-518d-b6b8-894f72e5c094-c000.zstd.parquet"
]
+11
View File
@@ -0,0 +1,11 @@
SELECT names.primary AS name, bbox, sources,
source_tags['military'] AS military,
ST_X(ST_PointOnSurface(geometry)) AS lon,
ST_Y(ST_PointOnSurface(geometry)) AS lat,
ST_Area_Spheroid(geometry) AS area
FROM read_parquet(?)
WHERE subtype = 'military'
AND nullif(trim(names.primary), '') IS NOT NULL
AND (source_tags['landuse'] = 'military'
OR source_tags['military'] IN
('airfield', 'naval_base', 'range', 'barracks', 'base', 'training_area'))
+145 -21
View File
@@ -9,7 +9,8 @@
"src/data/infrastructure.js",
"src/data/localGeojsonCore.js",
"src/data/localGeojsonLod.js",
"src/data/inputOwnership.js"
"src/data/inputOwnership.js",
"src/data/infrastructureOverlayEntry.js"
],
"external": ["cesium"]
},
@@ -249,7 +250,12 @@
"src/data/regionalModel.js",
"src/nominatimGeocode.js",
"src/sources/httpBody.js",
"src/sources/rateLimit.js"
"src/sources/rateLimit.js",
"src/data/local_data/natural_earth/marine.json",
"src/data/local_data/natural_earth/regions.json",
"src/data/naturalEarthRegions.js",
"src/data/bundledJson.js",
"src/data/retryableLoad.js"
],
"external": []
},
@@ -615,7 +621,8 @@
"src/ui/shellFacade.js",
"src/ui/localSdrControls.js",
"src/layers/localAdsb/feedNames.js",
"src/ui/localSdrPresentation.js"
"src/ui/localSdrPresentation.js",
"src/data/cameraArrival.js"
],
"external": ["cesium"]
},
@@ -719,7 +726,8 @@
"src/director/sharing/lifetime.js",
"src/director/authoring.js",
"src/scenes/sharing.js",
"src/ui/sceneSharing.js"
"src/ui/sceneSharing.js",
"src/data/cameraArrival.js"
],
"external": ["cesium"]
},
@@ -963,6 +971,7 @@
"installations-layer": {
"exports": ["./layers/installations"],
"modules": [
"src/data/cameraSensitivity.js",
"src/data/installationFeedback.js",
"src/data/militaryInstallationData.js",
"src/layers/installations/controls.js",
@@ -976,9 +985,23 @@
"src/layers/installations/source.js",
"src/layers/installations/state.js",
"src/layers/installations/viewport.js",
"src/data/inputOwnership.js"
"src/data/inputOwnership.js",
"src/data/tomtomTiles.js",
"src/sources/capability.js",
"src/sources/httpBody.js",
"src/sources/openFreeMap.js",
"src/sources/militaryTileGeometry.js",
"src/sources/vectorTiles.js",
"src/sources/phaseTiming.js",
"src/data/militaryNames.js",
"src/sources/requestDeadline.js",
"src/data/local_data/osm_military_names/names.json",
"src/data/bundledJson.js",
"src/data/retryableLoad.js",
"src/layers/installations/namedMarkers.js",
"src/data/infrastructureOverlayEntry.js"
],
"external": ["cesium"]
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
},
"awareness-layer": {
"exports": ["./layers/awareness"],
@@ -1022,18 +1045,29 @@
"src/layers/traffic/flow.js",
"src/layers/traffic/flowDecode.js",
"src/layers/traffic/flowSource.js",
"src/layers/traffic/footprint.js",
"src/layers/traffic/index.js",
"src/layers/traffic/ingestion.js",
"src/sources/requestDeadline.js",
"src/layers/traffic/lifecycle.js",
"src/layers/traffic/model.js",
"src/layers/traffic/policy.js",
"src/layers/traffic/rendering.js",
"src/layers/traffic/retention.js",
"src/layers/traffic/source.js",
"src/layers/traffic/state.js",
"src/layers/traffic/style.js",
"src/layers/traffic/surface.js",
"src/layers/traffic/timing.js",
"src/layers/traffic/viewport.js",
"src/sources/overpassRoads.js"
"src/sources/overpassRoads.js",
"src/sources/capability.js",
"src/sources/httpBody.js",
"src/sources/openFreeMap.js",
"src/sources/militaryTileGeometry.js",
"src/sources/vectorTiles.js",
"src/sources/phaseTiming.js",
"src/layers/traffic/roadModes.js"
],
"external": ["cesium", "pbf", "@mapbox/vector-tile"]
},
@@ -1111,9 +1145,16 @@
"src/layers/alpr/overlay.js",
"src/layers/alpr/source.js",
"src/data/inputOwnership.js",
"src/layers/alpr/records.js"
"src/layers/alpr/records.js",
"src/data/tomtomTiles.js",
"src/layers/alpr/tileRecords.js",
"src/sources/capability.js",
"src/sources/httpBody.js",
"src/sources/vectorTiles.js",
"src/sources/phaseTiming.js",
"src/layers/alpr/floors.js"
],
"external": ["cesium"]
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
},
"radio-layer": {
"exports": ["./layers/radio"],
@@ -1208,6 +1249,7 @@
"src/maps/imagery.js",
"src/maps/registry.js",
"src/maps/terrain.js",
"src/maps/terrainRetry.js",
"src/keySetupCore.mjs",
"src/maps/imageryHost.js"
],
@@ -1322,6 +1364,7 @@
"src/app/atmosphereCompat.js",
"src/bloom.js",
"src/camera.js",
"src/cameraGroundGuard.js",
"src/cameraVerbs.js",
"src/cctvFocusPolicy.js",
"src/cctvFocusRequest.js",
@@ -1410,6 +1453,10 @@
"src/data/modelVisualAnchor.js",
"src/data/motionModel.js",
"src/data/naturalEarthRegions.js",
"src/data/adminBoundaries.js",
"src/data/local_data/natural_earth/states_provinces.json",
"src/data/local_data/natural_earth/countries.json",
"src/data/local_data/us_census_counties/counties.json",
"src/data/neighborhoodPolygons.js",
"src/data/pickRegistry.js",
"src/data/radio.js",
@@ -1635,15 +1682,19 @@
"src/layers/traffic/flow.js",
"src/layers/traffic/flowDecode.js",
"src/layers/traffic/flowSource.js",
"src/layers/traffic/footprint.js",
"src/layers/traffic/index.js",
"src/layers/traffic/ingestion.js",
"src/sources/requestDeadline.js",
"src/layers/traffic/lifecycle.js",
"src/layers/traffic/model.js",
"src/layers/traffic/policy.js",
"src/layers/traffic/rendering.js",
"src/layers/traffic/retention.js",
"src/layers/traffic/source.js",
"src/layers/traffic/state.js",
"src/layers/traffic/style.js",
"src/layers/traffic/surface.js",
"src/layers/traffic/timing.js",
"src/layers/traffic/viewport.js",
"src/layers/vessels/cards.js",
@@ -1675,6 +1726,7 @@
"src/maps/imagery.js",
"src/maps/registry.js",
"src/maps/terrain.js",
"src/maps/terrainRetry.js",
"src/navigationPolicy.js",
"src/orbit.js",
"src/overlays/worldOverlay.js",
@@ -1961,7 +2013,21 @@
"src/layers/recentImagery/thumbnails.js",
"src/maps/imageryHost.js",
"src/ui/recentImagery.js",
"src/ui/imageryBoxTool.js"
"src/ui/imageryBoxTool.js",
"src/layers/alpr/tileRecords.js",
"src/sources/capability.js",
"src/sources/openFreeMap.js",
"src/sources/militaryTileGeometry.js",
"src/sources/vectorTiles.js",
"src/sources/phaseTiming.js",
"src/layers/traffic/roadModes.js",
"src/data/cameraArrival.js",
"src/layers/alpr/floors.js",
"src/data/militaryNames.js",
"src/sources/requestDeadline.js",
"src/data/local_data/osm_military_names/names.json",
"src/layers/installations/namedMarkers.js",
"src/data/infrastructureOverlayEntry.js"
],
"external": [
"hls.js",
@@ -2067,6 +2133,10 @@
"src/data/modelVisualAnchor.js",
"src/data/motionModel.js",
"src/data/naturalEarthRegions.js",
"src/data/adminBoundaries.js",
"src/data/local_data/natural_earth/states_provinces.json",
"src/data/local_data/natural_earth/countries.json",
"src/data/local_data/us_census_counties/counties.json",
"src/data/neighborhoodPolygons.js",
"src/data/pickRegistry.js",
"src/data/radioCountry.js",
@@ -2275,15 +2345,19 @@
"src/layers/traffic/flow.js",
"src/layers/traffic/flowDecode.js",
"src/layers/traffic/flowSource.js",
"src/layers/traffic/footprint.js",
"src/layers/traffic/index.js",
"src/layers/traffic/ingestion.js",
"src/sources/requestDeadline.js",
"src/layers/traffic/lifecycle.js",
"src/layers/traffic/model.js",
"src/layers/traffic/policy.js",
"src/layers/traffic/rendering.js",
"src/layers/traffic/retention.js",
"src/layers/traffic/source.js",
"src/layers/traffic/state.js",
"src/layers/traffic/style.js",
"src/layers/traffic/surface.js",
"src/layers/traffic/timing.js",
"src/layers/traffic/viewport.js",
"src/layers/vessels/cards.js",
@@ -2326,6 +2400,7 @@
"src/layers/vessels/snapshotRenderer.js",
"src/data/inputOwnership.js",
"src/annotations/worldAnnotationRenderer.js",
"src/cameraGroundGuard.js",
"src/cameraVerbs.js",
"src/services/groundFloor.js",
"src/data/routeSteps.js",
@@ -2422,7 +2497,21 @@
"src/layers/recentImagery/catalog.js",
"src/layers/recentImagery/rendering.js",
"src/layers/recentImagery/thumbnails.js",
"src/maps/imageryHost.js"
"src/maps/imageryHost.js",
"src/layers/alpr/tileRecords.js",
"src/sources/capability.js",
"src/sources/openFreeMap.js",
"src/sources/militaryTileGeometry.js",
"src/sources/vectorTiles.js",
"src/sources/phaseTiming.js",
"src/layers/traffic/roadModes.js",
"src/data/cameraArrival.js",
"src/layers/alpr/floors.js",
"src/data/militaryNames.js",
"src/sources/requestDeadline.js",
"src/data/local_data/osm_military_names/names.json",
"src/layers/installations/namedMarkers.js",
"src/data/infrastructureOverlayEntry.js"
],
"external": [
"hls.js",
@@ -2449,6 +2538,10 @@
"src/data/local_data/natural_earth/regions.json",
"src/data/local_data/neighborhoods/san-francisco.json",
"src/data/naturalEarthRegions.js",
"src/data/adminBoundaries.js",
"src/data/local_data/natural_earth/states_provinces.json",
"src/data/local_data/natural_earth/countries.json",
"src/data/local_data/us_census_counties/counties.json",
"src/data/neighborhoodPolygons.js",
"src/data/bundledJson.js",
"src/data/retryableLoad.js",
@@ -2456,6 +2549,7 @@
"src/keylessGeocoder.js",
"src/layers/firms/anchors.js",
"src/locations.js",
"src/cameraGroundGuard.js",
"src/search/defaults.js",
"src/search/geospatial.js",
"src/search/google.js",
@@ -2478,7 +2572,9 @@
"src/sources/featureGeometry.js",
"src/sources/overpassFeaturesRecords.js",
"src/sources/overpassFeatures.js",
"src/sources/featureSource.js"
"src/sources/featureSource.js",
"src/sources/capability.js",
"src/data/cameraArrival.js"
],
"external": [
"cesium",
@@ -2491,7 +2587,7 @@
},
"layer-lifecycle": {
"exports": ["./data/lifecycle"],
"modules": ["src/data/lifecycle.js"],
"modules": ["src/data/lifecycle.js", "src/data/cameraArrival.js"],
"external": []
},
"ui-navigation": {
@@ -2499,7 +2595,8 @@
"modules": [
"src/navigationPolicy.js",
"src/ui/navigationController.js",
"src/app/stateChannel.js"
"src/app/stateChannel.js",
"src/data/cameraArrival.js"
],
"external": ["cesium"]
},
@@ -2661,9 +2758,14 @@
"modules": [
"src/layers/alpr/policy.js",
"src/layers/alpr/records.js",
"src/layers/alpr/source.js"
"src/layers/alpr/source.js",
"src/data/tomtomTiles.js",
"src/layers/alpr/tileRecords.js",
"src/sources/httpBody.js",
"src/sources/vectorTiles.js",
"src/sources/phaseTiming.js"
],
"external": []
"external": ["@mapbox/vector-tile", "pbf"]
},
"bikeshare-source": {
"exports": ["./layers/bikeshare/source"],
@@ -2692,9 +2794,21 @@
"exports": ["./layers/installations/source"],
"modules": [
"src/layers/installations/source.js",
"src/data/militaryInstallationData.js"
"src/data/militaryInstallationData.js",
"src/data/tomtomTiles.js",
"src/sources/capability.js",
"src/sources/httpBody.js",
"src/sources/openFreeMap.js",
"src/sources/militaryTileGeometry.js",
"src/sources/vectorTiles.js",
"src/sources/phaseTiming.js",
"src/data/militaryNames.js",
"src/sources/requestDeadline.js",
"src/data/local_data/osm_military_names/names.json",
"src/data/bundledJson.js",
"src/data/retryableLoad.js"
],
"external": []
"external": ["@mapbox/vector-tile", "pbf"]
},
"launches-source": {
"exports": ["./layers/launches/source"],
@@ -2717,8 +2831,16 @@
"src/data/tomtomTiles.js",
"src/layers/traffic/flowDecode.js",
"src/layers/traffic/flowSource.js",
"src/layers/traffic/footprint.js",
"src/layers/traffic/source.js",
"src/sources/overpassRoads.js"
"src/sources/overpassRoads.js",
"src/sources/httpBody.js",
"src/sources/openFreeMap.js",
"src/sources/militaryTileGeometry.js",
"src/sources/vectorTiles.js",
"src/sources/phaseTiming.js",
"src/layers/traffic/roadModes.js",
"src/data/flowMatch.js"
],
"external": ["@mapbox/vector-tile", "pbf"]
},
@@ -2855,7 +2977,8 @@
"src/sources/featureGeometry.js",
"src/sources/overpassFeaturesRecords.js",
"src/sources/overpassFeatures.js",
"src/sources/featureSource.js"
"src/sources/featureSource.js",
"src/sources/capability.js"
],
"external": ["cesium", "egm96-universal", "pbf"]
},
@@ -2924,7 +3047,8 @@
"src/sources/featureGeometry.js",
"src/sources/overpassFeaturesRecords.js",
"src/sources/overpassFeatures.js",
"src/sources/featureSource.js"
"src/sources/featureSource.js",
"src/sources/capability.js"
],
"external": []
},
+655
View File
@@ -0,0 +1,655 @@
#!/usr/bin/env node
/**
* qa-admin-outlines.mjs — bundled state/province/county outlines, driven like a
* user would drive them.
*
* Run: node scripts/qa-admin-outlines.mjs http://localhost:4173 [--headful] [--no-video]
*
* Journey: frame Texas at state scale and outline "Texas"; pan and zoom toward
* Austin in smooth 2–3 s moves; outline "Travis County, Texas"; pan north and
* outline the neighbouring "Williamson County, Texas"; zoom out to see both;
* then outline "Bavaria" (framed, as a voice request for a distant place is),
* and finally Brandenburg (Berlin is a hole) and Hawaii (eight islands).
*
* Asserts for every outline: it resolves from the bundled packs (no geocode,
* boundary or Overpass request), is drawn as a draped outline within the
* budget, and holds still — no rebuilt primitives and no new entities while
* the camera is still. The whole journey must log zero console/page errors and
* send zero requests to any Overpass host or nominatim.openstreetmap.org.
*
* Evidence in qa-shots/ (gitignored): admin-outlines-<step>.png screenshots,
* admin-outlines-journey.mp4 (CDP screencast at ~24 fps, frames re-timed from
* their capture timestamps), and admin-outlines-result.json with per-step
* timings, frame-time p50/p95 and still-camera frame-difference statistics.
* Exits nonzero on any failed assertion.
*/
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
const args = process.argv.slice(2);
if (args.includes('--help')) {
console.log(
'Usage: node scripts/qa-admin-outlines.mjs <dev-server-url> [--headful] [--no-video]',
);
process.exit(0);
}
const url = args.find((arg) => !arg.startsWith('--'));
if (!url || !['http:', 'https:'].includes(new URL(url).protocol))
throw new Error('Supply a running dev server URL; see --help');
const recordVideo = !args.includes('--no-video');
const shots = path.resolve('qa-shots');
const framesDir = path.join(shots, 'admin-outlines-frames');
await fs.mkdir(shots, { recursive: true });
await fs.rm(framesDir, { recursive: true, force: true });
await fs.mkdir(framesDir, { recursive: true });
const OUTLINE_BUDGET_MS = 1500;
const STILL_MS = 3000;
const FRAME_INTERVAL_S = 1 / 24;
const result = {
url,
steps: [],
forbiddenRequests: [],
lookups: [],
packRequests: [],
consoleErrors: [],
pageErrors: [],
screenshots: [],
video: null,
};
const browser = await puppeteer.launch({
headless: !args.includes('--headful'),
protocolTimeout: 300_000,
args: [
'--no-sandbox',
'--use-gl=angle',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
});
let page;
const frames = [];
let lastKept = -Infinity;
let recording = false;
let marker = 'startup';
try {
page = await browser.newPage();
await page.evaluateOnNewDocument(() => {
sessionStorage.setItem('gev:first-run-mission-session:v1', 'dismissed');
});
await page.setViewport({ width: 1280, height: 800 });
const client = await page.createCDPSession();
await client.send('Network.enable');
client.on('Network.requestWillBeSent', ({ request }) => {
const target = new URL(request.url);
const where = `${request.method} ${target.origin}${target.pathname}`;
if (
target.hostname.includes('overpass') ||
target.hostname === 'nominatim.openstreetmap.org'
)
result.forbiddenRequests.push({ step: marker, request: where });
// Lookups an outline could make: a forward geocode, a place search, an
// Overpass boundary query. The HUD's reverse geocode of the camera centre
// (latlng=) is not one of them.
const local = target.origin === new URL(url).origin;
const reverse = target.searchParams.has('latlng');
const lookup =
!reverse &&
((local &&
/^\/api\/(overpass|geocode|google\/text-search|nominatim|photon)/.test(
target.pathname,
)) ||
(target.hostname === 'maps.googleapis.com' &&
target.pathname.includes('/geocode/')) ||
target.hostname === 'photon.komoot.io');
if (lookup) result.lookups.push({ step: marker, request: where });
if (/\/(states_provinces|countries|counties)\.json$/.test(target.pathname))
result.packRequests.push({ step: marker, pack: target.pathname });
});
page.on('pageerror', (error) =>
result.pageErrors.push({ step: marker, message: error.message }),
);
page.on('console', (message) => {
if (message.type() === 'error')
result.consoleErrors.push({ step: marker, message: message.text() });
});
client.on('Page.screencastFrame', async ({ data, metadata, sessionId }) => {
client.send('Page.screencastFrameAck', { sessionId }).catch(() => {});
if (!recording) return;
const t = metadata.timestamp;
if (t - lastKept < FRAME_INTERVAL_S * 0.9) return;
lastKept = t;
const file = path.join(
framesDir,
`${String(frames.length).padStart(6, '0')}.jpg`,
);
frames.push({ file, t, step: marker });
await fs.writeFile(file, Buffer.from(data, 'base64'));
});
console.log('Loading viewer...');
const navigationUrl = new URL(url);
navigationUrl.searchParams.set('welcome', '0');
await page.goto(navigationUrl.href, {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.annotations,
{ timeout: 90_000, polling: 500 },
);
await page.waitForFunction(
() =>
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 90_000, polling: 250 },
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await page.keyboard.press('Escape');
await page.waitForFunction(
() => !document.querySelector('#first-run-launcher:not([hidden])'),
{ timeout: 10_000 },
);
result.renderer = await page.evaluate(() => {
const gl = window.__godsEyeView.viewer.scene.context._gl;
const debug = gl.getExtension('WEBGL_debug_renderer_info');
return debug
? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL)
: gl.getParameter(gl.RENDERER);
});
console.log('Renderer:', result.renderer);
// In-page helpers: camera moves, frame timing and annotation state.
await page.evaluate(() => {
const { viewer } = window.__godsEyeView;
const Cesium = window.__CESIUM__;
window.__qaOutlines = {
fly(view, duration) {
return new Promise((resolve) => {
viewer.camera.cancelFlight();
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(
view.lon,
view.lat,
view.height,
),
orientation: {
heading: Cesium.Math.toRadians(view.heading ?? 0),
pitch: Cesium.Math.toRadians(view.pitch ?? -70),
roll: 0,
},
duration,
easingFunction: Cesium.EasingFunction.QUADRATIC_IN_OUT,
complete: resolve,
cancel: resolve,
});
});
},
frameTimes(ms) {
return new Promise((resolve) => {
const deltas = [];
let last = performance.now();
const end = last + ms;
const step = (now) => {
deltas.push(now - last);
last = now;
if (now < end) requestAnimationFrame(step);
else resolve(deltas.slice(1));
};
requestAnimationFrame(step);
});
},
annotationSource() {
for (let i = 0; i < viewer.dataSources.length; i++) {
const source = viewer.dataSources.get(i);
if (source.name === 'gev-annotations') return source;
}
return null;
},
groundPrimitives() {
const list = [];
const walk = (collection) => {
for (let i = 0; i < collection.length; i++) {
const item = collection.get(i);
if (item?.length !== undefined && typeof item.get === 'function')
walk(item);
else list.push(item);
}
};
walk(viewer.scene.groundPrimitives);
return list;
},
snapshot() {
const source = this.annotationSource();
const entities = source ? source.entities.values : [];
const primitives = this.groundPrimitives();
window.__qaPrimitiveIds ??= new WeakMap();
let next = window.__qaPrimitiveNext ?? 1;
const ids = primitives.map((primitive) => {
if (!window.__qaPrimitiveIds.has(primitive))
window.__qaPrimitiveIds.set(primitive, next++);
return window.__qaPrimitiveIds.get(primitive);
});
window.__qaPrimitiveNext = next;
return {
entities: entities.length,
polygons: entities.filter((e) => e.polygon).length,
polylines: entities.filter((e) => e.polyline).length,
draped:
entities.every(
(e) =>
!e.polygon ||
e.polygon.classificationType?.getValue() ===
Cesium.ClassificationType.BOTH,
) &&
entities.every(
(e) => !e.polyline || e.polyline.clampToGround?.getValue(),
),
primitiveIds: ids,
ready: viewer.dataSourceDisplay.ready,
};
},
};
});
const settle = () =>
page
.waitForFunction(
() => {
const { scene } = window.__godsEyeView.viewer;
for (let i = 0; i < scene.primitives.length; i++) {
const p = scene.primitives.get(i);
if (p?.show && typeof p.tilesLoaded === 'boolean' && !p.tilesLoaded)
return false;
}
return true;
},
{ timeout: 60_000, polling: 500 },
)
.catch(() => {});
async function shot(name) {
await page.evaluate(
() =>
new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
),
);
const filename = `admin-outlines-${name}.png`;
await page.screenshot({ path: path.join(shots, filename) });
result.screenshots.push(filename);
}
async function move(name, view, duration = 2.5) {
marker = `move:${name}`;
const [, times] = await Promise.all([
page.evaluate((v, d) => window.__qaOutlines.fly(v, d), view, duration),
page.evaluate((ms) => window.__qaOutlines.frameTimes(ms), duration * 900),
]);
result.steps.push({ step: `move:${name}`, frameTimes: stats(times) });
}
/** Annotate like the voice tool does, then watch the mark while still. */
async function outline(name, target, { flyTo = false } = {}) {
marker = `outline:${name}`;
const lookupsBefore = result.lookups.length;
const errorsBefore = result.consoleErrors.length + result.pageErrors.length;
const timing = await page.evaluate(
async (target, flyTo) => {
const started = performance.now();
const response = await window.__godsEyeView.annotations.annotate(
[{ type: 'area', target, label: target, footprint: true }],
{ flyTo },
);
const resolvedMs = performance.now() - started;
// "Drawn" = the first rendered frame after the entity visualizers have
// built the mark's primitives (each tick updates them before render).
const { viewer } = window.__godsEyeView;
await new Promise((resolve) => {
const off = viewer.scene.postRender.addEventListener(() => {
if (!viewer.dataSourceDisplay.ready) return;
off();
resolve();
});
});
const drawnMs = performance.now() - started;
const mark = window.__godsEyeView.annotations
.list()
.find((m) => m.id === response.ids?.[0]);
return {
response: response.results?.[0] ?? null,
resolvedMs,
drawnMs,
parts: mark?.polygons?.length ?? (mark?.ring ? 1 : 0),
holes: (mark?.polygons ?? []).reduce(
(n, part) => n + part.length - 1,
0,
),
ringVertices: mark?.ring?.length ?? 0,
pending: Boolean(mark?.pendingOutline),
};
},
target,
flyTo,
);
if (flyTo) await new Promise((r) => setTimeout(r, 2200));
await settle();
// Hold still: the drawn mark must not rebuild or grow.
marker = `still:${name}`;
const before = await page.evaluate(() => window.__qaOutlines.snapshot());
const stillStart = frames.length;
const times = await page.evaluate(
(ms) => window.__qaOutlines.frameTimes(ms),
STILL_MS,
);
const after = await page.evaluate(() => window.__qaOutlines.snapshot());
const step = {
step: `outline:${name}`,
target,
...timing,
entities: after.entities,
polygons: after.polygons,
polylines: after.polylines,
draped: after.draped,
stableWhileStill:
before.entities === after.entities &&
JSON.stringify(before.primitiveIds) ===
JSON.stringify(after.primitiveIds),
stillFrameTimes: stats(times),
stillFrames: [stillStart, frames.length],
lookups: result.lookups.slice(lookupsBefore),
errors:
result.consoleErrors.length + result.pageErrors.length - errorsBefore,
};
result.steps.push(step);
console.log(
`${name}: resolved ${timing.resolvedMs.toFixed(0)} ms, drawn ${timing.drawnMs.toFixed(0)} ms, ` +
`${timing.parts} part(s), ${timing.holes} hole(s), via ${timing.response?.resolvedVia}; ` +
`still p50/p95 ${step.stillFrameTimes.p50}/${step.stillFrameTimes.p95} ms; ` +
`stable=${step.stableWhileStill}`,
);
assert.equal(timing.response?.ok, true, `${name}: annotation drawn`);
assert.equal(timing.response.outline, true, `${name}: outline, not a pin`);
assert.equal(timing.response.resolvedVia, 'bundled', `${name}: bundled`);
assert.equal(timing.pending, false, `${name}: nothing pending`);
assert.ok(
timing.drawnMs < OUTLINE_BUDGET_MS,
`${name}: drawn in ${timing.drawnMs} ms`,
);
assert.equal(step.draped, true, `${name}: draped on terrain/tiles`);
assert.equal(step.stableWhileStill, true, `${name}: no redraw when still`);
assert.deepEqual(step.lookups, [], `${name}: no network lookups`);
assert.equal(step.errors, 0, `${name}: no console errors`);
return step;
}
// Start from a clean board and the default layers, as a user would.
await page.evaluate(() => window.__godsEyeView.annotations.clear?.());
await page.evaluate(() =>
window.__qaOutlines.fly(
{ lat: 25.5, lon: -99.6, height: 2_600_000, pitch: -80 },
0,
),
);
await settle();
if (recordVideo) {
await client.send('Page.startScreencast', {
format: 'jpeg',
quality: 72,
maxWidth: 1280,
maxHeight: 800,
everyNthFrame: 1,
});
recording = true;
}
await outline('texas', 'Texas');
await shot('texas');
await move('pan-west', {
lat: 25.2,
lon: -101.5,
height: 2_300_000,
pitch: -75,
});
await move('zoom-central-texas', {
lat: 28.8,
lon: -98.2,
height: 700_000,
pitch: -65,
});
await move('zoom-austin', {
lat: 29.75,
lon: -97.8,
height: 120_000,
pitch: -55,
});
await settle();
await outline('travis', 'Travis County, Texas');
await shot('travis');
await move('pan-williamson', {
lat: 30.05,
lon: -97.65,
height: 110_000,
pitch: -55,
});
await settle();
await outline('williamson', 'Williamson County, Texas');
// Close over the shared Travis/Williamson line near Pflugerville: the
// outline must lie on the photoreal mesh, not float above or sink into it.
const border = await page.evaluate(() => {
const travis = window.__godsEyeView.annotations
.list()
.find((m) => m.label === 'Travis County, Texas');
let best = null;
for (const [lon, lat] of travis.ring) {
const d = (lat - 30.46) ** 2 + (lon + 97.62) ** 2;
if (!best || d < best.d) best = { lon, lat, d };
}
return best;
});
await move(
'drape-close',
{ lat: border.lat - 0.035, lon: border.lon, height: 2500, pitch: -40 },
3,
);
await settle();
await page.evaluate((ms) => new Promise((r) => setTimeout(r, ms)), 2500);
await shot('drape-close');
await move('zoom-out-both', {
lat: 29.55,
lon: -97.7,
height: 260_000,
pitch: -60,
});
await settle();
await page.evaluate((ms) => new Promise((r) => setTimeout(r, ms)), 1500);
await shot('both-counties');
await outline('bavaria', 'Bavaria', { flyTo: true });
await shot('bavaria');
await move(
'orbit-bavaria',
{ lat: 46.6, lon: 11.9, height: 650_000, heading: 20, pitch: -55 },
3,
);
await settle();
await shot('bavaria-orbit');
// A hole (Berlin inside Brandenburg) and islands (Hawaii) draw as such.
const brandenburg = await outline('brandenburg', 'Brandenburg', {
flyTo: true,
});
assert.ok(brandenburg.holes >= 1, 'Brandenburg keeps the Berlin hole');
await shot('brandenburg');
const hawaii = await outline('hawaii', 'Hawaii', { flyTo: true });
assert.ok(hawaii.parts >= 7, 'Hawaii keeps its islands');
await shot('hawaii');
marker = 'end';
for (const [name, target, lat, lon, height] of [
['scotland', 'Scotland', 57, -4, 800000],
['england', 'England', 52.5, -1.5, 800000],
['wales', 'Wales', 52.3, -3.7, 420000],
['northern-ireland', 'Northern Ireland', 54.7, -6.7, 300000],
['switzerland', 'Switzerland', 46.8, 8.2, 500000],
['iran', 'Iran', 32, 54, 2200000],
['france', 'France', 46.6, 2.2, 1600000],
['japan', 'Japan', 37, 138, 2200000],
['georgia-state', 'Georgia', 32.6, -83.4, 650000],
['georgia-country', 'Georgia', 42, 43.6, 700000],
['georgia-qualified', 'the country of Georgia', 42, 43.6, 700000],
]) {
await page.evaluate(() => window.__godsEyeView.annotations.clear());
await move(name, { lat, lon, height, pitch: -90 }, 1);
await outline(name, target);
await shot(name);
const rendered = await page.evaluate(() => window.__qaOutlines.snapshot());
assert.ok(rendered.polygons > 0, `${target} polygon renders`);
}
if (recordVideo) {
recording = false;
await client.send('Page.stopScreencast');
}
result.totals = {
forbiddenRequests: result.forbiddenRequests.length,
consoleErrors: result.consoleErrors.length,
pageErrors: result.pageErrors.length,
};
assert.deepEqual(
result.forbiddenRequests,
[],
'no Overpass or nominatim.openstreetmap.org requests',
);
// The packs load on the first outline that needs them, never at start-up.
assert.deepEqual(
result.packRequests.map((r) => [r.step, path.basename(r.pack)]),
[
['outline:texas', 'states_provinces.json'],
['outline:travis', 'counties.json'],
['outline:scotland', 'countries.json'],
],
'packs load lazily, once each',
);
assert.deepEqual(result.pageErrors, [], 'no page errors');
assert.deepEqual(result.consoleErrors, [], 'no console errors');
} catch (error) {
result.failure = error.message;
console.error(error);
process.exitCode = 1;
if (page)
await page
.screenshot({ path: path.join(shots, 'admin-outlines-failure.png') })
.catch(() => {});
} finally {
await browser.close();
}
if (recordVideo && frames.length > 1) {
// Frame-to-frame change while the camera is still: steady pulses read as
// small, even values; a flicker (a part vanishing and returning) as a spike.
for (const step of result.steps.filter((s) => s.stillFrames)) {
const [start, end] = step.stillFrames;
const diffs = [];
let previous = null;
for (const frame of frames.slice(start, end)) {
const pixels = await sharp(frame.file)
.resize(160, 100, { fit: 'fill' })
.greyscale()
.raw()
.toBuffer();
if (previous) {
let sum = 0;
let max = 0;
for (let i = 0; i < pixels.length; i++) {
const d = Math.abs(pixels[i] - previous[i]);
sum += d;
if (d > max) max = d;
}
diffs.push(sum / pixels.length);
}
previous = pixels;
}
step.stillFrameDiff = {
frames: end - start,
meanAbsDiff: stats(diffs, 3),
};
}
const list = [];
for (let i = 0; i < frames.length; i++) {
const duration =
i + 1 < frames.length ? frames[i + 1].t - frames[i].t : FRAME_INTERVAL_S;
list.push(`file '${frames[i].file}'`, `duration ${duration.toFixed(4)}`);
}
list.push(`file '${frames.at(-1).file}'`);
const listFile = path.join(framesDir, 'frames.txt');
await fs.writeFile(listFile, list.join('\n') + '\n');
const video = path.join(shots, 'admin-outlines-journey.mp4');
execFileSync('ffmpeg', [
'-y',
'-loglevel',
'error',
'-f',
'concat',
'-safe',
'0',
'-i',
listFile,
'-vf',
'fps=24,format=yuv420p',
'-c:v',
'libx264',
'-crf',
'23',
video,
]);
const span = frames.at(-1).t - frames[0].t;
result.video = {
file: path.relative(process.cwd(), video),
frames: frames.length,
seconds: Number(span.toFixed(1)),
capturedFps: Number(((frames.length - 1) / span).toFixed(1)),
};
console.log('Video:', result.video);
await fs.rm(framesDir, { recursive: true, force: true });
}
await fs.writeFile(
path.join(shots, 'admin-outlines-result.json'),
JSON.stringify(result, null, 2),
);
console.table(
result.steps.map((s) => ({
step: s.step,
resolvedMs: s.resolvedMs?.toFixed(0),
drawnMs: s.drawnMs?.toFixed(0),
parts: s.parts,
stable: s.stableWhileStill,
p50: (s.stillFrameTimes || s.frameTimes)?.p50,
p95: (s.stillFrameTimes || s.frameTimes)?.p95,
stillDiffP95: s.stillFrameDiff?.meanAbsDiff.p95,
stillDiffMax: s.stillFrameDiff?.meanAbsDiff.max,
})),
);
console.log(process.exitCode ? 'FAILED' : 'PASSED');
function stats(values, digits = 1) {
if (!values.length) return { n: 0 };
const sorted = [...values].sort((a, b) => a - b);
const at = (q) =>
Number(
sorted[
Math.min(sorted.length - 1, Math.floor(q * sorted.length))
].toFixed(digits),
);
return { n: values.length, p50: at(0.5), p95: at(0.95), max: at(1) };
}
+588
View File
@@ -0,0 +1,588 @@
#!/usr/bin/env node
/**
* qa-alpr-journey.mjs — ALPR cameras on a realistic Austin camera journey.
*
* Run: node scripts/qa-alpr-journey.mjs http://localhost:4173 [--headful]
* [--passes off,on,on] [--out qa-shots/alpr-journey] [--no-assert]
*
* Drives the same eased camera path several times: a neighbourhood by Camp
* Mabry (~2 km across), two 600 m pans, a small zoom out and back, downtown
* (~6 km), the whole city (~30 km) and back to the neighbourhood. The first
* pass runs with Mapped ALPR Cameras off (it also warms the photoreal tiles),
* then ALPR is switched on mid-journey from its layer row and the path repeats
* cold and warm.
*
* Per stop it records: time from the camera stopping to cameras shown (or an
* explicit "zoom in" state), the camera count and row status, rAF interval and
* Cesium render CPU time (mean / p50 / p95) while moving and while holding, and
* marker stability while the camera is still (camera-set changes after the stop
* has settled). Asserts, unless --no-assert: neighbourhood and downtown show
* cameras within 2 s on the warm pass; the whole-city stop shows cameras or a
* zoom-in prompt and never ends a hold still loading; at most one visible
* marker update per stop and none after 3 s while still; warm-pass frame p50/p95 within 10% of the ALPR-off pass (plus a
* 2 ms floor for timer noise); zero console errors.
*
* Rendered-state checks read the recorded frames, not entity state: ALPR-cyan
* pixels per frame during each hold, minus the ALPR-off pass at the same stop,
* give when markers actually reached the screen. Cameras projected on screen
* must be painted, no later than 700 ms after the layer state settled (a
* missing render request fails here), within 2 s of the stop on the warm pass;
* a view whose loaded cameras are all off screen must say "None on screen".
* At the end of each hold the screen is compared with the same scene after
* forcing four frames; any marker difference is a missing render request.
* --self-test-drop-renders disables explicit render requests during the ALPR
* passes to prove those checks fail.
*
* Writes <out>/result.json, <out>/clip.mp4 (CDP screencast with real frame
* timing) and per-stop screenshots. Needs ffmpeg on PATH.
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
import {
createScreencast,
frameStats,
installFrameProbe,
moveCamera,
pageNow,
readFrames,
} from './qa-journey-recorder.mjs';
const argv = process.argv.slice(2);
if (argv.includes('--help') || argv.includes('-h')) {
console.log(
'Usage: node scripts/qa-alpr-journey.mjs <dev-server-url> [--headful] [--passes off,on,on] [--out dir] [--no-assert] [--self-test-drop-renders]',
);
process.exit(0);
}
const option = (name, fallback) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback;
};
const appUrl =
argv.find((arg) => /^https?:/.test(arg)) || 'http://localhost:4173';
const outDir = path.resolve(option('--out', 'qa-shots/alpr-journey'));
const passes = option('--passes', 'off,on,on').split(',');
const assertOn = !argv.includes('--no-assert');
const dropRenders = argv.includes('--self-test-drop-renders');
fs.mkdirSync(outDir, { recursive: true });
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const cyanCount = async (file) => {
const { data, info } = await sharp(file)
.resize({ width: 640 })
.raw()
.toBuffer({ resolveWithObject: true });
let n = 0;
for (let i = 0; i < data.length; i += info.channels) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
if (r < 140 && g > 160 && b > 205 && b - r > 80) n += 1;
}
return n;
};
/** Cyan pixels (at 640 px width) that mean at least one badge is painted. */
const MIN_MARKER_PIXELS = 12;
// Camp Mabry neighbourhood (Tarrytown / Bryker Woods), downtown, whole city.
const N = {
lat: 30.2985,
lon: -97.7655,
height: 1500,
heading: 20,
pitch: -55,
};
const east = (view, m) => ({
...view,
lon: view.lon + m / (111_320 * Math.cos((view.lat * Math.PI) / 180)),
});
const north = (view, m) => ({ ...view, lat: view.lat + m / 111_320 });
const STOPS = [
{ name: 'neighbourhood', view: N, move: 0, hold: 6, scale: 'neighbourhood' },
{
name: 'pan-east-600m',
view: east(N, 600),
move: 2.5,
hold: 5,
scale: 'neighbourhood',
},
{
name: 'pan-north-600m',
view: north(east(N, 600), 600),
move: 2.5,
hold: 5,
scale: 'neighbourhood',
},
{
name: 'zoom-out-3km',
view: { ...north(east(N, 600), 600), height: 3000 },
move: 2.5,
hold: 5,
scale: 'neighbourhood',
},
{
name: 'zoom-back-in',
view: north(east(N, 600), 600),
move: 2.5,
hold: 5,
scale: 'neighbourhood',
},
{
name: 'downtown-6km',
view: { lat: 30.262, lon: -97.7431, height: 4500, heading: 0, pitch: -60 },
move: 3,
hold: 7,
scale: 'downtown',
},
{
name: 'city-30km',
view: { lat: 30.2, lon: -97.75, height: 24000, heading: 0, pitch: -65 },
move: 3,
hold: 8,
scale: 'city',
},
{
name: 'back-to-neighbourhood',
view: N,
move: 3,
hold: 6,
scale: 'neighbourhood',
},
];
const browser = await puppeteer.launch({
headless: !argv.includes('--headful'),
protocolTimeout: 300_000,
args: [
'--no-sandbox',
...(process.platform === 'darwin'
? ['--use-angle=metal', '--enable-gpu']
: ['--use-gl=angle', '--use-angle=swiftshader']),
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--disable-backgrounding-occluded-windows',
],
});
const result = {
url: appUrl,
startedAt: new Date().toISOString(),
passes: [],
consoleErrors: [],
failures: [],
};
const fail = (message) => {
result.failures.push(message);
console.log(`[FAIL] ${message}`);
};
let page;
try {
page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.evaluateOnNewDocument(() =>
sessionStorage.setItem('gev:first-run-mission-session:v1', 'dismissed'),
);
page.on('console', (message) => {
if (message.type() === 'error') result.consoleErrors.push(message.text());
});
page.on('pageerror', (error) => result.consoleErrors.push(error.message));
const url = new URL(appUrl);
url.searchParams.set('welcome', '0');
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 90_000 });
await page.waitForFunction(
() =>
window.__godsEyeView?.dataManager &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 120_000 },
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await page.keyboard.press('Escape');
await installFrameProbe(page);
// ALPR watcher: row state every 100 ms and the rendered camera set (ids and
// visibility) as a cheap hash, so structural churn while still is visible.
await page.evaluate(() => {
const samples = [];
window.__gevAlprSamples = samples;
setInterval(() => {
const entry = window.__godsEyeView.dataManager.layers.get('alpr-cameras');
const stats = entry?.module?.getStats?.() || {};
// Layers that publish a render revision are measured by it; otherwise
// hash the rendered entity ids and their visibility.
let hash = 0;
let shown = 0;
if (Number.isFinite(stats.renderRevision)) {
hash = stats.renderRevision;
shown = stats.shown ?? stats.count ?? 0;
} else {
const entities =
entry?.module && entry.enabled
? window.__godsEyeView.viewer.dataSources.getByName(
'alpr-cameras',
)[0]?.entities.values || []
: [];
for (const entity of entities) {
if (entity.show === false) continue;
shown += 1;
const id = String(entity.id);
for (let i = 0; i < id.length; i++)
hash = (hash * 31 + id.charCodeAt(i)) | 0;
}
}
samples.push({
t: performance.now(),
enabled: Boolean(entry?.enabled),
count: stats.count ?? 0,
shown,
hash,
loading: Boolean(stats.loading),
status: stats.status || null,
label: stats.loadingLabel || '',
onScreen: stats.onScreen ?? null,
});
if (samples.length > 20_000) samples.splice(0, 5_000);
}, 100);
});
const recorder = await createScreencast(page, outDir);
await recorder.start();
async function waitTiles(maxMs = 20_000) {
await page
.waitForFunction(
() => {
const { scene } = window.__godsEyeView.viewer;
if (scene.globe.show) return scene.globe.tilesLoaded;
for (let i = 0; i < scene.primitives.length; i++) {
const p = scene.primitives.get(i);
if (p.show && p.tilesLoaded === false) return false;
}
return true;
},
{ timeout: maxMs, polling: 250 },
)
.catch(() => {});
}
for (const [passIndex, mode] of passes.entries()) {
const alpr = mode === 'on';
const passName = `${passIndex + 1}-${mode}${alpr && passes.slice(0, passIndex).includes('on') ? '-warm' : alpr ? '-cold' : ''}`;
console.log(`Pass ${passName}`);
await moveCamera(page, STOPS[0].view, 0);
await waitTiles();
if (
alpr !==
(await page.evaluate(() =>
window.__godsEyeView.dataManager.isEnabled('alpr-cameras'),
))
) {
// The user's switch: the layer row, not a private API.
await page.evaluate(
(enabled) =>
window.__godsEyeView.dataManager.setEnabled('alpr-cameras', enabled, {
origin: 'user',
}),
alpr,
);
}
if (alpr && dropRenders)
// Self-test: drop explicit render requests so markers reach the screen
// only with the next camera move. The rendered-pixel checks must fail.
await page.evaluate(() => {
window.__godsEyeView.viewer.scene.requestRender = () => {};
});
const pass = { name: passName, alpr, stops: [] };
for (const stop of STOPS) {
const moveStart = await pageNow(page);
if (stop.move) await moveCamera(page, stop.view, stop.move);
else await moveCamera(page, stop.view, 0);
const holdStart = await pageNow(page);
const holdStartEpoch = await page.evaluate(
() => performance.timeOrigin + performance.now(),
);
await sleep(stop.hold * 1000);
const holdEnd = await pageNow(page);
// Render truth: what the screen shows at the end of the hold versus the
// same scene after forcing a few frames. A difference means the layer
// changed state without asking for a render.
let staleFrame = null;
if (alpr) {
const shown = await cyanCount(await page.screenshot({ type: 'png' }));
await page.evaluate(async () => {
const { scene } = window.__godsEyeView.viewer;
const mode = scene.requestRenderMode;
scene.requestRenderMode = false;
for (let i = 0; i < 4; i++)
await new Promise((resolve) => requestAnimationFrame(resolve));
scene.requestRenderMode = mode;
});
const forced = await cyanCount(await page.screenshot({ type: 'png' }));
staleFrame = {
shown,
forced,
stale: Math.abs(forced - shown) > Math.max(8, forced * 0.15),
};
}
const moving = frameStats(await readFrames(page, moveStart, holdStart));
const holding = frameStats(await readFrames(page, holdStart, holdEnd));
const samples = await page.evaluate(
(a, b) => window.__gevAlprSamples.filter((s) => s.t >= a && s.t < b),
holdStart,
holdEnd,
);
// Time to cameras: the last visible change (or the end of loading) after
// the camera stopped. More than one visible change, or any change after
// 3 s, is flicker: a still camera must see one clean update at most.
let visualChanges = 0;
const changeLog = [];
let lastChangeAt = null;
let loadingEndAt = null;
let wasLoading = false;
let previous = null;
for (const s of samples) {
if (
previous &&
(s.hash !== previous.hash || s.shown !== previous.shown)
) {
visualChanges += 1;
lastChangeAt = s.t;
changeLog.push({
atMs: Math.round(s.t - holdStart),
shown: s.shown,
loading: s.loading,
});
}
if (s.loading) wasLoading = true;
else if (wasLoading && loadingEndAt === null) loadingEndAt = s.t;
previous = s;
}
const first = samples[0] || {};
const settledAt = Math.max(
lastChangeAt ?? holdStart,
loadingEndAt ?? (first.loading ? holdEnd : holdStart),
);
const end = samples.at(-1) || {};
const row = {
stop: stop.name,
scale: stop.scale,
holdEpochMs: [holdStartEpoch, holdStartEpoch + (holdEnd - holdStart)],
onScreen: end.onScreen ?? null,
staleFrame,
timeToCamerasMs: alpr ? Math.round(settledAt - holdStart) : null,
cameras: end.count ?? 0,
shown: end.shown ?? 0,
status: end.status,
label: end.label,
loadingAtHoldEnd: Boolean(end.loading),
visualChanges,
changeLog,
lateChanges: samples.filter(
(sample, i) =>
i > 0 &&
sample.t > holdStart + 3000 &&
(sample.hash !== samples[i - 1].hash ||
sample.shown !== samples[i - 1].shown),
).length,
moving,
holding,
};
pass.stops.push(row);
console.log(
` ${stop.name.padEnd(22)} cams ${String(row.cameras).padStart(5)} t ${String(row.timeToCamerasMs ?? '-').padStart(5)} ms frame p50/p95 ${holding.frameP50}/${holding.frameP95} render p50/p95 ${holding.renderP50}/${holding.renderP95} changes ${visualChanges}/${row.lateChanges} ${row.status || ''} ${row.label || ''}`,
);
if (passIndex === passes.length - 1) {
await page.screenshot({
path: path.join(outDir, `${passName}-${stop.name}.png`),
});
}
}
result.passes.push(pass);
}
const clip = await recorder.stop();
result.clip = clip;
// Clip-relative hold windows, so a reviewer can jump to each stop.
const clipStart = recorder.frames[0]?.timestamp * 1000;
for (const pass of result.passes)
for (const stop of pass.stops)
stop.clipSeconds = stop.holdEpochMs.map(
(ms) => Math.round((ms - clipStart) / 100) / 10,
);
console.log('clip', clip);
// Rendered-state check from the recorded frames themselves: count ALPR-cyan
// pixels per frame during each hold, subtract the ALPR-off pass at the same
// stop, and find when the markers actually became visible on screen. Entity
// state can be ready while nothing is painted (a missing render request),
// which this catches and the in-page samples cannot.
const framesIn = ([a, b]) =>
recorder.frames.filter(
(frame) => frame.timestamp * 1000 >= a && frame.timestamp * 1000 < b,
);
const median = (list) =>
list.length ? [...list].sort((x, y) => x - y)[list.length >> 1] : 0;
const offPass = result.passes.find((p) => !p.alpr);
for (const pass of result.passes) {
for (const [index, stop] of pass.stops.entries()) {
const frames = framesIn(stop.holdEpochMs);
const counts = [];
for (const frame of frames)
counts.push({
atMs: Math.round(frame.timestamp * 1000 - stop.holdEpochMs[0]),
cyan: await cyanCount(frame.file),
});
stop.pixelTimeline = counts;
if (!pass.alpr) {
stop.cyanBaseline = median(counts.map((c) => c.cyan));
continue;
}
const baseline = offPass?.stops[index]?.cyanBaseline ?? 0;
const tail = counts.filter(
(c) => c.atMs >= stop.holdEpochMs[1] - stop.holdEpochMs[0] - 1000,
);
const final = median(tail.map((c) => c.cyan)) - baseline;
stop.renderedMarkerPixels = final;
let visibleAt = null;
if (final >= MIN_MARKER_PIXELS) {
for (let i = counts.length - 1; i >= 0; i--) {
if (counts[i].cyan - baseline >= final * 0.8)
visibleAt = counts[i].atMs;
else break;
}
}
stop.renderedVisibleAtMs = visibleAt;
console.log(
` ${pass.name} ${stop.stop.padEnd(22)} on screen ${stop.onScreen ?? '-'} marker px ${final} visible at ${visibleAt ?? '-'} ms (state ${stop.timeToCamerasMs} ms) end-of-hold px ${stop.staleFrame?.shown} / forced ${stop.staleFrame?.forced}`,
);
}
}
// Aggregate and assert.
const aggregate = (pass, phase) => {
const frames = pass.stops.map((s) => s[phase]);
const weighted = (key) => {
const total = frames.reduce((n, f) => n + (f.frames || 0), 0);
return (
Math.round(
(frames.reduce((n, f) => n + (f[key] || 0) * (f.frames || 0), 0) /
Math.max(1, total)) *
10,
) / 10
);
};
return {
frameP50: weighted('frameP50'),
frameP95: weighted('frameP95'),
renderP50: weighted('renderP50'),
renderP95: weighted('renderP95'),
longFrames: frames.reduce((n, f) => n + f.longFrames, 0),
};
};
result.summary = result.passes.map((pass) => ({
pass: pass.name,
moving: aggregate(pass, 'moving'),
holding: aggregate(pass, 'holding'),
}));
console.table(
result.summary.map((s) => ({
pass: s.pass,
'move p50/p95': `${s.moving.frameP50}/${s.moving.frameP95}`,
'hold p50/p95': `${s.holding.frameP50}/${s.holding.frameP95}`,
'render hold p50/p95': `${s.holding.renderP50}/${s.holding.renderP95}`,
'long frames': s.moving.longFrames + s.holding.longFrames,
})),
);
if (assertOn) {
const off = result.summary.find((s) => s.pass.includes('off'));
const warm = result.summary.find((s) => s.pass.endsWith('warm'));
const warmPass = result.passes.find((p) => p.name.endsWith('warm'));
const onPasses = result.passes.filter((p) => p.alpr);
if (off && warm) {
for (const phase of ['moving', 'holding'])
for (const key of ['frameP50', 'frameP95'])
if (warm[phase][key] > off[phase][key] * 1.1 + 2)
fail(
`${phase} ${key} ${warm[phase][key]} ms with ALPR vs ${off[phase][key]} ms without`,
);
}
for (const stop of warmPass?.stops || []) {
if (
stop.scale !== 'city' &&
!(
stop.timeToCamerasMs !== null &&
stop.timeToCamerasMs <= 2000 &&
stop.cameras > 0
)
)
fail(
`${stop.stop}: cameras after ${stop.timeToCamerasMs} ms (${stop.cameras}) on the warm pass`,
);
}
for (const pass of onPasses)
for (const stop of pass.stops) {
if (stop.staleFrame?.stale)
fail(
`${pass.name} ${stop.stop}: screen showed ${stop.staleFrame.shown} marker px, ${stop.staleFrame.forced} after forcing a render`,
);
// Cameras on screen must be painted, and painted when state says so.
if (stop.onScreen > 0 && stop.renderedMarkerPixels < MIN_MARKER_PIXELS)
fail(
`${pass.name} ${stop.stop}: ${stop.onScreen} cameras on screen but no markers rendered`,
);
if (
stop.renderedVisibleAtMs !== null &&
stop.renderedVisibleAtMs > stop.timeToCamerasMs + 700
)
fail(
`${pass.name} ${stop.stop}: markers rendered ${stop.renderedVisibleAtMs} ms after the stop, ${stop.timeToCamerasMs} ms after state`,
);
if (
pass.name.endsWith('warm') &&
stop.scale !== 'city' &&
stop.renderedVisibleAtMs !== null &&
stop.renderedVisibleAtMs > 2000
)
fail(
`${pass.name} ${stop.stop}: markers visible only after ${stop.renderedVisibleAtMs} ms`,
);
if (
stop.cameras > 0 &&
stop.onScreen === 0 &&
!/none on screen/i.test(stop.label)
)
fail(
`${pass.name} ${stop.stop}: no camera on screen and no explicit state`,
);
if (stop.loadingAtHoldEnd)
fail(`${pass.name} ${stop.stop}: still loading when the hold ended`);
if (
stop.scale === 'city' &&
!(stop.cameras > 0 || /zoom in/i.test(stop.label))
)
fail(
`${pass.name} ${stop.stop}: neither cameras nor a zoom-in prompt`,
);
if (stop.visualChanges > 1 || stop.lateChanges > 0)
fail(
`${pass.name} ${stop.stop}: ${stop.visualChanges} marker changes (${stop.lateChanges} after 3 s) while the camera was still`,
);
}
const errors = result.consoleErrors.filter(
(text) => !/favicon/i.test(text),
);
if (errors.length)
fail(`console errors: ${errors.slice(0, 3).join(' | ')}`);
}
} catch (error) {
fail(error.stack || error.message);
} finally {
fs.writeFileSync(
path.join(outDir, 'result.json'),
JSON.stringify(result, null, 1),
);
await browser.close();
}
console.log(
`${result.failures.length ? 'FAIL' : 'PASS'} — ${path.join(outDir, 'result.json')}`,
);
process.exit(result.failures.length ? 1 : 0);
+590
View File
@@ -0,0 +1,590 @@
#!/usr/bin/env node
/**
* qa-context-arrivals.mjs — search arrivals above the surface, and Contacts
* listing mapped installations around a moving tracked aircraft.
*
* Run: node scripts/qa-context-arrivals.mjs http://localhost:4173 [--headful]
* [--out qa-shots/context-arrivals] [--only arrivals|contacts]
*
* Arrivals: types "Camp Mabry" and "39.7392, -104.9903" (downtown Denver)
* into the location search, waits for the flight and the photoreal tiles,
* orbits a little like a user looking around, and asserts the camera sits at
* least 60 m above the rendered surface under it and under the view centre.
*
* Contacts: serves two synthetic aircraft through the page's own
* /api/opensky response (one flying north over Camp Mabry at 2,500 m, one
* near Fort Cavazos) and an empty military feed, starts from a wide Texas
* view, and presses CONTACTS as a user would. Asserts the Mapped
* installations row lists at least one site with a distance within 15 s,
* never falls back to "?" while the aircraft moves (30 s), keeps its site
* entities (no rebuilds while the view is still), still lists sites in
* Cockpit, and that SEARCH NEARBY SITES reports what it found. Then re-enters
* Contacts on the Fort Cavazos aircraft and asserts a site within 30 km.
* Requires zero console errors.
*
* Writes <out>/result.json, screenshots and <out>/clip.mp4 (CDP screencast
* with real frame timing; needs ffmpeg).
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { createScreencast, moveCamera } from './qa-journey-recorder.mjs';
const argv = process.argv.slice(2);
if (argv.includes('--help') || argv.includes('-h')) {
console.log(
'Usage: node scripts/qa-context-arrivals.mjs <dev-server-url> [--headful] [--out dir] [--only arrivals|contacts]',
);
process.exit(0);
}
const option = (name, fallback) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback;
};
const appUrl =
argv.find((arg) => /^https?:/.test(arg)) || 'http://localhost:4173';
const outDir = path.resolve(option('--out', 'qa-shots/context-arrivals'));
const only = option('--only', null);
fs.mkdirSync(outDir, { recursive: true });
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const result = {
url: appUrl,
arrivals: [],
contacts: {},
consoleErrors: [],
failures: [],
};
const check = (name, ok, detail = '') => {
console.log(
`[${ok ? 'PASS' : 'FAIL'}] ${name}${detail ? ` — ${detail}` : ''}`,
);
if (!ok) result.failures.push(`${name}${detail ? ` — ${detail}` : ''}`);
};
const browser = await puppeteer.launch({
headless: !argv.includes('--headful'),
protocolTimeout: 300_000,
args: [
'--no-sandbox',
...(process.platform === 'darwin'
? ['--use-angle=metal', '--enable-gpu']
: ['--use-gl=angle', '--use-angle=swiftshader']),
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--disable-backgrounding-occluded-windows',
],
});
async function openPage({ aircraft = false } = {}) {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
page.on('console', (message) => {
if (message.type() === 'error') result.consoleErrors.push(message.text());
});
page.on('pageerror', (error) => result.consoleErrors.push(error.message));
page.on('response', (response) => {
if (response.status() < 400) return;
const url = new URL(response.url());
// Track/enrichment lookups for the synthetic ICAOs have no upstream record.
const synthetic = /aaa05\d/i.test(url.search + url.pathname);
(synthetic
? (result.expectedMisses ??= [])
: (result.httpErrors ??= [])
).push(`${response.status()} ${url.pathname}${url.search}`);
});
await page.evaluateOnNewDocument((aircraft) => {
sessionStorage.setItem('gev:first-run-mission-session:v1', 'dismissed');
if (!aircraft) return;
// Synthetic traffic through the app's own feed endpoints: one aircraft
// flying north over Camp Mabry, one loitering near Fort Cavazos.
const started = Date.now();
const realFetch = window.fetch.bind(window);
window.fetch = (input, init) => {
const raw =
typeof input === 'string' || input instanceof URL
? String(input)
: input?.url;
let url;
try {
url = new URL(raw, location.href);
} catch {
return realFetch(input, init);
}
if (url.origin === location.origin && url.pathname === '/api/opensky') {
const now = Math.floor(Date.now() / 1000);
const seconds = (Date.now() - started) / 1000;
const lat = 30.29 + (seconds * 120) / 111_320;
const json = {
time: now,
states: [
[
'aaa051',
'QA051',
'Synthetic',
now,
now,
-97.765,
lat,
2500,
false,
120,
0,
0,
null,
2500,
null,
false,
0,
],
[
'aaa052',
'QA052',
'Synthetic',
now,
now,
-97.72,
31.12,
3000,
false,
90,
270,
0,
null,
3000,
null,
false,
0,
],
],
};
return Promise.resolve(Response.json(json));
}
if (url.origin === location.origin && url.pathname === '/api/adsblol/mil')
return Promise.resolve(
Response.json({ ac: [], now: Date.now(), total: 0 }),
);
return realFetch(input, init);
};
}, aircraft);
const url = new URL(appUrl);
url.searchParams.set('welcome', '0');
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 90_000 });
await page.waitForFunction(
() =>
window.__godsEyeView?.dataManager &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 120_000 },
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await page.keyboard.press('Escape');
return page;
}
async function waitTiles(page, maxMs = 30_000) {
await page
.waitForFunction(
() => {
const { scene } = window.__godsEyeView.viewer;
if (scene.globe.show) return scene.globe.tilesLoaded;
for (let i = 0; i < scene.primitives.length; i++) {
const p = scene.primitives.get(i);
if (p.show && p.tilesLoaded === false) return false;
}
return true;
},
{ timeout: maxMs, polling: 250 },
)
.catch(() => {});
}
/** Camera height above the rendered surface under it and under the view centre. */
function clearance(page) {
return page.evaluate(() => {
const { viewer } = window.__godsEyeView;
const { scene, camera } = viewer;
const carto = camera.positionCartographic.clone();
const sample = (c) => {
try {
const h = scene.sampleHeight(c);
return Number.isFinite(h) ? h : null;
} catch {
return null;
}
};
const under = sample(carto);
let centre = null;
const canvas = scene.canvas;
const hit = scene.pickPosition?.({
x: canvas.clientWidth / 2,
y: canvas.clientHeight / 2,
});
if (hit) centre = scene.globe.ellipsoid.cartesianToCartographic(hit);
const centreSurface = centre ? sample(centre) : null;
const surface = Math.max(under ?? -Infinity, centreSurface ?? -Infinity);
return {
lat: (carto.latitude * 180) / Math.PI,
lon: (carto.longitude * 180) / Math.PI,
cameraM: Math.round(carto.height),
surfaceUnderM: under === null ? null : Math.round(under),
surfaceCentreM: centreSurface === null ? null : Math.round(centreSurface),
clearanceM: Number.isFinite(surface)
? Math.round(carto.height - surface)
: null,
pitchDeg: Math.round((camera.pitch * 180) / Math.PI),
};
});
}
async function arrivals() {
const page = await openPage();
const dir = path.join(outDir, 'arrivals');
fs.mkdirSync(dir, { recursive: true });
const recorder = await createScreencast(page, dir);
await recorder.start();
for (const query of ['Camp Mabry', '39.7392, -104.9903']) {
await page.evaluate(() =>
document.getElementById('search-toggle')?.click(),
);
await page.focus('#location-search');
await page.evaluate(() => {
document.getElementById('location-search').value = '';
});
await page.type('#location-search', query, { delay: 40 });
await page.keyboard.press('Enter');
// Wait for the flight to land: the camera still for 1.5 s.
const landed = Date.now();
let last = null;
let stillSince = null;
while (Date.now() - landed < 30_000) {
const now = await page.evaluate(() => {
const c = window.__godsEyeView.viewer.camera.positionWC;
return [c.x, c.y, c.z];
});
const moved =
!last ||
Math.hypot(now[0] - last[0], now[1] - last[1], now[2] - last[2]) > 0.5;
last = now;
if (moved) stillSince = null;
else stillSince ??= Date.now();
if (
Date.now() - landed > 4000 &&
stillSince &&
Date.now() - stillSince > 1500
)
break;
await sleep(250);
}
await waitTiles(page);
await sleep(2500); // the ground guard measures after tiles stream in
const arrived = await clearance(page);
const slug = query.replace(/\W+/g, '-').replace(/^-|-$/g, '').toLowerCase();
await page.screenshot({ path: path.join(dir, `arrival-${slug}.png`) });
// Look around: a gentle orbit, then check the eye still clears the ground.
const view = await page.evaluate(() => {
const c = window.__godsEyeView.viewer.camera;
return {
lat: (c.positionCartographic.latitude * 180) / Math.PI,
lon: (c.positionCartographic.longitude * 180) / Math.PI,
height: c.positionCartographic.height,
heading: (c.heading * 180) / Math.PI,
pitch: (c.pitch * 180) / Math.PI,
};
});
await moveCamera(page, { ...view, heading: view.heading + 25 }, 2.5);
await sleep(1500);
const lookedAround = await clearance(page);
result.arrivals.push({ query, arrived, lookedAround });
check(
`${query}: arrival clears the rendered surface`,
arrived.clearanceM !== null && arrived.clearanceM >= 60,
JSON.stringify(arrived),
);
}
result.arrivalClip = await recorder.stop();
await page.close();
}
async function contacts() {
const page = await openPage({ aircraft: true });
const dir = path.join(outDir, 'contacts');
fs.mkdirSync(dir, { recursive: true });
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('flights', true, {
origin: 'user',
}),
);
// A wide Texas view: installation viewports are unbounded here.
await moveCamera(
page,
{ lat: 30.35, lon: -97.76, height: 900_000, pitch: -90 },
0,
);
await sleep(3000);
// In-page probe: row text and installation entity churn every 250 ms.
await page.evaluate(() => {
const samples = [];
window.__gevContextSamples = samples;
let created = 0;
const seen = new WeakSet();
setInterval(() => {
const row = [
...document.querySelectorAll(
'#military-awareness-panel .military-awareness-row',
),
].find(
(candidate) =>
candidate.querySelector('strong')?.textContent?.trim() ===
'Mapped installations',
);
const ds = window.__godsEyeView.viewer.dataSources.getByName(
'military-installations',
)[0];
for (const entity of ds?.entities.values || [])
if (!seen.has(entity)) {
seen.add(entity);
created += 1;
}
samples.push({
t: performance.now(),
count: row?.querySelector('b')?.textContent?.trim() ?? null,
text: row?.innerText?.replace(/\s+/g, ' ').trim() ?? null,
installations: [
...(row?.querySelectorAll('button[data-awareness-id]') || []),
].map((button) => ({
id: button.dataset.awarenessId,
name: button.childNodes[0]?.textContent?.trim(),
distance: button.querySelector('span')?.textContent?.trim(),
})),
entities: ds?.entities.values.length ?? 0,
created,
tracked:
window.__godsEyeView.viewer.trackedEntity?.gevTrackedId ?? null,
cockpit: document.body.classList.contains('cockpit-mode'),
});
if (samples.length > 5000) samples.splice(0, 1000);
}, 250);
});
const recorder = await createScreencast(page, dir);
await recorder.start();
const t0 = await page.evaluate(() => performance.now());
await page.evaluate(() =>
document.getElementById('global-context-flights-btn').click(),
);
await page
.waitForFunction(
() =>
window.__godsEyeView.viewer.trackedEntity?.gevTrackedId ===
'flights:aaa051',
{ timeout: 60_000 },
)
.catch(() => {});
const readSamples = (from) =>
page.evaluate(
(a) => window.__gevContextSamples.filter((s) => s.t >= a),
from,
);
const numeric = (s) => /^\d+/.test(String(s.count ?? ''));
// Up to 15 s for the first answer, then watch 30 s of flight.
const deadline = Date.now() + 15_000;
let first = null;
while (Date.now() < deadline && !first) {
first = (await readSamples(t0)).find(
(s) =>
numeric(s) &&
Number.parseInt(s.count, 10) >= 1 &&
/\d+(\.\d+)? k?m\b/.test(s.text),
);
if (!first) await sleep(500);
}
check(
'Contacts lists a mapped installation with a distance',
Boolean(first),
first
? `${Math.round(first.t - t0)} ms: ${first.text}`
: 'row never answered',
);
await page.screenshot({ path: path.join(dir, 'contacts-camp-mabry.png') });
await sleep(30_000);
const flight = (await readSamples(first?.t ?? t0)).filter(
(s) => s.tracked === 'flights:aaa051',
);
const reverted = flight.filter((s) => !numeric(s));
const createdDuring = flight.length
? flight.at(-1).created - flight[0].created
: 0;
result.contacts.mabry = {
firstAnswerMs: first ? Math.round(first.t - t0) : null,
firstText: first?.text ?? null,
lastText: flight.at(-1)?.text ?? null,
samples: flight.length,
revertedSamples: reverted.length,
entities: flight.at(-1)?.entities ?? 0,
entitiesCreatedWhileTracking: createdDuring,
};
check(
'the row never falls back to "?" while the aircraft moves',
reverted.length === 0,
`${reverted.length}/${flight.length}`,
);
check(
'site entities are not rebuilt while tracking',
createdDuring === 0,
`${createdDuring} created`,
);
// Cockpit: the same context must hold with a horizon view.
const cockpit = await page.evaluate(() => {
const view = window.__godsEyeView.styleManager.cockpitView;
view.syncEntry();
return view.enter();
});
await sleep(12_000);
const inCockpit = (
await readSamples((await page.evaluate(() => performance.now())) - 8000)
).filter((s) => s.cockpit);
const scope = await page.evaluate(
() =>
document.getElementById('cockpit-context-subject')?.textContent || null,
);
await page.screenshot({ path: path.join(dir, 'cockpit-camp-mabry.png') });
result.contacts.cockpit = {
entered: cockpit,
samples: inCockpit.length,
last: inCockpit.at(-1)?.text ?? null,
scope,
};
check(
'Cockpit scope names the installation window',
/INSTALLATIONS WITHIN \d+ KM/.test(scope || ''),
scope || 'no scope',
);
check(
'Cockpit keeps mapped installations listed',
cockpit !== false &&
inCockpit.length > 0 &&
inCockpit.every((s) => numeric(s) && Number.parseInt(s.count, 10) >= 1),
inCockpit.at(-1)?.text || 'no cockpit samples',
);
await page.evaluate(() =>
window.__godsEyeView.styleManager.cockpitView.exit?.(),
);
await sleep(2000);
// SEARCH NEARBY SITES, as the user presses it.
await page.evaluate(() => {
document.getElementById('toast').textContent = '';
document.getElementById('installations-search-btn').click();
});
await page
.waitForFunction(
() => document.getElementById('toast').textContent.trim().length > 0,
{ timeout: 20_000 },
)
.catch(() => {});
const toast = await page.evaluate(() =>
document.getElementById('toast').textContent.trim(),
);
await sleep(1500);
const afterSearch = (
await readSamples((await page.evaluate(() => performance.now())) - 1000)
).at(-1);
result.contacts.search = { toast, row: afterSearch?.text ?? null };
check(
'SEARCH NEARBY SITES reports what it found and the row still lists sites',
/mapped sites? within \d+ km/i.test(toast) && numeric(afterSearch || {}),
`toast "${toast}" · ${afterSearch?.text}`,
);
// Fort Cavazos: leave Contacts, track the other aircraft, re-enter.
await page.evaluate(() =>
document.getElementById('global-context-flights-btn').click(),
);
await sleep(2000);
await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('flights')
.module.trackById('aaa052'),
);
await sleep(3000);
const t1 = await page.evaluate(() => performance.now());
await page.evaluate(() =>
document.getElementById('global-context-flights-btn').click(),
);
let cavazos = null;
const cavazosDeadline = Date.now() + 20_000;
while (Date.now() < cavazosDeadline) {
const latest = (await readSamples(t1))
.filter((s) => s.tracked === 'flights:aaa052' && numeric(s))
.at(-1);
const site = latest?.installations.find(
(record) => record.name === 'Fort Hood',
);
const nearest = site?.distance?.match(/^(\d+(?:\.\d+)?) (k?m)$/);
const km = nearest
? Number(nearest[1]) / (nearest[2] === 'm' ? 1000 : 1)
: null;
if (km !== null && km <= 30) {
cavazos = {
text: latest.text,
nearestKm: km,
site,
afterMs: Math.round(latest.t - t1),
};
break;
}
await sleep(500);
}
await sleep(2000);
await page.screenshot({ path: path.join(dir, 'contacts-fort-cavazos.png') });
result.contacts.cavazos = cavazos;
check(
'Fort Cavazos contact lists Fort Hood within 30 km',
Boolean(cavazos),
JSON.stringify(cavazos),
);
result.contactsClip = await recorder.stop();
await page.close();
}
try {
if (only !== 'contacts') await arrivals();
if (only !== 'arrivals') await contacts();
// A 404 for a synthetic aircraft's track logs "Failed to load resource";
// count those only when every failed response was such an expected miss.
const resourceErrors = result.consoleErrors.filter((text) =>
/Failed to load resource/.test(text),
).length;
const errors = result.consoleErrors.filter(
(text) =>
!/favicon/i.test(text) &&
!(
/Failed to load resource/.test(text) &&
!(result.httpErrors || []).length &&
resourceErrors <= (result.expectedMisses || []).length
),
);
check(
'zero console errors',
errors.length === 0,
errors.slice(0, 3).join(' | '),
);
check(
'zero failed app responses',
!(result.httpErrors || []).length,
(result.httpErrors || []).slice(0, 3).join(' | '),
);
} catch (error) {
check('harness completed', false, error.stack || error.message);
} finally {
fs.writeFileSync(
path.join(outDir, 'result.json'),
JSON.stringify(result, null, 1),
);
await browser.close();
}
console.log(
`${result.failures.length ? 'FAIL' : 'PASS'} — ${path.join(outDir, 'result.json')}`,
);
process.exit(result.failures.length ? 1 : 0);
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env node
/** Persistent inline attribution through actual OSM layer display lifetimes. */
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import puppeteer from 'puppeteer';
import { moveCamera } from './qa-journey-recorder.mjs';
const url = process.argv.find((a) => /^https?:/.test(a));
if (!url) throw new Error('Supply the running server URL');
const dir = process.env.QA_POLISH_DIR || 'qa-shots/final-polish';
await fs.mkdir(dir, { recursive: true });
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const result = { states: [], errors: [] };
const browser = await puppeteer.launch({
headless: true,
args: ['--use-angle=metal'],
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
page.on('pageerror', (e) => result.errors.push(e.message));
page.on('console', (m) => {
if (m.type() === 'error') result.errors.push(m.text());
});
await page.evaluateOnNewDocument(() =>
sessionStorage.setItem('gev:first-run-mission-session:v1', 'dismissed'),
);
await page.goto(`${url}/?welcome=0`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() =>
window.__godsEyeView?.dataManager &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 120000 },
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await page.keyboard.press('Escape');
await page.evaluate(async () => {
const g = window.__godsEyeView;
for (const [id] of g.dataManager.layers)
await g.dataManager.setEnabled(id, false);
g.styleManager.setDetection({ enabled: false });
});
await moveCamera(
page,
{ lon: -97.765, lat: 30.3125, height: 3500, pitch: -80 },
0,
);
const toggle = (id, value) =>
page.evaluate(
(id, value) => window.__godsEyeView.dataManager.setEnabled(id, value),
id,
value,
);
const text = () => page.$eval('#cesium-credits', (e) => e.innerText);
const shot = async (name, osm, tiles) => {
await sleep(300);
const state = await page.$eval('#cesium-credits', (e) => {
const nodes = [
...e.querySelectorAll('a,img,.cesium-credit-expand-link'),
].filter((n) =>
n.checkVisibility({ visibilityProperty: true, opacityProperty: true }),
);
return {
text: e.innerText,
rects: nodes
.map((n) => {
const b = n.getBoundingClientRect();
return {
text: n.textContent || n.alt || 'image',
x: b.x,
y: b.y,
width: b.width,
height: b.height,
reachable: n.contains(
document.elementFromPoint(
b.x + b.width / 2,
b.y + b.height / 2,
),
),
};
})
.filter((b) => b.width > 0 && b.height > 0),
};
});
assert.equal(state.text.includes('© OpenStreetMap'), osm);
assert.equal(state.text.includes('© OpenMapTiles'), tiles);
assert.equal(
(state.text.match(/© OpenStreetMap/g) || []).length,
osm ? 1 : 0,
);
assert.ok(state.rects.length >= 3);
const centers = state.rects.map((b) => b.y + b.height / 2);
assert.ok(
Math.max(...centers) - Math.min(...centers) < 12,
`${name}: credits stay on one desktop row`,
);
assert.ok(
state.rects.every(
(b) => b.x >= 0 && b.x + b.width <= page.viewport().width,
),
'no credit clipped',
);
assert.ok(
state.rects.every((b) => b.reachable),
'all credits and the attribution link remain clear of controls',
);
await page.screenshot({ path: `${dir}/after-${name}.png` });
await page.screenshot({
path: `${dir}/after-${name}-row.png`,
clip: { x: 16, y: 750, width: 1150, height: 150 },
});
result.states.push({ name, ...state });
};
await sleep(1600);
await shot('nothing', false, false);
await toggle('alpr-cameras', true);
await page.waitForFunction(
() =>
document
.querySelector('#cesium-credits')
.innerText.includes('© OpenStreetMap'),
{ timeout: 60000 },
);
await shot('alpr', true, false);
await sleep(6000);
await shot('alpr-persistent', true, false);
await toggle('traffic', true);
await page.waitForFunction(
() =>
document
.querySelector('#cesium-credits')
.innerText.includes('© OpenMapTiles'),
{ timeout: 60000 },
);
await shot('alpr-and-traffic', true, true);
await toggle('alpr-cameras', false);
await shot('traffic', true, true);
for (const width of [1280, 1920]) {
await page.setViewport({ width, height: 900 });
await sleep(500);
await shot(`traffic-${width}`, true, true);
}
await page.setViewport({ width: 1440, height: 900 });
await page.evaluate(() =>
window.__godsEyeView.styleManager.setCleanView(true),
);
await shot('traffic-clean-ui', true, true);
await page.evaluate(() =>
window.__godsEyeView.styleManager.setCleanView(false),
);
await toggle('traffic', false);
await shot('nothing-restored', false, false);
assert.deepEqual(result.errors, []);
console.log(JSON.stringify(result, null, 2));
} finally {
await fs.writeFile(`${dir}/credits.json`, JSON.stringify(result, null, 2));
await browser.close();
}
+412
View File
@@ -0,0 +1,412 @@
#!/usr/bin/env node
/** Mapped installations: isolated camera navigation, cached revisits and Contacts release. */
import fs from 'node:fs';
import puppeteer from 'puppeteer';
import { moveCamera } from './qa-journey-recorder.mjs';
const args = process.argv.slice(2);
if (args.includes('--help')) {
console.log(
'Usage: node scripts/qa-installation-navigation.mjs <url> [--baseline]',
);
process.exit(0);
}
const baseline = args.includes('--baseline');
const result = { views: [], errors: [], forbidden: [], failures: [] };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const browser = await puppeteer.launch({
headless: true,
args: ['--use-angle=metal', '--disable-background-timer-throttling'],
protocolTimeout: 120000,
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
const tiles = [];
page.on('request', (r) => {
const u = new URL(r.url());
if (
u.hostname === 'tiles.openfreemap.org' &&
/\/\d+\/\d+\/\d+(\.pbf)?$/.test(u.pathname)
)
tiles.push(u.pathname);
if (
/overpass/i.test(u.hostname) ||
u.hostname === 'nominatim.openstreetmap.org'
)
result.forbidden.push(u.hostname);
});
page.on('pageerror', (e) => result.errors.push(e.message));
page.on('console', (m) => {
if (m.type() === 'error') result.errors.push(m.text());
});
await page.evaluateOnNewDocument(() =>
sessionStorage.setItem('gev:first-run-mission-session:v1', 'dismissed'),
);
await page.goto(
args.find((a) => /^https?:/.test(a)),
{ waitUntil: 'domcontentloaded' },
);
await page.waitForFunction(
() =>
window.__godsEyeView?.dataManager &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 120000 },
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await page.keyboard.press('Escape');
await page.evaluate(async () => {
const g = window.__godsEyeView;
for (const [id] of g.dataManager.layers)
await g.dataManager.setEnabled(id, false);
g.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
window.installationEvents = { changed: 0, end: 0 };
window.installationEntityRefs = new Map();
g.viewer.camera.changed.addEventListener(
() => window.installationEvents.changed++,
);
g.viewer.camera.moveEnd.addEventListener(
() => window.installationEvents.end++,
);
});
let low = 60000,
high = 400000;
for (let i = 0; i < 9; i++) {
const height = (low + high) / 2;
await moveCamera(page, { lat: 32.72, lon: -117.16, height, pitch: -90 }, 0);
const fits = await page.evaluate(async () => {
const { installationTileZoom } =
await import('/src/layers/installations/source.js');
const r = window.__godsEyeView.viewer.camera.computeViewRectangle();
const d = 180 / Math.PI;
return (
r &&
installationTileZoom({
west: r.west * d,
east: r.east * d,
south: r.south * d,
north: r.north * d,
}) !== null
);
});
if (fits) low = height;
else high = height;
}
const widestHeight = Math.floor(low);
result.widestHeight = widestHeight;
result.firstTooWideHeight = Math.ceil(high);
let enabled = false;
const places = [
['san-diego', 32.72, -117.16],
['pendleton', 33.33, -117.4],
['miramar', 32.87, -117.14],
['san-diego-return', 32.72, -117.16],
];
for (const height of [
5000,
20000,
60000,
150000,
widestHeight,
400000,
5000,
]) {
for (const [name, lat, lon] of height >= 150000
? places.slice(0, 1)
: places) {
const before = tiles.length;
const previous = await page.evaluate(
() =>
window.__godsEyeView.dataManager.layers
.get('military-installations')
.module.getStats().lastUpdate,
);
await moveCamera(
page,
{ lat, lon, height, pitch: -90 },
enabled ? 0.6 : 0,
);
const stop = Date.now();
if (!enabled) {
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled(
'military-installations',
true,
),
);
enabled = true;
}
await page
.waitForFunction(
(prev, wide) => {
const s = window.__godsEyeView.dataManager.layers
.get('military-installations')
.module.getStats();
return (
!s.loading &&
(s.lastUpdate > prev || (wide && s.status === 'zoom-in'))
);
},
{ timeout: 6000 },
previous,
height === 400000,
)
.catch(() => {});
const snapshot = await page.evaluate(() => {
const g = window.__godsEyeView;
const ds = g.viewer.dataSources.getByName('military-installations')[0];
const points = ds.entities.values.filter((e) => e.point);
const stable = points.every(
(e) =>
!window.installationEntityRefs.has(e.id) ||
window.installationEntityRefs.get(e.id) === e,
);
window.installationEntityRefs = new Map(points.map((e) => [e.id, e]));
return {
...g.dataManager.layers
.get('military-installations')
.module.getStats(),
stable,
ids: points.map((e) => e.id),
events: { ...window.installationEvents },
};
});
const row = {
name,
height,
ms: Date.now() - stop,
tiles: tiles.length - before,
updated: snapshot.lastUpdate > previous,
...snapshot,
};
if (
(!row.stable && result.views.at(-1)?.height === height) ||
new Set(row.ids).size !== row.ids.length
)
result.failures.push(
`${name}/${height}: unstable or duplicate site entities`,
);
if (name.endsWith('return')) {
const first = result.views.find(
(v) => v.name === 'san-diego' && v.height === height,
);
if (
first &&
JSON.stringify([...first.ids].sort()) !==
JSON.stringify([...row.ids].sort())
)
result.failures.push(
`${name}/${height}: changed site ids on revisit`,
);
}
result.views.push(row);
console.log(JSON.stringify(row));
if (!row.updated && row.status !== 'zoom-in')
result.failures.push(`${name}/${height}: no reload`);
if (row.ms > 1500 && row.status !== 'zoom-in')
result.failures.push(`${name}/${height}: ${row.ms} ms`);
if (name.endsWith('return') && row.tiles)
result.failures.push(`${name}/${height}: revisit fetched ${row.tiles}`);
}
}
for (const gesture of [
'wheel',
'keyboard',
'contacts-close',
'contacts-clear',
]) {
let previous = await page.evaluate(
() =>
window.__godsEyeView.dataManager.layers
.get('military-installations')
.module.getStats().lastUpdate,
);
if (gesture === 'wheel') {
await page.mouse.move(640, 400);
await page.mouse.wheel({ deltaY: -350 });
} else if (gesture === 'keyboard') {
await page.evaluate(() =>
document.getElementById('search-toggle').click(),
);
await page.focus('#location-search');
await page.evaluate(() => {
document.getElementById('location-search').value = '';
});
await page.type('#location-search', '33.33, -117.40');
await page.keyboard.press('Enter');
await sleep(4500);
} else {
await moveCamera(
page,
{ lat: 32.87, lon: -117.14, height: 5000, pitch: -90 },
0.6,
);
await page.waitForFunction(
() =>
window.__godsEyeView.viewer.dataSources
.getByName('military-installations')[0]
.entities.values.some((e) => e.point),
{ timeout: 5000 },
);
await page.evaluate(() => {
const button = document.getElementById('global-context-flights-btn');
if (button.getAttribute('aria-selected') !== 'true') button.click();
});
await page.waitForFunction(
() => {
const g = window.__godsEyeView;
return (
g.dataManager.isEffectivelyEnabled('military-awareness') &&
!g.dataManager.layers.get('military-awareness').module.getParams()
.passive
);
},
{ timeout: 15000 },
);
await sleep(1000);
await page.evaluate(async () => {
const g = window.__godsEyeView;
const ds = g.viewer.dataSources.getByName('military-installations')[0];
const id = ds.entities.values.find((e) => e.point).id;
window.installationSubjectId = id;
await g.dataManager.layers
.get('military-awareness')
.module.focusTarget('military-installations', id);
});
await page.waitForFunction(
() => {
const a =
window.__godsEyeView.dataManager.layers.get(
'military-awareness',
).module;
const subject = a.getContextSnapshot()?.subject;
return (
subject?.layerId === 'military-installations' &&
subject.id === window.installationSubjectId &&
!a._getAwarenessNavigationStateForTest().pendingSelectionKey
);
},
{ timeout: 15000 },
);
await page.waitForFunction(
() => !window.__godsEyeView.viewer.camera._currentFlight,
{ timeout: 10000 },
);
await page.waitForFunction(
() =>
window.__godsEyeView.dataManager.layers
.get('military-installations')
.module.getStats().coverage.kind === 'subject',
{ timeout: 10000 },
);
await page.waitForFunction(
() =>
document
.getElementById('global-context-flights-btn')
.getAttribute('aria-busy') === 'false',
{ timeout: 30000 },
);
const anchored = await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('military-installations')
.module.getStats(),
);
if (anchored.coverage.kind !== 'subject')
result.failures.push(`${gesture}: Contacts did not acquire subject`);
result.contextClear = await page.evaluate(async (clear) => {
if (clear) {
const { clearSelectedEntityContextForLayer, getContextStore } =
await import('/src/data/contextStore.js');
const store = getContextStore();
const before = {
selectedId: store.selectedEntityId,
record: store.entities.get(store.selectedEntityId)?.id,
subject: window.__godsEyeView.dataManager.layers
.get('military-awareness')
.module.getContextSnapshot()?.subject,
};
clearSelectedEntityContextForLayer('military-installations');
return before;
} else document.getElementById('global-context-flights-btn').click();
}, gesture === 'contacts-clear');
if (gesture === 'contacts-clear') {
await page.waitForFunction(
() =>
!window.__godsEyeView.dataManager.layers
.get('military-awareness')
.module.getContextSnapshot(),
{ timeout: 5000 },
);
} else {
await page.waitForFunction(
() => {
const button = document.getElementById(
'global-context-flights-btn',
);
return (
button.getAttribute('aria-selected') === 'false' &&
button.getAttribute('aria-busy') === 'false'
);
},
{ timeout: 30000 },
);
}
await page.waitForFunction(
() => !window.__godsEyeView.viewer.camera._currentFlight,
{ timeout: 10000 },
);
await sleep(500);
previous = await page.evaluate(
() =>
window.__godsEyeView.dataManager.layers
.get('military-installations')
.module.getStats().lastUpdate,
);
await moveCamera(
page,
{ lat: 33.33, lon: -117.4, height: 5000, pitch: -90 },
0.6,
);
}
await page
.waitForFunction(
(prev) => {
const s = window.__godsEyeView.dataManager.layers
.get('military-installations')
.module.getStats();
return (
!s.loading && s.lastUpdate > prev && s.coverage.kind === 'viewport'
);
},
{ timeout: 6000 },
previous,
)
.catch(() => result.failures.push(`${gesture}: no viewport reload`));
const row = await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('military-installations')
.module.getStats(),
);
if (gesture.startsWith('contacts') && row.count !== 1)
result.failures.push(
`${gesture}: destination count ${row.count}, expected 1`,
);
result.views.push({ name: gesture, ...row });
console.log(JSON.stringify({ name: gesture, ...row }));
}
await page.screenshot({ path: 'qa-shots/installations-navigation.png' });
} finally {
fs.mkdirSync('qa-shots', { recursive: true });
fs.writeFileSync(
`qa-shots/installations-navigation-${baseline ? 'before' : 'after'}.json`,
JSON.stringify(result, null, 2),
);
await browser.close();
}
if (
!baseline &&
(result.failures.length || result.errors.length || result.forbidden.length)
)
process.exitCode = 1;
+404
View File
@@ -0,0 +1,404 @@
/** Shared browser assertions for installation label persistence and selection. */
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import { moveCamera, createScreencast } from './qa-journey-recorder.mjs';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export async function checkInstallationPan(page, dir) {
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('local-datacenters', true),
);
await sleep(3000);
await page.evaluate(async () => {
const { getOverlayPaintRect } =
await import('/src/overlays/worldOverlay.js');
const g = window.__godsEyeView;
const installations = g.dataManager.layers.get(
'military-installations',
).module;
const centerIds = [];
for (let i = 0; i < g.viewer.dataSources.length; i++)
for (const e of g.viewer.dataSources.get(i).entities.values)
if (e.__localLayerId === 'local-datacenters') centerIds.push(e.id);
window.__polishPanFrames = [];
window.__stopPolishPan = g.viewer.scene.postRender.addEventListener(() => {
const sites = [],
centers = [];
installations.visitNamedMarkers((id) => {
if (getOverlayPaintRect('military-installations', id)) sites.push(id);
});
for (const id of centerIds)
if (getOverlayPaintRect('local-datacenters', id)) centers.push(id);
window.__polishPanFrames.push({ t: performance.now(), sites, centers });
});
});
await moveCamera(page, { lon: -99, lat: 39, height: 6500000, pitch: -90 }, 8);
const frames = await page.evaluate(() => {
window.__stopPolishPan();
return window.__polishPanFrames;
});
const count = (key) => {
let appeared = 0,
disappeared = 0,
reappeared = 0;
const seen = new Set(frames[0][key]);
for (let i = 1; i < frames.length; i++) {
const previous = new Set(frames[i - 1][key]),
next = new Set(frames[i][key]);
frames[i][`${key}Appeared`] = [...next].filter((id) => !previous.has(id));
frames[i][`${key}Disappeared`] = [...previous].filter(
(id) => !next.has(id),
);
for (const id of frames[i][`${key}Appeared`]) {
appeared++;
if (seen.has(id)) reappeared++;
seen.add(id);
}
disappeared += frames[i][`${key}Disappeared`].length;
}
const mean =
frames.reduce((sum, f) => sum + f[key].length, 0) / frames.length;
return {
appeared,
disappeared,
reappeared,
mean,
eventsPerLabel: (appeared + disappeared) / mean,
};
};
const result = {
frames: frames.length,
installations: count('sites'),
datacenters: count('centers'),
};
await fs.writeFile(
`${dir}/slow-pan-frames.json`,
JSON.stringify(frames, null, 2),
);
await fs.writeFile(`${dir}/slow-pan.json`, JSON.stringify(result, null, 2));
assert.ok(frames.length > 100);
assert.ok(
result.installations.mean >= 5 && result.datacenters.mean >= 5,
'both real layers contribute visible labels on the same pan',
);
assert.ok(
frames.every((f) => f.sites.length <= 24),
'shared-domain budget cannot exceed the installation title cap',
);
assert.ok(
result.installations.eventsPerLabel <=
result.datacenters.eventsPerLabel + 0.1,
'installation label transitions per label match Data Centers on the same slow pan',
);
assert.ok(
result.installations.reappeared <= result.datacenters.reappeared,
'installation labels do not flicker off and back on more than Data Centers',
);
await page.screenshot({ path: `${dir}/slow-pan-side-by-side.png` });
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('local-datacenters', false),
);
return result;
}
export async function checkInstallationSelection(
page,
dir,
{ video = true } = {},
) {
const result = [];
const recorder = video
? await createScreencast(page, `${dir}/selection-orbit`, { fps: 24 })
: null;
await recorder?.start();
try {
for (const site of [
{
key: 'mabry',
name: 'Camp Mabry',
lon: -97.765,
lat: 30.3125,
height: 3500,
},
{
key: 'naval',
name: 'Naval Base San Diego',
lon: -117.119,
lat: 32.684,
height: 6500,
},
]) {
await moveCamera(page, { ...site, pitch: -80 }, 1.5);
await page.waitForFunction(
() => {
const s = window.__godsEyeView.dataManager.layers
.get('military-installations')
.module.getStats();
return !s.loading && s.count > 0;
},
{ timeout: 60000 },
);
await sleep(1500);
const picked = await page.evaluate(async (site) => {
const C = await import('/node_modules/cesium/Build/Cesium/index.js');
const g = window.__godsEyeView,
layer = g.dataManager.layers.get('military-installations').module;
const record = layer
.getNearby(C.Cartesian3.fromDegrees(site.lon, site.lat), 100000, 1000)
.find((r) => r.name === site.name && r.footprints?.length);
if (!record) throw new Error(`No polygon for ${site.name}`);
let point;
layer.visitNamedMarkers((id, p) => {
if (id === record.id) point = p.position;
});
const screen = C.SceneTransforms.worldToWindowCoordinates(
g.viewer.scene,
point,
);
window.__polishSelectedId = record.id;
window.__polishTitlePaints = [];
const canvas = document.getElementById('world-overlay-canvas'),
ctx = canvas.getContext('2d');
const original = ctx.fillText;
const matches = (text) =>
String(text).toLowerCase() === site.name.toLowerCase();
ctx.fillText = function (text, ...args) {
if (matches(text))
window.__polishTitlePaints.push({ t: performance.now(), text });
return original.call(this, text, ...args);
};
window.__restorePolishPaint = () => {
ctx.fillText = original;
};
window.__polishSelectionFrames = [];
const { getSelectedEntityContext } =
await import('/src/data/contextStore.js');
const { getOverlayPaintRect, getWorldOverlayDiagnostics } =
await import('/src/overlays/worldOverlay.js');
const knownFills = new Set();
const fillEntities = () => {
const list = [];
for (let i = 0; i < g.viewer.dataSources.length; i++)
for (const e of g.viewer.dataSources.get(i).entities.values)
if (e.id.startsWith(`${record.id}:selected-fill:`)) list.push(e);
for (const e of list) knownFills.add(e);
return list;
};
// Geometry-instance ids prove the ground primitive actually built, not just an entity declaration.
const primitives = () => {
let count = 0;
const walk = (collection) => {
if (collection?._collectionsArray) {
for (const child of collection._collectionsArray) walk(child);
return;
}
for (let i = 0; i < (collection?.length || 0); i++) {
const p = collection.get(i);
if (
p.ready &&
typeof p.getGeometryInstanceAttributes === 'function'
) {
for (const e of knownFills) {
try {
const a = p.getGeometryInstanceAttributes(e);
if (a && (!a.show || a.show[0])) count++;
} catch {}
}
}
if (typeof p.get === 'function' || p._collectionsArray) walk(p);
}
};
walk(g.viewer.scene.groundPrimitives);
return count;
};
window.__polishFillProbe = () => ({
entities: fillEntities().length,
primitives: primitives(),
classification: fillEntities().map((e) =>
e.polygon.classificationType.getValue(),
),
selected: getSelectedEntityContext()?.id || null,
});
window.__stopPolishSelection =
g.viewer.scene.postRender.addEventListener(() => {
window.__polishSelectionFrames.push({
paints: window.__polishTitlePaints.splice(0).length,
own: Boolean(
getOverlayPaintRect('military-installations', record.id),
),
tracked: Boolean(
getOverlayPaintRect('tracked', `installations:${record.id}`),
),
selected: getSelectedEntityContext()?.id === record.id,
total:
getWorldOverlayDiagnostics().paintedBySource[
'military-installations'
] || 0,
});
});
return {
id: record.id,
x: screen.x,
y: screen.y,
parts: record.footprints.length,
};
}, site);
await page.screenshot({
path: `${dir}/after-${site.key}-unselected.png`,
});
await page.mouse.click(picked.x, picked.y);
await page
.waitForFunction(
() => {
const p = window.__polishFillProbe();
return p.selected === window.__polishSelectedId && p.primitives > 0;
},
{ timeout: 20000 },
)
.catch(async (error) => {
await page.screenshot({ path: `${dir}/selection-failure.png` });
const diagnostic = await page.evaluate(() => {
const g = window.__godsEyeView;
const describe = (p) => ({
type: p.constructor.name,
ready: p.ready,
length: p.length,
keys: Object.keys(p).filter((k) =>
/primitive|instance|collection/i.test(k),
),
ids: p._instanceIds?.map((id) => id.id || String(id)),
children:
typeof p.get === 'function'
? Array.from({ length: p.length }, (_, i) =>
describe(p.get(i)),
)
: p._collectionsArray?.map(describe),
});
return {
probe: window.__polishFillProbe(),
stats: g.dataManager.layers
.get('military-installations')
.module.getStats(),
primitives: describe(g.viewer.scene.groundPrimitives),
frames: window.__polishSelectionFrames.slice(-5),
};
});
await fs.writeFile(
`${dir}/selection-failure.json`,
JSON.stringify({ picked, ...diagnostic }, null, 2),
);
throw error;
});
await sleep(500);
const selected = await page.evaluate(() => window.__polishFillProbe());
await page.screenshot({ path: `${dir}/after-selection-${site.key}.png` });
// Close, oblique angles expose surface clipping and floating fills.
for (const heading of [30, 120]) {
await page.evaluate(
async (site, heading) => {
const C =
await import('/node_modules/cesium/Build/Cesium/index.js');
const g = window.__godsEyeView;
const target = C.Cartesian3.fromDegrees(
site.lon,
site.lat,
site.key === 'mabry' ? 180 : 10,
);
g.viewer.camera.lookAt(
target,
new C.HeadingPitchRange(
C.Math.toRadians(heading),
C.Math.toRadians(-30),
800,
),
);
g.viewer.camera.lookAtTransform(C.Matrix4.IDENTITY);
},
site,
heading,
);
await sleep(1700);
assert.ok(
(await page.evaluate(() => window.__polishFillProbe())).selected,
'orbit must preserve installation selection',
);
await page.screenshot({
path: `${dir}/after-${site.key}-orbit-${heading}.png`,
});
}
await moveCamera(page, { ...site, pitch: -80 }, 1);
await sleep(900);
const empty = await page.evaluate(() => {
const g = window.__godsEyeView;
for (let y = 250; y < 650; y += 75)
for (let x = 400; x < 850; x += 75) {
const pick = g.viewer.scene.pick({ x, y });
if (!pick?.id?.installationId && !pick?.id?.gevTrackedId)
return { x, y };
}
throw new Error('No empty map point');
});
await page.mouse.click(empty.x, empty.y);
await page.waitForFunction(
() =>
window.__polishFillProbe().selected === null &&
window.__polishFillProbe().entities === 0,
);
await sleep(600);
const deselected = await page.evaluate(() => {
window.__stopPolishSelection();
window.__restorePolishPaint();
return {
...window.__polishFillProbe(),
frames: window.__polishSelectionFrames,
};
});
assert.equal(selected.entities, picked.parts);
assert.equal(selected.primitives, picked.parts);
assert.ok(
selected.classification.every((c) => c === 1),
'Google 3D tile classification',
);
assert.equal(deselected.entities, 0);
assert.equal(deselected.primitives, 0);
const selectedFrames = deselected.frames.filter(
(f) => f.selected && f.own,
);
assert.ok(selectedFrames.length > 10);
assert.ok(
selectedFrames.every((f) => !f.tracked && f.paints === 1),
'selected record has exactly one painted overlay element',
);
assert.ok(
deselected.frames
.slice(-5)
.every((f) => !f.selected && f.own && !f.tracked && f.paints === 1),
'deselect restores the one ambient entry',
);
await page.screenshot({
path: `${dir}/after-${site.key}-deselected.png`,
});
result.push({ site, picked, selected, deselected });
}
} finally {
await fs.writeFile(
`${dir}/selection-checks.json`,
JSON.stringify(result, null, 2),
);
if (recorder)
await fs.writeFile(
`${dir}/selection-video.json`,
JSON.stringify(await recorder.stop(), null, 2),
);
}
return result.map(({ site, picked, selected, deselected }) => ({
site: site.name,
id: picked.id,
selected,
deselected: {
entities: deselected.entities,
primitives: deselected.primitives,
},
frames: deselected.frames.length,
}));
}
+223
View File
@@ -0,0 +1,223 @@
/**
* qa-journey-recorder.mjs — shared support for journey QA gates (not a gate).
*
* - `createScreencast(page, dir)` records CDP `Page.startScreencast` frames
* (every compositor frame) with their real timestamps and builds
* `<dir>/clip.mp4` through ffmpeg's concat demuxer with per-frame
* durations, so held frames last as long as they did on screen.
* - `installFrameProbe(page)` measures every animation frame in the page:
* the requestAnimationFrame interval and, for frames Cesium rendered, the
* CPU time between `scene.preUpdate` and `scene.postRender`.
* - `frameStats(frames)` reduces a probe window to count / mean / p50 / p95.
* - `moveCamera(page, view, seconds)` eases the camera like a drag or wheel
* zoom rather than teleporting it.
*/
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
/** Percentile of a numeric list (nearest rank); NaN for an empty list. */
export function percentile(values, p) {
if (!values.length) return Number.NaN;
const sorted = [...values].sort((a, b) => a - b);
return sorted[
Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1)
];
}
/** Summarize probe frames: rAF interval and Cesium render CPU time (ms). */
export function frameStats(frames) {
const intervals = frames.map((f) => f.dt).filter(Number.isFinite);
const renders = frames.map((f) => f.renderMs).filter(Number.isFinite);
const round = (v) => (Number.isFinite(v) ? Math.round(v * 10) / 10 : null);
const mean = (list) =>
list.length ? list.reduce((a, b) => a + b, 0) / list.length : Number.NaN;
return {
frames: intervals.length,
frameMean: round(mean(intervals)),
frameP50: round(percentile(intervals, 50)),
frameP95: round(percentile(intervals, 95)),
longFrames: intervals.filter((v) => v > 50).length,
renderedFrames: renders.length,
renderMean: round(mean(renders)),
renderP50: round(percentile(renders, 50)),
renderP95: round(percentile(renders, 95)),
};
}
/** Install the in-page frame probe once per document; returns nothing. */
export async function installFrameProbe(page) {
await page.evaluate(() => {
if (window.__gevFrameProbe) return;
const { viewer } = window.__godsEyeView;
const probe = { frames: [], started: 0 };
window.__gevFrameProbe = probe;
let last = performance.now();
let renderStart = null;
let pendingRender = null;
viewer.scene.preUpdate.addEventListener(() => {
renderStart = performance.now();
});
viewer.scene.postRender.addEventListener(() => {
if (renderStart !== null) pendingRender = performance.now() - renderStart;
renderStart = null;
});
const tick = (now) => {
probe.frames.push({ t: now, dt: now - last, renderMs: pendingRender });
if (probe.frames.length > 40_000) probe.frames.splice(0, 10_000);
pendingRender = null;
last = now;
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
});
}
/** Read probe frames with t in [t0, t1) (page performance.now() times). */
export async function readFrames(page, t0, t1 = Infinity) {
return page.evaluate(
(a, b) =>
window.__gevFrameProbe.frames.filter(
(f) => f.t >= a && f.t < (b ?? Infinity),
),
t0,
Number.isFinite(t1) ? t1 : null,
);
}
/** Page clock in ms (performance.now()). */
export function pageNow(page) {
return page.evaluate(() => performance.now());
}
/**
* Ease the camera to a view over `seconds`, resolving when it lands.
* @param {import('puppeteer').Page} page Page.
* @param {{lat:number, lon:number, height:number, heading?:number, pitch?:number}} view
* Destination (degrees, metres).
* @param {number} seconds Flight duration; 0 jumps.
*/
export async function moveCamera(page, view, seconds) {
await page.evaluate(
(v, duration) =>
new Promise((resolve) => {
const { viewer } = window.__godsEyeView;
const C = viewer.camera;
C.cancelFlight();
C.flyTo({
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
latitude: (v.lat * Math.PI) / 180,
longitude: (v.lon * Math.PI) / 180,
height: v.height,
}),
orientation: {
heading: ((v.heading ?? 0) * Math.PI) / 180,
pitch: ((v.pitch ?? -45) * Math.PI) / 180,
roll: 0,
},
duration,
// Straight, eased motion like a drag or wheel zoom, not an arc.
maximumHeight: Math.max(v.height, C.positionCartographic.height),
complete: resolve,
cancel: resolve,
});
}),
view,
seconds,
);
}
/**
* Record the page with CDP screencast frames and real per-frame timing.
* @param {import('puppeteer').Page} page Page to record.
* @param {string} dir Output directory (frames + clip.mp4).
* @param {{maxWidth?: number, maxHeight?: number, quality?: number, fps?: number|null}} [options]
*/
export async function createScreencast(page, dir, options = {}) {
const {
maxWidth = 1280,
maxHeight = 800,
quality = 70,
fps = null,
} = options;
const framesDir = path.join(dir, 'frames');
fs.rmSync(framesDir, { recursive: true, force: true });
fs.mkdirSync(framesDir, { recursive: true });
const cdp = await page.createCDPSession();
const frames = [];
let recording = false;
cdp.on('Page.screencastFrame', async (frame) => {
const file = path.join(
framesDir,
`f${String(frames.length).padStart(5, '0')}.jpg`,
);
if (recording) {
fs.writeFileSync(file, Buffer.from(frame.data, 'base64'));
frames.push({ file, timestamp: frame.metadata.timestamp });
}
await cdp
.send('Page.screencastFrameAck', { sessionId: frame.sessionId })
.catch(() => {});
});
return {
frames,
async start() {
recording = true;
await cdp.send('Page.startScreencast', {
format: 'jpeg',
quality,
maxWidth,
maxHeight,
everyNthFrame: 1,
});
},
/** Stop, encode with real timing, and return clip facts. */
async stop() {
recording = false;
await cdp.send('Page.stopScreencast').catch(() => {});
await cdp.detach().catch(() => {});
if (frames.length < 2) return { clip: null, frames: frames.length };
const lines = [];
for (let i = 0; i < frames.length; i++) {
const next = frames[i + 1]?.timestamp;
const duration = next
? Math.min(2, Math.max(0.001, next - frames[i].timestamp))
: 0.05;
lines.push(`file '${path.resolve(frames[i].file)}'`);
lines.push(`duration ${duration.toFixed(4)}`);
}
// The concat demuxer ignores the last duration unless the file repeats.
lines.push(`file '${path.resolve(frames.at(-1).file)}'`);
const list = path.join(dir, 'frames.txt');
fs.writeFileSync(list, `${lines.join('\n')}\n`);
const clip = path.join(dir, 'clip.mp4');
execFileSync('ffmpeg', [
'-y',
'-loglevel',
'error',
'-f',
'concat',
'-safe',
'0',
'-i',
list,
'-fps_mode',
fps ? 'cfr' : 'vfr',
'-c:v',
'libx264',
'-pix_fmt',
'yuv420p',
'-vf',
`scale=trunc(iw/2)*2:trunc(ih/2)*2${fps ? `,fps=${fps}` : ''}`,
clip,
]);
const seconds = frames.at(-1).timestamp - frames[0].timestamp;
return {
clip,
frames: frames.length,
seconds: Math.round(seconds * 10) / 10,
fps: Math.round((frames.length / Math.max(seconds, 0.001)) * 10) / 10,
};
},
};
}
+485
View File
@@ -0,0 +1,485 @@
#!/usr/bin/env node
/** Named military areas: continent performance, close identities and a recorded zoom. */
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import puppeteer from 'puppeteer';
import {
checkInstallationPan,
checkInstallationSelection,
} from './qa-installation-polish.mjs';
import {
createScreencast,
installFrameProbe,
readFrames,
frameStats,
pageNow,
moveCamera,
} from './qa-journey-recorder.mjs';
const args = process.argv.slice(2);
if (args.includes('--help')) {
console.log('Usage: node scripts/qa-military-names.mjs <url> [--no-video]');
process.exit(0);
}
const url = args.find((a) => /^https?:/.test(a));
if (!url) throw new Error('Supply the running server URL');
const dir = process.env.QA_NAMES_DIR || 'qa-shots/military-names';
await fs.mkdir(dir, { recursive: true });
const result = {
views: [],
sites: [],
errors: [],
forbidden: [],
packs: [],
video: null,
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const browser = await puppeteer.launch({
headless: true,
protocolTimeout: 180000,
args: [
'--use-angle=metal',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
});
let recorder;
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
page.on('pageerror', (e) => result.errors.push(e.message));
page.on('console', (m) => {
if (m.type() === 'error') result.errors.push(m.text());
});
page.on('request', (request) => {
const u = new URL(request.url());
if (
/overpass/i.test(u.hostname) ||
u.hostname === 'nominatim.openstreetmap.org'
)
result.forbidden.push(u.origin);
if (u.pathname.endsWith('/osm_military_names/names.json'))
result.packs.push(u.pathname);
});
await page.evaluateOnNewDocument(() =>
sessionStorage.setItem('gev:first-run-mission-session:v1', 'dismissed'),
);
await page.goto(url + (url.includes('?') ? '&' : '?') + 'welcome=0', {
waitUntil: 'domcontentloaded',
});
await page.waitForFunction(
() =>
window.__godsEyeView?.dataManager &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 120000 },
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await page.keyboard.press('Escape');
result.page = await page.evaluate(() => ({
url: location.href,
title: document.title,
canvas: Boolean(document.querySelector('#cesiumContainer canvas')),
errorOverlay: Boolean(document.querySelector('vite-error-overlay')),
}));
assert.equal(new URL(result.page.url).origin, new URL(url).origin);
assert.ok(
result.page.title && result.page.canvas && !result.page.errorOverlay,
);
await page.evaluate(async () => {
const g = window.__godsEyeView;
for (const [id] of g.dataManager.layers)
await g.dataManager.setEnabled(id, false);
g.styleManager.setDetection({ enabled: false });
g.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
});
assert.equal(result.packs.length, 0, 'names pack stays unloaded at startup');
result.renderer = await page.evaluate(() => {
const gl = window.__godsEyeView.viewer.scene.context._gl;
const ext = gl.getExtension('WEBGL_debug_renderer_info');
return ext
? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)
: gl.getParameter(gl.RENDERER);
});
assert.doesNotMatch(result.renderer, /swiftshader|software/i);
await installFrameProbe(page);
const stats = () =>
page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('military-installations')
.module.getStats(),
);
const settle = async () => {
await sleep(500);
await page.waitForFunction(
() => {
const s = window.__godsEyeView.dataManager.layers
.get('military-installations')
.module.getStats();
return !s.loading && s.count > 0 && s.status === 'ready';
},
{ timeout: 60000 },
);
await sleep(1200);
};
const us = { lon: -100, lat: 39, height: 6500000, pitch: -90 };
await moveCamera(page, us, 0);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('military-installations', true),
);
await settle();
result.credit = await page.evaluate(() => {
const el = document.querySelector('#cesium-credits');
return {
text: el?.innerText || '',
visible: Boolean(el && getComputedStyle(el).display !== 'none'),
};
});
assert.match(result.credit.text, /© OpenStreetMap/);
assert.doesNotMatch(
result.credit.text,
/© OpenMapTiles/,
'wide bundled names use no vector tiles',
);
assert.ok(result.credit.visible);
await page.screenshot({ path: `${dir}/credit.png` });
for (const [name, view] of [
['us', us],
['europe', { lon: 12, lat: 50, height: 4500000, pitch: -90 }],
]) {
await moveCamera(page, view, 2);
await settle();
const before = await stats();
const overlaps = await page.evaluate(async () => {
const { getOverlayPaintRect } =
await import('/src/overlays/worldOverlay.js');
const layer = window.__godsEyeView.dataManager.layers.get(
'military-installations',
).module;
const rects = [];
layer.visitNamedMarkers((id) => {
const rect = getOverlayPaintRect('military-installations', id);
if (rect) rects.push(rect);
});
let overlaps = 0;
for (let a = 0; a < rects.length; a++)
for (let b = a + 1; b < rects.length; b++) {
const x = rects[a],
y = rects[b];
if (
x.x < y.x + y.w &&
y.x < x.x + x.w &&
x.y < y.y + y.h &&
y.y < x.y + x.h
)
overlaps++;
}
return overlaps;
});
// Incumbents retain their placements during camera motion, as for Data Centers.
assert.ok(
before.namedInView > 0 &&
before.namedMarkers > 0 &&
before.pointsOnScreen > 0,
);
assert.ok(
before.labelsOnScreen > 0 && before.labelsOnScreen <= before.labelCap,
);
await page.evaluate(async () => {
// Use the governor's hold so an asynchronous idle transition cannot
// overwrite a raw scene flag during the sustained-render measurement.
const { holdContinuousRender } = await import('/src/renderGovernor.js');
holdContinuousRender('qa-military-names-fps');
});
const t0 = await pageNow(page);
await sleep(3500);
const frames = await readFrames(page, t0);
await page.evaluate(async () => {
const { releaseContinuousRender } =
await import('/src/renderGovernor.js');
releaseContinuousRender('qa-military-names-fps');
});
const metrics = frameStats(frames);
const seconds = frames.reduce((sum, frame) => sum + frame.dt, 0) / 1000;
const fps = metrics.renderedFrames / seconds;
const rafFps = metrics.frames / seconds;
result.views.push({ name, ...before, overlaps, fps, rafFps, ...metrics });
console.log(name, JSON.stringify(result.views.at(-1)));
await page.screenshot({ path: `${dir}/${name}.png` });
assert.ok(fps >= 55, `${name} fps ${fps.toFixed(1)} < 55`);
}
await page.evaluate(() =>
window.__godsEyeView.styleManager.setCleanView(true),
);
await sleep(300);
result.cleanUi = await page.evaluate(async () => {
const host = await import('/src/overlays/worldOverlay.js');
const root = document.getElementById('world-overlay-root');
return {
active: document.body.classList.contains('ui-clean-view'),
visible: root.checkVisibility({
visibilityProperty: true,
opacityProperty: true,
}),
labels:
host.getWorldOverlayDiagnostics().paintedBySource[
'military-installations'
] || 0,
};
});
assert.ok(
result.cleanUi.active &&
result.cleanUi.visible &&
result.cleanUi.labels > 0,
);
await page.screenshot({ path: `${dir}/clean-ui.png` });
await page.evaluate(() =>
window.__godsEyeView.styleManager.setCleanView(false),
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled(
'military-installations',
false,
),
);
result.disabled = await page.evaluate(async () => {
const host = await import('/src/overlays/worldOverlay.js');
return host.getWorldOverlayDiagnostics().entriesBySource[
'military-installations'
];
});
assert.equal(result.disabled, 0);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('military-installations', true),
);
await moveCamera(page, us, 1);
await settle();
result.slowPan = await checkInstallationPan(page, dir);
await moveCamera(page, us, 1);
await settle();
if (!args.includes('--no-video')) {
recorder = await createScreencast(page, `${dir}/us-to-san-diego`, {
fps: 24,
});
await recorder.start();
}
await page.evaluate(async () => {
const C = await import('/node_modules/cesium/Build/Cesium/index.js');
const { getOverlayPaintRect } =
await import('/src/overlays/worldOverlay.js');
const g = window.__godsEyeView;
const layer = g.dataManager.layers.get('military-installations').module;
const overlay = document.createElement('div');
overlay.id = 'installation-label-probe';
overlay.style.cssText =
'position:fixed;top:60px;left:360px;z-index:99999;background:#07121cdd;color:#fff;padding:8px 12px;font:13px monospace;pointer-events:none';
document.body.append(overlay);
const screen = new C.Cartesian2();
window.__installationFrames = [];
window.__stopInstallationFrames =
g.viewer.scene.postRender.addEventListener(() => {
const stats = layer.getStats();
const markers = [],
labels = [],
rects = [];
layer.visitNamedMarkers((id, point) => {
const rect = getOverlayPaintRect('military-installations', id);
if (rect) rects.push(rect);
const p = C.SceneTransforms.worldToWindowCoordinates(
g.viewer.scene,
point.position,
screen,
);
if (
point.show &&
p &&
p.x >= 0 &&
p.y >= 0 &&
p.x < 1280 &&
p.y < 800
) {
markers.push(id);
if (rect) labels.push(id);
}
});
let overlaps = 0;
for (let a = 0; a < rects.length; a++)
for (let b = a + 1; b < rects.length; b++) {
const x = rects[a],
y = rects[b];
if (
x.x < y.x + y.w &&
y.x < x.x + x.w &&
x.y < y.y + y.h &&
y.y < x.y + x.h
)
overlaps++;
}
const altitude = g.viewer.camera.positionCartographic.height;
window.__installationFrames.push({
t: performance.now(),
altitude,
markers,
labels,
paintedLabels: stats.labelsOnScreen,
overlaps,
loading: stats.loading,
wide: stats.wide,
});
overlay.textContent = `${(altitude / 1000).toFixed(1)} km | markers ${markers.length} | labels ${labels.length} / 24`;
});
});
const clipStart = await pageNow(page);
await sleep(1200);
for (const view of [
{ lon: -116, lat: 35, height: 1500000, pitch: -85 },
{ lon: -117.2, lat: 33, height: 200000, pitch: -80 },
{ lon: -117.1, lat: 32.86, height: 18000, pitch: -80 },
]) {
await moveCamera(page, view, 3);
await sleep(800);
}
await settle();
await sleep(1500);
result.clipFrames = await readFrames(page, clipStart);
result.labelFrames = await page.evaluate(() => {
window.__stopInstallationFrames();
document.getElementById('installation-label-probe').remove();
return window.__installationFrames;
});
await fs.writeFile(
`${dir}/label-frames.json`,
JSON.stringify(result.labelFrames, null, 2),
);
assert.ok(result.labelFrames.length > 100);
assert.ok(
result.labelFrames.every(
(frame) =>
frame.markers.length > 0 &&
frame.labels.length > 0 &&
frame.paintedLabels > 0 &&
frame.paintedLabels <= 24,
),
'zoom never loses all visible named markers or labels',
);
if (recorder) {
result.video = await recorder.stop();
recorder = null;
}
result.selection = await checkInstallationSelection(page, dir, {
video: !args.includes('--no-video'),
});
const sites = [
['mabry', 'Camp Mabry', -97.765, 30.3125, 3500],
['pendleton', 'Marine Corps Base Camp Pendleton', -117.4, 33.33, 25000],
['miramar', 'Marine Corps Air Station Miramar', -117.14, 32.87, 18000],
['ramstein', 'Ramstein Air Base', 7.6, 49.44, 7000],
['lakenheath', 'RAF Lakenheath', 0.56, 52.41, 7000],
['hood', 'Fort Hood', -97.78, 31.135, 30000],
];
for (const [key, expected, lon, lat, height] of sites) {
await moveCamera(page, { lon, lat, height, pitch: -80 }, 1.5);
await settle();
const snapshot = await page.evaluate(
(lat, lon) => {
const g = window.__godsEyeView;
const layer = g.dataManager.layers.get('military-installations').module;
const position = g.viewer.scene.globe.ellipsoid.cartographicToCartesian(
{
longitude: (lon * Math.PI) / 180,
latitude: (lat * Math.PI) / 180,
height: 0,
},
);
return {
stats: layer.getStats(),
records: layer.getNearby(position, 100000, 1000).map((r) => ({
id: r.id,
name: r.name,
memberNames: r.memberNames || [],
hasPolygon: Boolean(r.footprints?.length),
})),
};
},
lat,
lon,
);
result.sites.push({ key, expected, ...snapshot });
assert.equal(
new Set(snapshot.records.map((r) => r.id)).size,
snapshot.records.length,
);
assert.ok(
snapshot.stats.labelsOnScreen > 0 &&
snapshot.stats.labelsOnScreen <= snapshot.stats.labelCap,
);
assert.ok(
snapshot.records.some((r) => r.name === expected && r.hasPolygon),
`${key}: missing named polygon ${expected}; got ${snapshot.records.map((r) => r.name).join(', ')}`,
);
await page.screenshot({ path: `${dir}/${key}.png` });
console.log(key, expected);
}
await page.evaluate(() => {
if (
document
.getElementById('global-context-panel')
.classList.contains('collapsed')
)
document
.querySelector('[data-collapse-target="global-context-panel"]')
.click();
});
await page.waitForSelector('#global-context-flights-btn', { visible: true });
await page.evaluate(() => {
const button = document.getElementById('global-context-flights-btn');
if (button.getAttribute('aria-selected') !== 'true') button.click();
});
await page.waitForFunction(
() => {
const state = window.__godsEyeView.styleManager.getContextModeState();
return state.mode === 'flights' && !state.changing;
},
{ timeout: 60000 },
);
result.card = await page.evaluate(() => {
const g = window.__godsEyeView;
return g.dataManager.layers
.get('military-installations')
.module.focusById('osm:military:r13529728');
});
assert.equal(result.card, true);
await page.waitForFunction(
() =>
document
.getElementById('military-awareness-panel')
?.textContent.includes('Named areas') &&
document
.getElementById('military-awareness-panel')
.checkVisibility({ visibilityProperty: true, opacityProperty: true }),
{ timeout: 15000 },
);
result.cardText = await page.$eval(
'#military-awareness-panel',
(el) => el.textContent,
);
assert.match(result.cardText, /Fort Hood/);
assert.match(result.cardText, /TA-25/);
await sleep(1800);
await page.$eval(
'#military-awareness-panel .military-awareness-names',
(el) => {
el.open = true;
},
);
await page.screenshot({ path: `${dir}/member-names-card.png` });
assert.equal(result.packs.length, 1, 'single lazy asset fetch');
assert.deepEqual(result.forbidden, []);
assert.deepEqual(result.errors, []);
} finally {
if (recorder)
result.video = await recorder.stop().catch((e) => ({ error: e.message }));
await fs.writeFile(`${dir}/result.json`, JSON.stringify(result, null, 2));
await browser.close();
}
File diff suppressed because it is too large. Load diff
+362
View File
@@ -0,0 +1,362 @@
#!/usr/bin/env node
/**
* Keyless terrain throttling probe.
*
* On the keyless map source the browser fetches Re:Earth quantized-mesh tiles
* (`terrain.reearth.land/cesium-mesh/ellipsoid/{z}/{x}/{y}.terrain`) directly.
* A zoom-out burst asks for a few dozen shallow tiles at once and the upstream
* answers HTTP 429 for most of them. This harness drives that burst, counts the
* throttled responses, keeps the first 429's headers as evidence, and then
* checks whether every throttled tile eventually loaded — the contract the
* client-side retry in `src/maps/terrain.js` provides.
*
* Run against a keyless dev server (no Google or ion credentials):
* env -u GOOGLE_MAPS_API_KEY -u CESIUM_ION_TOKEN npm run dev -- --port 4180
* QA_BASE_URL=http://localhost:4180 node scripts/qa-terrain-429.mjs
*
* Flags: `--mode zoomOut|wheel` (default zoomOut), `--notches N` (default 12),
* `--settle MS` (default 25000), `--rounds N` (default 1), `--inject N`
* (answer the first N tile requests of each burst with a synthetic 429),
* `--headful`.
* Exits 1 when a throttled tile never recovered or a terrain tile is left in
* Cesium's FAILED state.
*/
import fs from 'node:fs';
import puppeteer from 'puppeteer';
const appUrl = process.env.QA_BASE_URL || 'http://localhost:4173';
const argv = process.argv.slice(2);
const flag = (name, fallback) => {
const index = argv.indexOf(`--${name}`);
return index >= 0 && argv[index + 1] !== undefined
? argv[index + 1]
: fallback;
};
const mode = flag('mode', 'zoomOut');
const notches = Number(flag('notches', 12));
const settleMs = Number(flag('settle', 25000));
const rounds = Number(flag('rounds', 1));
// The upstream throttle is load-dependent and does not fire on demand. With
// `--inject N`, the first N tile requests of each burst are answered 429 by an
// XMLHttpRequest shim inside the page so the recovery contract can be proven
// deterministically; real 429s are still counted and kept as evidence. (CDP
// request interception is not used: it stalls Cesium's request scheduler.)
const inject = Number(flag('inject', 0));
const headful = argv.includes('--headful');
const TERRAIN_HOST = 'terrain.reearth.land';
const TILE_PATTERN = /\/cesium-mesh\/ellipsoid\/\d+\/\d+\/\d+\.terrain/;
const executablePath =
process.env.PUPPETEER_EXECUTABLE_PATH ||
(await puppeteer.executablePath().catch(() => null));
if (!executablePath || !fs.existsSync(executablePath)) {
throw new Error('Puppeteer Chrome for Testing is unavailable');
}
const browser = await puppeteer.launch({
headless: headful ? false : 'new',
executablePath,
args: [
...(process.platform === 'darwin'
? ['--use-angle=metal', '--enable-gpu']
: ['--use-gl=angle', '--use-angle=swiftshader']),
'--no-sandbox',
],
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
/** @type {Map<string, {statuses: number[], failures: number}>} per tile URL. */
const tiles = new Map();
const evidence = [];
let layerJsonSeen = false;
let recording = false;
let requestsInWindow = 0;
let throttledInWindow = 0;
const tileEntry = (url) => {
if (!tiles.has(url)) tiles.set(url, { statuses: [], failures: 0 });
return tiles.get(url);
};
if (inject > 0) {
// Cesium fetches terrain through XMLHttpRequest and reads status, response
// and getAllResponseHeaders() in its onload handler; shadowing those on the
// instance is enough to make it see a throttled reply.
await page.evaluateOnNewDocument((pattern) => {
const tilePattern = new RegExp(pattern);
const shim = { remaining: 0, injected: [] };
window.__qaTerrainThrottle = shim;
const open = XMLHttpRequest.prototype.open;
const send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
this.__qaUrl = String(url);
return open.call(this, method, url, ...rest);
};
XMLHttpRequest.prototype.send = function (...args) {
if (!(shim.remaining > 0 && tilePattern.test(this.__qaUrl || ''))) {
return send.apply(this, args);
}
shim.remaining -= 1;
shim.injected.push(this.__qaUrl);
const xhr = this;
setTimeout(() => {
for (const [key, value] of [
['status', 429],
['readyState', 4],
['response', null],
]) {
Object.defineProperty(xhr, key, { value, configurable: true });
}
xhr.getAllResponseHeaders = () => 'retry-after: 1\r\n';
xhr.onload?.(new Event('load'));
}, 30);
return undefined;
};
}, TILE_PATTERN.source);
}
page.on('response', async (response) => {
const url = response.url();
if (!url.includes(TERRAIN_HOST)) return;
if (url.endsWith('layer.json')) layerJsonSeen = true;
if (!TILE_PATTERN.test(url)) return;
const status = response.status();
tileEntry(url).statuses.push(status);
if (recording) requestsInWindow += 1;
if (status !== 429) return;
if (recording) throttledInWindow += 1;
if (evidence.length < 3) {
let body = '';
try {
body = (await response.text()).slice(0, 160);
} catch {
/* body unavailable */
}
evidence.push({ url, status, headers: response.headers(), body });
}
});
page.on('requestfailed', (request) => {
const url = request.url();
if (!TILE_PATTERN.test(url)) return;
tileEntry(url).failures += 1;
if (recording) requestsInWindow += 1;
});
const consoleErrors = [];
page.on('pageerror', (error) => consoleErrors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error') consoleErrors.push(message.text());
});
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const waitFor = async (predicate, timeoutMs, label) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await predicate()) return;
await sleep(250);
}
throw new Error(`timed out waiting for ${label}`);
};
const setCamera = (lon, lat, height) =>
page.evaluate(
({ lon, lat, height }) => {
const viewer = window.__godsEyeView.viewer;
// The app's own startup flight would otherwise keep steering the camera
// through the burst and turn the zoom-out into a zoom-in.
viewer.camera.cancelFlight();
const cartographic =
viewer.camera.positionCartographic.constructor.fromDegrees(
lon,
lat,
height,
);
viewer.camera.setView({
destination:
viewer.scene.globe.ellipsoid.cartographicToCartesian(cartographic),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
window.__godsEyeView.requestRender?.();
},
{ lon, lat, height },
);
const cameraHeight = () =>
page.evaluate(
() => window.__godsEyeView.viewer.camera.positionCartographic.height,
);
const globeState = () =>
page.evaluate(() => {
const viewer = window.__godsEyeView.viewer;
const surface = viewer.scene.globe._surface;
let failed = 0;
let loaded = 0;
const visit = (tile) => {
if (!tile) return;
const state = tile.data?.terrainState;
if (state === 0) failed += 1;
else if (state === 6) loaded += 1;
for (const child of tile._children || []) visit(child);
};
for (const tile of surface._levelZeroTiles || []) visit(tile);
return {
failed,
loaded,
tilesLoaded: viewer.scene.globe.tilesLoaded,
provider: viewer.terrainProvider?.constructor?.name,
};
});
const burst = async () => {
if (mode === 'zoomOut') {
for (let i = 0; i < notches; i += 1) {
await page.evaluate(() => {
const viewer = window.__godsEyeView.viewer;
viewer.camera.zoomOut(viewer.camera.positionCartographic.height * 0.6);
window.__godsEyeView.requestRender?.();
});
await sleep(40);
}
return;
}
await page.mouse.move(640, 400);
for (let i = 0; i < notches; i += 1) {
await page.mouse.wheel({ deltaY: 200 });
await page.evaluate(() => window.__godsEyeView.requestRender?.());
await sleep(40);
}
};
try {
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(() => Boolean(window.__godsEyeView?.viewer), {
timeout: 60000,
});
await waitFor(() => layerJsonSeen, 60000, 'the Re:Earth layer.json fetch');
await waitFor(
async () => (await globeState()).provider === 'CesiumTerrainProvider',
30000,
'the keyless terrain provider',
);
const summaries = [];
for (let round = 1; round <= rounds; round += 1) {
// Re-apply the view until it sticks: a still-running startup flight can
// overwrite the first setView on the very next frame.
for (let attempt = 0; attempt < 5; attempt += 1) {
await setCamera(-97.7431, 30.2672, 40000);
await sleep(1500);
const height = await cameraHeight();
if (Math.abs(height - 40000) < 1000) break;
}
await waitFor(
async () => (await globeState()).tilesLoaded,
30000,
'the 40 km view to settle',
).catch(() => {});
await sleep(2000);
const settledHeight = await cameraHeight();
if (Math.abs(settledHeight - 40000) > 1000) {
throw new Error(
`camera did not hold the 40 km start view (at ${Math.round(settledHeight)} m)`,
);
}
const before = await globeState();
tiles.clear();
requestsInWindow = 0;
throttledInWindow = 0;
if (inject > 0) {
await page.evaluate((count) => {
window.__qaTerrainThrottle.remaining = count;
window.__qaTerrainThrottle.injected = [];
}, inject);
}
recording = true;
const startHeight = await cameraHeight();
await burst();
const endHeight = await cameraHeight();
await sleep(settleMs);
recording = false;
// Injected throttles never reach the network, so fold them in from the
// page: a later real 200 for the same tile is the retry succeeding.
const injected =
inject > 0
? await page.evaluate(() => window.__qaTerrainThrottle.injected)
: [];
for (const url of injected) tileEntry(url).statuses.unshift(429);
const throttled = [...tiles.entries()].filter(([, entry]) =>
entry.statuses.includes(429),
);
const recovered = throttled.filter(([, entry]) =>
entry.statuses.includes(200),
);
const unrecovered = throttled.filter(
([, entry]) => !entry.statuses.includes(200),
);
const after = await globeState();
summaries.push({
round,
mode,
notches,
startHeightKm: Math.round(startHeight / 1000),
endHeightKm: Math.round(endHeight / 1000),
tileResponses: requestsInWindow,
throttledResponses: throttledInWindow,
injectedThrottles: injected.length,
throttledTiles: throttled.length,
recoveredTiles: recovered.length,
unrecoveredTiles: unrecovered.length,
failedTilesBefore: before.failed,
failedTilesAfter: after.failed,
loadedTilesAfter: after.loaded,
});
console.log(JSON.stringify(summaries.at(-1)));
if (unrecovered.length) {
console.log(
' unrecovered: ' +
unrecovered
.slice(0, 8)
.map(
([url, entry]) =>
`${new URL(url).pathname} [${entry.statuses.join(',')}]`,
)
.join(' · '),
);
}
}
if (evidence.length) {
console.log('first 429 evidence:');
for (const item of evidence) {
const headers = Object.fromEntries(
Object.entries(item.headers).filter(([name]) =>
/^(cf-|retry-after|server|content-type|date|x-ratelimit|ratelimit|access-control)/i.test(
name,
),
),
);
console.log(
` ${new URL(item.url).pathname} ${JSON.stringify(headers)} body=${JSON.stringify(item.body)}`,
);
}
}
const failures = summaries.filter(
(summary) => summary.unrecoveredTiles > 0 || summary.failedTilesAfter > 0,
);
const terrainErrors = consoleErrors.filter((text) =>
/terrain tile/i.test(text),
);
if (terrainErrors.length) {
console.log(`terrain console errors: ${terrainErrors.length}`);
console.log(` ${terrainErrors[0].slice(0, 200)}`);
}
console.log(
failures.length
? `FAIL — ${failures.length}/${summaries.length} round(s) left throttled terrain tiles unloaded`
: `PASS — every throttled terrain tile recovered in ${summaries.length} round(s)`,
);
process.exitCode = failures.length ? 1 : 0;
} finally {
await browser.close();
}
+533
View File
@@ -0,0 +1,533 @@
#!/usr/bin/env node
/**
* Traffic motion acceptance: real-GPU Puppeteer, stable identities, stationary
* camera holds and timestamped CDP JPEGs encoded with ffmpeg. No tile mocks.
* Run: node scripts/qa-traffic-motion.mjs http://localhost:4186
* --baseline records the unmodified pre-fix implementation (object identities
* via WeakMap, never collection indices); it reports failures without exiting 1.
* --mode=tomtom|osm|hybrid limits a diagnostic run. --tag=name keeps captures.
* --journey follows a neighborhood-to-city route with layer toggles, then
* checks Capitol/greenbelt/parking/corridor views against a 1.5 s limit.
*/
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import puppeteer from 'puppeteer';
const args = process.argv.slice(2);
if (args.includes('--help')) {
console.log(
'Usage: node scripts/qa-traffic-motion.mjs <url> [--baseline] [--mode=tomtom|osm|hybrid] [--tag=name] [--journey]',
);
process.exit(0);
}
const url = args.find((a) => /^https?:/.test(a));
if (!url) throw new Error('A running dev server URL is required');
const baseline = args.includes('--baseline');
const journey = args.includes('--journey');
const option = (key) =>
args.find((a) => a.startsWith(`--${key}=`))?.split('=')[1];
const modes = option('mode') ? [option('mode')] : ['tomtom', 'osm', 'hybrid'];
if (modes.some((m) => !['tomtom', 'osm', 'hybrid'].includes(m)))
throw new Error('Invalid road mode');
const root = path.resolve(
'qa-shots/motion',
option('tag') || (baseline ? 'before' : '.'),
);
fs.mkdirSync(root, { recursive: true });
const revision = execFileSync('git', ['rev-parse', 'HEAD'], {
encoding: 'utf8',
}).trim();
const browser = await puppeteer.launch({
headless: true,
protocolTimeout: 300000,
args: [
'--use-angle=metal',
'--window-size=1280,800',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
});
const results = [];
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
try {
for (const mode of modes) {
const dir = path.join(root, mode);
fs.mkdirSync(dir, { recursive: true });
const page = await browser.newPage();
const errors = [],
forbidden = [],
logs = [],
tileRequests = [];
page.on('pageerror', (e) => errors.push(e.message));
page.on('console', (m) => {
if (m.type() === 'error') errors.push(m.text());
if (m.text().startsWith('[Data:Traffic]')) logs.push(m.text());
});
page.on('request', (r) => {
const u = new URL(r.url());
if (
u.hostname === 'tiles.openfreemap.org' &&
/\/\d+\/\d+\/\d+(?:\.pbf)?$/.test(u.pathname)
)
tileRequests.push(u.pathname);
if (
/overpass/i.test(u.hostname) ||
u.hostname === 'nominatim.openstreetmap.org'
)
forbidden.push(u.hostname);
});
await page.setViewport({ width: 1280, height: 800 });
await page.evaluateOnNewDocument(() =>
sessionStorage.setItem('gev:first-run-mission-session:v1', 'dismissed'),
);
const u = new URL(url);
u.searchParams.set('trafficRoads', mode);
u.searchParams.set('trafficDebug', '1');
await page.goto(u.href, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() =>
window.__godsEyeView?.dataManager &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 120000 },
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await page.keyboard.press('Escape');
await page.evaluate(() => {
const g = window.__godsEyeView;
g.styleManager.setDetection({ enabled: false });
for (const [id] of g.dataManager.layers)
g.dataManager.setEnabled(id, false);
g.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
});
const identity = await page.evaluate(() => ({
title: document.title,
text: document.body.innerText.length,
overlay: Boolean(document.querySelector('vite-error-overlay')),
}));
await page.evaluate((baseline) => {
const { viewer, dataManager } = window.__godsEyeView;
const layer = dataManager.layers.get('traffic').module;
if (!baseline && typeof layer.visitMotionDots !== 'function')
throw new Error('Traffic must expose stable dot identities');
const scene = viewer.scene;
const weakIds = new WeakMap();
let nextId = 0,
rebuilds = 0;
const collections = [];
function discoverCollections() {
for (let i = 0; i < scene.primitives.length; i++) {
const p = scene.primitives.get(i);
if (!Array.isArray(p._pointPrimitives) || collections.includes(p))
continue;
collections.push(p);
const removeAll = p.removeAll;
p.removeAll = function () {
if (this.length) rebuilds++;
return removeAll.call(this);
};
}
}
let previous = new Map(),
previousN = 0,
lastRebuilds = 0,
lastTime = 0;
const rows = [];
window.__motion = {
rows,
active: false,
moveEnds: [],
label: 'camera moving',
holdStart: null,
};
const clock = document.createElement('div');
clock.style.cssText =
'position:fixed;bottom:4px;left:38%;padding:3px 8px;background:#000c;color:#fff;font:12px monospace;z-index:999999;pointer-events:none';
document.body.append(clock);
viewer.camera.moveEnd.addEventListener(() =>
window.__motion.moveEnds.push(performance.now()),
);
scene.postRender.addEventListener(() => {
if (!window.__motion.active) return;
discoverCollections();
const now = performance.now(),
points = new Map();
clock.textContent =
`QA: ${window.__motion.label}` +
(window.__motion.holdStart === null
? ''
: ` | camera stopped +${((now - window.__motion.holdStart) / 1000).toFixed(1)}s`);
const visit = (id, roadId, point) => {
if (!point.show) return;
const s = scene.cartesianToCanvasCoordinates(point.position);
if (s && s.x >= 0 && s.y >= 0 && s.x <= 1280 && s.y <= 800)
points.set(id, { x: s.x, y: s.y, roadId });
};
let n = 0,
offRoad = 0;
if (!baseline && layer.visitMotionDots) {
layer.visitMotionDots((id, roadId, point, road) => {
if (point.show) {
const p = road?.roadProperties;
const allowed = road?.directFlow
? [
'Motorway',
'International road',
'Major road',
'Secondary road',
'Connecting road',
'Major local road',
'Local road',
'Minor local road',
].includes(p?.roadType)
: [
'motorway',
'trunk',
'primary',
'secondary',
'tertiary',
'minor',
].includes(p?.class) &&
!p?.service &&
!p?.subclass &&
!['no', 'private'].includes(p?.access) &&
p?.brunnel !== 'tunnel';
if (!allowed) offRoad++;
}
if (point.show) n++;
visit(id, roadId, point);
});
} else {
for (const coll of collections)
if (coll.show)
for (let i = 0; i < coll.length; i++) {
const p = coll.get(i);
if (!weakIds.has(p)) weakIds.set(p, ++nextId);
if (p.show) n++;
visit(weakIds.get(p), null, p);
}
}
let tracked = 0,
jumps = 0,
reassigned = 0,
births = 0,
deaths = 0;
for (const [id, p] of points) {
const prev = previous.get(id);
if (!prev) {
births++;
continue;
}
tracked++;
if (Math.hypot(p.x - prev.x, p.y - prev.y) > 30) jumps++;
if (p.roadId !== prev.roadId) reassigned++;
}
for (const id of previous.keys()) if (!points.has(id)) deaths++;
rows.push({
t: now,
loading: layer.getStats().loading,
syncing:
document
.getElementById('traffic-sync-chip')
?.classList.contains('visible') &&
document.getElementById('traffic-sync-progress')?.textContent ===
'...',
surfacePending: layer.getStats().surfacePending,
offRoad,
dt: now - lastTime,
n,
visible: points.size,
tracked,
jumps,
jumpFrac: tracked ? jumps / tracked : 0,
reassigned,
births,
deaths,
sizeChanged: n !== previousN,
rebuilds: rebuilds - lastRebuilds,
});
previous = points;
previousN = n;
lastRebuilds = rebuilds;
lastTime = now;
});
}, baseline);
const fly = (lat, lon, height, pitch, duration, heading = 20) =>
page.evaluate(
async (v) => {
const { viewer } = window.__godsEyeView;
window.__motion.label = 'camera moving';
window.__motion.holdStart = null;
viewer.camera.cancelFlight();
await new Promise((resolve) =>
viewer.camera.flyTo({
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian(
{
latitude: (v.lat * Math.PI) / 180,
longitude: (v.lon * Math.PI) / 180,
height: v.height,
},
),
orientation: {
heading: (v.heading * Math.PI) / 180,
pitch: (v.pitch * Math.PI) / 180,
roll: 0,
},
duration: v.duration,
complete: resolve,
cancel: resolve,
}),
);
},
{ lat, lon, height, pitch, duration, heading },
);
const cdp = await page.createCDPSession();
let fi = 0;
const frames = [];
cdp.on('Page.screencastFrame', (f) => {
const name = `f${String(fi++).padStart(5, '0')}.jpg`;
fs.writeFileSync(path.join(dir, name), Buffer.from(f.data, 'base64'));
frames.push({ name, timestamp: f.metadata.timestamp });
cdp
.send('Page.screencastFrameAck', { sessionId: f.sessionId })
.catch(() => {});
});
await fly(
...(journey
? [30.25, -97.7497, 700, -40, 0, 10]
: [30.2685, -97.7425, 450, -35, 0]),
);
await cdp.send('Page.startScreencast', {
format: 'jpeg',
quality: 65,
maxWidth: 1280,
maxHeight: 800,
everyNthFrame: journey ? 1 : 3,
});
await page.evaluate(() => {
window.__motion.active = true;
});
const holds = [];
const hold = async (name, warm = false, holdMs = 10000, empty = false) => {
const start = await page.evaluate((label) => {
const start = performance.now();
window.__motion.label = label;
window.__motion.holdStart = start;
return start;
}, name);
await delay(holdMs);
const end = await page.evaluate(() => performance.now());
holds.push({
name,
warm,
start,
end,
empty,
settleMs: journey ? 1500 : warm ? 1000 : 3000,
});
await page.screenshot({ path: path.join(dir, `${name}.png`) });
console.log(`${mode}: captured ${name}`);
};
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('traffic', true),
);
// Match motion-reference: begin the camera-motion scenario from a
// settled street view. Initial mesh acquisition is recorded in the clip
// and first-dot diagnostics, but is not a stationary motion hold.
if (journey) {
await delay(4000);
await hold('south-congress', true, 6000);
const step = async (name, view, ms = 6000, empty = false) => {
await fly(...view);
await hold(name, false, ms, empty);
};
await step('pan-north-400m', [30.2536, -97.7497, 700, -40, 2, 10], 4000);
await step('pan-east-500m', [30.2536, -97.7445, 700, -40, 2, 10], 4000);
await step('zoom-out-2.5km', [30.2536, -97.7445, 2500, -45, 2.5, 10]);
await step(
'downtown-2.5km',
[30.2672, -97.7431, 2500, -45, 2.5, 10],
5000,
);
await step('downtown-500m', [30.2672, -97.7431, 500, -35, 2.5, 20]);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('alpr-cameras', true),
);
await hold('enable-alpr', true, 6000);
await step('pan-west-600m', [30.2672, -97.7494, 500, -35, 2, 20], 5000);
await step('city-12km', [30.28, -97.75, 12000, -60, 3, 0], 8000, true);
await fly(30.3125, -97.765, 1000, -45, 3, 0);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled(
'military-installations',
true,
),
);
await hold('camp-mabry-1km', false, 8000);
await step('camp-mabry-pan', [30.3125, -97.76, 1000, -45, 2, 30], 5000);
await step('greenbelt', [30.312, -97.775, 1000, -55, 3, 0], 6000);
await step('capitol', [30.2747, -97.7404, 500, -70, 3, 0], 6000);
await step('commercial-parking', [30.307, -97.735, 600, -60, 3, 0], 6000);
await step('corridor', [30.347, -97.799, 1000, -45, 3, 0], 6000);
} else {
await delay(12000);
await page.waitForFunction(
() => window.__motion.rows.some((r) => r.n > 0),
{ timeout: 1000 },
);
await hold('street-warm', true);
await fly(30.2685, -97.7425, 3000, -45, 1.5);
await hold('zoom-out');
await fly(30.2685, -97.72, 3000, -45, 1.5);
await hold('pan-east');
await fly(30.2685, -97.725, 600, -35, 1.5);
await hold('zoom-in');
await fly(-23.5505, -46.6333, 1250, -35, 1.5);
await hold('sao-paulo');
await fly(30.2685, -97.7425, 450, -35, 1.5);
await hold('austin-revisit', true);
}
await page.evaluate(() => {
window.__motion.active = false;
});
await cdp.send('Page.stopScreencast');
const rows = await page.evaluate(() => window.__motion.rows);
const surfacePhases = await page.evaluate(() =>
performance
.getEntriesByType('measure')
.filter(
(e) => e.name.startsWith('roads:') || e.name.startsWith('traffic:'),
)
.map((e) => ({
name: e.name,
start: e.startTime,
duration: e.duration,
detail: e.detail,
})),
);
const stats = await page.evaluate(() =>
window.__godsEyeView.dataManager.layers.get('traffic').module.getStats(),
);
const phases = holds.map((h) => {
const all = rows.filter((r) => r.t >= h.start && r.t < h.end);
const settled = all.filter((r) => r.t >= h.start + h.settleMs);
const n = settled.map((r) => r.n);
const lastBusy = all.findLastIndex(
(r) => r.loading || r.syncing || r.sizeChanged || r.jumpFrac > 0.02,
);
// First observed settled frame after the last population/loading event.
const steadyMs =
lastBusy < 0
? 0
: Math.round((all[lastBusy + 1]?.t ?? h.end) - h.start);
const phase = {
mode,
phase: h.name,
steadyMs,
lateLoadingFrames: settled.filter((r) => r.loading).length,
offRoadDots: Math.max(0, ...all.map((r) => r.offRoad)),
settleMs: h.settleMs,
frames: settled.length,
rebuilds: all.reduce((s, r) => s + r.rebuilds, 0),
lateRebuilds: settled.reduce((s, r) => s + r.rebuilds, 0),
jumpFrames: settled.filter((r) => r.jumpFrac > 0.02).length,
reassigned: settled.reduce((s, r) => s + r.reassigned, 0),
sizeChanges: settled.filter((r) => r.sizeChanged).length,
minDots: Math.min(...n),
maxDots: Math.max(...n),
maxFrameMs: Math.round(Math.max(...settled.map((r) => r.dt))),
};
phase.passed =
phase.frames >= 20 &&
phase.rebuilds <= 1 &&
phase.lateRebuilds === 0 &&
phase.jumpFrames === 0 &&
phase.reassigned === 0 &&
phase.sizeChanges === 0 &&
(h.empty ? phase.maxDots === 0 : phase.minDots > 0) &&
(!journey ||
(phase.steadyMs <= 1500 &&
phase.offRoadDots === 0 &&
all.at(-1)?.surfacePending === 0));
return phase;
});
fs.writeFileSync(
path.join(dir, 'frames.ffconcat'),
'ffconcat version 1.0\n' +
frames
.map(
(f, i) =>
`file '${f.name}'\nduration ${Math.max(0.001, Math.min(2, (frames[i + 1]?.timestamp || f.timestamp + 0.1) - f.timestamp))}\n`,
)
.join(''),
);
execFileSync('ffmpeg', [
'-y',
'-loglevel',
'error',
'-safe',
'0',
'-f',
'concat',
'-i',
path.join(dir, 'frames.ffconcat'),
'-fps_mode',
'vfr',
'-c:v',
'libx264',
'-pix_fmt',
'yuv420p',
'-vf',
journey ? 'scale=1280:-2' : 'scale=960:-2',
path.join(dir, 'clip.mp4'),
]);
const result = {
mode,
revision,
baseline,
identity,
url: u.href,
phases,
errors,
forbidden,
tileRequests,
duplicateTiles: tileRequests.filter(
(p, i) => tileRequests.indexOf(p) !== i,
),
stats,
surfacePhases,
logs,
holds,
rows,
moveEnds: await page.evaluate(() => window.__motion.moveEnds),
clip: path.join(dir, 'clip.mp4'),
frames: frames.length,
};
result.passed =
phases.every((p) => p.passed) &&
!errors.length &&
!forbidden.length &&
result.duplicateTiles.length === 0 &&
!identity.overlay &&
identity.text > 100;
fs.writeFileSync(
path.join(dir, 'metrics.json'),
JSON.stringify(result, null, 2),
);
results.push(result);
console.table(phases);
await page.close();
}
} finally {
await browser.close();
fs.writeFileSync(
path.join(root, 'results.json'),
JSON.stringify(
results.map(({ rows, ...r }) => r),
null,
2,
),
);
}
if (!baseline && results.some((r) => !r.passed)) process.exitCode = 1;
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env node
/** Reticle coverage on oblique arrivals: source requests, toggle oracle and zoom revisits. */
import fs from 'node:fs';
import puppeteer from 'puppeteer';
import {
moveCamera,
createScreencast,
installFrameProbe,
} from './qa-journey-recorder.mjs';
const args = process.argv.slice(2);
if (args.includes('--help')) {
console.log(
'Usage: node scripts/qa-traffic-oblique.mjs <url> [--baseline] [--mode=hybrid|osm|tomtom]',
);
process.exit(0);
}
const baseline = args.includes('--baseline');
const modeArg = args.find((a) => a.startsWith('--mode='))?.split('=')[1];
const modes = modeArg ? [modeArg] : ['tomtom', 'osm', 'hybrid'];
const out = `qa-shots/oblique-${baseline ? 'before' : 'after'}`;
fs.mkdirSync(out, { recursive: true });
const result = { views: [], errors: [], forbidden: [], failures: [] };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const browser = await puppeteer.launch({
headless: true,
protocolTimeout: 180000,
args: [
'--use-angle=metal',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
});
try {
for (const mode of modes) {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
const counts = { ofm: 0, tomtom: 0 };
page.on('request', (r) => {
const u = new URL(r.url());
if (
u.hostname === 'tiles.openfreemap.org' &&
/\/\d+\/\d+\/\d+(\.pbf)?$/.test(u.pathname)
)
counts.ofm++;
if (u.pathname.startsWith('/api/tomtom/flow/')) counts.tomtom++;
if (
/overpass/i.test(u.hostname) ||
u.hostname === 'nominatim.openstreetmap.org'
)
result.forbidden.push(u.hostname);
});
page.on('pageerror', (e) => result.errors.push(e.message));
page.on('console', (m) => {
if (m.type() === 'error') result.errors.push(m.text());
});
await page.evaluateOnNewDocument(() =>
sessionStorage.setItem('gev:first-run-mission-session:v1', 'dismissed'),
);
const url = new URL(args.find((a) => /^https?:/.test(a)));
url.searchParams.set('trafficRoads', mode);
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() =>
window.__godsEyeView?.dataManager &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 120000 },
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await page.keyboard.press('Escape');
await page.evaluate(async () => {
const g = window.__godsEyeView;
for (const [id] of g.dataManager.layers)
await g.dataManager.setEnabled(id, false);
g.styleManager.setDetection({ enabled: false });
g.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
});
const snap = () =>
page.evaluate(() => {
const { viewer, dataManager } = window.__godsEyeView;
let upper = 0,
lower = 0,
near = 0;
const camera = viewer.camera.positionWC;
dataManager.layers
.get('traffic')
.module.visitMotionDots((id, key, p) => {
if (!p.show) return;
const s = viewer.scene.cartesianToCanvasCoordinates(p.position);
if (s && s.x >= 240 && s.x <= 1040 && s.y >= 0 && s.y <= 800) {
if (s.y < 400) upper++;
else lower++;
}
});
return {
upper,
lower,
...dataManager.layers.get('traffic').module.getStats(),
};
});
const settle = async () => {
await sleep(700);
await page
.waitForFunction(
() => {
const s = window.__godsEyeView.dataManager.layers
.get('traffic')
.module.getStats();
return !s.loading;
},
{ timeout: 15000 },
)
.catch(() => {});
await sleep(1200);
return snap();
};
for (const [name, lat, lon, height] of [
['dubai', 24.964, 55.037, 276],
['austin', 30.2672, -97.7431, 300],
['london', 51.507, -0.128, 300],
]) {
const before = { ...counts };
await moveCamera(page, { lat, lon, height: 3000, pitch: -45 }, 0);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('traffic', true),
);
await moveCamera(page, { lat, lon, height, pitch: -15 }, 1.4);
const settled = await settle();
const load = {
ofm: counts.ofm - before.ofm,
tomtom: counts.tomtom - before.tomtom,
};
await page.screenshot({ path: `${out}/${name}-${mode}.png` });
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('traffic', false),
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('traffic', true),
);
const toggled = await settle();
const oracle =
Math.abs(settled.upper - toggled.upper) / Math.max(1, toggled.upper);
const preZoom = { ...counts };
await moveCamera(page, { lat, lon, height: 3000, pitch: -15 }, 0.7);
await settle();
const atWide = { ...counts };
await moveCamera(page, { lat, lon, height, pitch: -15 }, 0.7);
await settle();
const revisit = {
ofm: counts.ofm - atWide.ofm,
tomtom: counts.tomtom - atWide.tomtom,
};
const row = {
mode,
name,
load,
settled,
toggled,
oracle,
revisit,
zoomOut: {
ofm: atWide.ofm - preZoom.ofm,
tomtom: atWide.tomtom - preZoom.tomtom,
},
};
result.views.push(row);
console.log(JSON.stringify(row));
if (oracle > 0.15)
result.failures.push(`${name}/${mode}: upper oracle ${oracle}`);
if (revisit.ofm || revisit.tomtom)
result.failures.push(`${name}/${mode}: revisit requests`);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('traffic', false),
);
}
const locationView = { lat: 24.973, lon: 55.037 };
await moveCamera(
page,
{ lat: 24.964, lon: 55.037, height: 3000, pitch: -45 },
0,
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('traffic', true),
);
const dir = `${out}/dubai-${mode}-journey`;
const recording =
mode === 'hybrid' ? await createScreencast(page, dir, { fps: 24 }) : null;
await installFrameProbe(page);
await page.evaluate(() => {
const { viewer, dataManager } = window.__godsEyeView;
window.__obliqueFrames = [];
window.__obliqueMoving = true;
viewer.scene.postRender.addEventListener(() => {
const s = dataManager.layers.get('traffic').module.getStats();
let upper = 0,
lower = 0,
near = 0;
const camera = viewer.camera.positionWC;
dataManager.layers
.get('traffic')
.module.visitMotionDots((id, key, p) => {
if (!p.show) return;
const q = viewer.scene.cartesianToCanvasCoordinates(p.position);
if (q && q.x >= 240 && q.x <= 1040 && q.y >= 0 && q.y <= 800) {
if (q.y < 400) upper++;
else lower++;
const dx = p.position.x - camera.x,
dy = p.position.y - camera.y,
dz = p.position.z - camera.z;
if (dx * dx + dy * dy + dz * dz <= 1500 * 1500) near++;
}
});
window.__obliqueFrames.push({
t: performance.now(),
moving: window.__obliqueMoving,
count: s.count,
loading: s.loading,
upper,
lower,
near,
publishes: s.motion.publishes,
rebuilds: s.motion.rebuilds,
});
});
});
await recording?.start();
await page.evaluate(async (v) => {
const { flyToLandmark } = await import('/src/locations.js');
await new Promise((resolve) =>
flyToLandmark(window.__godsEyeView.viewer, v.lat, v.lon, {
range: 1000,
pitch: -15,
heading: 0,
duration: 1.6,
buildingHeight: 0,
onComplete: resolve,
onCancel: resolve,
}),
);
window.__obliqueMoving = false;
}, locationView);
const arrived = await settle();
await page.evaluate(() => {
window.__obliqueMoving = true;
});
await moveCamera(
page,
{ lat: 24.964, lon: 55.042, height: 300, pitch: -15 },
1.2,
);
await page.evaluate(() => {
window.__obliqueMoving = false;
});
const panned = await settle();
await sleep(1800);
const frames = await page.evaluate(() => window.__obliqueFrames);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('traffic', false),
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('traffic', true),
);
const fresh = await settle();
const oracle =
Math.abs(panned.upper - fresh.upper) / Math.max(1, fresh.upper);
const stops = frames.flatMap((f, i) =>
i && !f.moving && frames[i - 1].moving ? [i] : [],
);
const nearSettles = stops.map((start, index) => {
let end = start + 1;
while (end < frames.length && !frames[end].moving) end++;
const phase = frames.slice(start, end),
target = phase.at(-1).near;
const reached = phase.find((f) => f.near >= target * 0.85);
const ms = reached ? Math.round(reached.t - phase[0].t) : Infinity;
if (target && ms > 1100)
result.failures.push(
`dubai-location-pan/${mode}/${index}: near settle ${ms} ms`,
);
return {
phase: index === 0 ? 'arrival' : 'pan',
ms,
target,
first: phase[0].near,
};
});
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(`${dir}/metrics.json`, JSON.stringify(frames));
const clip = await recording?.stop();
result.views.push({
mode,
name: 'dubai-location-pan',
nearSettles,
arrived,
settled: panned,
toggled: fresh,
oracle,
clip,
});
if (oracle > 0.15)
result.failures.push(
`dubai-location-pan/${mode}: upper oracle ${oracle}`,
);
await page.close();
}
} finally {
fs.writeFileSync(`${out}/result.json`, JSON.stringify(result, null, 2));
await browser.close();
}
if (
!baseline &&
(result.failures.length || result.errors.length || result.forbidden.length)
)
process.exitCode = 1;