Draw nine layers' fills through engine.ground, one display each

Earthquakes, air quality, GNSS Integrity, fire perimeters, Prohibited &
Restricted Airspace, Active Fires, S2 GhostMaps, cyclone advisories and
submarine cables each have one render/display.js on the globe contract;
their render/cesium* and render/navara* files go. Their discs, cells,
polygons and corridors are engine.ground sets, so on Navara they come from
the one ground screen pass with exact colours instead of nine hand-written
classification passes and lit SDK vector layers. Cesium's look stays the
reference:

- Earthquake and air-quality discs have no outline (Cesium drew none on
  clamped ellipses); earthquake discs can now be picked on Navara too.
- Perimeters, drone zones and GhostMaps polygons outline their outer
  ring only, as Cesium did.
- GNSS cells stay out of the pick pass; cyclone cones, tracks and centres
  are culled by a port of Cesium's horizon occluder.
- Submarine cables are one ground corridor set, as Cesium batched them;
  landing points, tips and stems are movers and lines.
- FIRMS glows are a sprite set and its cells a ground set. The far-zoom
  heat raster, which no primitive draws yet, sits behind one seam,
  render/heatField.js, with a Cesium and a Navara backend.

The shared Navara ground helpers that only these layers used go
(navaraGroundCells, the cell and ground geometry, the zone renderers), as
does the ground prototype script.

Coupling: layer renderer lines fall from 1636/6953 to 1011/4832
(Cesium/Navara), Navara ground passes from 7 to 5, Cesium-importing files
from 65 to 53, engine.extensions reads from 26 to 19.
This commit is contained in:
German Khokhlov committed 2026-11-07 06:20:48 +04:00
1 parent 974a0b6361
commit 54fe0aea40
124 files changed
+8407 -9576

No files matched your search

+4
View File
@@ -34,6 +34,10 @@ const navaraPackage = /^(?:(?:three|postprocessing)(?:\/|$)|@navaramap\/)/;
// cesium only in render/cesium*.js, three and Navara only in
// render/navara*.js. The list grows with each layer moved onto renderers.
const RENDERED_LAYER_DIRS = [
'src/layers/droneAirspace/',
'src/layers/cyclones/',
'src/layers/ghostMaps/',
'src/layers/submarineCables/',
'src/layers/alpr/',
'src/layers/cctv/',
'src/layers/transit/',
+10 -10
View File
@@ -1,16 +1,16 @@
{
"metrics": {
"cesiumLayerRendererLines": 1636,
"navaraLayerRendererLines": 6953,
"cesiumImportFiles": 65,
"viewerLines": 1901,
"navaraKindChecks": 12,
"cesiumLayerRendererLines": 1011,
"navaraLayerRendererLines": 4832,
"cesiumImportFiles": 53,
"viewerLines": 1745,
"navaraKindChecks": 9,
"engineSniffCalls": 14,
"engineExtensionReads": 26,
"groundEffectPasses": 7,
"engineExtensionReads": 19,
"groundEffectPasses": 5,
"cesiumSdkLoads": 6,
"screenSpaceHandlerFiles": 10,
"engineNameLines": 133
"screenSpaceHandlerFiles": 8,
"engineNameLines": 108
},
"measuredAt": "after the radio, transit and CCTV displays; one engine.extensions read is CCTV's meshes seam, 2026-11-07"
"measuredAt": "after the ground layers (earthquakes, air quality, GNSS, perimeters, drone airspace, FIRMS, GhostMaps, cyclones, cables), 2026-11-07"
}
+23 -23
View File
@@ -625,13 +625,8 @@
"src/layers/submarineCables/bundledSource.js",
"src/layers/submarineCables/geometry.js",
"src/layers/submarineCables/index.js",
"src/layers/submarineCables/ingestion.js",
"src/layers/submarineCables/interaction.js",
"src/layers/submarineCables/lifecycle.js",
"src/layers/submarineCables/overlay.js",
"src/layers/submarineCables/policy.js",
"src/layers/submarineCables/rendering.js",
"src/layers/submarineCables/state.js",
"src/layers/submarineCables/surface.js",
"src/data/telegeographySubmarineCables.js",
"src/data/telegeographySubmarineCables.test.mjs",
@@ -908,7 +903,6 @@
"src/layers/cyclones/rendering.js",
"src/layers/cyclones/labels.js",
"src/layers/cyclones/source.js",
"src/layers/cyclones/rendering.test.mjs",
"src/layers/cyclones/labels.test.mjs",
"src/layers/cyclones/index.test.mjs",
"src/layers/cyclones/source.test.mjs",
@@ -1403,23 +1397,10 @@
"src/layers/localAdsb/qaDrawn.test.mjs",
"src/tooling/w1RuntimeEvidence.test.mjs",
"scripts/fixtures/w2/earthquakes.mjs",
"src/layers/earthquakes/render/cesium.js",
"src/layers/earthquakes/render/cesium.test.mjs",
"src/layers/earthquakes/render/navara.js",
"src/layers/earthquakes/render/navaraGeometry.js",
"src/layers/earthquakes/render/navaraGeometry.test.mjs",
"src/layers/earthquakes/render/navaraGrid.js",
"src/layers/earthquakes/render/navaraGrid.test.mjs",
"src/layers/earthquakes/render/navara.test.mjs",
"src/layers/firms/render/cesium.js",
"src/layers/firms/render/cesium.test.mjs",
"src/layers/firms/rectangle.js",
"src/layers/firms/rectangle.test.mjs",
"src/layers/firms/viewport.test.mjs",
"scripts/fixtures/w2/firms.mjs",
"src/layers/firms/render/navara.test.mjs",
"src/layers/firms/render/navaraGeometry.test.mjs",
"src/layers/firms/render/navaraSprites.test.mjs",
"src/layers/firms/lifecycle.test.mjs",
"scripts/fixtures/w2/radio.mjs",
"scripts/qa-map-policy.mjs",
@@ -1620,11 +1601,30 @@
"src/globe/navara/ground/pack.test.mjs",
"src/globe/navara/ground/pass.js",
"src/globe/navara/ground/vector.js",
"src/layers/earthquakes/render/ground.js",
"src/layers/perimeters/render/ground.js",
"scripts/qa-ground-suite.mjs",
"scripts/qa-ground-bench.mjs",
"scripts/qa-ground-prototypes.mjs",
"scripts/qa-ground-switch.mjs",
"docs/GLOBE-PRIMITIVES.md"
"docs/GLOBE-PRIMITIVES.md",
"src/layers/airQuality/render/display.js",
"src/layers/airQuality/render/display.test.mjs",
"src/layers/cyclones/render/display.js",
"src/layers/cyclones/render/display.test.mjs",
"src/layers/droneAirspace/render/display.js",
"src/layers/droneAirspace/render/display.test.mjs",
"src/layers/earthquakes/render/display.js",
"src/layers/earthquakes/render/display.test.mjs",
"src/layers/firms/render/cesiumHeat.js",
"src/layers/firms/render/display.js",
"src/layers/firms/render/display.test.mjs",
"src/layers/firms/render/heatField.js",
"src/layers/firms/render/sprites.js",
"src/layers/ghostMaps/render/display.js",
"src/layers/ghostMaps/render/display.test.mjs",
"src/layers/gnss/render/display.js",
"src/layers/gnss/render/display.test.mjs",
"src/layers/perimeters/render/display.js",
"src/layers/perimeters/render/display.test.mjs",
"src/layers/submarineCables/render/display.js",
"src/layers/submarineCables/render/display.test.mjs",
"src/testSupport/groundDisplayEngines.mjs"
]
+54 -60
View File
@@ -1583,7 +1583,8 @@
"src/layers/earthquakes/source.js",
"src/layers/earthquakes/records.js",
"src/geo/",
"src/layers/shared/color.js"
"src/layers/shared/color.js",
"src/layers/earthquakes/render/display.js"
],
"external": []
},
@@ -2180,13 +2181,8 @@
"src/layers/submarineCables/geometry.js",
"src/layers/submarineCables/index.js",
"src/layers/submarineCables/session.js",
"src/layers/submarineCables/ingestion.js",
"src/layers/submarineCables/interaction.js",
"src/layers/submarineCables/lifecycle.js",
"src/layers/submarineCables/overlay.js",
"src/layers/submarineCables/policy.js",
"src/layers/submarineCables/rendering.js",
"src/layers/submarineCables/state.js",
"src/layers/submarineCables/surface.js",
"src/data/inputOwnership.js",
"src/i18n/index.js",
@@ -2202,7 +2198,10 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/geo/"
"src/geo/",
"src/sources/requestDeadline.js",
"src/layers/shared/color.js",
"src/layers/submarineCables/render/display.js"
],
"external": ["cesium"]
},
@@ -2779,13 +2778,8 @@
"src/layers/submarineCables/geometry.js",
"src/layers/submarineCables/index.js",
"src/layers/submarineCables/session.js",
"src/layers/submarineCables/ingestion.js",
"src/layers/submarineCables/interaction.js",
"src/layers/submarineCables/lifecycle.js",
"src/layers/submarineCables/overlay.js",
"src/layers/submarineCables/policy.js",
"src/layers/submarineCables/rendering.js",
"src/layers/submarineCables/state.js",
"src/layers/submarineCables/surface.js",
"src/layers/traffic/animation.js",
"src/layers/traffic/controls.js",
@@ -3058,20 +3052,16 @@
"src/layers/earthquakes/records.js",
"src/app/layers/perimeters.js",
"src/layers/perimeters/index.js",
"src/layers/perimeters/render/cesium.js",
"src/layers/shared/render/cesiumZones.js",
"src/layers/perimeters/source.js",
"src/layers/perimeters/records.js",
"src/layers/perimeters/cards.js",
"src/layers/perimeters/inciweb.js",
"src/app/layers/gnss.js",
"src/layers/gnss/index.js",
"src/layers/gnss/render/cesium.js",
"src/layers/gnss/source.js",
"src/layers/gnss/records.js",
"src/app/layers/ghostMaps.js",
"src/layers/ghostMaps/index.js",
"src/layers/ghostMaps/render/cesium.js",
"src/layers/ghostMaps/catalog.js",
"src/layers/ghostMaps/records.js",
"src/layers/ghostMaps/kml.js",
@@ -3299,7 +3289,6 @@
"src/data/geoImportImages.js",
"src/app/layers/airQuality.js",
"src/layers/airQuality/index.js",
"src/layers/airQuality/render/cesium.js",
"src/layers/airQuality/model.js",
"src/layers/airQuality/source.js",
"src/ui/statusText.js",
@@ -3477,23 +3466,13 @@
"src/layers/shared/render/engineView.js",
"src/celestialRing/render/cesium.js",
"src/celestialRing/view.js",
"src/layers/earthquakes/render/cesium.js",
"src/layers/earthquakes/render/navara.js",
"src/layers/airQuality/render/navara.js",
"src/layers/gnss/render/navara.js",
"src/layers/perimeters/render/navara.js",
"src/layers/shared/render/navaraPolygons.js",
"src/layers/shared/render/navaraZones.js",
"src/layers/droneAirspace/render/navara.js",
"src/layers/earthquakes/render/navaraGeometry.js",
"src/layers/earthquakes/render/navaraGrid.js",
"src/layers/installations/render/navara.js",
"src/layers/awareness/render/navara.js",
"src/layers/awareness/render/navaraPulses.js",
"src/layers/infrastructure/render/navara.js",
"src/layers/shared/render/navaraStems.js",
"src/layers/shared/render/navaraSceneDepth.js",
"src/layers/submarineCables/render/navara.js",
"src/layers/directions/render/navara.js",
"src/layers/traffic/render/navara.js",
"src/layers/streetLevel/render/navara.js",
@@ -3505,9 +3484,7 @@
"src/layers/weather/render/navaraSurface.js",
"src/layers/wind/render/navara.js",
"src/layers/wind/render/flowMesh.js",
"src/layers/ghostMaps/render/navara.js",
"src/layers/shared/render/navaraGroundPicks.js",
"src/layers/cyclones/render/navara.js",
"src/layers/shared/render/groundRing.js",
"src/layers/bikeshare/stationPoints.js",
"src/layers/shared/viewRectangle.js",
@@ -3515,8 +3492,6 @@
"src/layers/installations/render/navaraOutlineBatch.js",
"src/layers/installations/render/navaraStencilBatch.js",
"src/layers/firms/rectangle.js",
"src/layers/firms/render/cesium.js",
"src/layers/firms/render/navara.js",
"src/layers/shared/render/navaraMoverBank.js",
"src/layers/shared/render/moverBank.js",
"src/layers/recentImagery/render/navara.js",
@@ -3526,13 +3501,8 @@
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/radio/screenClusters.js",
"src/layers/shared/render/navaraCellGeometry.js",
"src/layers/shared/render/navaraGroundCells.js",
"src/layers/firms/render/navaraSprites.js",
"src/layers/shared/render/imageAtlas.js",
"src/layers/firms/render/navaraHeat.js",
"src/layers/shared/render/navaraGroundGeometry.js",
"src/layers/shared/render/navaraGroundGrid.js",
"src/layers/shared/render/navaraGroundSurface.js",
"src/layers/shared/render/navaraGroundEffect.js",
"src/annotations/outlineRungs.js",
@@ -3602,7 +3572,19 @@
"src/layers/cctv/render/cesiumMeshes.js",
"src/layers/cctv/render/meshes.js",
"src/layers/cctv/render/display.js",
"src/layers/transit/render/display.js"
"src/layers/transit/render/display.js",
"src/layers/earthquakes/render/display.js",
"src/layers/airQuality/render/display.js",
"src/layers/submarineCables/render/display.js",
"src/layers/firms/render/sprites.js",
"src/layers/cyclones/render/display.js",
"src/layers/droneAirspace/render/display.js",
"src/layers/ghostMaps/render/display.js",
"src/layers/perimeters/render/display.js",
"src/layers/gnss/render/display.js",
"src/layers/firms/render/cesiumHeat.js",
"src/layers/firms/render/heatField.js",
"src/layers/firms/render/display.js"
],
"external": [
"hls.js",
@@ -3935,13 +3917,8 @@
"src/layers/submarineCables/geometry.js",
"src/layers/submarineCables/index.js",
"src/layers/submarineCables/session.js",
"src/layers/submarineCables/ingestion.js",
"src/layers/submarineCables/interaction.js",
"src/layers/submarineCables/lifecycle.js",
"src/layers/submarineCables/overlay.js",
"src/layers/submarineCables/policy.js",
"src/layers/submarineCables/rendering.js",
"src/layers/submarineCables/state.js",
"src/layers/submarineCables/surface.js",
"src/layers/traffic/animation.js",
"src/layers/traffic/controls.js",
@@ -4031,20 +4008,16 @@
"src/layers/earthquakes/records.js",
"src/app/layers/perimeters.js",
"src/layers/perimeters/index.js",
"src/layers/perimeters/render/cesium.js",
"src/layers/shared/render/cesiumZones.js",
"src/layers/perimeters/source.js",
"src/layers/perimeters/records.js",
"src/layers/perimeters/cards.js",
"src/layers/perimeters/inciweb.js",
"src/app/layers/gnss.js",
"src/layers/gnss/index.js",
"src/layers/gnss/render/cesium.js",
"src/layers/gnss/source.js",
"src/layers/gnss/records.js",
"src/app/layers/ghostMaps.js",
"src/layers/ghostMaps/index.js",
"src/layers/ghostMaps/render/cesium.js",
"src/layers/ghostMaps/catalog.js",
"src/layers/ghostMaps/records.js",
"src/layers/ghostMaps/kml.js",
@@ -4211,7 +4184,6 @@
"src/data/cameraFraming.js",
"src/app/layers/airQuality.js",
"src/layers/airQuality/index.js",
"src/layers/airQuality/render/cesium.js",
"src/layers/airQuality/model.js",
"src/layers/airQuality/source.js",
"src/ui/statusText.js",
@@ -4282,9 +4254,7 @@
"src/celestialRing/render/cesium.js",
"src/celestialRing/view.js",
"src/layers/shared/render/engineView.js",
"src/layers/earthquakes/render/cesium.js",
"src/layers/firms/rectangle.js",
"src/layers/firms/render/cesium.js",
"src/annotations/outlineRungs.js",
"src/sources/openFreeMapOutlines.js",
"src/sources/nominatimOutlines.js",
@@ -4337,7 +4307,20 @@
"src/layers/cctv/render/cesiumMeshes.js",
"src/layers/cctv/render/meshes.js",
"src/layers/cctv/render/display.js",
"src/layers/transit/render/display.js"
"src/layers/transit/render/display.js",
"src/layers/earthquakes/render/display.js",
"src/layers/airQuality/render/display.js",
"src/layers/cyclones/render/display.js",
"src/layers/gnss/render/display.js",
"src/layers/firms/render/sprites.js",
"src/layers/submarineCables/render/display.js",
"src/layers/perimeters/render/display.js",
"src/layers/droneAirspace/render/display.js",
"src/globe/ground.js",
"src/layers/ghostMaps/render/display.js",
"src/layers/firms/render/cesiumHeat.js",
"src/layers/firms/render/heatField.js",
"src/layers/firms/render/display.js"
],
"external": [
"hls.js",
@@ -5185,8 +5168,6 @@
"src/geo/cartesian3.js",
"src/geo/math.js",
"src/layers/perimeters/index.js",
"src/layers/perimeters/render/cesium.js",
"src/layers/shared/render/cesiumZones.js",
"src/sources/httpBody.js",
"src/i18n/marker.js",
"src/layers/perimeters/source.js",
@@ -5205,7 +5186,9 @@
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json"
"src/i18n/locales/zh-Hant.json",
"src/layers/shared/color.js",
"src/layers/perimeters/render/display.js"
],
"external": ["cesium"]
},
@@ -5213,7 +5196,6 @@
"exports": ["./layers/gnss"],
"modules": [
"src/layers/gnss/index.js",
"src/layers/gnss/render/cesium.js",
"src/sources/httpBody.js",
"src/i18n/marker.js",
"src/layers/gnss/source.js",
@@ -5232,7 +5214,18 @@
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/ui/statusText.js",
"src/ui/layerNames.js"
"src/ui/layerNames.js",
"src/layers/shared/viewRectangle.js",
"src/layers/shared/color.js",
"src/geo/math.js",
"src/geo/cartesian2.js",
"src/geo/matrix3.js",
"src/geo/cartesian3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/boundingSphere.js",
"src/geo/",
"src/layers/gnss/render/display.js"
],
"external": ["cesium"]
},
@@ -5279,7 +5272,6 @@
"exports": ["./layers/ghostmaps"],
"modules": [
"src/layers/ghostMaps/index.js",
"src/layers/ghostMaps/render/cesium.js",
"src/layers/ghostMaps/catalog.js",
"src/layers/ghostMaps/records.js",
"src/layers/ghostMaps/kml.js",
@@ -5335,7 +5327,6 @@
"modules": [
"src/layers/droneAirspace/index.js",
"src/geo/",
"src/layers/shared/render/cesiumZones.js",
"src/sources/httpBody.js",
"src/layers/droneAirspace/source.js",
"src/layers/droneAirspace/records.js",
@@ -5353,7 +5344,9 @@
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json"
"src/i18n/locales/zh-Hant.json",
"src/layers/shared/color.js",
"src/layers/droneAirspace/render/display.js"
],
"external": ["cesium"]
},
@@ -5538,7 +5531,6 @@
"src/geo/cartesian3.js",
"src/geo/math.js",
"src/layers/airQuality/index.js",
"src/layers/airQuality/render/cesium.js",
"src/layers/airQuality/model.js",
"src/layers/airQuality/source.js",
"src/sources/httpBody.js",
@@ -5557,7 +5549,9 @@
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/ui/statusText.js",
"src/ui/layerNames.js"
"src/ui/layerNames.js",
"src/layers/shared/color.js",
"src/layers/airQuality/render/display.js"
],
"external": ["cesium"]
},
-313
View File
@@ -1,313 +0,0 @@
#!/usr/bin/env node
/**
* The engine.ground prototypes of earthquakes and fire perimeters
* (src/layers/earthquakes/render/ground.js,
* src/layers/perimeters/render/ground.js) against what each engine draws
* today, on fixed data over California in daylight. Per engine: the canvas
* without the layers, with today's renderers, with the ground prototypes;
* the pixels each drawing changed (a channel off by more than 24), and
* their overlap (intersection over union) with today's Cesium drawing; a
* pick of a quake and of a perimeter through engine.picker. Writes the
* images and a side-by-side to --out.
*
* GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4271
* node scripts/qa-ground-prototypes.mjs http://localhost:4271 --out=DIR
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
const BLOCKED_HOSTS =
/(^|\.)(googleapis\.com|google\.com|gstatic\.com|cesium\.com)$/;
const args = process.argv.slice(2);
const origin = args.find((arg) => !arg.startsWith('--'));
const option = (name) =>
args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
const out = option('out') ?? 'qa-ground-prototypes';
if (!origin) {
console.error(
'Usage: node scripts/qa-ground-prototypes.mjs <origin> --out=DIR',
);
process.exit(2);
}
fs.mkdirSync(out, { recursive: true });
const VIEW = { lon: -119.6, lat: 37.2, alt: 700000 };
async function capture(page, kind) {
const ui = await page.evaluate(
async (kind, view) => {
const { engine } = window.__godsEyeView;
const frame = () =>
new Promise((resolve) => {
const remove = engine.frames.postRender.addEventListener(() => {
remove();
resolve();
});
engine.render.request('ground-prototypes');
});
const settle = async () => {
let loaded = 0;
const started = performance.now();
while (loaded < 20 && performance.now() - started < 60000) {
await frame();
loaded = engine.surface.tilesLoaded() ? loaded + 1 : 0;
}
};
const shot = async () => {
await settle();
const canvas = engine.host.canvas;
const copy = document.createElement('canvas');
copy.width = canvas.clientWidth;
copy.height = canvas.clientHeight;
copy.getContext('2d').drawImage(canvas, 0, 0, copy.width, copy.height);
return copy.toDataURL('image/png');
};
const geo = await import('/src/geo/index.js');
const { depthColor } = await import('/src/layers/earthquakes/model.js');
// Fixed quakes and perimeters (one with a hole) in the view.
const quakes = [
[-121.8, 36.6, 3.1, 8],
[-120.6, 35.9, 4.6, 12],
[-118.4, 37.6, 5.4, 9],
[-117.6, 36.2, 2.8, 120],
[-121.0, 38.4, 3.9, 400],
[-119.0, 38.9, 6.0, 15],
].map(([lon, lat, mag, depthKm], index) => ({
stableId: `q${index}`,
usgsId: `q${index}`,
position: geo.cartesian3.fromDegrees(lon, lat),
baseRadius: 2 ** mag * 1000,
color: depthColor(depthKm),
isSignificant: mag >= 5,
fillAlpha: mag >= 5 ? 0.4 : 0.3,
outlineAlpha: mag >= 5 ? 1 : 0.8,
mag,
place: `fixture ${index}`,
time: 0,
depthKm,
}));
const blob = (lon, lat, rx, ry, count = 40) => {
const ring = [];
for (let k = 0; k < count; k++) {
const angle = (k / count) * 2 * Math.PI;
const wobble = 1 + 0.18 * Math.sin(angle * 5);
ring.push([
lon + rx * wobble * Math.cos(angle),
lat + ry * wobble * Math.sin(angle),
]);
}
ring.push(ring[0]);
return ring;
};
const perimeters = [
{
stableId: 'p0',
containedPct: 10,
polygons: [
[blob(-120.2, 37.9, 0.35, 0.25), blob(-120.2, 37.9, 0.12, 0.08)],
],
},
{
stableId: 'p1',
containedPct: 60,
polygons: [[blob(-118.9, 36.6, 0.25, 0.3)]],
},
{
stableId: 'p2',
containedPct: 100,
polygons: [
[blob(-121.4, 37.4, 0.15, 0.12)],
[blob(-121.1, 37.1, 0.1, 0.1)],
],
},
];
// A restored view settles after the loading screen: take the camera.
await new Promise((resolve) => setTimeout(resolve, 3000));
engine.camera.setView({
destination: geo.cartesian3.fromDegrees(view.lon, view.lat, view.alt),
});
const images = { base: await shot() };
// Today's renderers on this engine.
let quakeRenderer;
let perimeterRenderer;
if (kind === 'cesium') {
const { viewer } = engine.extensions.cesium;
const { createEarthquakesCesiumRenderer } =
await import('/src/layers/earthquakes/render/cesium.js');
const { createPerimetersCesiumRenderer } =
await import('/src/layers/perimeters/render/cesium.js');
quakeRenderer = createEarthquakesCesiumRenderer(engine, { viewer });
perimeterRenderer = createPerimetersCesiumRenderer({
viewer,
screenSpaceEventHandlerFactory: () => ({
setInputAction() {},
destroy() {},
}),
});
} else {
const navara = engine.extensions.navara;
const { createEarthquakesNavaraRenderer } =
await import('/src/layers/earthquakes/render/navara.js');
const { createPerimetersNavaraRenderer } =
await import('/src/layers/perimeters/render/navara.js');
quakeRenderer = createEarthquakesNavaraRenderer(engine, navara);
perimeterRenderer = createPerimetersNavaraRenderer(engine, navara);
}
quakeRenderer.replace(quakes);
quakeRenderer.setVisible(true);
perimeterRenderer.replace(perimeters);
perimeterRenderer.setVisible(true);
for (let index = 0; index < 60; index++) await frame();
images.today = await shot();
quakeRenderer.destroy();
perimeterRenderer.destroy();
// The prototypes on engine.ground.
const { createEarthquakesGroundRenderer } =
await import('/src/layers/earthquakes/render/ground.js');
const { createPerimetersGroundRenderer } =
await import('/src/layers/perimeters/render/ground.js');
const quakesOnGround = createEarthquakesGroundRenderer(engine);
const perimetersOnGround = createPerimetersGroundRenderer(engine);
quakesOnGround.replace(quakes);
quakesOnGround.setVisible(true);
perimetersOnGround.replace(perimeters);
perimetersOnGround.setVisible(true);
const ready = () =>
engine.ground.list().every(({ id }) => id) &&
['earthquakes', 'fire-perimeters'].every((id) =>
engine.ground.list().some((entry) => entry.id === id),
);
while (!ready()) await frame();
for (let index = 0; index < 60; index++) await frame();
images.ground = await shot();
const screen = (lon, lat) =>
engine.projector.worldToWindow(geo.cartesian3.fromDegrees(lon, lat));
const picks = {
quake: engine.picker.pick(screen(-118.4, 37.6))?.id ?? null,
perimeter: engine.picker.pick(screen(-118.9, 36.6))?.id ?? null,
hole: engine.picker.pick(screen(-120.2, 37.9))?.id ?? null,
records: quakesOnGround.records(10).length,
};
quakesOnGround.destroy();
perimetersOnGround.destroy();
return { images, picks };
},
kind,
VIEW,
);
return ui;
}
async function openEngine(browser, kind) {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
page.on('pageerror', (error) => console.log(' page error:', error.message));
await page.setRequestInterception(true);
page.on('request', (request) => {
if (BLOCKED_HOSTS.test(new URL(request.url()).hostname)) request.abort();
else request.continue();
});
await page.goto(
`${origin}/?welcome=0&engine=${kind}#v=2&lat=${VIEW.lat}&lon=${VIEW.lon}&alt=${VIEW.alt}&heading=0&pitch=-90&map=osm&l=`,
);
await page.waitForFunction(
(expected) =>
window.__godsEyeView?.engine?.kind === expected &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 300_000, polling: 250 },
kind,
);
// Daylight over California (Navara lights the scene by the app clock).
await page.evaluate(async () => {
const { appClock } = await import('/src/time/clock.js');
appClock.freeze(Date.UTC(2026, 10, 7, 20, 0));
});
return page;
}
const decode = async (dataUrl) => {
const { data, info } = await sharp(
Buffer.from(dataUrl.split(',')[1], 'base64'),
)
.raw()
.ensureAlpha()
.toBuffer({ resolveWithObject: true });
return { data, width: info.width, height: info.height };
};
// Pixels where `image` differs from `base` by more than 24 on a channel.
const changed = (base, image) => {
const mask = new Uint8Array(base.width * base.height);
for (let i = 0; i < mask.length; i++)
for (let c = 0; c < 3; c++)
if (Math.abs(base.data[i * 4 + c] - image.data[i * 4 + c]) > 24) {
mask[i] = 1;
break;
}
return mask;
};
const iou = (a, b) => {
let both = 0;
let either = 0;
for (let i = 0; i < a.length; i++) {
both += a[i] & b[i];
either += a[i] | b[i];
}
return either ? both / either : 1;
};
const count = (mask) => mask.reduce((total, value) => total + value, 0);
const browser = await puppeteer.launch({ headless: true });
const results = {};
try {
for (const kind of ['cesium', 'navara']) {
const page = await openEngine(browser, kind);
results[kind] = await capture(page, kind);
await page.close();
}
} finally {
await browser.close();
}
const masks = {};
for (const kind of ['cesium', 'navara']) {
const { images, picks } = results[kind];
const base = await decode(images.base);
masks[kind] = {
today: changed(base, await decode(images.today)),
ground: changed(base, await decode(images.ground)),
};
for (const name of ['base', 'today', 'ground'])
fs.writeFileSync(
path.join(out, `${kind}-${name}.png`),
Buffer.from(images[name].split(',')[1], 'base64'),
);
console.log(
`${kind}: today changed ${count(masks[kind].today)} px, ground ${count(masks[kind].ground)} px; picks ${JSON.stringify(picks)}`,
);
}
const reference = masks.cesium.today;
console.log(
`overlap with today's Cesium drawing (IoU of changed pixels): Cesium ground ${iou(reference, masks.cesium.ground).toFixed(3)}, Navara ground ${iou(reference, masks.navara.ground).toFixed(3)}, Navara today ${iou(reference, masks.navara.today).toFixed(3)}`,
);
// Side by side: Cesium today, Cesium ground, Navara today, Navara ground.
const tiles = await Promise.all(
['cesium-today', 'cesium-ground', 'navara-today', 'navara-ground'].map(
(name) =>
sharp(path.join(out, `${name}.png`))
.resize(640, 400)
.png()
.toBuffer(),
),
);
await sharp({
create: { width: 1280, height: 800, channels: 3, background: '#000' },
})
.composite(
tiles.map((input, index) => ({
input,
left: (index % 2) * 640,
top: Math.floor(index / 2) * 400,
})),
)
.png()
.toFile(path.join(out, 'side-by-side.png'));