Cut duplicated and dead code across the engines, the layers and the QA scripts

- src/geo drops the ported debug argument checks and the tests that
  bitExact already covers; package boundaries accept a directory entry,
  so src/geo/ is listed once per group.
- Cesium no longer carries movers and lines nobody draws with: they are
  optional engine capabilities. The fake engine and the Cesium camera use
  the shared cameraMath; unused contract tables, clock.ticked, the
  'finest' surface detail and Navara click statistics are gone.
- ALPR keeps one overlay core and one presentation model for both
  engines, bikeshare one station bookkeeping, the Navara ground effects
  one definition helper; image atlases, view centres, mover banks and
  colours are shared.
- The legacy switch route, the clock offset and rate controls and other
  unused exports are removed.
- The acceptance runs the Navara draw, geo import and scope checks;
  scenarios use the harness switch and page helpers; the main chunk
  check keeps the size budget only and runs once in CI.
This commit is contained in:
German Khokhlov committed 2026-11-05 13:32:39 +04:00
1 parent c502404e1d
commit 7abf74518f
272 files changed
+4828 -10213

No files matched your search

+2 -7
View File
@@ -59,7 +59,8 @@ jobs:
- name: Build production bundle - name: Build production bundle
run: npm run build run: npm run build
- name: Main chunk (no three or Navara in Cesium mode; size budget when recorded) - name: Main chunk size budget
if: matrix.node == '24.22.2'
run: npm run check:main-chunk run: npm run check:main-chunk
street-level-browser: street-level-browser:
@@ -90,9 +91,6 @@ jobs:
MAPILLARY_CLIENT_TOKEN: 'MLY|0|qa-fixture' MAPILLARY_CLIENT_TOKEN: 'MLY|0|qa-fixture'
run: npm run build run: npm run build
- name: Main chunk (no three or Navara in Cesium mode; size budget when recorded)
run: npm run check:main-chunk
- name: Start the preview server - name: Start the preview server
env: env:
MAPILLARY_CLIENT_TOKEN: 'MLY|0|qa-fixture' MAPILLARY_CLIENT_TOKEN: 'MLY|0|qa-fixture'
@@ -154,6 +152,3 @@ jobs:
- name: Build production bundle - name: Build production bundle
run: npm run build run: npm run build
- name: Main chunk (no three or Navara in Cesium mode; size budget when recorded)
run: npm run check:main-chunk
+3 -4
View File
@@ -52,14 +52,13 @@ resolved dependency inventory remains in `package-lock.json`.
are adapted from `@cesium/engine/Source/Core` (`Math.js`, `Ellipsoid.js`, are adapted from `@cesium/engine/Source/Core` (`Math.js`, `Ellipsoid.js`,
`scaleToGeodeticSurface.js`, `Cartographic.js`, `Cartesian2.js`, `scaleToGeodeticSurface.js`, `Cartographic.js`, `Cartesian2.js`,
`Cartesian3.js`, `Matrix3.js`, `Matrix4.js`, `Quaternion.js`, `Cartesian3.js`, `Matrix3.js`, `Matrix4.js`, `Quaternion.js`,
`Transforms.js`, `Check.js`, `DeveloperError.js`) and say so in their first `Transforms.js`) and say so in their first
line. The changes: the classes' static functions became module functions, line. The changes: the classes' static functions became module functions,
vectors, geodetic positions and quaternions became plain-data instances of vectors, geodetic positions and quaternions became plain-data instances of
module-private constructors (own enumerable fields only, no methods), module-private constructors (own enumerable fields only, no methods),
matrices plain arrays, and the ellipsoid a frozen object; the debug matrices plain arrays, and the ellipsoid a frozen object; the debug
argument checks were ported with Cesium's conditions and messages and, like argument checks were left out, as in Cesium's release build; only the subset
Cesium's, are active in development and tests and stripped from production gev uses was kept.
builds; only the subset gev uses was kept.
- Author: Copyright 2011-2024 CesiumJS Contributors - Author: Copyright 2011-2024 CesiumJS Contributors
- Source: <https://github.com/CesiumGS/cesium> - Source: <https://github.com/CesiumGS/cesium>
- License: Apache License 2.0 - License: Apache License 2.0
+1 -2
View File
@@ -2,8 +2,7 @@
// initial chunks are every entry chunk and what it imports statically; // initial chunks are every entry chunk and what it imports statically;
// navaraMode.js reaches Navara only through import(), so three and // navaraMode.js reaches Navara only through import(), so three and
// @navaramap/* belong to lazy chunks. This checks the bundle's module ids // @navaramap/* belong to lazy chunks. This checks the bundle's module ids
// (what Rollup put in each chunk), and fails the build otherwise; // (what Rollup put in each chunk), and fails the build otherwise.
// scripts/check-main-chunk.mjs checks the built files' text as well.
/** /**
* Module ids that only the Navara engine may load, when no package list is * Module ids that only the Navara engine may load, when no package list is
+2 -1
View File
@@ -55,7 +55,8 @@ See [application construction](APPLICATION.md) and the
2. `scripts/check-package-boundaries.mjs` builds every declared export without 2. `scripts/check-package-boundaries.mjs` builds every declared export without
app Vite configuration or environment files. `scripts/package-boundaries.json` app Vite configuration or environment files. `scripts/package-boundaries.json`
assigns each export exactly once and lists its owned modules and external assigns each export exactly once and lists its owned modules and external
dependencies. Unused imports still count. Node exports have only a `node` dependencies; a module entry ending in `/` (such as `src/geo/`) owns every
file under that directory. Unused imports still count. Node exports have only a `node`
condition; browser groups cannot use build-only dependency exceptions. condition; browser groups cannot use build-only dependency exceptions.
Portable source graphs cannot reach application/rendering, Node, Cesium or Portable source graphs cannot reach application/rendering, Node, Cesium or
+7 -7
View File
@@ -61,21 +61,21 @@
"@jtarrio/webrtlsdr": "^3.0.6", "@jtarrio/webrtlsdr": "^3.0.6",
"@mapbox/vector-tile": "^3.0.0", "@mapbox/vector-tile": "^3.0.0",
"@meri-imperiumi/eccodes-wasm": "^2.48.2", "@meri-imperiumi/eccodes-wasm": "^2.48.2",
"@navaramap/three": "0.2.1",
"@navaramap/three-default-plugin": "0.2.1",
"cesium": "^1.124.0", "cesium": "^1.124.0",
"egm96-universal": "^1.1.1", "egm96-universal": "^1.1.1",
"hls.js": "^1.7.3", "hls.js": "^1.7.3",
"kdbush": "4.0.2",
"mapillary-js": "^4.1.2", "mapillary-js": "^4.1.2",
"mgrs": "^2.1.0", "mgrs": "^2.1.0",
"pbf": "^5.1.2", "pbf": "^5.1.2",
"pinia": "^2.3.1", "pinia": "^2.3.1",
"satellite.js": "^6.0.2",
"vue": "^3.5.44",
"vue-i18n": "^11.4.15",
"@navaramap/three": "0.2.1",
"@navaramap/three-default-plugin": "0.2.1",
"kdbush": "4.0.2",
"postprocessing": "6.39.4", "postprocessing": "6.39.4",
"three": "0.185.1" "satellite.js": "^6.0.2",
"three": "0.185.1",
"vue": "^3.5.44",
"vue-i18n": "^11.4.15"
}, },
"devDependencies": { "devDependencies": {
"@happy-dom/global-registrator": "^20.14.6", "@happy-dom/global-registrator": "^20.14.6",
+3 -3
View File
@@ -30,9 +30,9 @@ const globeEngine = (file) => /^src\/globe\/(?:cesium|navara)\//.test(file);
const cesiumPackage = /^(?:cesium(?:\/|$)|@cesium\/)/; const cesiumPackage = /^(?:cesium(?:\/|$)|@cesium\/)/;
// Navara renders through three.js and its postprocessing peer. // Navara renders through three.js and its postprocessing peer.
const navaraPackage = /^(?:(?:three|postprocessing)(?:\/|$)|@navaramap\/)/; const navaraPackage = /^(?:(?:three|postprocessing)(?:\/|$)|@navaramap\/)/;
// Phase 3: the layers moved onto layer renderers (the *a tasks) keep each // Layers built on layer renderers keep each engine in their renderers:
// engine in their renderers: cesium only in render/cesium*.js, three and // cesium only in render/cesium*.js, three and Navara only in
// Navara only in render/navara*.js. The list grows with each moved layer. // render/navara*.js. The list grows with each layer moved onto renderers.
const RENDERED_LAYER_DIRS = [ const RENDERED_LAYER_DIRS = [
'src/layers/alpr/', 'src/layers/alpr/',
'src/layers/airQuality/', 'src/layers/airQuality/',
+17 -97
View File
@@ -1,17 +1,12 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* The main-chunk gate of the Cesium-mode bundle (run after `npm run build`): * The size budget of the Cesium-mode main chunk (run after `npm run build`):
* gzip-9 of the entry chunk the built page loads (from the Vite manifest, or
* the module script dist/index.html loads) may exceed the baseline in
* scripts/main-chunk-baseline.json by at most its allowance. What the initial
* chunks may hold is the build's own check (build/initialChunkGuard.js).
* *
* 1. Composition: the main chunk (the Vite manifest's entry, or the module * Prints the size. `--json` prints the whole result; `--dist=DIR` reads
* script dist/index.html loads) and every chunk it imports statically carry
* no three or Navara code (text markers; the build itself also fails on
* three/@navaramap module ids in initial chunks, build/initialChunkGuard.js).
* 2. Budget, when scripts/main-chunk-baseline.json exists: gzip-9 of the
* largest dist/assets/index-*.js may exceed the recorded baseline by at
* most its allowance (the phase-4 branch's check; same exports,
* checkMainChunk and measureMainChunk, so the two merge into this script).
*
* Prints the sizes. `--json` prints the whole result; `--dist=DIR` reads
* another build. * another build.
* *
* npm run build && npm run check:main-chunk * npm run build && npm run check:main-chunk
@@ -20,12 +15,7 @@ import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { gzipSync } from 'node:zlib'; import { gzipSync } from 'node:zlib';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { import { entryFromHtml, entryFromManifest } from '../src/tooling/mainChunk.js';
entryFromHtml,
entryFromManifest,
forbiddenMarkersIn,
initialChunks,
} from '../src/tooling/mainChunk.js';
const ROOT = fileURLToPath(new URL('..', import.meta.url)); const ROOT = fileURLToPath(new URL('..', import.meta.url));
@@ -44,38 +34,14 @@ export function checkMainChunk(
/** /**
* gzip-9 size of the main chunk, with its name: the entry the built page * gzip-9 size of the main chunk, with its name: the entry the built page
* loads (`entry`, or found from dist/index.html or the Vite manifest), else * loads (`entry`, else found from the Vite manifest or dist/index.html).
* (phase 4's rule, for a dist without a page) the largest
* dist/assets/index-*.js.
*/ */
export function measureMainChunk(distDir, { entry = null } = {}) { export function measureMainChunk(distDir, { entry = null } = {}) {
let file = entry; const file = entry ?? findEntry(distDir).entry;
if (!file) { const bytes = fs.readFileSync(path.join(distDir, file));
try {
file = findEntry(distDir).entry;
} catch {
file = null;
}
}
if (file) {
const bytes = fs.readFileSync(path.join(distDir, file));
return {
name: path.basename(file),
gzipBytes: gzipSync(bytes, { level: 9 }).length,
};
}
const assets = path.join(distDir, 'assets');
const candidates = fs
.readdirSync(assets)
.filter((name) => /^index-.*\.js$/.test(name))
.map((name) => ({ name, bytes: fs.readFileSync(path.join(assets, name)) }))
.sort((a, b) => b.bytes.length - a.bytes.length);
if (!candidates.length)
throw new Error('no dist/assets/index-*.js: run npm run build first');
const [main] = candidates;
return { return {
name: main.name, name: path.basename(file),
gzipBytes: gzipSync(main.bytes, { level: 9 }).length, gzipBytes: gzipSync(bytes, { level: 9 }).length,
}; };
} }
@@ -96,44 +62,15 @@ function findEntry(dist) {
return { entry, via: 'index.html' }; return { entry, via: 'index.html' };
} }
/** Composition of the initial chunks: sizes and forbidden markers. */
export function checkComposition(dist) {
const read = (file) => {
const absolute = path.join(dist, file);
return fs.existsSync(absolute) ? fs.readFileSync(absolute, 'utf8') : null;
};
const { entry, via } = findEntry(dist);
const chunks = initialChunks(entry, read).map((file) => {
const code = read(file);
if (code == null)
return { file, missing: true, bytes: 0, gzipBytes: 0, markers: [] };
return {
file,
bytes: Buffer.byteLength(code),
gzipBytes: gzipSync(code).length,
markers: forbiddenMarkersIn(code),
};
});
return {
entry,
via,
mainBytes: chunks[0].bytes,
mainGzipBytes: chunks[0].gzipBytes,
initialBytes: chunks.reduce((sum, chunk) => sum + chunk.bytes, 0),
chunks,
ok: chunks.every((chunk) => !chunk.missing && !chunk.markers.length),
};
}
function main() { function main() {
const option = (name) => const option = (name) =>
process.argv.find((arg) => arg.startsWith(`--${name}=`))?.split('=')[1]; process.argv.find((arg) => arg.startsWith(`--${name}=`))?.split('=')[1];
const dist = path.resolve(ROOT, option('dist') ?? 'dist'); const dist = path.resolve(ROOT, option('dist') ?? 'dist');
const composition = checkComposition(dist); const { entry, via } = findEntry(dist);
const baselinePath = path.join(ROOT, 'scripts/main-chunk-baseline.json'); const baselinePath = path.join(ROOT, 'scripts/main-chunk-baseline.json');
let budget = null; let budget = null;
if (fs.existsSync(baselinePath)) { if (fs.existsSync(baselinePath)) {
const measured = measureMainChunk(dist, { entry: composition.entry }); const measured = measureMainChunk(dist, { entry });
budget = { budget = {
...measured, ...measured,
...checkMainChunk( ...checkMainChunk(
@@ -142,28 +79,11 @@ function main() {
), ),
}; };
} }
const ok = composition.ok && (budget?.ok ?? true); const ok = budget?.ok ?? true;
if (process.argv.includes('--json')) { if (process.argv.includes('--json')) {
console.log(JSON.stringify({ composition, budget, ok }, null, 2)); console.log(JSON.stringify({ entry, via, budget, ok }, null, 2));
} else { } else {
const kb = (bytes) => `${(bytes / 1000).toFixed(2)} kB`; console.log(`[check:main-chunk] entry ${entry} (via ${via})`);
console.log(
`[check:main-chunk] entry ${composition.entry} (via ${composition.via})`,
);
for (const chunk of composition.chunks)
console.log(
`[check:main-chunk] ${chunk.file}: ${kb(chunk.bytes)} (gzip ${kb(chunk.gzipBytes)})` +
(chunk.missing ? ' MISSING' : '') +
(chunk.markers.length
? ` FORBIDDEN: ${chunk.markers.join(', ')}`
: ''),
);
console.log(
`[check:main-chunk] main ${composition.mainBytes} bytes (gzip ${composition.mainGzipBytes}); initial chunks ${composition.initialBytes} bytes; ` +
(composition.ok
? 'no three or Navara'
: 'three or Navara in the initial chunks'),
);
console.log( console.log(
budget budget
? `[check:main-chunk] ${budget.name}: ${budget.message}` ? `[check:main-chunk] ${budget.name}: ${budget.message}`
+11 -3
View File
@@ -37,7 +37,11 @@ export async function checkPackageBoundaries(root) {
) { ) {
throw new Error(`Invalid package boundary: ${name}`); throw new Error(`Invalid package boundary: ${name}`);
} }
// A module entry ending in "/" owns every file under that directory.
const allowed = new Set(); const allowed = new Set();
const allowedDirectories = [];
const owns = (id) =>
allowed.has(id) || allowedDirectories.some((dir) => id.startsWith(dir));
for (const module of group.modules) { for (const module of group.modules) {
if ( if (
typeof module !== 'string' || typeof module !== 'string' ||
@@ -56,7 +60,11 @@ export async function checkPackageBoundaries(root) {
) { ) {
throw new Error(`Boundary module escapes repository: ${name}`); throw new Error(`Boundary module escapes repository: ${name}`);
} }
allowed.add(normalizePath(resolved)); if (module.endsWith('/')) {
allowedDirectories.push(`${normalizePath(resolved)}/`);
} else {
allowed.add(normalizePath(resolved));
}
} }
for (const external of group.external) { for (const external of group.external) {
if ( if (
@@ -82,7 +90,7 @@ export async function checkPackageBoundaries(root) {
if ( if (
typeof target !== 'string' || typeof target !== 'string' ||
!target.startsWith('./') || !target.startsWith('./') ||
!allowed.has(normalizePath(path.resolve(root, target))) !owns(normalizePath(path.resolve(root, target)))
) { ) {
throw new Error(`Export must point to an owned module: ${key}`); throw new Error(`Export must point to an owned module: ${key}`);
} }
@@ -109,7 +117,7 @@ export async function checkPackageBoundaries(root) {
// template blocks belong to the component file itself. // template blocks belong to the component file itself.
if (info.id.startsWith('\0')) return; if (info.id.startsWith('\0')) return;
const id = info.id.split('?')[0]; const id = info.id.split('?')[0];
if (!allowed.has(id)) { if (!owns(id)) {
throw new Error( throw new Error(
`Package boundary ${name} imports an unowned module: ${path.relative(root, id)}`, `Package boundary ${name} imports an unowned module: ${path.relative(root, id)}`,
); );
+10 -4
View File
@@ -589,6 +589,8 @@
"src/layers/alpr/model.js", "src/layers/alpr/model.js",
"src/layers/alpr/policy.js", "src/layers/alpr/policy.js",
"src/layers/alpr/visuals.js", "src/layers/alpr/visuals.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/alpr/overlay.test.mjs", "src/layers/alpr/overlay.test.mjs",
"src/layers/alpr/source.js", "src/layers/alpr/source.js",
"src/layers/alpr/source.test.mjs", "src/layers/alpr/source.test.mjs",
@@ -1217,6 +1219,7 @@
"src/layers/aircraft/render/contract.js", "src/layers/aircraft/render/contract.js",
"src/layers/shared/color.test.mjs", "src/layers/shared/color.test.mjs",
"src/layers/shared/color.js", "src/layers/shared/color.js",
"src/layers/shared/viewRectangle.js",
"src/layers/military/render/cesium.js", "src/layers/military/render/cesium.js",
"src/layers/military/render/navara.js", "src/layers/military/render/navara.js",
"src/layers/aircraft/qaDrawn.js", "src/layers/aircraft/qaDrawn.js",
@@ -1225,13 +1228,18 @@
"src/layers/vessels/render/cesium.js", "src/layers/vessels/render/cesium.js",
"src/layers/vessels/render/contract.js", "src/layers/vessels/render/contract.js",
"src/layers/vessels/renderer.test.mjs", "src/layers/vessels/renderer.test.mjs",
"src/layers/vessels/testFixtures.mjs",
"src/layers/vessels/render/navara.js", "src/layers/vessels/render/navara.js",
"src/layers/shared/render/engineView.js", "src/layers/shared/render/engineView.js",
"src/layers/shared/render/navaraGroundEffect.js",
"src/layers/shared/render/navaraMoverBank.js",
"src/layers/shared/render/imageAtlas.js",
"src/layers/installations/render/cesium/lifecycle.js", "src/layers/installations/render/cesium/lifecycle.js",
"src/layers/installations/render/cesium/rendering.js", "src/layers/installations/render/cesium/rendering.js",
"src/layers/installations/render/cesium/selection.js", "src/layers/installations/render/cesium/selection.js",
"src/layers/bikeshare/render/cesium/lifecycle.js", "src/layers/bikeshare/render/cesium/lifecycle.js",
"src/layers/bikeshare/presentation.js", "src/layers/bikeshare/presentation.js",
"src/layers/bikeshare/stationPoints.js",
"src/layers/bikeshare/presentation.test.mjs", "src/layers/bikeshare/presentation.test.mjs",
"src/layers/bikeshare/selectionModel.js", "src/layers/bikeshare/selectionModel.js",
"src/layers/bikeshare/render/cesium.js", "src/layers/bikeshare/render/cesium.js",
@@ -1329,8 +1337,8 @@
"src/globe/cesium/render.js", "src/globe/cesium/render.js",
"src/globe/cesium/surface.js", "src/globe/cesium/surface.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/globe/navara/cameraMath.js", "src/globe/cameraMath.js",
"src/globe/navara/cameraMath.test.mjs", "src/globe/cameraMath.test.mjs",
"src/globe/navara/cameraModel.js", "src/globe/navara/cameraModel.js",
"src/globe/navara/cameraModel.test.mjs", "src/globe/navara/cameraModel.test.mjs",
"src/globe/navara/createNavaraEngine.js", "src/globe/navara/createNavaraEngine.js",
@@ -1364,14 +1372,12 @@
"src/tooling/mainChunkComposition.test.mjs", "src/tooling/mainChunkComposition.test.mjs",
"src/tooling/initialChunkGuard.test.mjs", "src/tooling/initialChunkGuard.test.mjs",
"src/globe/movers.test.mjs", "src/globe/movers.test.mjs",
"src/globe/cesium/movers.test.mjs",
"src/globe/navara/movers/pack.test.mjs", "src/globe/navara/movers/pack.test.mjs",
"src/globe/navara/movers/pickIds.test.mjs", "src/globe/navara/movers/pickIds.test.mjs",
"src/globe/navara/movers/moverMesh.test.mjs", "src/globe/navara/movers/moverMesh.test.mjs",
"src/globe/navara/movers/modelMover.test.mjs", "src/globe/navara/movers/modelMover.test.mjs",
"src/bench/syntheticFlights.test.mjs", "src/bench/syntheticFlights.test.mjs",
"src/globe/lines.test.mjs", "src/globe/lines.test.mjs",
"src/globe/cesium/lines.test.mjs",
"src/globe/navara/lines/lineMesh.test.mjs", "src/globe/navara/lines/lineMesh.test.mjs",
"src/globe/pickDelivery.test.mjs", "src/globe/pickDelivery.test.mjs",
"src/globe/navara/picking.test.mjs", "src/globe/navara/picking.test.mjs",
+47 -534
View File
@@ -897,24 +897,7 @@
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
"src/geo/index.js", "src/geo/"
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1176,24 +1159,7 @@
"src/globe/cesium/engineFromViewer.js", "src/globe/cesium/engineFromViewer.js",
"src/cameraAngles.js", "src/cameraAngles.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/app/layers/directory.js", "src/app/layers/directory.js",
"src/globe/follow.js", "src/globe/follow.js",
"src/ui/kit/UiSegmented.vue" "src/ui/kit/UiSegmented.vue"
@@ -1333,24 +1299,7 @@
"src/ui/i18n.js", "src/ui/i18n.js",
"src/globe/cesium/engineFromViewer.js", "src/globe/cesium/engineFromViewer.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/geo/index.js", "src/geo/"
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium", "vue", "pinia", "vue-i18n"] "external": ["cesium", "vue", "pinia", "vue-i18n"]
}, },
@@ -1432,24 +1381,7 @@
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
"src/data/render/cesiumProjection.js", "src/data/render/cesiumProjection.js",
"src/data/render/cesiumConstants.js", "src/data/render/cesiumConstants.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/time/clock.js", "src/time/clock.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js", "src/globe/cesium/engineFromViewer.js",
@@ -1516,24 +1448,7 @@
"src/data/render/cesiumProjection.js", "src/data/render/cesiumProjection.js",
"src/data/render/cesiumConstants.js", "src/data/render/cesiumConstants.js",
"src/layers/shared/color.js", "src/layers/shared/color.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/time/clock.js", "src/time/clock.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js", "src/globe/cesium/engineFromViewer.js",
@@ -1571,24 +1486,7 @@
"src/data/inputOwnership.js", "src/data/inputOwnership.js",
"src/data/render/cesiumProjection.js", "src/data/render/cesiumProjection.js",
"src/data/render/cesiumConstants.js", "src/data/render/cesiumConstants.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js" "src/globe/cesium/engineFromViewer.js"
], ],
@@ -1635,24 +1533,7 @@
"src/layers/vessels/icons.js", "src/layers/vessels/icons.js",
"src/layers/shared/color.js", "src/layers/shared/color.js",
"src/data/render/cesiumProjection.js", "src/data/render/cesiumProjection.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js" "src/globe/cesium/engineFromViewer.js"
], ],
@@ -1665,24 +1546,7 @@
"src/layers/earthquakes/model.js", "src/layers/earthquakes/model.js",
"src/layers/earthquakes/source.js", "src/layers/earthquakes/source.js",
"src/layers/earthquakes/records.js", "src/layers/earthquakes/records.js",
"src/geo/boundingSphere.js", "src/geo/",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/cartographic.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/ellipsoid.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/layers/shared/color.js" "src/layers/shared/color.js"
], ],
"external": [] "external": []
@@ -1751,24 +1615,7 @@
"src/i18n/locales/uk.json", "src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/layers/firms/rectangle.js", "src/layers/firms/rectangle.js",
"src/layers/shared/color.js" "src/layers/shared/color.js"
], ],
@@ -1810,24 +1657,7 @@
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/time/clock.js", "src/time/clock.js",
"src/layers/shared/color.js" "src/layers/shared/color.js"
], ],
@@ -1872,24 +1702,7 @@
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/layers/shared/color.js", "src/layers/shared/color.js",
"src/layers/launches/render/cesium.js", "src/layers/launches/render/cesium.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/time/clock.js", "src/time/clock.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js" "src/globe/cesium/engineFromViewer.js"
@@ -1946,24 +1759,7 @@
"src/layers/installations/render/cesium/selection.js", "src/layers/installations/render/cesium/selection.js",
"src/layers/shared/color.js", "src/layers/shared/color.js",
"src/layers/installations/render/cesium/namedMarkers.js", "src/layers/installations/render/cesium/namedMarkers.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js" "src/globe/cesium/engineFromViewer.js"
], ],
@@ -2015,24 +1811,7 @@
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
"src/data/render/cesiumProjection.js", "src/data/render/cesiumProjection.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js" "src/globe/cesium/engineFromViewer.js"
], ],
@@ -2094,24 +1873,7 @@
"src/i18n/locales/uk.json", "src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js" "src/globe/cesium/engineFromViewer.js"
], ],
@@ -2151,24 +1913,7 @@
"src/layers/bikeshare/selectionModel.js", "src/layers/bikeshare/selectionModel.js",
"src/layers/bikeshare/presentation.js", "src/layers/bikeshare/presentation.js",
"src/layers/shared/color.js", "src/layers/shared/color.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js" "src/globe/cesium/engineFromViewer.js"
], ],
@@ -2227,24 +1972,7 @@
"src/i18n/locales/zh-CN.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/shared/color.js",
"src/geo/index.js", "src/geo/"
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["hls.js"] "external": ["hls.js"]
}, },
@@ -2318,24 +2046,7 @@
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/layers/streetLevel/providers/mapillary/policy.js", "src/layers/streetLevel/providers/mapillary/policy.js",
"src/geo/index.js", "src/geo/"
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"] "external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"]
}, },
@@ -2371,24 +2082,7 @@
"src/i18n/locales/uk.json", "src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/geo/index.js", "src/geo/"
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["@mapbox/vector-tile", "pbf"] "external": ["@mapbox/vector-tile", "pbf"]
}, },
@@ -2437,24 +2131,7 @@
"src/i18n/locales/uk.json", "src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/geo/index.js", "src/geo/"
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": [] "external": []
}, },
@@ -2486,24 +2163,7 @@
"src/i18n/locales/uk.json", "src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/geo/index.js", "src/geo/"
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -3692,7 +3352,7 @@
"src/globe/cesium/render.js", "src/globe/cesium/render.js",
"src/globe/cesium/surface.js", "src/globe/cesium/surface.js",
"src/globe/cesium/engineFromViewer.js", "src/globe/cesium/engineFromViewer.js",
"src/globe/navara/cameraMath.js", "src/globe/cameraMath.js",
"src/globe/navara/cameraModel.js", "src/globe/navara/cameraModel.js",
"src/globe/navara/createNavaraEngine.js", "src/globe/navara/createNavaraEngine.js",
"src/globe/navara/credits.js", "src/globe/navara/credits.js",
@@ -3722,6 +3382,8 @@
"src/data/render/cesiumTrailRenderer.js", "src/data/render/cesiumTrailRenderer.js",
"src/layers/alpr/render/cesiumPresentation.js", "src/layers/alpr/render/cesiumPresentation.js",
"src/layers/alpr/render/cesiumOverlay.js", "src/layers/alpr/render/cesiumOverlay.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/alpr/render/cesium.js", "src/layers/alpr/render/cesium.js",
"src/layers/bikeshare/selectionModel.js", "src/layers/bikeshare/selectionModel.js",
"src/layers/bikeshare/presentation.js", "src/layers/bikeshare/presentation.js",
@@ -3742,35 +3404,16 @@
"src/layers/localAdsb/qaDrawn.js", "src/layers/localAdsb/qaDrawn.js",
"src/ui/imageryBoxNavara.js", "src/ui/imageryBoxNavara.js",
"src/layers/installations/render/cesium/namedMarkers.js", "src/layers/installations/render/cesium/namedMarkers.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/time/clock.js", "src/time/clock.js",
"src/globe/navara/overlay.js", "src/globe/navara/overlay.js",
"src/globe/navara/frameDescriptor.js", "src/globe/navara/frameDescriptor.js",
"src/globe/cesium/clock.js", "src/globe/cesium/clock.js",
"src/globe/pickDelivery.js", "src/globe/pickDelivery.js",
"src/globe/navara/clock.js", "src/globe/navara/clock.js",
"src/globe/cesium/lines.js",
"src/globe/follow.js", "src/globe/follow.js",
"src/globe/lines.js", "src/globe/lines.js",
"src/globe/navara/movers/pickIds.js", "src/globe/navara/movers/pickIds.js",
"src/globe/cesium/movers.js",
"src/globe/movers.js", "src/globe/movers.js",
"src/globe/cesium/follow.js", "src/globe/cesium/follow.js",
"src/globe/cpuPicks.js", "src/globe/cpuPicks.js",
@@ -3847,6 +3490,8 @@
"src/layers/cyclones/render/navara.js", "src/layers/cyclones/render/navara.js",
"src/layers/shared/render/groundRing.js", "src/layers/shared/render/groundRing.js",
"src/layers/bikeshare/render/navara.js", "src/layers/bikeshare/render/navara.js",
"src/layers/bikeshare/stationPoints.js",
"src/layers/shared/viewRectangle.js",
"src/layers/installations/render/navaraGeometry.js", "src/layers/installations/render/navaraGeometry.js",
"src/layers/installations/render/navaraOutlineBatch.js", "src/layers/installations/render/navaraOutlineBatch.js",
"src/layers/installations/render/navaraStencilBatch.js", "src/layers/installations/render/navaraStencilBatch.js",
@@ -3857,6 +3502,7 @@
"src/layers/radio/render/navara.js", "src/layers/radio/render/navara.js",
"src/layers/transit/render/navara.js", "src/layers/transit/render/navara.js",
"src/layers/transit/render/navaraSprites.js", "src/layers/transit/render/navaraSprites.js",
"src/layers/shared/render/navaraMoverBank.js",
"src/layers/transit/render/navaraView.js", "src/layers/transit/render/navaraView.js",
"src/layers/transit/render/navaraTrail.js", "src/layers/transit/render/navaraTrail.js",
"src/layers/recentImagery/render/navara.js", "src/layers/recentImagery/render/navara.js",
@@ -3868,14 +3514,18 @@
"src/layers/cctv/render/navaraLine.js", "src/layers/cctv/render/navaraLine.js",
"src/layers/alpr/render/navara.js", "src/layers/alpr/render/navara.js",
"src/layers/alpr/render/navaraOverlay.js", "src/layers/alpr/render/navaraOverlay.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/radio/screenClusters.js", "src/layers/radio/screenClusters.js",
"src/layers/shared/render/navaraCellGeometry.js", "src/layers/shared/render/navaraCellGeometry.js",
"src/layers/shared/render/navaraGroundCells.js", "src/layers/shared/render/navaraGroundCells.js",
"src/layers/firms/render/navaraSprites.js", "src/layers/firms/render/navaraSprites.js",
"src/layers/shared/render/imageAtlas.js",
"src/layers/firms/render/navaraHeat.js", "src/layers/firms/render/navaraHeat.js",
"src/layers/shared/render/navaraGroundGeometry.js", "src/layers/shared/render/navaraGroundGeometry.js",
"src/layers/shared/render/navaraGroundGrid.js", "src/layers/shared/render/navaraGroundGrid.js",
"src/layers/shared/render/navaraGroundSurface.js", "src/layers/shared/render/navaraGroundSurface.js",
"src/layers/shared/render/navaraGroundEffect.js",
"src/annotations/outlineRungs.js", "src/annotations/outlineRungs.js",
"src/sources/openFreeMapOutlines.js", "src/sources/openFreeMapOutlines.js",
"src/sources/nominatimOutlines.js", "src/sources/nominatimOutlines.js",
@@ -4364,6 +4014,7 @@
"src/layers/transit/state.js", "src/layers/transit/state.js",
"src/layers/transit/testing.js", "src/layers/transit/testing.js",
"src/layers/transit/viewport.js", "src/layers/transit/viewport.js",
"src/layers/shared/viewRectangle.js",
"src/data/cameraSensitivity.js", "src/data/cameraSensitivity.js",
"src/data/contactPlayback.js", "src/data/contactPlayback.js",
"src/data/contactTrailRenderer.js", "src/data/contactTrailRenderer.js",
@@ -4528,6 +4179,7 @@
"src/globe/cesium/input.js", "src/globe/cesium/input.js",
"src/globe/cesium/convert.js", "src/globe/cesium/convert.js",
"src/globe/cesium/camera.js", "src/globe/cesium/camera.js",
"src/globe/cameraMath.js",
"src/globe/cesium/frames.js", "src/globe/cesium/frames.js",
"src/globe/cesium/picker.js", "src/globe/cesium/picker.js",
"src/globe/cesium/events.js", "src/globe/cesium/events.js",
@@ -4543,6 +4195,8 @@
"src/data/render/cesiumTrailRenderer.js", "src/data/render/cesiumTrailRenderer.js",
"src/layers/alpr/render/cesiumPresentation.js", "src/layers/alpr/render/cesiumPresentation.js",
"src/layers/alpr/render/cesiumOverlay.js", "src/layers/alpr/render/cesiumOverlay.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/alpr/render/cesium.js", "src/layers/alpr/render/cesium.js",
"src/layers/bikeshare/selectionModel.js", "src/layers/bikeshare/selectionModel.js",
"src/layers/bikeshare/presentation.js", "src/layers/bikeshare/presentation.js",
@@ -4550,6 +4204,7 @@
"src/layers/bikeshare/render/cesium/viewport.js", "src/layers/bikeshare/render/cesium/viewport.js",
"src/layers/bikeshare/render/cesium/lifecycle.js", "src/layers/bikeshare/render/cesium/lifecycle.js",
"src/layers/bikeshare/render/cesium/rendering.js", "src/layers/bikeshare/render/cesium/rendering.js",
"src/layers/bikeshare/stationPoints.js",
"src/layers/bikeshare/render/cesium/selection.js", "src/layers/bikeshare/render/cesium/selection.js",
"src/layers/installations/render/cesium/lifecycle.js", "src/layers/installations/render/cesium/lifecycle.js",
"src/layers/installations/render/cesium.js", "src/layers/installations/render/cesium.js",
@@ -4561,24 +4216,7 @@
"src/data/render/cesiumContactTrailRenderer.js", "src/data/render/cesiumContactTrailRenderer.js",
"src/layers/localAdsb/qaDrawn.js", "src/layers/localAdsb/qaDrawn.js",
"src/layers/installations/render/cesium/namedMarkers.js", "src/layers/installations/render/cesium/namedMarkers.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/time/clock.js", "src/time/clock.js",
"src/globe/follow.js", "src/globe/follow.js",
"src/globe/cesium/follow.js", "src/globe/cesium/follow.js",
@@ -4687,24 +4325,7 @@
"src/globe/cesium/engineFromViewer.js", "src/globe/cesium/engineFromViewer.js",
"src/globe/cesium/credits.js", "src/globe/cesium/credits.js",
"src/globe/contract.js", "src/globe/contract.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/globe/surfaceDetail.js", "src/globe/surfaceDetail.js",
"src/sources/openFreeMap.js", "src/sources/openFreeMap.js",
"src/sources/vectorTiles.js", "src/sources/vectorTiles.js",
@@ -4985,24 +4606,7 @@
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
"src/geo/julianDate.js", "src/geo/"
"src/geo/simon1994.js",
"src/geo/developerError.js",
"src/geo/check.js",
"src/geo/math.js",
"src/geo/cartesian2.js",
"src/geo/cartographic.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/quaternion.js",
"src/geo/matrix4.js",
"src/geo/ellipsoid.js",
"src/geo/transforms.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/debug.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/index.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -5315,6 +4919,7 @@
"src/layers/transit/testing.js", "src/layers/transit/testing.js",
"src/layers/transit/trails.js", "src/layers/transit/trails.js",
"src/layers/transit/viewport.js", "src/layers/transit/viewport.js",
"src/layers/shared/viewRectangle.js",
"src/overlays/worldOverlay.js", "src/overlays/worldOverlay.js",
"src/overlays/worldOverlayDraw.js", "src/overlays/worldOverlayDraw.js",
"src/overlays/worldOverlayTokens.js", "src/overlays/worldOverlayTokens.js",
@@ -5349,6 +4954,7 @@
"src/globe/cesium/picker.js", "src/globe/cesium/picker.js",
"src/globe/cesium/frames.js", "src/globe/cesium/frames.js",
"src/globe/cesium/camera.js", "src/globe/cesium/camera.js",
"src/globe/cameraMath.js",
"src/globe/cesium/convert.js", "src/globe/cesium/convert.js",
"src/globe/cesium/input.js", "src/globe/cesium/input.js",
"src/maps/google3d.js", "src/maps/google3d.js",
@@ -5373,24 +4979,7 @@
"src/data/render/cesiumProjection.js", "src/data/render/cesiumProjection.js",
"src/layers/shared/color.js", "src/layers/shared/color.js",
"src/layers/transit/render/cesium.js", "src/layers/transit/render/cesium.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/time/clock.js", "src/time/clock.js",
"src/globe/surfaceDetail.js", "src/globe/surfaceDetail.js",
"src/celestialRing/render/cesium.js" "src/celestialRing/render/cesium.js"
@@ -5418,6 +5007,7 @@
"src/layers/transit/state.js", "src/layers/transit/state.js",
"src/layers/transit/testing.js", "src/layers/transit/testing.js",
"src/layers/transit/viewport.js", "src/layers/transit/viewport.js",
"src/layers/shared/viewRectangle.js",
"src/data/contactPlayback.js", "src/data/contactPlayback.js",
"src/data/contactTrailRenderer.js", "src/data/contactTrailRenderer.js",
"src/data/transitProxy.js", "src/data/transitProxy.js",
@@ -5445,24 +5035,7 @@
"src/i18n/locales/zh-CN.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/shared/color.js",
"src/geo/index.js", "src/geo/",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/globe/contract.js" "src/globe/contract.js"
], ],
"external": ["cesium", "pbf"] "external": ["cesium", "pbf"]
@@ -5506,9 +5079,6 @@
"exports": ["./layers/perimeters"], "exports": ["./layers/perimeters"],
"modules": [ "modules": [
"src/geo/cartesian3.js", "src/geo/cartesian3.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/math.js", "src/geo/math.js",
"src/layers/perimeters/index.js", "src/layers/perimeters/index.js",
"src/layers/perimeters/render/cesium.js", "src/layers/perimeters/render/cesium.js",
@@ -5611,9 +5181,6 @@
"src/layers/ghostMaps/kml.js", "src/layers/ghostMaps/kml.js",
"src/layers/ghostMaps/source.js", "src/layers/ghostMaps/source.js",
"src/geo/cartesian3.js", "src/geo/cartesian3.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/math.js", "src/geo/math.js",
"src/sources/httpBody.js", "src/sources/httpBody.js",
"src/data/geoFileImport.js", "src/data/geoFileImport.js",
@@ -5663,24 +5230,7 @@
"exports": ["./layers/drone-airspace"], "exports": ["./layers/drone-airspace"],
"modules": [ "modules": [
"src/layers/droneAirspace/index.js", "src/layers/droneAirspace/index.js",
"src/geo/index.js", "src/geo/",
"src/geo/ellipsoid.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/debug.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/transforms.js",
"src/geo/matrix4.js",
"src/geo/cartesian3.js",
"src/geo/quaternion.js",
"src/geo/math.js",
"src/geo/matrix3.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/check.js",
"src/geo/developerError.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/layers/shared/render/cesiumZones.js", "src/layers/shared/render/cesiumZones.js",
"src/sources/httpBody.js", "src/sources/httpBody.js",
"src/layers/droneAirspace/source.js", "src/layers/droneAirspace/source.js",
@@ -5769,24 +5319,7 @@
"src/tools/queries/panelRequest.js", "src/tools/queries/panelRequest.js",
"src/geoDistance.js", "src/geoDistance.js",
"src/visualStylePolicy.js", "src/visualStylePolicy.js",
"src/geo/boundingSphere.js", "src/geo/"
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/cartographic.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/ellipsoid.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/index.js",
"src/geo/julianDate.js",
"src/geo/math.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/simon1994.js",
"src/geo/transforms.js"
], ],
"external": ["satellite.js"] "external": ["satellite.js"]
}, },
@@ -5898,9 +5431,6 @@
"exports": ["./layers/air-quality"], "exports": ["./layers/air-quality"],
"modules": [ "modules": [
"src/geo/cartesian3.js", "src/geo/cartesian3.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/math.js", "src/geo/math.js",
"src/layers/airQuality/index.js", "src/layers/airQuality/index.js",
"src/layers/airQuality/render/cesium.js", "src/layers/airQuality/render/cesium.js",
@@ -5946,24 +5476,7 @@
"src/loadingFeedback.js", "src/loadingFeedback.js",
"src/data/installationFeedback.js", "src/data/installationFeedback.js",
"src/i18n/marker.js", "src/i18n/marker.js",
"src/geo/boundingSphere.js", "src/geo/"
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/cartographic.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/ellipsoid.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/index.js",
"src/geo/julianDate.js",
"src/geo/math.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/simon1994.js",
"src/geo/transforms.js"
], ],
"external": ["cesium"] "external": ["cesium"]
} }
+41 -57
View File
@@ -83,42 +83,28 @@ try {
// Every name the checkout's .env files define, plus key-like names in this // Every name the checkout's .env files define, plus key-like names in this
// environment: all blank for the build and the preview. // environment: all blank for the build and the preview.
const KEY_LIKE = /(_KEY|_TOKEN|_SECRET|_CLIENT_ID|PASSWORD)$/; const KEY_LIKE = /(_KEY|_TOKEN|_SECRET|_CLIENT_ID|PASSWORD)$/;
const dotenvNames = () => { /** The [name, value] pairs of the checkout's .env and .env.local, in order. */
const names = new Set(); const dotenv = () =>
for (const file of ['.env', '.env.local']) ['.env', '.env.local']
if (existsSync(join(root, file))) .filter((file) => existsSync(join(root, file)))
for (const line of readFileSync(join(root, file), 'utf8').split('\n')) { .flatMap((file) => readFileSync(join(root, file), 'utf8').split('\n'))
const name = line.match(/^\s*([A-Z0-9_]+)\s*=/)?.[1]; .map((line) => line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)/))
if (name) names.add(name); .filter(Boolean)
} .map(([, name, raw]) => [name, raw.trim().replace(/^["']|["']$/g, '')]);
return [...names].sort(); const dotenvNames = () => [...new Set(dotenv().map(([name]) => name))].sort();
}; const envNames = () =>
const envNames = () => { [
const names = new Set(); ...new Set([
for (const file of ['.env', '.env.local']) ...dotenvNames(),
if (existsSync(join(root, file))) ...Object.keys(process.env).filter((name) => KEY_LIKE.test(name)),
for (const line of readFileSync(join(root, file), 'utf8').split('\n')) { ]),
const name = line.match(/^\s*([A-Z0-9_]+)\s*=/)?.[1]; ].sort();
if (name) names.add(name); // Credentials only (model names and URLs are settings the code may also
} // carry as defaults).
for (const name of Object.keys(process.env)) const envValues = () =>
if (KEY_LIKE.test(name)) names.add(name); dotenv()
return [...names].sort(); .filter(([name, value]) => KEY_LIKE.test(name) && value.length >= 8)
}; .map(([, value]) => value);
const envValues = () => {
const values = [];
for (const file of ['.env', '.env.local'])
if (existsSync(join(root, file)))
for (const line of readFileSync(join(root, file), 'utf8').split('\n')) {
const [, name, raw] = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)$/) ?? [];
const value = raw?.trim().replace(/^["']|["']$/g, '');
// Credentials only (model names and URLs are settings the code may
// also carry as defaults).
if (KEY_LIKE.test(name ?? '') && value && value.length >= 8)
values.push(value);
}
return values;
};
const scrubbed = () => ({ const scrubbed = () => ({
...process.env, ...process.env,
...Object.fromEntries(envNames().map((name) => [name, ''])), ...Object.fromEntries(envNames().map((name) => [name, ''])),
@@ -368,9 +354,9 @@ const runs = [
['rocket-launches'], ['rocket-launches'],
['--missions'], ['--missions'],
], ],
// Stage 3's own checks (one application per page, voice across a switch). // One application per page, and voice across a switch: these block paid
// They block paid routes themselves but do not run under the strict // routes themselves but do not run under the strict harness, so they add
// harness, so they add no assertion manifest and no network ledger. // no assertion manifest and no network ledger.
['app-once', 'app-once', [], [], 600, true], ['app-once', 'app-once', [], [], 600, true],
['voice-engine-switch', 'voice-engine-switch', [], [], 300, true], ['voice-engine-switch', 'voice-engine-switch', [], [], 300, true],
['strict-rapid-switch', 'strict-rapid-switch', []], ['strict-rapid-switch', 'strict-rapid-switch', []],
@@ -379,14 +365,18 @@ const runs = [
['strict-failure-matrix', 'strict-failure-matrix', [], [], 600], ['strict-failure-matrix', 'strict-failure-matrix', [], [], 600],
['strict-cold-chunks', 'strict-cold-chunks', []], ['strict-cold-chunks', 'strict-cold-chunks', []],
['strict-prod-smoke', 'strict-prod-smoke', []], ['strict-prod-smoke', 'strict-prod-smoke', []],
// Stage 6: the app's visual styles, bloom, contact sonar, cockpit clouds // The app's visual styles, bloom, contact sonar, cockpit clouds, and the
// and annotations drawn on Navara (annotations is a tool, not a catalog // annotations, Draw, Import and the scope mask with the celestial ring on
// layer: TOOL_IDS). // Navara (those are tools and display overlays, not catalog layers:
// TOOL_IDS).
['navara-visual-styles', 'navara-visual-styles', []], ['navara-visual-styles', 'navara-visual-styles', []],
['navara-bloom', 'navara-bloom', []], ['navara-bloom', 'navara-bloom', []],
['navara-sonar', 'navara-sonar', []], ['navara-sonar', 'navara-sonar', []],
['navara-cockpit-clouds', 'navara-cockpit-clouds', []], ['navara-cockpit-clouds', 'navara-cockpit-clouds', []],
['navara-annotations', 'navara-annotations', ['annotations']], ['navara-annotations', 'navara-annotations', ['annotations']],
['navara-draw', 'navara-draw', ['draw']],
['navara-geo-import', 'navara-geo-import', ['geo-import']],
['navara-scope', 'navara-scope', ['scope-mask', 'celestial-ring']],
['strict-soak', 'strict-soak', [], soakRuns, 1200], ['strict-soak', 'strict-soak', [], soakRuns, 1200],
[ [
'strict-soak--long', 'strict-soak--long',
@@ -398,7 +388,13 @@ const runs = [
]; ];
// Ids a scenario covers that are tools, not catalog layers: their // Ids a scenario covers that are tools, not catalog layers: their
// assertions are recorded, but the catalog comparison leaves them out. // assertions are recorded, but the catalog comparison leaves them out.
const TOOL_IDS = new Set(['annotations']); const TOOL_IDS = new Set([
'annotations',
'draw',
'geo-import',
'scope-mask',
'celestial-ring',
]);
const scenarioIds = [ const scenarioIds = [
...new Set( ...new Set(
runs.flatMap(([, , ids]) => ids).filter((id) => !TOOL_IDS.has(id)), runs.flatMap(([, , ids]) => ids).filter((id) => !TOOL_IDS.has(id)),
@@ -414,9 +410,7 @@ const liveCatalog = {};
layers: [], layers: [],
name: 'catalog', name: 'catalog',
}); });
const browser = await strict.launch({ const browser = await strict.launch();
args: ['--use-angle=metal', '--enable-gpu'],
});
try { try {
for (const engine of ENGINES) { for (const engine of ENGINES) {
const page = await browser.newPage(); const page = await browser.newPage();
@@ -497,17 +491,7 @@ for (const [label, name, layers, extra = [], seconds = 300, external] of runs) {
const script = 'scripts/qa-' + name + '.mjs'; const script = 'scripts/qa-' + name + '.mjs';
const evidence = join(output, 'evidence', label + '.json'); const evidence = join(output, 'evidence', label + '.json');
rmSync(evidence, { force: true }); rmSync(evidence, { force: true });
const args = const args = [base.origin, ...extra];
name === 'w2-imagery'
? [
'--url',
base.origin + '/?engine=navara',
'--report',
join(output, 'evidence', 'imagery-report.json'),
'--switch',
'--preview',
]
: [base.origin, ...extra];
if (external) args.splice(0, 1, base.origin + '/'); if (external) args.splice(0, 1, base.origin + '/');
else args.push(`--layers=${layers.join(',')}`, `--evidence=${evidence}`); else args.push(`--layers=${layers.join(',')}`, `--evidence=${evidence}`);
const start = Date.now(); const start = Date.now();
+1 -18
View File
@@ -221,7 +221,7 @@ try {
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
report.firstRoundTrip = await listenerCounts(page, { bySource: true }); report.firstRoundTrip = await listenerCounts(page, { bySource: true });
} }
// Both engines draw the session's marks and files (Navara since Stage 6). // Both engines draw the session's marks and files (Navara included).
await page.waitForFunction( await page.waitForFunction(
() => window.__gevGeoImport.diagnostics().drawn === 1, () => window.__gevGeoImport.diagnostics().drawn === 1,
{ timeout: 30_000 }, { timeout: 30_000 },
@@ -412,23 +412,6 @@ try {
report.errors = errors; report.errors = errors;
check(errors.length === 0, 'page errors', errors); check(errors.length === 0, 'page errors', errors);
// The legacy route (?switch=legacy) still builds an application per
// engine, for one release.
const legacy = await openSessionPage(
browser,
sessionAddress(base, { engine: 'cesium', extra: { switch: 'legacy' } }),
);
await waitReady(legacy.page, 'cesium');
await switchEngine(legacy.page, 'navara');
await switchEngine(legacy.page, 'cesium');
report.legacy = await readSession(legacy.page);
check(
report.legacy.counters.applications === 3,
'the legacy route builds an application per engine',
report.legacy.counters,
);
check(legacy.errors.length === 0, 'legacy page errors', legacy.errors);
await legacy.page.close();
console.log(JSON.stringify(report, null, 2)); console.log(JSON.stringify(report, null, 2));
if (!reportOnly) console.log('PASS application created once; session kept'); if (!reportOnly) console.log('PASS application created once; session kept');
} finally { } finally {
+1 -1
View File
@@ -45,7 +45,7 @@ export function isExpectedPreviewSetupError(
} }
} }
// W1 acceptance shares one strict runtime verdict across its main page and // The runtime evidence gate shares one strict runtime verdict across its main page and
// every share-link page. Response diagnostics never retain query credentials. // every share-link page. Response diagnostics never retain query credentials.
export function watchW1Page(page, report, { name, preview = false, baseUrl }) { export function watchW1Page(page, report, { name, preview = false, baseUrl }) {
const errors = (report.browserErrors ??= []); const errors = (report.browserErrors ??= []);
+1 -1
View File
@@ -37,7 +37,7 @@ const { stdout: video } = await promisify(execFile)(
{ encoding: 'buffer', maxBuffer: 16000000 }, { encoding: 'buffer', maxBuffer: 16000000 },
); );
const strict = createStrict({ origin: url.origin, layers: ['cctv'] }); const strict = createStrict({ origin: url.origin, layers: ['cctv'] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const p = await b.newPage();
const errors = []; const errors = [];
p.on('pageerror', (e) => { p.on('pageerror', (e) => {
+2 -2
View File
@@ -42,7 +42,7 @@
* *
* This legacy harness is the geometry, lifecycle and real-input oracle. * This legacy harness is the geometry, lifecycle and real-input oracle.
* Migration runs use headless Metal and strict runtime error observation. * Migration runs use headless Metal and strict runtime error observation.
* Screenshots remain visual-review evidence here; the W2 companion must * Screenshots remain visual-review evidence here; a separate pixel gate must
* independently prove actual marker, image-plane and video pixels. * independently prove actual marker, image-plane and video pixels.
* *
* No `cesium` package import for in-page math: the app does not expose a * No `cesium` package import for in-page math: the app does not expose a
@@ -2245,7 +2245,7 @@ async function main() {
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Screenshots supplement this state/geometry oracle. Positive rendered // Screenshots supplement this state/geometry oracle. Positive rendered
// media pixels are a separate mandatory W2 companion gate. // media pixels are a separate mandatory pixel gate.
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
await sleep(1000); await sleep(1000);
const shot = path.join(SHOTS_DIR, 'cctv-v2-active.png'); const shot = path.join(SHOTS_DIR, 'cctv-v2-active.png');
+10 -26
View File
@@ -1,6 +1,11 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { fromDegrees } from '../src/geo/cartesian3.js'; import { fromDegrees } from '../src/geo/cartesian3.js';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic'; base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -10,14 +15,8 @@ const strict = createStrict({
origin: base.origin, origin: base.origin,
layers: ['military-awareness', 'military-installations'], layers: ['military-awareness', 'military-installations'],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b);
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await p.evaluateOnNewDocument(() => { await p.evaluateOnNewDocument(() => {
const fetch = window.fetch.bind(window); const fetch = window.fetch.bind(window);
window.fetch = (input, options) => { window.fetch = (input, options) => {
@@ -89,21 +88,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
); );
}); });
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => { await waitForEngine(p, 'navara');
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await p.evaluate(async () => { await p.evaluate(async () => {
const { dataManager, styleManager, tileset } = window.__godsEyeView; const { dataManager, styleManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__) if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -235,8 +220,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
baseline, baseline,
); );
for (const kind of ['cesium', 'navara']) { for (const kind of ['cesium', 'navara']) {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
const current = await sample(); const current = await sample();
console.log('handoff', kind, JSON.stringify(current)); console.log('handoff', kind, JSON.stringify(current));
assert.deepEqual(current, { assert.deepEqual(current, {
+11 -28
View File
@@ -1,19 +1,18 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic'; base.search = '?engine=navara&welcome=0&look=cinematic';
base.hash = base.hash =
'v=2&lat=55.3241&lon=10.7995&alt=25000&heading=0&pitch=-90&roll=0&map=sentinel2&l='; 'v=2&lat=55.3241&lon=10.7995&alt=25000&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
const strict = createStrict({ origin: base.origin, layers: ['directions'] }); const strict = createStrict({ origin: base.origin, layers: ['directions'] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b);
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
let routeReads = 0, let routeReads = 0,
release; release;
await p.setRequestInterception(true); await p.setRequestInterception(true);
@@ -55,21 +54,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}); });
}); });
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => { await waitForEngine(p, 'navara');
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await p.evaluate(async () => { await p.evaluate(async () => {
const { tileset, dataManager } = window.__godsEyeView; const { tileset, dataManager } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__) if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -91,8 +76,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
'directions', 'directions',
'A and B set by clicks on the globe; one route request', 'A and B set by clicks on the globe; one route request',
); );
await strict.pick(p, 'cesium'); await strict.switchTo(p, 'cesium');
await ready('cesium');
release(); release();
await p.waitForFunction(() => window.qaRoute.getStats().count === 3); await p.waitForFunction(() => window.qaRoute.getStats().count === 3);
await p.waitForFunction(() => { await p.waitForFunction(() => {
@@ -105,8 +89,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
'the route asked for on Navara arrives on Cesium; a step is selectable', 'the route asked for on Navara arrives on Cesium; a step is selectable',
); );
const transfer = async (kind) => { const transfer = async (kind) => {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
assert.equal( assert.equal(
await p.evaluate( await p.evaluate(
() => () =>
+1 -1
View File
@@ -135,7 +135,7 @@ const strict = createStrict({
layers: [], layers: [],
fixtures: ['https://earthquake.usgs.gov/*'], fixtures: ['https://earthquake.usgs.gov/*'],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const p = await b.newPage();
await p.setViewport({ width: 1280, height: 900 }); await p.setViewport({ width: 1280, height: 900 });
const errors = []; const errors = [];
+444 -460
View File
@@ -7,478 +7,462 @@ base.search = '?welcome=0&look=cinematic';
base.hash = base.hash =
'v=2&lat=42.3554&lon=-71.0605&alt=6000&heading=0&pitch=-90&map=osm&l=e'; 'v=2&lat=42.3554&lon=-71.0605&alt=6000&heading=0&pitch=-90&map=osm&l=e';
const strict = createStrict({ origin: base.origin, layers: ['earthquakes'] }); const strict = createStrict({ origin: base.origin, layers: ['earthquakes'] });
await strict.run( await strict.run({}, async (browser) => {
{ args: ['--use-angle=metal', '--enable-gpu'] }, const page = await browser.newPage();
async (browser) => { await page.evaluateOnNewDocument(() => {
const page = await browser.newPage(); localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
await page.evaluateOnNewDocument(() => { const fetch = window.fetch.bind(window);
localStorage.setItem('gev:first-run-mission:v1', 'suppressed'); // Two Boston vessels for the Contacts session; empty air and site feeds.
const fetch = window.fetch.bind(window); const fixtures = {
// Two Boston vessels for the Contacts session; empty air and site feeds. '/api/vessels': () => ({
const fixtures = { status: 'live',
'/api/vessels': () => ({ rows: ['123456789', '223456789'].map((mmsi, i) => ({
status: 'live', mmsi,
rows: ['123456789', '223456789'].map((mmsi, i) => ({ name: 'Boston ' + i,
mmsi, lat: 42.3554,
name: 'Boston ' + i, lon: -71.0605 + i * 0.05,
lat: 42.3554, last_position_epoch: Date.now() / 1000,
lon: -71.0605 + i * 0.05, })),
last_position_epoch: Date.now() / 1000, }),
})), '/api/vessels/track': () => ({ samples: [] }),
}), '/api/flights': () => ({ time: Date.now() / 1000, states: [] }),
'/api/vessels/track': () => ({ samples: [] }), '/api/military': () => ({ now: Date.now(), ac: [] }),
'/api/flights': () => ({ time: Date.now() / 1000, states: [] }), '/api/military-installations': () => ({ elements: [], status: 'ok' }),
'/api/military': () => ({ now: Date.now(), ac: [] }), };
'/api/military-installations': () => ({ elements: [], status: 'ok' }), window.fetch = (url, options) => {
};
window.fetch = (url, options) => {
if (
String(url).startsWith(
'https://earthquake.usgs.gov/earthquakes/feed/',
)
)
return Promise.resolve(
Response.json({
type: 'FeatureCollection',
features: [
{
type: 'Feature',
id: 'qa-boston',
geometry: {
type: 'Point',
coordinates: [-71.0605, 42.3554, 8],
},
properties: { mag: 4, time: Date.now(), place: 'QA Boston' },
},
],
}),
);
const fixture =
fixtures[new URL(url.url ?? url, document.baseURI).pathname];
return fixture
? Promise.resolve(Response.json(fixture()))
: fetch(url, options);
};
});
await page.setViewport({ width: 1280, height: 800 });
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
async function ready(kind) {
await page.waitForFunction(
(kind) =>
window.__godsEyeView?.engine?.kind === kind &&
document.querySelector('#globe-engine')?.value === kind &&
!document.querySelector('#globe-engine').disabled &&
document
.getElementById('loading-screen')
?.classList.contains('hidden'),
{ timeout: 90000 },
kind,
);
await page.evaluate(async () => {
await window.__godsEyeView.styleManager.initialRestorePromise;
if (window.__GOOGLE_MAPS_API_KEY__ || window.__godsEyeView.tileset)
throw new Error('Keyless preview required');
});
assert.equal(await page.$eval('#globe-engine', (el) => el.value), kind);
}
async function verifyQuakeSurface(kind) {
const meanRed = async (enabled) => {
await page.evaluate(async (enabled) => {
const a = window.__godsEyeView;
await a.dataManager.setEnabled('earthquakes', enabled, {
origin: 'user',
});
a.engine.render.request('qa-quake-surface');
}, enabled);
if (enabled)
await page.waitForFunction(
() =>
window.__godsEyeView.dataManager.layers
.get('earthquakes')
.module.getAnalystRecords().length === 1,
);
await new Promise((resolve) => setTimeout(resolve, 1800));
const bytes = await page.screenshot({
clip: { x: 675, y: 403, width: 20, height: 20 },
});
const { channels } = await sharp(bytes).stats();
return channels[0].mean - channels[1].mean;
};
const off = await meanRed(false),
on = await meanRed(true);
assert.ok(
on > off + 8,
kind +
' must draw the shallow red earthquake footprint: ' +
off +
' -> ' +
on,
);
await strict.passed(page, 'earthquakes', 'shallow red footprint drawn', {
off,
on,
});
console.log(
'PASS',
kind,
'earthquake footprint red minus green:',
off,
'->',
on,
);
}
const stored = () =>
page.evaluate(() => localStorage.getItem('gev.engine'));
async function switchTo(kind) {
if ( if (
await page.$eval( String(url).startsWith('https://earthquake.usgs.gov/earthquakes/feed/')
'#control-panel-toggle',
(el) => el.getAttribute('aria-expanded') !== 'true',
)
) {
await page.focus('#control-panel-toggle');
await page.keyboard.press('Enter');
}
await page.waitForSelector('#globe-engine', { visible: true });
await page.evaluate(() => {
window.__switchDocument = document;
window.__quakeEntry =
window.__godsEyeView.dataManager.layers.get('earthquakes');
window.__quakeTimer = window.__quakeEntry.intervalId;
window.__renderers ??= new Map();
window.__renderers.set(
window.__godsEyeView.engine.kind,
window.__godsEyeView.engine,
);
window.__switchSplash = false;
window.__switchObserver = new MutationObserver(() => {
if (!document.querySelector('#loading-screen.hidden'))
window.__switchSplash = true;
});
window.__switchObserver.observe(
document.querySelector('#loading-screen'),
{ attributes: true },
);
window.__outgoingEngine = window.__godsEyeView.engine;
});
await strict.pick(page, kind);
await ready(kind);
assert.ok(
await page.evaluate(() => window.__switchDocument === document),
);
assert.ok(
await page.evaluate(() => {
window.__switchObserver.disconnect();
const { engine } = window.__godsEyeView;
const previous = window.__renderers.get(engine.kind);
return (
!window.__switchSplash &&
window.__quakeEntry ===
window.__godsEyeView.dataManager.layers.get('earthquakes') &&
window.__quakeTimer === window.__quakeEntry.intervalId &&
(!previous || previous === engine) &&
!window.__outgoingEngine.isDestroyed() &&
!window.__outgoingEngine.host.canvas.isConnected &&
engine.host.canvas.isConnected &&
window.__godsEyeView.dataManager
.getEnabledLayerIds()
.has('earthquakes')
);
}),
);
await strict.passed(
page,
'earthquakes',
'same layer entry, model and poll timer; no splash; outgoing canvas detached',
);
await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
);
assert.equal(
await page.evaluate(async () => {
const parked = window.__outgoingEngine;
let frames = 0;
const remove = parked.frames.postRender.addEventListener(() => {
frames++;
});
parked.render.request('qa-parked');
await new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
);
remove();
return frames;
}),
0,
);
const url = new URL(page.url());
assert.equal(url.searchParams.get('engine'), kind);
assert.equal(url.searchParams.get('look'), 'cinematic');
const hash = new URLSearchParams(url.hash.slice(1));
assert.equal(hash.get('map'), 'osm');
assert.ok(Math.abs(Number(hash.get('lat')) - 42.3554) < 0.01);
assert.ok(Math.abs(Number(hash.get('lon')) + 71.0605) < 0.01);
assert.ok(Math.abs(Number(hash.get('alt')) - 6000) < 30);
const view = await page.evaluate(() => {
const { engine, mapStackController } = window.__godsEyeView;
const { latitude, longitude, height } =
engine.camera.pose().cartographic;
return {
latitude: latitude * (180 / Math.PI),
longitude: longitude * (180 / Math.PI),
height,
map: mapStackController.getActiveId(),
};
});
assert.equal(view.map, 'osm');
assert.ok(Math.abs(view.latitude - 42.3554) < 0.01);
assert.ok(Math.abs(view.longitude + 71.0605) < 0.01);
assert.ok(Math.abs(view.height - 6000) < 30);
await strict.passed(
page,
'earthquakes',
'parked engine draws no frame; pose, map and layers carried over',
);
}
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
await ready('cesium');
await verifyQuakeSurface('cesium');
const sharpen = () =>
page.evaluate(
() => window.__godsEyeView.styleManager.getControlState().sharpen,
);
await page.evaluate(() => {
const style = window.__godsEyeView.styleManager;
style.setStyle('retro');
style.setStyle('normal');
});
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 49 });
await switchTo('navara');
await verifyQuakeSurface('navara');
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 80 });
assert.ok(
await page.evaluate(
() =>
Math.abs(window.__godsEyeView.engine.render.sharpen().amount - 1.7) <
1e-6,
),
);
assert.equal(await stored(), 'navara');
await page.goBack({ waitUntil: 'domcontentloaded' });
await ready('cesium');
assert.equal((await sharpen()).intensityPct, 49);
await page.goForward();
await ready('navara');
assert.equal((await sharpen()).intensityPct, 80);
await page.evaluate(() =>
window.__godsEyeView.styleManager.setSharpen({
enabled: false,
intensityPct: 49,
}),
);
await switchTo('cesium');
await switchTo('navara');
assert.deepEqual(await sharpen(), { enabled: false, intensityPct: 49 });
// The address carries the running layer set once its debounced write lands.
const settledAddress = async () => {
let previous;
for (;;) {
await new Promise((resolve) => setTimeout(resolve, 1200));
const current = await page.evaluate(() =>
new URLSearchParams(location.hash.slice(1)).get('l'),
);
if (current === previous) return current;
previous = current;
}
};
const pick = async (kind) => {
await strict.pick(page, kind);
await ready(kind);
await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
);
};
const visit = async (direction, kind) => {
await (direction === 'back'
? page.goBack({ waitUntil: 'domcontentloaded' })
: page.goForward());
await ready(kind);
await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
);
};
const quakes = () =>
page.evaluate(() => {
const { dataManager } = window.__godsEyeView;
return {
model: window.__quakeModel === dataManager.layers.get('earthquakes'),
enabled: dataManager.getEnabledLayerIds().has('earthquakes'),
};
});
// Like a same-engine Back, each entry brings back its own layer set, on
// the same layer models.
await page.evaluate(() => {
window.__quakeModel =
window.__godsEyeView.dataManager.layers.get('earthquakes');
});
const navaraLayers = await settledAddress();
await pick('cesium');
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('earthquakes', false, {
origin: 'user',
}),
);
const cesiumLayers = await settledAddress();
assert.notEqual(cesiumLayers, navaraLayers);
await visit('back', 'navara');
assert.deepEqual(await quakes(), { model: true, enabled: true });
await strict.passed(
page,
'earthquakes',
'Back restores the enabled layer on the same model',
);
assert.equal(await settledAddress(), navaraLayers, 'Back keeps its entry');
await visit('forward', 'cesium');
assert.deepEqual(await quakes(), { model: true, enabled: false });
await strict.passed(
page,
'earthquakes',
'Forward restores the disabled layer on the same model',
);
assert.equal(
await settledAddress(),
cesiumLayers,
'Forward keeps its entry',
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('earthquakes', true, {
origin: 'user',
}),
);
await pick('navara');
console.log(
"PASS Back/Forward restore each entry's layers on the same models",
);
// Back/Forward hand the session off like the picker: the Contacts subject
// and its navigation history, its layers and the Director survive.
await page.evaluate(async () => {
const { dataManager, styleManager, sceneDirector } = window.__godsEyeView;
await dataManager.setEnabled('ais-live-vessels', true, {
origin: 'user',
});
const outcome = await styleManager.setContextMode('contacts');
if (!outcome.ok) throw Error(JSON.stringify(outcome));
window.qaSession = {
director: sceneDirector,
context: dataManager.layers.get('military-awareness').module,
vessels: dataManager.layers.get('ais-live-vessels'),
};
if (!window.qaSession.vessels.module.selectById('123456789'))
throw Error('Missing first vessel');
if (
!window.qaSession.context.navigateNext({
targetLayer: 'ais-live-vessels',
})
) )
throw Error('Cannot navigate to the second vessel'); return Promise.resolve(
Response.json({
type: 'FeatureCollection',
features: [
{
type: 'Feature',
id: 'qa-boston',
geometry: {
type: 'Point',
coordinates: [-71.0605, 42.3554, 8],
},
properties: { mag: 4, time: Date.now(), place: 'QA Boston' },
},
],
}),
);
const fixture =
fixtures[new URL(url.url ?? url, document.baseURI).pathname];
return fixture
? Promise.resolve(Response.json(fixture()))
: fetch(url, options);
};
});
await page.setViewport({ width: 1280, height: 800 });
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
async function ready(kind) {
await page.waitForFunction(
(kind) =>
window.__godsEyeView?.engine?.kind === kind &&
document.querySelector('#globe-engine')?.value === kind &&
!document.querySelector('#globe-engine').disabled &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 90000 },
kind,
);
await page.evaluate(async () => {
await window.__godsEyeView.styleManager.initialRestorePromise;
if (window.__GOOGLE_MAPS_API_KEY__ || window.__godsEyeView.tileset)
throw new Error('Keyless preview required');
}); });
const session = () => assert.equal(await page.$eval('#globe-engine', (el) => el.value), kind);
page.evaluate(() => { }
const { dataManager, styleManager, sceneDirector } = async function verifyQuakeSurface(kind) {
window.__godsEyeView; const meanRed = async (enabled) => {
const { director, context, vessels } = window.qaSession; await page.evaluate(async (enabled) => {
return { const a = window.__godsEyeView;
director: director === sceneDirector && !director._destroyed, await a.dataManager.setEnabled('earthquakes', enabled, {
owners:
context === dataManager.layers.get('military-awareness').module &&
vessels === dataManager.layers.get('ais-live-vessels'),
mode: styleManager.getContextModeState().mode,
subject: context.getContextSnapshot()?.subject?.id,
history: context._getAwarenessNavigationStateForTest().historyKeys,
enabled: [...dataManager.getEnabledLayerIds()].sort(),
};
});
const live = await session();
assert.equal(live.mode, 'flights');
assert.equal(live.subject, '223456789');
assert.ok(live.history.includes('ais-live-vessels:123456789'));
await settledAddress();
const historyLength = await page.evaluate(() => history.length);
await pick('cesium');
assert.deepEqual(await session(), live, 'a pick keeps the session');
await settledAddress();
await visit('back', 'navara');
assert.deepEqual(await session(), live, 'Back keeps the session');
await visit('forward', 'cesium');
assert.deepEqual(await session(), live, 'Forward keeps the session');
assert.equal(await page.evaluate(() => history.length), historyLength + 1);
assert.ok(
await page.evaluate(async () => {
const { dataManager, styleManager } = window.__godsEyeView;
const result = await styleManager.setContextMode('off');
await dataManager.setEnabled('ais-live-vessels', false, {
origin: 'user', origin: 'user',
}); });
return result.ok; a.engine.render.request('qa-quake-surface');
}), }, enabled);
); if (enabled)
await pick('navara'); await page.waitForFunction(
console.log( () =>
'PASS Back/Forward keep Contacts subject, history, layers and Director', window.__godsEyeView.dataManager.layers
); .get('earthquakes')
await page.goto(base.href, { waitUntil: 'domcontentloaded' }); .module.getAnalystRecords().length === 1,
await ready('navara'); );
await switchTo('cesium'); await new Promise((resolve) => setTimeout(resolve, 1800));
assert.equal(await stored(), 'cesium'); const bytes = await page.screenshot({
await page.evaluate(() => { clip: { x: 675, y: 403, width: 20, height: 20 },
const write = Storage.prototype.setItem; });
Storage.prototype.setItem = function (key, value) { const { channels } = await sharp(bytes).stats();
if (key === 'gev.engine') return channels[0].mean - channels[1].mean;
throw new DOMException('Blocked', 'SecurityError'); };
return write.call(this, key, value); const off = await meanRed(false),
}; on = await meanRed(true);
});
await switchTo('navara');
assert.equal(await stored(), 'cesium');
// The real Map Source chips move into daylight inspection and back to live lighting.
await page.$eval('[data-stack-id="sentinel2"]', (el) => el.click());
await page.waitForFunction(
() =>
window.__godsEyeView.mapStackController.getActiveId() === 'sentinel2' &&
window.__godsEyeView.engine.host.container.hasAttribute(
'data-terrain-daylight',
),
);
// Zooming out on the satellite map must keep the same inspection sun.
assert.ok( assert.ok(
await page.evaluate(async () => { on > off + 8,
kind +
' must draw the shallow red earthquake footprint: ' +
off +
' -> ' +
on,
);
await strict.passed(page, 'earthquakes', 'shallow red footprint drawn', {
off,
on,
});
console.log(
'PASS',
kind,
'earthquake footprint red minus green:',
off,
'->',
on,
);
}
const stored = () => page.evaluate(() => localStorage.getItem('gev.engine'));
async function switchTo(kind) {
if (
await page.$eval(
'#control-panel-toggle',
(el) => el.getAttribute('aria-expanded') !== 'true',
)
) {
await page.focus('#control-panel-toggle');
await page.keyboard.press('Enter');
}
await page.waitForSelector('#globe-engine', { visible: true });
await page.evaluate(() => {
window.__switchDocument = document;
window.__quakeEntry =
window.__godsEyeView.dataManager.layers.get('earthquakes');
window.__quakeTimer = window.__quakeEntry.intervalId;
window.__renderers ??= new Map();
window.__renderers.set(
window.__godsEyeView.engine.kind,
window.__godsEyeView.engine,
);
window.__switchSplash = false;
window.__switchObserver = new MutationObserver(() => {
if (!document.querySelector('#loading-screen.hidden'))
window.__switchSplash = true;
});
window.__switchObserver.observe(
document.querySelector('#loading-screen'),
{ attributes: true },
);
window.__outgoingEngine = window.__godsEyeView.engine;
});
await strict.pick(page, kind);
await ready(kind);
assert.ok(await page.evaluate(() => window.__switchDocument === document));
assert.ok(
await page.evaluate(() => {
window.__switchObserver.disconnect();
const { engine } = window.__godsEyeView; const { engine } = window.__godsEyeView;
engine.camera.zoomOut(2_000_000); const previous = window.__renderers.get(engine.kind);
await new Promise((resolve) => {
const remove = engine.frames.postRender.addEventListener(() => {
remove();
resolve();
});
engine.render.request('qa-orbital-daylight');
});
return ( return (
engine.camera.height() > 1_500_000 && !window.__switchSplash &&
engine.host.container.hasAttribute('data-terrain-daylight') window.__quakeEntry ===
window.__godsEyeView.dataManager.layers.get('earthquakes') &&
window.__quakeTimer === window.__quakeEntry.intervalId &&
(!previous || previous === engine) &&
!window.__outgoingEngine.isDestroyed() &&
!window.__outgoingEngine.host.canvas.isConnected &&
engine.host.canvas.isConnected &&
window.__godsEyeView.dataManager
.getEnabledLayerIds()
.has('earthquakes')
); );
}), }),
'the orbital frame keeps daylight on the inspected globe',
); );
await page.$eval('[data-stack-id="osm"]', (el) => el.click()); await strict.passed(
page,
'earthquakes',
'same layer entry, model and poll timer; no splash; outgoing canvas detached',
);
await page.waitForFunction( await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
);
assert.equal(
await page.evaluate(async () => {
const parked = window.__outgoingEngine;
let frames = 0;
const remove = parked.frames.postRender.addEventListener(() => {
frames++;
});
parked.render.request('qa-parked');
await new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
);
remove();
return frames;
}),
0,
);
const url = new URL(page.url());
assert.equal(url.searchParams.get('engine'), kind);
assert.equal(url.searchParams.get('look'), 'cinematic');
const hash = new URLSearchParams(url.hash.slice(1));
assert.equal(hash.get('map'), 'osm');
assert.ok(Math.abs(Number(hash.get('lat')) - 42.3554) < 0.01);
assert.ok(Math.abs(Number(hash.get('lon')) + 71.0605) < 0.01);
assert.ok(Math.abs(Number(hash.get('alt')) - 6000) < 30);
const view = await page.evaluate(() => {
const { engine, mapStackController } = window.__godsEyeView;
const { latitude, longitude, height } = engine.camera.pose().cartographic;
return {
latitude: latitude * (180 / Math.PI),
longitude: longitude * (180 / Math.PI),
height,
map: mapStackController.getActiveId(),
};
});
assert.equal(view.map, 'osm');
assert.ok(Math.abs(view.latitude - 42.3554) < 0.01);
assert.ok(Math.abs(view.longitude + 71.0605) < 0.01);
assert.ok(Math.abs(view.height - 6000) < 30);
await strict.passed(
page,
'earthquakes',
'parked engine draws no frame; pose, map and layers carried over',
);
}
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
await ready('cesium');
await verifyQuakeSurface('cesium');
const sharpen = () =>
page.evaluate(
() => window.__godsEyeView.styleManager.getControlState().sharpen,
);
await page.evaluate(() => {
const style = window.__godsEyeView.styleManager;
style.setStyle('retro');
style.setStyle('normal');
});
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 49 });
await switchTo('navara');
await verifyQuakeSurface('navara');
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 80 });
assert.ok(
await page.evaluate(
() => () =>
window.__godsEyeView.mapStackController.getActiveId() === 'osm' && Math.abs(window.__godsEyeView.engine.render.sharpen().amount - 1.7) <
!window.__godsEyeView.engine.host.container.hasAttribute( 1e-6,
'data-terrain-daylight', ),
), );
assert.equal(await stored(), 'navara');
await page.goBack({ waitUntil: 'domcontentloaded' });
await ready('cesium');
assert.equal((await sharpen()).intensityPct, 49);
await page.goForward();
await ready('navara');
assert.equal((await sharpen()).intensityPct, 80);
await page.evaluate(() =>
window.__godsEyeView.styleManager.setSharpen({
enabled: false,
intensityPct: 49,
}),
);
await switchTo('cesium');
await switchTo('navara');
assert.deepEqual(await sharpen(), { enabled: false, intensityPct: 49 });
// The address carries the running layer set once its debounced write lands.
const settledAddress = async () => {
let previous;
for (;;) {
await new Promise((resolve) => setTimeout(resolve, 1200));
const current = await page.evaluate(() =>
new URLSearchParams(location.hash.slice(1)).get('l'),
);
if (current === previous) return current;
previous = current;
}
};
const pick = async (kind) => {
await strict.pick(page, kind);
await ready(kind);
await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
); );
assert.deepEqual(errors, []); };
console.log( const visit = async (direction, kind) => {
'PASS Cesium default, both switches, same document, warm engine identity, parked canvas, no startup splash, share pose/map/layers, Back/Forward, remembered engine, blocked preference storage', await (direction === 'back'
? page.goBack({ waitUntil: 'domcontentloaded' })
: page.goForward());
await ready(kind);
await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
); );
}, };
); const quakes = () =>
page.evaluate(() => {
const { dataManager } = window.__godsEyeView;
return {
model: window.__quakeModel === dataManager.layers.get('earthquakes'),
enabled: dataManager.getEnabledLayerIds().has('earthquakes'),
};
});
// Like a same-engine Back, each entry brings back its own layer set, on
// the same layer models.
await page.evaluate(() => {
window.__quakeModel =
window.__godsEyeView.dataManager.layers.get('earthquakes');
});
const navaraLayers = await settledAddress();
await pick('cesium');
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('earthquakes', false, {
origin: 'user',
}),
);
const cesiumLayers = await settledAddress();
assert.notEqual(cesiumLayers, navaraLayers);
await visit('back', 'navara');
assert.deepEqual(await quakes(), { model: true, enabled: true });
await strict.passed(
page,
'earthquakes',
'Back restores the enabled layer on the same model',
);
assert.equal(await settledAddress(), navaraLayers, 'Back keeps its entry');
await visit('forward', 'cesium');
assert.deepEqual(await quakes(), { model: true, enabled: false });
await strict.passed(
page,
'earthquakes',
'Forward restores the disabled layer on the same model',
);
assert.equal(await settledAddress(), cesiumLayers, 'Forward keeps its entry');
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('earthquakes', true, {
origin: 'user',
}),
);
await pick('navara');
console.log(
"PASS Back/Forward restore each entry's layers on the same models",
);
// Back/Forward hand the session off like the picker: the Contacts subject
// and its navigation history, its layers and the Director survive.
await page.evaluate(async () => {
const { dataManager, styleManager, sceneDirector } = window.__godsEyeView;
await dataManager.setEnabled('ais-live-vessels', true, {
origin: 'user',
});
const outcome = await styleManager.setContextMode('contacts');
if (!outcome.ok) throw Error(JSON.stringify(outcome));
window.qaSession = {
director: sceneDirector,
context: dataManager.layers.get('military-awareness').module,
vessels: dataManager.layers.get('ais-live-vessels'),
};
if (!window.qaSession.vessels.module.selectById('123456789'))
throw Error('Missing first vessel');
if (
!window.qaSession.context.navigateNext({
targetLayer: 'ais-live-vessels',
})
)
throw Error('Cannot navigate to the second vessel');
});
const session = () =>
page.evaluate(() => {
const { dataManager, styleManager, sceneDirector } = window.__godsEyeView;
const { director, context, vessels } = window.qaSession;
return {
director: director === sceneDirector && !director._destroyed,
owners:
context === dataManager.layers.get('military-awareness').module &&
vessels === dataManager.layers.get('ais-live-vessels'),
mode: styleManager.getContextModeState().mode,
subject: context.getContextSnapshot()?.subject?.id,
history: context._getAwarenessNavigationStateForTest().historyKeys,
enabled: [...dataManager.getEnabledLayerIds()].sort(),
};
});
const live = await session();
assert.equal(live.mode, 'flights');
assert.equal(live.subject, '223456789');
assert.ok(live.history.includes('ais-live-vessels:123456789'));
await settledAddress();
const historyLength = await page.evaluate(() => history.length);
await pick('cesium');
assert.deepEqual(await session(), live, 'a pick keeps the session');
await settledAddress();
await visit('back', 'navara');
assert.deepEqual(await session(), live, 'Back keeps the session');
await visit('forward', 'cesium');
assert.deepEqual(await session(), live, 'Forward keeps the session');
assert.equal(await page.evaluate(() => history.length), historyLength + 1);
assert.ok(
await page.evaluate(async () => {
const { dataManager, styleManager } = window.__godsEyeView;
const result = await styleManager.setContextMode('off');
await dataManager.setEnabled('ais-live-vessels', false, {
origin: 'user',
});
return result.ok;
}),
);
await pick('navara');
console.log(
'PASS Back/Forward keep Contacts subject, history, layers and Director',
);
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
await ready('navara');
await switchTo('cesium');
assert.equal(await stored(), 'cesium');
await page.evaluate(() => {
const write = Storage.prototype.setItem;
Storage.prototype.setItem = function (key, value) {
if (key === 'gev.engine')
throw new DOMException('Blocked', 'SecurityError');
return write.call(this, key, value);
};
});
await switchTo('navara');
assert.equal(await stored(), 'cesium');
// The real Map Source chips move into daylight inspection and back to live lighting.
await page.$eval('[data-stack-id="sentinel2"]', (el) => el.click());
await page.waitForFunction(
() =>
window.__godsEyeView.mapStackController.getActiveId() === 'sentinel2' &&
window.__godsEyeView.engine.host.container.hasAttribute(
'data-terrain-daylight',
),
);
// Zooming out on the satellite map must keep the same inspection sun.
assert.ok(
await page.evaluate(async () => {
const { engine } = window.__godsEyeView;
engine.camera.zoomOut(2_000_000);
await new Promise((resolve) => {
const remove = engine.frames.postRender.addEventListener(() => {
remove();
resolve();
});
engine.render.request('qa-orbital-daylight');
});
return (
engine.camera.height() > 1_500_000 &&
engine.host.container.hasAttribute('data-terrain-daylight')
);
}),
'the orbital frame keeps daylight on the inspected globe',
);
await page.$eval('[data-stack-id="osm"]', (el) => el.click());
await page.waitForFunction(
() =>
window.__godsEyeView.mapStackController.getActiveId() === 'osm' &&
!window.__godsEyeView.engine.host.container.hasAttribute(
'data-terrain-daylight',
),
);
assert.deepEqual(errors, []);
console.log(
'PASS Cesium default, both switches, same document, warm engine identity, parked canvas, no startup splash, share pose/map/layers, Back/Forward, remembered engine, blocked preference storage',
);
});
+1 -1
View File
@@ -67,7 +67,7 @@ const getOpt = (name, dflt) => {
}; };
const BASE_URL = getOpt('--url', 'http://localhost:4173'); const BASE_URL = getOpt('--url', 'http://localhost:4173');
// --engine=cesium|navara (phase 3): Navara reads the layer through the QA // --engine=cesium|navara: Navara reads the layer through the QA
// probe (window.__godsEyeView.layers.drawn) and moves the engine camera. // probe (window.__godsEyeView.layers.drawn) and moves the engine camera.
const ENGINE = (argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9); const ENGINE = (argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
const APP_URL = const APP_URL =
+1 -1
View File
@@ -21,7 +21,7 @@ const strict = createStrict({
// installAlprFixture answers the camera tiles. // installAlprFixture answers the camera tiles.
fixtures: ['https://tiles.dontgetflocked.com/*'], fixtures: ['https://tiles.dontgetflocked.com/*'],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const p = await b.newPage();
await p.setViewport({ width: 1280, height: 900 }); await p.setViewport({ width: 1280, height: 900 });
const errors = []; const errors = [];
+2 -2
View File
@@ -11,7 +11,7 @@ import fs from 'node:fs';
// QA_BASE_URL matches the sibling harnesses (qa-height-datum / qa-cctv-v2) so a // QA_BASE_URL matches the sibling harnesses (qa-height-datum / qa-cctv-v2) so a
// secondary checkout can verify against its own dev server instead of the default :4173. // secondary checkout can verify against its own dev server instead of the default :4173.
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173'; const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
// --engine=cesium|navara (phase 3): on Navara the camera, the mesh probes and // --engine=cesium|navara: on Navara the camera, the mesh probes and
// the sprite census go through the engine and the QA probe. // the sprite census go through the engine and the QA probe.
const ENGINE = (process.argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9); const ENGINE = (process.argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
const APP_URL = const APP_URL =
@@ -125,7 +125,7 @@ const report = await page.evaluate(async () => {
drawnBy: modelAt ? 'model' : 'sprite', drawnBy: modelAt ? 'model' : 'sprite',
meshM: meshH != null ? +meshH.toFixed(1) : null, meshM: meshH != null ? +meshH.toFixed(1) : null,
aboveMeshM: meshH != null ? +(c.height - meshH).toFixed(1) : null, aboveMeshM: meshH != null ? +(c.height - meshH).toFixed(1) : null,
// Why a contact sits where it does (10b review 6): the probe's own // Why a contact sits where it does: the probe's own
// answer (source, tile size) and the coarse DEM floor (the prior). // answer (source, tile size) and the coarse DEM floor (the prior).
probe: answer ? { source: answer.source, tileSizeM: answer.tileSizeM ?? null } : null, probe: answer ? { source: answer.source, tileSizeM: answer.tileSizeM ?? null } : null,
demFloorM: cachedGroundFloor(c.latitude * 180 / Math.PI, c.longitude * 180 / Math.PI) ?? null, demFloorM: cachedGroundFloor(c.latitude * 180 / Math.PI, c.longitude * 180 / Math.PI) ?? null,
+10 -26
View File
@@ -1,21 +1,20 @@
import { fromDegrees } from '../src/geo/cartesian3.js'; import { fromDegrees } from '../src/geo/cartesian3.js';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic'; base.search = '?engine=navara&welcome=0&look=cinematic';
base.hash = base.hash =
'v=2&lat=43.6500&lon=-79.3800&alt=46000&heading=0&pitch=-90&roll=0&map=sentinel2&l='; 'v=2&lat=43.6500&lon=-79.3800&alt=46000&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
const strict = createStrict({ origin: base.origin, layers: ['s2-ghostmaps'] }); const strict = createStrict({ origin: base.origin, layers: ['s2-ghostmaps'] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b);
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
const file = await readFile( const file = await readFile(
new URL('./fixtures/ghostmaps-scenes.kmz', import.meta.url), new URL('./fixtures/ghostmaps-scenes.kmz', import.meta.url),
); );
@@ -63,21 +62,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
[...file], [...file],
); );
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => { await waitForEngine(p, 'navara');
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await p.evaluate(async () => { await p.evaluate(async () => {
const { dataManager } = window.__godsEyeView; const { dataManager } = window.__godsEyeView;
await dataManager.setEnabled('s2-ghostmaps', true, { origin: 'user' }); await dataManager.setEnabled('s2-ghostmaps', true, { origin: 'user' });
@@ -111,8 +96,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await p.evaluate(() => { await p.evaluate(() => {
window.ghostStats.titles = []; window.ghostStats.titles = [];
}); });
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
await p.waitForFunction(() => await p.waitForFunction(() =>
window.ghostStats.titles.some((v) => /QA zone/i.test(v)), window.ghostStats.titles.some((v) => /QA zone/i.test(v)),
); );
+12 -30
View File
@@ -1,7 +1,12 @@
import { fromDegrees } from '../src/geo/cartesian3.js'; import { fromDegrees } from '../src/geo/cartesian3.js';
import sharp from 'sharp'; import sharp from 'sharp';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic'; base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -16,14 +21,8 @@ const strict = createStrict({
'fire-perimeters', 'fire-perimeters',
], ],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b);
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await p.evaluateOnNewDocument(() => { await p.evaluateOnNewDocument(() => {
const fetch = window.fetch.bind(window); const fetch = window.fetch.bind(window);
window.fetch = (input, o) => { window.fetch = (input, o) => {
@@ -122,20 +121,6 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}; };
}); });
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
const capture = async (visible, layerId = 'air-quality') => { const capture = async (visible, layerId = 'air-quality') => {
await p.evaluate( await p.evaluate(
async (show, id) => { async (show, id) => {
@@ -230,7 +215,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
} }
return on; return on;
}; };
await ready('navara'); await waitForEngine(p, 'navara');
await p.evaluate(() => { await p.evaluate(() => {
const d = window.__godsEyeView; const d = window.__godsEyeView;
if (d.tileset || window.__GOOGLE_MAPS_API_KEY__) if (d.tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -240,8 +225,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}); });
for (const kind of ['navara', 'cesium', 'navara']) { for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
const same = await p.evaluate( const same = await p.evaluate(
() => () =>
window.__godsEyeView.dataManager.layers.get('air-quality').module === window.__godsEyeView.dataManager.layers.get('air-quality').module ===
@@ -357,8 +341,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}; };
for (const kind of ['navara', 'cesium', 'navara']) { for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
await card(true); await card(true);
await strict.check( await strict.check(
p, p,
@@ -478,8 +461,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
let histories = await p.evaluate(() => window.qaVesselHistories); let histories = await p.evaluate(() => window.qaVesselHistories);
for (const kind of ['navara', 'cesium', 'navara']) { for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
} }
const sample = await p.evaluate(() => { const sample = await p.evaluate(() => {
const entry = const entry =
+10 -26
View File
@@ -1,7 +1,12 @@
import { fromDegrees } from '../src/geo/cartesian3.js'; import { fromDegrees } from '../src/geo/cartesian3.js';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic'; base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -26,14 +31,8 @@ const strict = createStrict({
origin: base.origin, origin: base.origin,
layers: ['local-dams', 'local-datacenters'], layers: ['local-dams', 'local-datacenters'],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b);
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
const reads = { datacenters: 0, dams: 0 }; const reads = { datacenters: 0, dams: 0 };
p.on('request', (req) => { p.on('request', (req) => {
const path = new URL(req.url()).pathname; const path = new URL(req.url()).pathname;
@@ -52,21 +51,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}); });
}); });
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => { await waitForEngine(p, 'navara');
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
const ids = ['local-datacenters', 'local-dams']; const ids = ['local-datacenters', 'local-dams'];
const dataset = await p.evaluate(async (ids) => { const dataset = await p.evaluate(async (ids) => {
const { dataManager, tileset } = window.__godsEyeView; const { dataManager, tileset } = window.__godsEyeView;
@@ -184,8 +169,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await p.evaluate(() => { await p.evaluate(() => {
window.qaSelections = []; window.qaSelections = [];
}); });
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
await p.waitForFunction( await p.waitForFunction(
() => window.qaSelections.includes('local-datacenters:342664739'), () => window.qaSelections.includes('local-datacenters:342664739'),
{ timeout: 15000 }, { timeout: 15000 },
+1 -2
View File
@@ -314,8 +314,7 @@ async function main() {
(ids) => ids.filter((id) => !window.__godsEyeView.layers.available(id)), (ids) => ids.filter((id) => !window.__godsEyeView.layers.available(id)),
fieldLayers, fieldLayers,
); );
// An engine with flights but not the other field layers (Navara, W1 so // An engine with flights but not the other field layers: the same 12,000 / 5,200 observations, all as aircraft, so the
// far): the same 12,000 / 5,200 observations, all as aircraft, so the
// detection lane is measured on the layers the engine has. // detection lane is measured on the layers the engine has.
let fieldCounts = FIELD_COUNTS; let fieldCounts = FIELD_COUNTS;
let normalCounts = NORMAL_COUNTS; let normalCounts = NORMAL_COUNTS;
+482 -493
View File
@@ -15,514 +15,503 @@ url.hash =
// left. // left.
const HIDDEN_PICKER = 'the Globe Engine picker is hidden in cockpit mode'; const HIDDEN_PICKER = 'the Globe Engine picker is hidden in cockpit mode';
const strict = createStrict({ origin: url.origin, layers: [layerId] }); const strict = createStrict({ origin: url.origin, layers: [layerId] });
await strict.run( await strict.run({}, async (browser) => {
{ args: ['--use-angle=metal', '--enable-gpu'] }, const page = await browser.newPage();
async (browser) => { const errors = [];
const page = await browser.newPage(); page.on('pageerror', (e) => errors.push(e.message));
const errors = []; await page.setViewport({ width: 1280, height: 720 });
page.on('pageerror', (e) => errors.push(e.message)); await page.evaluateOnNewDocument(
await page.setViewport({ width: 1280, height: 720 }); (layerId, aircraftId) => {
await page.evaluateOnNewDocument( window.__qaAircraft = { layerId, aircraftId };
(layerId, aircraftId) => { const fetch = window.fetch.bind(window);
window.__qaAircraft = { layerId, aircraftId }; window.__flightPolls = 0;
const fetch = window.fetch.bind(window); const interval = window.setInterval.bind(window);
window.__flightPolls = 0; window.setInterval = function (callback, delay, ...args) {
const interval = window.setInterval.bind(window); if (typeof callback !== 'function')
window.setInterval = function (callback, delay, ...args) { return interval(callback, delay, ...args);
if (typeof callback !== 'function') const id = interval(
return interval(callback, delay, ...args); function (...values) {
const id = interval( if (window.__handoff?.timer === id) window.__handoff.ticks++;
function (...values) { return callback.apply(this, values);
if (window.__handoff?.timer === id) window.__handoff.ticks++; },
return callback.apply(this, values); delay,
}, ...args,
delay, );
...args, return id;
);
return id;
};
window.fetch = (input, options) => {
const path = new URL(input.url ?? input, document.baseURI).pathname;
const now = Math.floor(Date.now() / 1000);
if (path === '/api/flights') {
if (layerId === 'flights') window.__flightPolls++;
return Promise.resolve(
Response.json({
time: now,
states: [
[
'71be26',
'GEV123',
'QA',
now,
now,
-125.5779,
40.2539,
9700,
false,
200,
45,
0,
null,
9700,
null,
false,
0,
],
],
}),
);
}
if (path === '/api/military') {
if (layerId === 'military') window.__flightPolls++;
return Promise.resolve(
Response.json({
now: Date.now(),
ac: [
{
hex: 'aaa052',
flight: 'GEVMIL',
lon: -125.5779,
lat: 40.2539,
alt_geom: 31825,
alt_baro: 31825,
gs: 389,
track: 45,
seen: 0,
seen_pos: 0,
dbFlags: 1,
type: 'adsb_icao',
},
],
}),
);
}
if (path === '/api/flights/track')
return Promise.resolve(Response.json({ path: [] }));
if (path.startsWith('/api/flights/'))
return Promise.resolve(Response.json({}));
return fetch(input, options);
};
},
layerId,
aircraftId,
);
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
const ready = (kind) =>
page.waitForFunction(
(engine) =>
window.__godsEyeView?.engine?.kind === engine &&
!document.querySelector('#globe-engine')?.disabled &&
document.querySelector('#loading-screen.hidden') &&
!document.querySelector('.engine-switch-frame'),
{ timeout: 90000 },
kind,
);
await ready('navara');
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await page.waitForSelector('#cockpit-entry', { visible: true });
await page.click('#cockpit-entry');
await page.waitForFunction(
() => window.__godsEyeView.styleManager.getCockpitState().active,
);
await page.evaluate(() => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
throw Error('Keyless preview required');
styleManager.cockpitView.setVisionMode('thermal');
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
window.__handoff = {
dataManager,
entry,
row: document.querySelector(
`[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`,
),
timer: entry.intervalId,
ticks: 0,
model: entry.module,
pollCount: window.__flightPolls,
timestamp: entry.module.getStats().lastUpdate,
}; };
}); window.fetch = (input, options) => {
// Refuse the first Cesium canvas. The application must roll back with the const path = new URL(input.url ?? input, document.baseURI).pathname;
// original aircraft still in the cockpit, then allow an ordinary retry. const now = Math.floor(Date.now() / 1000);
await page.evaluate(() => { if (path === '/api/flights') {
const original = HTMLCanvasElement.prototype.getContext; if (layerId === 'flights') window.__flightPolls++;
HTMLCanvasElement.prototype.getContext = function (type, ...args) { return Promise.resolve(
if (String(type).includes('webgl')) { Response.json({
window.__refusedContext = true; time: now,
throw new Error('QA renderer startup failure'); states: [
[
'71be26',
'GEV123',
'QA',
now,
now,
-125.5779,
40.2539,
9700,
false,
200,
45,
0,
null,
9700,
null,
false,
0,
],
],
}),
);
} }
return original.call(this, type, ...args); if (path === '/api/military') {
}; if (layerId === 'military') window.__flightPolls++;
window.__restoreContext = () => { return Promise.resolve(
HTMLCanvasElement.prototype.getContext = original; Response.json({
}; now: Date.now(),
}); ac: [
const historyLength = await page.evaluate(() => history.length); {
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER); hex: 'aaa052',
await page.waitForFunction(() => window.__refusedContext === true); flight: 'GEVMIL',
await ready('navara'); lon: -125.5779,
// A failed switch leaves the session usable on the engine that worked and lat: 40.2539,
// adds no history entry. alt_geom: 31825,
const survived = (kind) => alt_baro: 31825,
page.evaluate( gs: 389,
(kind, length) => { track: 45,
const { dataManager, engine, styleManager } = window.__godsEyeView; seen: 0,
return ( seen_pos: 0,
engine.kind === kind && dbFlags: 1,
new URL(location.href).searchParams.get('engine') === kind && type: 'adsb_icao',
history.length === length && },
dataManager === window.__handoff.dataManager && ],
dataManager.layers.get(window.__qaAircraft.layerId) === }),
window.__handoff.entry &&
styleManager.getCockpitState().active &&
styleManager.getCockpitState().visionMode === 'thermal' &&
styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId
); );
}, }
kind, if (path === '/api/flights/track')
historyLength, return Promise.resolve(Response.json({ path: [] }));
); if (path.startsWith('/api/flights/'))
await page.evaluate(() => window.__restoreContext()); return Promise.resolve(Response.json({}));
await strict.check( return fetch(input, options);
page,
layerId,
'cold Cesium failure rolls back to Navara: same session, cockpit, aircraft, no history entry',
await survived('navara'),
);
console.log(
'PASS cold Cesium failure rolled back to Navara with the same flight, cockpit and history',
);
for (const kind of ['cesium', 'navara']) {
await strict.selectEngine(page, kind, HIDDEN_PICKER);
await ready(kind);
const result = await page.evaluate(() => {
const { dataManager, engine, styleManager } = window.__godsEyeView;
const before = window.__handoff;
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
const cockpit = styleManager.getCockpitState();
return {
sameOwner:
before.dataManager === dataManager &&
before.entry === entry &&
before.model === entry.module,
sameRow:
Boolean(before.row) &&
before.row ===
document.querySelector(
`[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`,
),
sameTimer:
before.timer === entry.intervalId && entry.intervalId != null,
sourcePolls: window.__flightPolls - before.pollCount,
timerTicks: before.ticks,
stamp: entry.module.getStats().lastUpdate,
initialStamp: before.timestamp,
id: cockpit.subject?.id,
active: cockpit.active,
mode: cockpit.visionMode,
hud: !document.querySelector('#cockpit-hud').hidden,
drawn: entry.module
.qaDrawn()
.some((row) => row.id === window.__qaAircraft.aircraftId),
height: engine.camera.height(),
};
});
assert.ok(
result.sameOwner &&
result.sameTimer &&
result.sameRow &&
result.active &&
result.hud &&
result.drawn,
JSON.stringify(result),
);
assert.equal(result.id, aircraftId);
assert.equal(result.mode, 'thermal');
assert.ok(
result.sourcePolls <= result.timerTicks,
'switch must not add a source request',
);
if (result.timerTicks === 0) {
assert.equal(result.stamp, result.initialStamp);
}
await strict.passed(
page,
layerId,
'same manager entry, model, row, poll timer, cockpit, thermal mode, HUD and drawn aircraft',
result,
);
// Polling goes on after the switch: the same timer ticks and each tick
// reads the source once (no tick may be skipped, none added).
const polls = await page.evaluate(async (layerId) => {
const entry = window.__godsEyeView.dataManager.layers.get(layerId);
const every =
Number(entry.module.refreshInterval) ||
Number(entry.module.updateInterval);
const start = {
polls: window.__flightPolls,
ticks: window.__handoff.ticks,
stamp: entry.module.getStats().lastUpdate,
};
const deadline = Date.now() + 2.5 * every + 5000;
while (
window.__handoff.ticks - start.ticks < 2 &&
Date.now() < deadline
)
await new Promise((resolve) => setTimeout(resolve, 250));
// Let the last tick's request land.
await new Promise((resolve) => setTimeout(resolve, 1500));
return {
every,
ticks: window.__handoff.ticks - start.ticks,
polls: window.__flightPolls - start.polls,
updated: entry.module.getStats().lastUpdate !== start.stamp,
sameTimer: entry.intervalId === window.__handoff.timer,
};
}, layerId);
await strict.check(
page,
layerId,
'polling continues after the switch: two timer ticks, one source read each, fresh data',
polls.every > 0 &&
polls.ticks >= 2 &&
polls.polls === polls.ticks &&
polls.updated &&
polls.sameTimer,
polls,
);
await page.waitForFunction(
() =>
Math.abs(window.__godsEyeView.engine.camera.height() - 9700) < 1000,
);
console.log(
`PASS ${kind} cockpit, aircraft, HUD, manager and timer (${result.sourcePolls} polls since entry)`,
);
}
// The other direction, warm: the parked Navara renderer fails to resume on
// the way back from Cesium. The session rolls back to Cesium, then an
// ordinary retry starts Navara cold.
await page.evaluate(() => {
window.__parkedNavara = window.__godsEyeView.engine;
});
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER);
await ready('cesium');
const lengthOnCesium = await page.evaluate(() => {
const request = window.__parkedNavara.render.request;
window.__parkedNavara.render.request = function (...args) {
window.__parkedNavara.render.request = request;
window.__refusedResume = true;
throw new Error('QA parked renderer failure');
}; };
return history.length; },
}); layerId,
await strict.selectEngine(page, 'navara', HIDDEN_PICKER); aircraftId,
await page.waitForFunction(() => window.__refusedResume === true); );
await ready('cesium'); await page.goto(url.href, { waitUntil: 'domcontentloaded' });
assert.ok( const ready = (kind) =>
(await page.evaluate( page.waitForFunction(
(length) => history.length === length, (engine) =>
lengthOnCesium, window.__godsEyeView?.engine?.kind === engine &&
)) && !document.querySelector('#globe-engine')?.disabled &&
(await page.evaluate(() => { document.querySelector('#loading-screen.hidden') &&
const { styleManager } = window.__godsEyeView; !document.querySelector('.engine-switch-frame'),
return ( { timeout: 90000 },
styleManager.getCockpitState().active && kind,
styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId &&
new URL(location.href).searchParams.get('engine') === 'cesium'
);
})),
'a failed warm Navara resume must leave Cesium usable without a history entry',
); );
await strict.passed( await ready('navara');
page, await page.evaluate(
layerId, () => window.__godsEyeView.styleManager.initialRestorePromise,
'warm Navara failure rolls back to Cesium: same aircraft and cockpit, no history entry', );
); await page.waitForSelector('#cockpit-entry', { visible: true });
console.log( await page.click('#cockpit-entry');
'PASS warm Navara failure rolled back to Cesium with the same flight and cockpit', await page.waitForFunction(
); () => window.__godsEyeView.styleManager.getCockpitState().active,
await page.evaluate(() => { );
window.__parkedCesium = window.__godsEyeView.engine; await page.evaluate(() => {
}); const { dataManager, styleManager, tileset } = window.__godsEyeView;
await strict.selectEngine(page, 'navara', HIDDEN_PICKER); if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
await ready('navara'); throw Error('Keyless preview required');
assert.ok( styleManager.cockpitView.setVisionMode('thermal');
await page.evaluate(() => { const entry = dataManager.layers.get(window.__qaAircraft.layerId);
const { engine, styleManager } = window.__godsEyeView; window.__handoff = {
return ( dataManager,
engine !== window.__parkedNavara && entry,
styleManager.getCockpitState().active && row: document.querySelector(
styleManager.getAircraftTrackingTarget()?.id === `[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`,
window.__qaAircraft.aircraftId
);
}),
'the retry starts a new Navara renderer on the same session',
);
console.log('PASS retry after the warm failure started Navara cold');
// Both engines fail: the warm Cesium resume and the warm Navara recovery.
// No engine runs, but the error screen offers the engine picker, and a
// retry starts Cesium cold on the same session without a reload.
const lengthBeforeDoubleFailure = await page.evaluate(() => {
window.__failResume = true;
for (const engine of [
window.__godsEyeView.engine,
window.__parkedCesium,
]) {
const request = engine.render.request;
engine.render.request = function (reason, ...args) {
if (reason === 'engine-resume' && window.__failResume) {
window.__refusedResumes = (window.__refusedResumes ?? 0) + 1;
throw new Error('QA parked renderer failure');
}
return request.call(this, reason, ...args);
};
}
window.__startDocument = document;
return history.length;
});
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER);
await page
.waitForSelector('#loading-screen .engine-switch-retry button', {
visible: true,
timeout: 90000,
})
.catch(async (error) => {
console.log(
'no retry picker',
JSON.stringify(
await page.evaluate(() => ({
refused: window.__refusedResumes,
engine: window.__godsEyeView?.engine?.kind ?? null,
retry: !!document.querySelector('.engine-switch-retry'),
loading: document.querySelector('#loading-screen')?.className,
status: document.querySelector('#loading-screen .loader-status')
?.textContent,
address: location.search,
})),
),
);
throw error;
});
assert.ok(
await page.evaluate(
(length) =>
window.__refusedResumes === 2 &&
// No globe runs (the page's debug object may stay, without one).
!window.__godsEyeView?.engine &&
history.length === length &&
!document
.querySelector('#loading-screen')
.classList.contains('hidden') &&
/QA parked renderer failure/.test(
document.querySelector('#loading-screen .loader-status')
.textContent,
),
lengthBeforeDoubleFailure,
), ),
'a failed recovery shows its error and the engine picker ' + timer: entry.intervalId,
JSON.stringify( ticks: 0,
await page.evaluate(() => ({ model: entry.module,
refused: window.__refusedResumes, pollCount: window.__flightPolls,
app: !!window.__godsEyeView, timestamp: entry.module.getStats().lastUpdate,
engine: window.__godsEyeView?.engine?.kind ?? null, };
history: history.length, });
loading: document.querySelector('#loading-screen')?.className, // Refuse the first Cesium canvas. The application must roll back with the
status: document.querySelector('#loading-screen .loader-status') // original aircraft still in the cockpit, then allow an ordinary retry.
?.textContent, await page.evaluate(() => {
})), const original = HTMLCanvasElement.prototype.getContext;
), HTMLCanvasElement.prototype.getContext = function (type, ...args) {
); if (String(type).includes('webgl')) {
await page.evaluate(() => { window.__refusedContext = true;
window.__failResume = false; throw new Error('QA renderer startup failure');
}); }
await page.click( return original.call(this, type, ...args);
'#loading-screen .engine-switch-retry [data-engine="cesium"]', };
); window.__restoreContext = () => {
await ready('cesium'); HTMLCanvasElement.prototype.getContext = original;
assert.ok( };
await page.evaluate(() => { });
const { dataManager, styleManager } = window.__godsEyeView; const historyLength = await page.evaluate(() => history.length);
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER);
await page.waitForFunction(() => window.__refusedContext === true);
await ready('navara');
// A failed switch leaves the session usable on the engine that worked and
// adds no history entry.
const survived = (kind) =>
page.evaluate(
(kind, length) => {
const { dataManager, engine, styleManager } = window.__godsEyeView;
return ( return (
window.__startDocument === document && engine.kind === kind &&
!document.querySelector('.engine-switch-retry') && new URL(location.href).searchParams.get('engine') === kind &&
history.length === length &&
dataManager === window.__handoff.dataManager && dataManager === window.__handoff.dataManager &&
dataManager.layers.get(window.__qaAircraft.layerId) === dataManager.layers.get(window.__qaAircraft.layerId) ===
window.__handoff.entry && window.__handoff.entry &&
styleManager.getCockpitState().active && styleManager.getCockpitState().active &&
styleManager.getCockpitState().visionMode === 'thermal' &&
styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId
);
},
kind,
historyLength,
);
await page.evaluate(() => window.__restoreContext());
await strict.check(
page,
layerId,
'cold Cesium failure rolls back to Navara: same session, cockpit, aircraft, no history entry',
await survived('navara'),
);
console.log(
'PASS cold Cesium failure rolled back to Navara with the same flight, cockpit and history',
);
for (const kind of ['cesium', 'navara']) {
await strict.selectEngine(page, kind, HIDDEN_PICKER);
await ready(kind);
const result = await page.evaluate(() => {
const { dataManager, engine, styleManager } = window.__godsEyeView;
const before = window.__handoff;
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
const cockpit = styleManager.getCockpitState();
return {
sameOwner:
before.dataManager === dataManager &&
before.entry === entry &&
before.model === entry.module,
sameRow:
Boolean(before.row) &&
before.row ===
document.querySelector(
`[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`,
),
sameTimer:
before.timer === entry.intervalId && entry.intervalId != null,
sourcePolls: window.__flightPolls - before.pollCount,
timerTicks: before.ticks,
stamp: entry.module.getStats().lastUpdate,
initialStamp: before.timestamp,
id: cockpit.subject?.id,
active: cockpit.active,
mode: cockpit.visionMode,
hud: !document.querySelector('#cockpit-hud').hidden,
drawn: entry.module
.qaDrawn()
.some((row) => row.id === window.__qaAircraft.aircraftId),
height: engine.camera.height(),
};
});
assert.ok(
result.sameOwner &&
result.sameTimer &&
result.sameRow &&
result.active &&
result.hud &&
result.drawn,
JSON.stringify(result),
);
assert.equal(result.id, aircraftId);
assert.equal(result.mode, 'thermal');
assert.ok(
result.sourcePolls <= result.timerTicks,
'switch must not add a source request',
);
if (result.timerTicks === 0) {
assert.equal(result.stamp, result.initialStamp);
}
await strict.passed(
page,
layerId,
'same manager entry, model, row, poll timer, cockpit, thermal mode, HUD and drawn aircraft',
result,
);
// Polling goes on after the switch: the same timer ticks and each tick
// reads the source once (no tick may be skipped, none added).
const polls = await page.evaluate(async (layerId) => {
const entry = window.__godsEyeView.dataManager.layers.get(layerId);
const every =
Number(entry.module.refreshInterval) ||
Number(entry.module.updateInterval);
const start = {
polls: window.__flightPolls,
ticks: window.__handoff.ticks,
stamp: entry.module.getStats().lastUpdate,
};
const deadline = Date.now() + 2.5 * every + 5000;
while (window.__handoff.ticks - start.ticks < 2 && Date.now() < deadline)
await new Promise((resolve) => setTimeout(resolve, 250));
// Let the last tick's request land.
await new Promise((resolve) => setTimeout(resolve, 1500));
return {
every,
ticks: window.__handoff.ticks - start.ticks,
polls: window.__flightPolls - start.polls,
updated: entry.module.getStats().lastUpdate !== start.stamp,
sameTimer: entry.intervalId === window.__handoff.timer,
};
}, layerId);
await strict.check(
page,
layerId,
'polling continues after the switch: two timer ticks, one source read each, fresh data',
polls.every > 0 &&
polls.ticks >= 2 &&
polls.polls === polls.ticks &&
polls.updated &&
polls.sameTimer,
polls,
);
await page.waitForFunction(
() => Math.abs(window.__godsEyeView.engine.camera.height() - 9700) < 1000,
);
console.log(
`PASS ${kind} cockpit, aircraft, HUD, manager and timer (${result.sourcePolls} polls since entry)`,
);
}
// The other direction, warm: the parked Navara renderer fails to resume on
// the way back from Cesium. The session rolls back to Cesium, then an
// ordinary retry starts Navara cold.
await page.evaluate(() => {
window.__parkedNavara = window.__godsEyeView.engine;
});
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER);
await ready('cesium');
const lengthOnCesium = await page.evaluate(() => {
const request = window.__parkedNavara.render.request;
window.__parkedNavara.render.request = function (...args) {
window.__parkedNavara.render.request = request;
window.__refusedResume = true;
throw new Error('QA parked renderer failure');
};
return history.length;
});
await strict.selectEngine(page, 'navara', HIDDEN_PICKER);
await page.waitForFunction(() => window.__refusedResume === true);
await ready('cesium');
assert.ok(
(await page.evaluate(
(length) => history.length === length,
lengthOnCesium,
)) &&
(await page.evaluate(() => {
const { styleManager } = window.__godsEyeView;
return (
styleManager.getCockpitState().active &&
styleManager.getAircraftTrackingTarget()?.id === styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId && window.__qaAircraft.aircraftId &&
new URL(location.href).searchParams.get('engine') === 'cesium' new URL(location.href).searchParams.get('engine') === 'cesium'
); );
}), })),
'the retry restores the same session on Cesium', 'a failed warm Navara resume must leave Cesium usable without a history entry',
); );
await strict.passed( await strict.passed(
page, page,
layerId, layerId,
'after both engines fail, the retry restores the same session', 'warm Navara failure rolls back to Cesium: same aircraft and cockpit, no history entry',
); );
console.log( console.log(
'PASS failed recovery offered the picker; the retry restored the session on Cesium', 'PASS warm Navara failure rolled back to Cesium with the same flight and cockpit',
); );
await page.evaluate(() => await page.evaluate(() => {
window.__godsEyeView.styleManager.controlCockpit('exit'), window.__parkedCesium = window.__godsEyeView.engine;
); });
await page.waitForFunction( await strict.selectEngine(page, 'navara', HIDDEN_PICKER);
() => !window.__godsEyeView.styleManager.getCockpitState().active, await ready('navara');
); assert.ok(
// Out of the cockpit the dock shows the picker again: a switch with
// real input keeps the same entry and timer, the aircraft drawn, and the
// source polled on its timer.
await page.evaluate(() => { await page.evaluate(() => {
window.__handoff.ticks = 0; const { engine, styleManager } = window.__godsEyeView;
window.__handoff.pollCount = window.__flightPolls; return (
window.__handoff.timer = window.__godsEyeView.dataManager.layers.get( engine !== window.__parkedNavara &&
window.__qaAircraft.layerId, styleManager.getCockpitState().active &&
).intervalId; styleManager.getAircraftTrackingTarget()?.id ===
}); window.__qaAircraft.aircraftId
await strict.pick(page, 'navara'); );
await ready('navara'); }),
const after = await page.evaluate(async () => { 'the retry starts a new Navara renderer on the same session',
const { dataManager } = window.__godsEyeView; );
const entry = dataManager.layers.get(window.__qaAircraft.layerId); console.log('PASS retry after the warm failure started Navara cold');
const every =
Number(entry.module.refreshInterval) || // Both engines fail: the warm Cesium resume and the warm Navara recovery.
Number(entry.module.updateInterval); // No engine runs, but the error screen offers the engine picker, and a
const deadline = Date.now() + 2.5 * every + 5000; // retry starts Cesium cold on the same session without a reload.
while (window.__handoff.ticks < 1 && Date.now() < deadline) const lengthBeforeDoubleFailure = await page.evaluate(() => {
await new Promise((resolve) => setTimeout(resolve, 250)); window.__failResume = true;
await new Promise((resolve) => setTimeout(resolve, 1500)); for (const engine of [window.__godsEyeView.engine, window.__parkedCesium]) {
return { const request = engine.render.request;
same: engine.render.request = function (reason, ...args) {
dataManager === window.__handoff.dataManager && if (reason === 'engine-resume' && window.__failResume) {
entry === window.__handoff.entry, window.__refusedResumes = (window.__refusedResumes ?? 0) + 1;
timer: entry.intervalId === window.__handoff.timer, throw new Error('QA parked renderer failure');
drawn: entry.module }
.qaDrawn() return request.call(this, reason, ...args);
.some((row) => row.id === window.__qaAircraft.aircraftId),
ticks: window.__handoff.ticks,
polls: window.__flightPolls - window.__handoff.pollCount,
}; };
}
window.__startDocument = document;
return history.length;
});
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER);
await page
.waitForSelector('#loading-screen .engine-switch-retry button', {
visible: true,
timeout: 90000,
})
.catch(async (error) => {
console.log(
'no retry picker',
JSON.stringify(
await page.evaluate(() => ({
refused: window.__refusedResumes,
engine: window.__godsEyeView?.engine?.kind ?? null,
retry: !!document.querySelector('.engine-switch-retry'),
loading: document.querySelector('#loading-screen')?.className,
status: document.querySelector('#loading-screen .loader-status')
?.textContent,
address: location.search,
})),
),
);
throw error;
}); });
await strict.check( assert.ok(
page, await page.evaluate(
layerId, (length) =>
'picker switch keeps the entry, timer and drawn aircraft; polling continues', window.__refusedResumes === 2 &&
after.same && // No globe runs (the page's debug object may stay, without one).
after.timer && !window.__godsEyeView?.engine &&
after.drawn && history.length === length &&
after.ticks >= 1 && !document
after.polls === after.ticks, .querySelector('#loading-screen')
after, .classList.contains('hidden') &&
); /QA parked renderer failure/.test(
assert.deepEqual(errors, []); document.querySelector('#loading-screen .loader-status').textContent,
}, ),
); lengthBeforeDoubleFailure,
),
'a failed recovery shows its error and the engine picker ' +
JSON.stringify(
await page.evaluate(() => ({
refused: window.__refusedResumes,
app: !!window.__godsEyeView,
engine: window.__godsEyeView?.engine?.kind ?? null,
history: history.length,
loading: document.querySelector('#loading-screen')?.className,
status: document.querySelector('#loading-screen .loader-status')
?.textContent,
})),
),
);
await page.evaluate(() => {
window.__failResume = false;
});
await page.click(
'#loading-screen .engine-switch-retry [data-engine="cesium"]',
);
await ready('cesium');
assert.ok(
await page.evaluate(() => {
const { dataManager, styleManager } = window.__godsEyeView;
return (
window.__startDocument === document &&
!document.querySelector('.engine-switch-retry') &&
dataManager === window.__handoff.dataManager &&
dataManager.layers.get(window.__qaAircraft.layerId) ===
window.__handoff.entry &&
styleManager.getCockpitState().active &&
styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId &&
new URL(location.href).searchParams.get('engine') === 'cesium'
);
}),
'the retry restores the same session on Cesium',
);
await strict.passed(
page,
layerId,
'after both engines fail, the retry restores the same session',
);
console.log(
'PASS failed recovery offered the picker; the retry restored the session on Cesium',
);
await page.evaluate(() =>
window.__godsEyeView.styleManager.controlCockpit('exit'),
);
await page.waitForFunction(
() => !window.__godsEyeView.styleManager.getCockpitState().active,
);
// Out of the cockpit the dock shows the picker again: a switch with
// real input keeps the same entry and timer, the aircraft drawn, and the
// source polled on its timer.
await page.evaluate(() => {
window.__handoff.ticks = 0;
window.__handoff.pollCount = window.__flightPolls;
window.__handoff.timer = window.__godsEyeView.dataManager.layers.get(
window.__qaAircraft.layerId,
).intervalId;
});
await strict.pick(page, 'navara');
await ready('navara');
const after = await page.evaluate(async () => {
const { dataManager } = window.__godsEyeView;
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
const every =
Number(entry.module.refreshInterval) ||
Number(entry.module.updateInterval);
const deadline = Date.now() + 2.5 * every + 5000;
while (window.__handoff.ticks < 1 && Date.now() < deadline)
await new Promise((resolve) => setTimeout(resolve, 250));
await new Promise((resolve) => setTimeout(resolve, 1500));
return {
same:
dataManager === window.__handoff.dataManager &&
entry === window.__handoff.entry,
timer: entry.intervalId === window.__handoff.timer,
drawn: entry.module
.qaDrawn()
.some((row) => row.id === window.__qaAircraft.aircraftId),
ticks: window.__handoff.ticks,
polls: window.__flightPolls - window.__handoff.pollCount,
};
});
await strict.check(
page,
layerId,
'picker switch keeps the entry, timer and drawn aircraft; polling continues',
after.same &&
after.timer &&
after.drawn &&
after.ticks >= 1 &&
after.polls === after.ticks,
after,
);
assert.deepEqual(errors, []);
});
+10 -26
View File
@@ -1,19 +1,18 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic'; base.search = '?engine=navara&welcome=0&look=cinematic';
base.hash = base.hash =
'v=2&lat=41.5340&lon=-90.5070&alt=20000&heading=0&pitch=-90&roll=0&map=sentinel2&l='; 'v=2&lat=41.5340&lon=-90.5070&alt=20000&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
const strict = createStrict({ origin: base.origin, layers: ['local-adsb'] }); const strict = createStrict({ origin: base.origin, layers: ['local-adsb'] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b);
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await p.evaluateOnNewDocument(() => { await p.evaluateOnNewDocument(() => {
const raw = window.fetch; const raw = window.fetch;
window.fetch = function (input, ...args) { window.fetch = function (input, ...args) {
@@ -53,21 +52,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
); );
}); });
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => { await waitForEngine(p, 'navara');
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await p.evaluate(async () => { await p.evaluate(async () => {
const { dataManager, tileset } = window.__godsEyeView; const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__) if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -107,8 +92,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await p.evaluate(() => { await p.evaluate(() => {
window.qaSelections = []; window.qaSelections = [];
}); });
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
await p.waitForFunction(() => await p.waitForFunction(() =>
window.qaSelections.includes('local-adsb:abc123'), window.qaSelections.includes('local-adsb:abc123'),
); );
+7 -20
View File
@@ -9,7 +9,11 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import fs from 'node:fs'; import fs from 'node:fs';
import sharp from 'sharp'; import sharp from 'sharp';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
waitForEngine,
} from './qa-strict-harness.mjs';
const LAT = 37.7749; const LAT = 37.7749;
const LON = -122.4194; const LON = -122.4194;
@@ -31,8 +35,6 @@ const RING = [
await strict.run( await strict.run(
{ {
args: [ args: [
'--use-angle=metal',
'--enable-gpu',
'--disable-background-timer-throttling', '--disable-background-timer-throttling',
'--disable-renderer-backgrounding', '--disable-renderer-backgrounding',
], ],
@@ -43,21 +45,7 @@ await strict.run(
const errors = []; const errors = [];
page.on('pageerror', (error) => errors.push(error.message)); page.on('pageerror', (error) => errors.push(error.message));
await page.goto(base.href); await page.goto(base.href);
const ready = async (kind) => { await waitForEngine(page, 'navara');
await page.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 120000 },
kind,
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
assert.equal( assert.equal(
await page.evaluate(() => await page.evaluate(() =>
Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__), Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__),
@@ -172,8 +160,7 @@ await strict.run(
if ( if (
(await page.evaluate(() => window.__godsEyeView.engine.kind)) !== kind (await page.evaluate(() => window.__godsEyeView.engine.kind)) !== kind
) { ) {
await strict.pick(page, kind); await strict.switchTo(page, kind);
await ready(kind);
} }
await wait(2500); await wait(2500);
const state = await page.evaluate(() => ({ const state = await page.evaluate(() => ({
+140 -143
View File
@@ -1,4 +1,4 @@
// The app's Bloom on Navara (Stage 6 B2): the Display bloom glows the // The app's Bloom on Navara: the Display bloom glows the
// Navara frame (brighter, more at a higher slider), it sits after the // Navara frame (brighter, more at a higher slider), it sits after the
// overlays and before the visual styles, Cesium's dead zone draws nothing, // overlays and before the visual styles, Cesium's dead zone draws nothing,
// turning it off restores the frame exactly, and on Cesium the scene's own // turning it off restores the frame exactly, and on Cesium the scene's own
@@ -53,153 +53,150 @@ const setBloom = (page, enabled, intensityPct) =>
intensityPct, intensityPct,
); );
await strict.run( await strict.run({}, async (browser) => {
{ args: ['--use-angle=metal', '--enable-gpu'] }, const page = await browser.newPage();
async (browser) => { const errors = [];
const page = await browser.newPage(); page.on('pageerror', (error) => errors.push(error.message));
const errors = []; // The same sun on every run: the frame's brightness decides what blooms
page.on('pageerror', (error) => errors.push(error.message)); // and what each style's signature reads.
// The same sun on every run: the frame's brightness decides what blooms await pinDaytime(page);
// and what each style's signature reads. await page.evaluateOnNewDocument(() => {
await pinDaytime(page); localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
await page.evaluateOnNewDocument(() => { });
localStorage.setItem('gev:first-run-mission:v1', 'suppressed'); await page.setViewport({ width: 1280, height: 800 });
}); await page.goto(visualPageUrl(base.origin, 'navara'), {
await page.setViewport({ width: 1280, height: 800 }); waitUntil: 'domcontentloaded',
await page.goto(visualPageUrl(base.origin, 'navara'), { });
waitUntil: 'domcontentloaded', await waitForEngine(page, 'navara');
}); await freeze(page);
await waitForEngine(page, 'navara');
await freeze(page);
const initial = await page.evaluate(() => { const initial = await page.evaluate(() => {
const { engine } = window.__godsEyeView; const { engine } = window.__godsEyeView;
return { bloom: engine.render.bloom(), pipeline: engine.pipeline() }; return { bloom: engine.render.bloom(), pipeline: engine.pipeline() };
}); });
check( check(
'Navara blooms itself and starts with bloom off', 'Navara blooms itself and starts with bloom off',
initial.bloom.enabled === false && !initial.pipeline?.bloomShown, initial.bloom.enabled === false && !initial.pipeline?.bloomShown,
initial, initial,
); );
const normal = await capture(page); const normal = await capture(page);
save('normal', normal); save('normal', normal);
const normalStats = stats(normal); const normalStats = stats(normal);
const full = await setBloom(page, true, 200); const full = await setBloom(page, true, 200);
check( check(
'the slider at 200 reaches the engine at full strength', 'the slider at 200 reaches the engine at full strength',
full.engine.enabled === true && full.engine.enabled === true &&
full.engine.strength === 1 && full.engine.strength === 1 &&
full.pipeline.bloomShown === true, full.pipeline.bloomShown === true,
full, full,
); );
const order = full.pipeline.passes ?? []; const order = full.pipeline.passes ?? [];
const at = order.indexOf('gevBloom'); const at = order.indexOf('gevBloom');
check( check(
'bloom runs after the scene and overlays, just before the final copy', 'bloom runs after the scene and overlays, just before the final copy',
at >= 0 && at >= 0 &&
order[at + 1] === 'final' && order[at + 1] === 'final' &&
['toneMapping', 'gevSharpen', 'gevOverlay', 'smaa', 'fxaa'].every( ['toneMapping', 'gevSharpen', 'gevOverlay', 'smaa', 'fxaa'].every(
(key) => !order.includes(key) || order.indexOf(key) < at, (key) => !order.includes(key) || order.indexOf(key) < at,
), ),
order, order,
); );
const glowFull = await capture(page); const glowFull = await capture(page);
save('bloom-200', glowFull); save('bloom-200', glowFull);
const fullChange = diff(normal, glowFull); const fullChange = diff(normal, glowFull);
const fullStats = stats(glowFull); const fullStats = stats(glowFull);
check( check(
'bloom at 200 glows the frame brighter', 'bloom at 200 glows the frame brighter',
fullChange.mean > 3 && luma(fullStats) > luma(normalStats) + 2, fullChange.mean > 3 && luma(fullStats) > luma(normalStats) + 2,
{ {
...fullChange, ...fullChange,
normalLuma: luma(normalStats), normalLuma: luma(normalStats),
bloomLuma: luma(fullStats), bloomLuma: luma(fullStats),
}, },
); );
await setBloom(page, true, 100); await setBloom(page, true, 100);
const glowHalf = await capture(page); const glowHalf = await capture(page);
save('bloom-100', glowHalf); save('bloom-100', glowHalf);
const halfChange = diff(normal, glowHalf); const halfChange = diff(normal, glowHalf);
check( check(
'a lower slider glows less', 'a lower slider glows less',
halfChange.mean > 0.5 && halfChange.mean < fullChange.mean, halfChange.mean > 0.5 && halfChange.mean < fullChange.mean,
{ half: halfChange.mean, full: fullChange.mean }, { half: halfChange.mean, full: fullChange.mean },
); );
const dead = await setBloom(page, true, 10); const dead = await setBloom(page, true, 10);
const deadFrame = await capture(page); const deadFrame = await capture(page);
check( check(
"Cesium's dead zone: the toggle on, nothing glows", "Cesium's dead zone: the toggle on, nothing glows",
dead.engine.enabled === false && dead.engine.enabled === false &&
dead.pipeline.bloomShown === false && dead.pipeline.bloomShown === false &&
diff(normal, deadFrame).mean === 0, diff(normal, deadFrame).mean === 0,
{ engine: dead.engine, diff: diff(normal, deadFrame) }, { engine: dead.engine, diff: diff(normal, deadFrame) },
); );
// With a style: bloom first, then the style, then the final copy. // With a style: bloom first, then the style, then the final copy.
await setBloom(page, true, 200); await setBloom(page, true, 200);
const styled = await pickStyle(page, 'noir', 'normal'); const styled = await pickStyle(page, 'noir', 'normal');
const styledOrder = styled.pipeline?.passes ?? []; const styledOrder = styled.pipeline?.passes ?? [];
check( check(
'bloom runs before the visual styles', 'bloom runs before the visual styles',
styledOrder.indexOf('gevBloom') >= 0 && styledOrder.indexOf('gevBloom') >= 0 &&
styledOrder.indexOf('gevBloom') + 1 === styledOrder.indexOf('gevBloom') + 1 ===
styledOrder.indexOf('gevStyles') && styledOrder.indexOf('gevStyles') &&
styledOrder.indexOf('gevStyles') + 1 === styledOrder.indexOf('final'), styledOrder.indexOf('gevStyles') + 1 === styledOrder.indexOf('final'),
styledOrder, styledOrder,
); );
await pickStyle(page, 'normal', 'noir'); await pickStyle(page, 'normal', 'noir');
const off = await setBloom(page, false, 200); const off = await setBloom(page, false, 200);
const offFrame = await capture(page); const offFrame = await capture(page);
check( check(
'turning bloom off restores the frame', 'turning bloom off restores the frame',
off.engine.enabled === false && off.engine.enabled === false &&
off.pipeline.bloomShown === false && off.pipeline.bloomShown === false &&
diff(normal, offFrame).mean === 0, diff(normal, offFrame).mean === 0,
{ engine: off.engine, diff: diff(normal, offFrame) }, { engine: off.engine, diff: diff(normal, offFrame) },
); );
// Cesium keeps its scene bloom stage; its engine has no bloom. // Cesium keeps its scene bloom stage; its engine has no bloom.
await strict.switchTo(page, 'cesium'); await strict.switchTo(page, 'cesium');
await freeze(page); await freeze(page);
await setBloom(page, false, 200); await setBloom(page, false, 200);
const cesiumNormal = await capture(page); const cesiumNormal = await capture(page);
const cesium = await setBloom(page, true, 200); const cesium = await setBloom(page, true, 200);
const cesiumStage = await page.evaluate(() => { const cesiumStage = await page.evaluate(() => {
const a = window.__godsEyeView; const a = window.__godsEyeView;
const stage = a.viewer?.scene?.postProcessStages?.bloom; const stage = a.viewer?.scene?.postProcessStages?.bloom;
return { return {
setBloom: typeof a.engine.render.setBloom, setBloom: typeof a.engine.render.setBloom,
enabled: stage?.enabled ?? null, enabled: stage?.enabled ?? null,
contrast: stage?.uniforms?.contrast ?? null, contrast: stage?.uniforms?.contrast ?? null,
}; };
}); });
const cesiumGlow = await capture(page); const cesiumGlow = await capture(page);
save('cesium-bloom-200', cesiumGlow); save('cesium-bloom-200', cesiumGlow);
check( check(
"Cesium: its scene's bloom stage glows, the engine draws none", "Cesium: its scene's bloom stage glows, the engine draws none",
cesium.engine === null && cesium.engine === null &&
cesiumStage.setBloom === 'undefined' && cesiumStage.setBloom === 'undefined' &&
cesiumStage.enabled !== false && cesiumStage.enabled !== false &&
diff(cesiumNormal, cesiumGlow).mean > 1, diff(cesiumNormal, cesiumGlow).mean > 1,
{ stage: cesiumStage, diff: diff(cesiumNormal, cesiumGlow) }, { stage: cesiumStage, diff: diff(cesiumNormal, cesiumGlow) },
); );
await setBloom(page, false, 200); await setBloom(page, false, 200);
const cesiumBack = diff(cesiumNormal, await capture(page)); const cesiumBack = diff(cesiumNormal, await capture(page));
check( check(
'Cesium: bloom off restores its frame', 'Cesium: bloom off restores its frame',
cesiumBack.mean < 0.5, cesiumBack.mean < 0.5,
cesiumBack, cesiumBack,
); );
check('no page errors', errors.length === 0, errors); check('no page errors', errors.length === 0, errors);
if (outDir) if (outDir)
fs.writeFileSync( fs.writeFileSync(
path.join(outDir, 'bloom.json'), path.join(outDir, 'bloom.json'),
JSON.stringify(report, null, 2), JSON.stringify(report, null, 2),
); );
}, });
);
+137 -142
View File
@@ -1,4 +1,4 @@
// The cockpit weather clouds on Navara (Stage 6 C3): in cockpit mode with // The cockpit weather clouds on Navara: in cockpit mode with
// the cockpit weather on, Navara's own volumetric clouds take their cover // the cockpit weather on, Navara's own volumetric clouds take their cover
// from the weather under the camera (clear: none; overcast: a heavy deck), // from the weather under the camera (clear: none; overcast: a heavy deck),
// with no second canvas or WebGL context; leaving the cockpit gives the look // with no second canvas or WebGL context; leaving the cockpit gives the look
@@ -101,152 +101,147 @@ const coverage = (page) =>
() => window.__godsEyeView.engine.render.cloudCover?.().coverage, () => window.__godsEyeView.engine.render.cloudCover?.().coverage,
); );
await strict.run( await strict.run({}, async (browser) => {
{ args: ['--use-angle=metal', '--enable-gpu'] }, const page = await browser.newPage();
async (browser) => { const errors = [];
const page = await browser.newPage(); page.on('pageerror', (error) => errors.push(error.message));
const errors = []; await page.evaluateOnNewDocument((utc) => {
page.on('pageerror', (error) => errors.push(error.message)); localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
await page.evaluateOnNewDocument((utc) => { const offset = Date.parse(utc) - Date.now();
localStorage.setItem('gev:first-run-mission:v1', 'suppressed'); const RealDate = Date;
const offset = Date.parse(utc) - Date.now(); class ShiftedDate extends RealDate {
const RealDate = Date; constructor(...args) {
class ShiftedDate extends RealDate { if (args.length === 0) super(RealDate.now() + offset);
constructor(...args) { else super(...args);
if (args.length === 0) super(RealDate.now() + offset);
else super(...args);
}
static now() {
return RealDate.now() + offset;
}
} }
globalThis.Date = ShiftedDate; static now() {
}, MIDDAY_UTC); return RealDate.now() + offset;
await page.setViewport({ width: 1280, height: 800 }); }
await page.goto(visualPageUrl(base.origin, 'navara', '', LOW_VIEW), { }
waitUntil: 'domcontentloaded', globalThis.Date = ShiftedDate;
}); }, MIDDAY_UTC);
await waitForEngine(page, 'navara'); await page.setViewport({ width: 1280, height: 800 });
await freeze(page); await page.goto(visualPageUrl(base.origin, 'navara', '', LOW_VIEW), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
await freeze(page);
const setup = await page.evaluate(() => { const setup = await page.evaluate(() => {
const a = window.__godsEyeView; const a = window.__godsEyeView;
return { return {
cover: a.engine.render.cloudCover?.() ?? null, cover: a.engine.render.cloudCover?.() ?? null,
canvas: Boolean(document.getElementById('cockpit-cloud-effects')),
canvases: document.querySelectorAll('canvas').length,
};
});
check(
'Navara takes the cockpit cloud cover itself, no cloud canvas',
setup.cover?.coverage === null && !setup.canvas,
setup,
);
const lookOwn = await converged(page, 'look-own');
await weatherToggle(page, true);
await cockpit(page, true);
await page.waitForFunction(
() => window.__godsEyeView.engine.render.cloudCover().coverage === 0,
{ timeout: 10000, polling: 100 },
);
check('clear weather in the cockpit: no cloud cover', asked.length >= 1, {
requests: asked.length,
});
const clear = await converged(page, 'cockpit-clear');
// Overcast 30 km east: the movement makes the weather stale.
weather = WEATHER.overcast;
await page.evaluate(() => {
const { engine } = window.__godsEyeView;
const place = engine.camera.pose().cartographic;
const east =
place.longitude + 30000 / (6378137 * Math.cos(place.latitude));
const { sin, cos } = Math;
// WGS84 Earth-fixed position of (east, latitude, height).
const a = 6378137;
const e2 = 0.00669437999014;
const n = a / Math.sqrt(1 - e2 * sin(place.latitude) ** 2);
const h = place.height;
engine.camera.setView({
destination: {
x: (n + h) * cos(place.latitude) * cos(east),
y: (n + h) * cos(place.latitude) * sin(east),
z: (n * (1 - e2) + h) * sin(place.latitude),
},
orientation: { heading: Math.PI / 2, pitch: -0.14, roll: 0 },
});
});
await page.waitForFunction(
() => window.__godsEyeView.engine.render.cloudCover().coverage > 0.4,
{ timeout: 15000, polling: 100 },
);
const overcastCover = await coverage(page);
const overcast = await converged(page, 'cockpit-overcast');
const greyed = {
lookOwn: saturation(lookOwn),
clear: saturation(clear),
overcast: saturation(overcast),
clearToOvercast: diff(clear, overcast).mean,
};
check(
'overcast weather covers the view with Navara clouds, clear leaves it open',
overcastCover > 0.4 &&
greyed.overcast < greyed.clear * 0.6 &&
greyed.lookOwn < greyed.clear &&
greyed.clearToOvercast > 15,
{ overcastCover, greyed, requests: asked.length },
);
const contexts = await page.evaluate(() => ({
canvas: Boolean(document.getElementById('cockpit-cloud-effects')), canvas: Boolean(document.getElementById('cockpit-cloud-effects')),
canvases: document.querySelectorAll('canvas').length, canvases: document.querySelectorAll('canvas').length,
})); };
check( });
'no second canvas or WebGL context in the cockpit', check(
!contexts.canvas && contexts.canvases === setup.canvases, 'Navara takes the cockpit cloud cover itself, no cloud canvas',
{ before: setup.canvases, after: contexts }, setup.cover?.coverage === null && !setup.canvas,
); setup,
);
const lookOwn = await converged(page, 'look-own');
await cockpit(page, false); await weatherToggle(page, true);
check( await cockpit(page, true);
'leaving the cockpit gives the look its own clouds back', await page.waitForFunction(
(await coverage(page)) === null, () => window.__godsEyeView.engine.render.cloudCover().coverage === 0,
); { timeout: 10000, polling: 100 },
);
check('clear weather in the cockpit: no cloud cover', asked.length >= 1, {
requests: asked.length,
});
const clear = await converged(page, 'cockpit-clear');
// Cesium keeps its own cockpit cloud canvas; its engine draws none. // Overcast 30 km east: the movement makes the weather stale.
await strict.switchTo(page, 'cesium'); weather = WEATHER.overcast;
await freeze(page); await page.evaluate(() => {
weather = WEATHER.overcast; const { engine } = window.__godsEyeView;
await cockpit(page, true); const place = engine.camera.pose().cartographic;
await page.waitForFunction( const east = place.longitude + 30000 / (6378137 * Math.cos(place.latitude));
() => const { sin, cos } = Math;
Number( // WGS84 Earth-fixed position of (east, latitude, height).
document.getElementById('cockpit-cloud-effects')?.dataset const a = 6378137;
.cloudStrength, const e2 = 0.00669437999014;
) > 0.3, const n = a / Math.sqrt(1 - e2 * sin(place.latitude) ** 2);
{ timeout: 15000, polling: 100 }, const h = place.height;
); engine.camera.setView({
const cesium = await page.evaluate(() => { destination: {
const canvas = document.getElementById('cockpit-cloud-effects'); x: (n + h) * cos(place.latitude) * cos(east),
return { y: (n + h) * cos(place.latitude) * sin(east),
setCloudCover: typeof window.__godsEyeView.engine.render.setCloudCover, z: (n * (1 - e2) + h) * sin(place.latitude),
strength: Number(canvas.dataset.cloudStrength), },
cockpit: canvas.dataset.cockpit, orientation: { heading: Math.PI / 2, pitch: -0.14, roll: 0 },
};
}); });
check( });
'Cesium: the cockpit draws its own cloud canvas, the engine none', await page.waitForFunction(
cesium.setCloudCover === 'undefined' && cesium.cockpit === 'true', () => window.__godsEyeView.engine.render.cloudCover().coverage > 0.4,
cesium, { timeout: 15000, polling: 100 },
); );
await cockpit(page, false); const overcastCover = await coverage(page);
await weatherToggle(page, false); const overcast = await converged(page, 'cockpit-overcast');
const greyed = {
lookOwn: saturation(lookOwn),
clear: saturation(clear),
overcast: saturation(overcast),
clearToOvercast: diff(clear, overcast).mean,
};
check(
'overcast weather covers the view with Navara clouds, clear leaves it open',
overcastCover > 0.4 &&
greyed.overcast < greyed.clear * 0.6 &&
greyed.lookOwn < greyed.clear &&
greyed.clearToOvercast > 15,
{ overcastCover, greyed, requests: asked.length },
);
const contexts = await page.evaluate(() => ({
canvas: Boolean(document.getElementById('cockpit-cloud-effects')),
canvases: document.querySelectorAll('canvas').length,
}));
check(
'no second canvas or WebGL context in the cockpit',
!contexts.canvas && contexts.canvases === setup.canvases,
{ before: setup.canvases, after: contexts },
);
check('no page errors', errors.length === 0, errors); await cockpit(page, false);
if (outDir) check(
fs.writeFileSync( 'leaving the cockpit gives the look its own clouds back',
path.join(outDir, 'cockpit-clouds.json'), (await coverage(page)) === null,
JSON.stringify(report, null, 2), );
);
}, // Cesium keeps its own cockpit cloud canvas; its engine draws none.
); await strict.switchTo(page, 'cesium');
await freeze(page);
weather = WEATHER.overcast;
await cockpit(page, true);
await page.waitForFunction(
() =>
Number(
document.getElementById('cockpit-cloud-effects')?.dataset.cloudStrength,
) > 0.3,
{ timeout: 15000, polling: 100 },
);
const cesium = await page.evaluate(() => {
const canvas = document.getElementById('cockpit-cloud-effects');
return {
setCloudCover: typeof window.__godsEyeView.engine.render.setCloudCover,
strength: Number(canvas.dataset.cloudStrength),
cockpit: canvas.dataset.cockpit,
};
});
check(
'Cesium: the cockpit draws its own cloud canvas, the engine none',
cesium.setCloudCover === 'undefined' && cesium.cockpit === 'true',
cesium,
);
await cockpit(page, false);
await weatherToggle(page, false);
check('no page errors', errors.length === 0, errors);
if (outDir)
fs.writeFileSync(
path.join(outDir, 'cockpit-clouds.json'),
JSON.stringify(report, null, 2),
);
});
-2
View File
@@ -32,8 +32,6 @@ const AREA = [
await strict.run( await strict.run(
{ {
args: [ args: [
'--use-angle=metal',
'--enable-gpu',
'--disable-background-timer-throttling', '--disable-background-timer-throttling',
'--disable-renderer-backgrounding', '--disable-renderer-backgrounding',
], ],
-2
View File
@@ -136,8 +136,6 @@ const FILES = [
await strict.run( await strict.run(
{ {
args: [ args: [
'--use-angle=metal',
'--enable-gpu',
'--disable-background-timer-throttling', '--disable-background-timer-throttling',
'--disable-renderer-backgrounding', '--disable-renderer-backgrounding',
], ],
+7 -20
View File
@@ -10,7 +10,11 @@ import assert from 'node:assert/strict';
import fs from 'node:fs'; import fs from 'node:fs';
import sharp from 'sharp'; import sharp from 'sharp';
import { fromDegrees } from '../src/geo/cartesian3.js'; import { fromDegrees } from '../src/geo/cartesian3.js';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0'; base.search = '?engine=navara&welcome=0';
@@ -31,8 +35,6 @@ const EDGE = { x: 1250, y: 560, width: 26, height: 90 };
await strict.run( await strict.run(
{ {
args: [ args: [
'--use-angle=metal',
'--enable-gpu',
'--disable-background-timer-throttling', '--disable-background-timer-throttling',
'--disable-renderer-backgrounding', '--disable-renderer-backgrounding',
], ],
@@ -43,21 +45,7 @@ await strict.run(
const errors = []; const errors = [];
page.on('pageerror', (error) => errors.push(error.message)); page.on('pageerror', (error) => errors.push(error.message));
await page.goto(base.href); await page.goto(base.href);
const ready = async (kind) => { await waitForEngine(page, 'navara');
await page.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 120000 },
kind,
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
assert.equal( assert.equal(
await page.evaluate(() => await page.evaluate(() =>
Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__), Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__),
@@ -92,8 +80,7 @@ await strict.run(
if ( if (
(await page.evaluate(() => window.__godsEyeView.engine.kind)) !== kind (await page.evaluate(() => window.__godsEyeView.engine.kind)) !== kind
) { ) {
await strict.pick(page, kind); await strict.switchTo(page, kind);
await ready(kind);
} }
await wait(2500); await wait(2500);
const mask = await page.evaluate(() => { const mask = await page.evaluate(() => {
+175 -179
View File
@@ -1,4 +1,4 @@
// The Cyber HUD sonar on Navara (Stage 6 C2): with the Cyber HUD on, the // The Cyber HUD sonar on Navara: with the Cyber HUD on, the
// sweep lights the contacts it passes and dims the rest to the opacity // sweep lights the contacts it passes and dims the rest to the opacity
// floor, by each contact's place on screen exactly as the CPU sampler of // floor, by each contact's place on screen exactly as the CPU sampler of
// src/cyberSonar.js rates it; the sweep turns; with the sweep off every // src/cyberSonar.js rates it; the sweep turns; with the sweep off every
@@ -98,193 +98,189 @@ const framesIn = (page, ms) =>
const mean = (values) => const mean = (values) =>
values.reduce((sum, value) => sum + value, 0) / Math.max(1, values.length); values.reduce((sum, value) => sum + value, 0) / Math.max(1, values.length);
await strict.run( await strict.run({}, async (browser) => {
{ args: ['--use-angle=metal', '--enable-gpu'] }, const page = await browser.newPage();
async (browser) => { const errors = [];
const page = await browser.newPage(); page.on('pageerror', (error) => errors.push(error.message));
const errors = []; await page.evaluateOnNewDocument(() => {
page.on('pageerror', (error) => errors.push(error.message)); localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
await page.evaluateOnNewDocument(() => { });
localStorage.setItem('gev:first-run-mission:v1', 'suppressed'); await page.setViewport({ width: WIDTH, height: HEIGHT });
await page.goto(visualPageUrl(base.origin, 'navara'), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
await freeze(page);
const places = contactPlaces();
const placed = await page.evaluate((places) => {
const { engine } = window.__godsEyeView;
const set = engine.movers.createSet({
id: 'qa-sonar-contacts',
kind: 'point',
capacity: places.length,
depthTest: 'always',
point: { sizePx: 14, outlinePx: 0 },
}); });
await page.setViewport({ width: WIDTH, height: HEIGHT }); window.__qaSonarSet = set;
await page.goto(visualPageUrl(base.origin, 'navara'), { let missing = 0;
waitUntil: 'domcontentloaded', places.forEach((place, index) => {
const position = engine.camera.pickEllipsoid(place);
if (!position) missing++;
set.set(index, { position, color: [1, 1, 1, 1] });
}); });
await waitForEngine(page, 'navara'); set.setCount(places.length);
await freeze(page); set.commit();
return { missing, sonar: engine.render.contactSonar?.() ?? null };
}, places);
check(
'Navara treats contacts itself; the sonar starts off',
placed.missing === 0 && placed.sonar?.enabled === false,
placed,
);
const baseline = await read(page, places);
const places = contactPlaces(); await page.evaluate(() => {
const placed = await page.evaluate((places) => { const { styleManager } = window.__godsEyeView;
const { engine } = window.__godsEyeView; styleManager._setHudVariant('cyber');
const set = engine.movers.createSet({ // Normal keeps the contacts' own colours; the HUD stays on (in Normal
id: 'qa-sonar-contacts', // it would hide itself, and the sweep with it).
kind: 'point', styleManager.setStyle('normal');
capacity: places.length, styleManager.hud.setMode('on');
depthTest: 'always', styleManager._setCyberSonarEnabled(true);
point: { sizePx: 14, outlinePx: 0 }, styleManager._setCyberSonarSetting('opacity', 35);
}); });
window.__qaSonarSet = set; await page.waitForFunction(
let missing = 0; () => window.__godsEyeView.styleManager.transitions.size === 0,
places.forEach((place, index) => { );
const position = engine.camera.pickEllipsoid(place); const theme = await page.evaluate(() => ({
if (!position) missing++; theme: document.documentElement.dataset.uiTheme,
set.set(index, { position, color: [1, 1, 1, 1] }); gpu: document.documentElement.dataset.cyberSonarGpu,
}); hud: document.getElementById('intel-hud')?.classList.contains('active'),
set.setCount(places.length); }));
set.commit(); check(
return { missing, sonar: engine.render.contactSonar?.() ?? null }; 'the Cyber HUD runs with the contact adapter',
}, places); theme.theme === 'cyber' && theme.gpu === 'supported' && theme.hud,
check( theme,
'Navara treats contacts itself; the sonar starts off', );
placed.missing === 0 && placed.sonar?.enabled === false,
placed, const sweeps = [];
for (const wait of [0, 1300, 1300]) {
await new Promise((resolve) => setTimeout(resolve, wait));
const { luma, sonar } = await read(page, places);
const expected = places.map(({ x, y }) =>
sonarFactorAt(x, y, WIDTH, HEIGHT, sonar),
); );
const baseline = await read(page, places); const lit = luma.filter((_, i) => expected[i] > 0.9);
const dim = luma.filter((_, i) => expected[i] <= sonar.opacity / 100);
await page.evaluate(() => { sweeps.push({
const { styleManager } = window.__godsEyeView; spread: Math.max(...luma.slice(0, 32)) - Math.min(...luma.slice(0, 32)),
styleManager._setHudVariant('cyber'); angle: sonar.angleDeg,
// Normal keeps the contacts' own colours; the HUD stays on (in Normal lit: lit.length,
// it would hide itself, and the sweep with it). litLuma: mean(lit),
styleManager.setStyle('normal'); dimLuma: mean(dim),
styleManager.hud.setMode('on'); brightest: luma.indexOf(Math.max(...luma)),
styleManager._setCyberSonarEnabled(true); expectedBrightest: expected.indexOf(Math.max(...expected)),
styleManager._setCyberSonarSetting('opacity', 35); sonar,
}); });
await page.waitForFunction(
() => window.__godsEyeView.styleManager.transitions.size === 0,
);
const theme = await page.evaluate(() => ({
theme: document.documentElement.dataset.uiTheme,
gpu: document.documentElement.dataset.cyberSonarGpu,
hud: document.getElementById('intel-hud')?.classList.contains('active'),
}));
check( check(
'the Cyber HUD runs with the contact adapter', `the sweep at ${Math.round(sonar.angleDeg)}° lights the contacts it passes, the rest sit at the floor`,
theme.theme === 'cyber' && theme.gpu === 'supported' && theme.hud, sonar.enabled &&
theme, sonar.active &&
sonar.opacity === 35 &&
lit.length >= 1 &&
dim.length >= 8 &&
mean(lit) > mean(dim) + 20 &&
expected[places.length - 1] === 0.35,
sweeps.at(-1),
); );
}
check(
'the sweep turns: another contact is lit',
new Set(sweeps.map((s) => s.expectedBrightest)).size > 1 &&
new Set(sweeps.map((s) => s.brightest)).size > 1,
sweeps.map(({ angle, brightest, expectedBrightest }) => ({
angle,
brightest,
expectedBrightest,
})),
);
const cadence = await framesIn(page, 1500);
check(
'a parked scene draws at the fleet cadence while the sweep runs over contacts',
cadence >= 8,
{ frames: cadence },
);
const sweeps = []; await page.evaluate(() =>
for (const wait of [0, 1300, 1300]) { window.__godsEyeView.styleManager._setCyberSonarEnabled(false),
await new Promise((resolve) => setTimeout(resolve, wait)); );
const { luma, sonar } = await read(page, places); const neutral = await read(page, places);
const expected = places.map(({ x, y }) => const spread =
sonarFactorAt(x, y, WIDTH, HEIGHT, sonar), Math.max(...neutral.luma.slice(0, 32)) -
); Math.min(...neutral.luma.slice(0, 32));
const lit = luma.filter((_, i) => expected[i] > 0.9); await new Promise((resolve) => setTimeout(resolve, 300));
const dim = luma.filter((_, i) => expected[i] <= sonar.opacity / 100); const idle = await framesIn(page, 1500);
sweeps.push({ check(
spread: Math.max(...luma.slice(0, 32)) - Math.min(...luma.slice(0, 32)), 'with the sweep off every contact takes the neutral factor and the scene rests',
angle: sonar.angleDeg, neutral.sonar.enabled &&
lit: lit.length, !neutral.sonar.active &&
litLuma: mean(lit), // What still differs is the map under the translucent contacts.
dimLuma: mean(dim), spread < Math.min(...sweeps.map((sweep) => sweep.spread)) * 0.6 &&
brightest: luma.indexOf(Math.max(...luma)), idle <= 3,
expectedBrightest: expected.indexOf(Math.max(...expected)), { spread, sweeping: sweeps.map((sweep) => sweep.spread), frames: idle },
sonar, );
});
check(
`the sweep at ${Math.round(sonar.angleDeg)}° lights the contacts it passes, the rest sit at the floor`,
sonar.enabled &&
sonar.active &&
sonar.opacity === 35 &&
lit.length >= 1 &&
dim.length >= 8 &&
mean(lit) > mean(dim) + 20 &&
expected[places.length - 1] === 0.35,
sweeps.at(-1),
);
}
check(
'the sweep turns: another contact is lit',
new Set(sweeps.map((s) => s.expectedBrightest)).size > 1 &&
new Set(sweeps.map((s) => s.brightest)).size > 1,
sweeps.map(({ angle, brightest, expectedBrightest }) => ({
angle,
brightest,
expectedBrightest,
})),
);
const cadence = await framesIn(page, 1500);
check(
'a parked scene draws at the fleet cadence while the sweep runs over contacts',
cadence >= 8,
{ frames: cadence },
);
await page.evaluate(() => await page.evaluate(() => {
window.__godsEyeView.styleManager._setCyberSonarEnabled(false), const { styleManager } = window.__godsEyeView;
); styleManager._setCyberSonarEnabled(true);
const neutral = await read(page, places); styleManager._setHudVariant('tactical');
const spread = styleManager.setStyle('normal');
Math.max(...neutral.luma.slice(0, 32)) - styleManager.hud.setMode('auto');
Math.min(...neutral.luma.slice(0, 32)); });
await new Promise((resolve) => setTimeout(resolve, 300)); await page.waitForFunction(
const idle = await framesIn(page, 1500); () => window.__godsEyeView.styleManager.transitions.size === 0,
check( );
'with the sweep off every contact takes the neutral factor and the scene rests', const after = await read(page, places);
neutral.sonar.enabled && const drift = Math.max(
!neutral.sonar.active && ...after.luma.map((value, i) => Math.abs(value - baseline.luma[i])),
// What still differs is the map under the translucent contacts. );
spread < Math.min(...sweeps.map((sweep) => sweep.spread)) * 0.6 && check(
idle <= 3, 'leaving the Cyber HUD gives the contacts back unchanged',
{ spread, sweeping: sweeps.map((sweep) => sweep.spread), frames: idle }, after.sonar.enabled === false && drift < 3,
); { drift },
);
await page.evaluate(() => window.__qaSonarSet.destroy());
await page.evaluate(() => { // Cesium keeps its command adapter; its engine draws no sonar.
const { styleManager } = window.__godsEyeView; await strict.switchTo(page, 'cesium');
styleManager._setCyberSonarEnabled(true); await page.evaluate(() => {
styleManager._setHudVariant('tactical'); const { styleManager } = window.__godsEyeView;
styleManager.setStyle('normal'); styleManager._setHudVariant('cyber');
styleManager.hud.setMode('auto'); });
}); await page.waitForFunction(
await page.waitForFunction( () => document.documentElement.dataset.cyberSonarGpu,
() => window.__godsEyeView.styleManager.transitions.size === 0, { timeout: 10000 },
); );
const after = await read(page, places); const cesium = await page.evaluate(() => ({
const drift = Math.max( setContactSonar: typeof window.__godsEyeView.engine.render.setContactSonar,
...after.luma.map((value, i) => Math.abs(value - baseline.luma[i])), gpu: document.documentElement.dataset.cyberSonarGpu,
); }));
check( check(
'leaving the Cyber HUD gives the contacts back unchanged', 'Cesium: the command adapter treats contacts, the engine none',
after.sonar.enabled === false && drift < 3, cesium.setContactSonar === 'undefined' && cesium.gpu === 'supported',
{ drift }, cesium,
); );
await page.evaluate(() => window.__qaSonarSet.destroy()); await page.evaluate(() =>
window.__godsEyeView.styleManager._setHudVariant('tactical'),
);
// Cesium keeps its command adapter; its engine draws no sonar. check('no page errors', errors.length === 0, errors);
await strict.switchTo(page, 'cesium'); if (outDir) {
await page.evaluate(() => { fs.mkdirSync(outDir, { recursive: true });
const { styleManager } = window.__godsEyeView; fs.writeFileSync(
styleManager._setHudVariant('cyber'); path.join(outDir, 'sonar.json'),
}); JSON.stringify(report, null, 2),
await page.waitForFunction(
() => document.documentElement.dataset.cyberSonarGpu,
{ timeout: 10000 },
); );
const cesium = await page.evaluate(() => ({ }
setContactSonar: });
typeof window.__godsEyeView.engine.render.setContactSonar,
gpu: document.documentElement.dataset.cyberSonarGpu,
}));
check(
'Cesium: the command adapter treats contacts, the engine none',
cesium.setContactSonar === 'undefined' && cesium.gpu === 'supported',
cesium,
);
await page.evaluate(() =>
window.__godsEyeView.styleManager._setHudVariant('tactical'),
);
check('no page errors', errors.length === 0, errors);
if (outDir) {
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(
path.join(outDir, 'sonar.json'),
JSON.stringify(report, null, 2),
);
}
},
);
+253 -270
View File
@@ -1,4 +1,4 @@
// The app's visual styles on Navara (Stage 6 B1): each of CRT, NVG, FLIR, // The app's visual styles on Navara: each of CRT, NVG, FLIR,
// Anime and Noir filters the Navara frame (pixels and a style-specific // Anime and Noir filters the Navara frame (pixels and a style-specific
// signature against Normal), switching styles crossfades (both styles drawn // signature against Normal), switching styles crossfades (both styles drawn
// at once mid-way, the fade completes), Normal restores the frame, the // at once mid-way, the fade completes), Normal restores the frame, the
@@ -45,291 +45,274 @@ const check = checker(report, assert);
const save = (name, pixels) => saveCapture(outDir, name, pixels); const save = (name, pixels) => saveCapture(outDir, name, pixels);
await strict.run( await strict.run({}, async (browser) => {
{ args: ['--use-angle=metal', '--enable-gpu'] }, const page = await browser.newPage();
async (browser) => { const errors = [];
const page = await browser.newPage(); page.on('pageerror', (error) => errors.push(error.message));
const errors = []; // The same sun on every run: the frame's brightness decides what blooms
page.on('pageerror', (error) => errors.push(error.message)); // and what each style's signature reads.
// The same sun on every run: the frame's brightness decides what blooms await pinDaytime(page);
// and what each style's signature reads. await page.evaluateOnNewDocument(() => {
await pinDaytime(page); localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
await page.evaluateOnNewDocument(() => { });
localStorage.setItem('gev:first-run-mission:v1', 'suppressed'); await page.setViewport({ width: 1280, height: 800 });
}); await page.goto(pageUrl('navara'), { waitUntil: 'domcontentloaded' });
await page.setViewport({ width: 1280, height: 800 }); await waitForEngine(page, 'navara');
await page.goto(pageUrl('navara'), { waitUntil: 'domcontentloaded' }); await freeze(page);
await waitForEngine(page, 'navara');
await freeze(page);
const capabilities = await page.evaluate(() => { const capabilities = await page.evaluate(() => {
const { engine } = window.__godsEyeView; const { engine } = window.__godsEyeView;
return { return {
styles: engine.render.visualStyles, styles: engine.render.visualStyles,
pipeline: engine.pipeline(), pipeline: engine.pipeline(),
}; };
}); });
check(
'Navara draws every app style itself',
JSON.stringify(capabilities.styles) === JSON.stringify(STYLES),
capabilities,
);
check(
'no style pass shows on Normal',
capabilities.pipeline?.shown === false,
capabilities.pipeline,
);
// The military styles' presets set sharpening and detection (as on
// Cesium); Normal keeps them, so they are put back before comparing.
const display = await page.evaluate(
() => window.__godsEyeView.styleManager.getControlState().sharpen,
);
const normal = await capture(page);
const normalAgain = await capture(page);
const noise = diff(normal, normalAgain);
check('Normal is a still frame to compare against', noise.mean < 0.5, noise);
save('normal', normal);
const normalStats = stats(normal);
let previous = 'normal';
for (const name of STYLES) {
const fade = await pickStyle(page, name, previous);
check( check(
'Navara draws every app style itself', `${name}: the crossfade completes`,
JSON.stringify(capabilities.styles) === JSON.stringify(STYLES), fade.pending === 0 && fade.ms < 1500 && fade.active === name,
capabilities, { ms: fade.ms, pending: fade.pending },
);
const between = fade.samples.filter(
([, next]) => next > 0.02 && next < 0.98,
); );
check( check(
'no style pass shows on Normal', `${name}: the engine fades it in over several frames`,
capabilities.pipeline?.shown === false, between.length >= 1,
capabilities.pipeline, { frames: fade.samples.length, between: between.length },
); );
if (previous !== 'normal') {
// The military styles' presets set sharpening and detection (as on const both = fade.samples.filter(
// Cesium); Normal keeps them, so they are put back before comparing. ([out, next]) => out > 0.05 && out < 0.95 && next > 0.05 && next < 0.95,
const display = await page.evaluate(
() => window.__godsEyeView.styleManager.getControlState().sharpen,
);
const normal = await capture(page);
const normalAgain = await capture(page);
const noise = diff(normal, normalAgain);
check(
'Normal is a still frame to compare against',
noise.mean < 0.5,
noise,
);
save('normal', normal);
const normalStats = stats(normal);
let previous = 'normal';
for (const name of STYLES) {
const fade = await pickStyle(page, name, previous);
check(
`${name}: the crossfade completes`,
fade.pending === 0 && fade.ms < 1500 && fade.active === name,
{ ms: fade.ms, pending: fade.pending },
);
const between = fade.samples.filter(
([, next]) => next > 0.02 && next < 0.98,
); );
check( check(
`${name}: the engine fades it in over several frames`, `${name}: ${previous} and ${name} draw together mid-crossfade`,
between.length >= 1, both.length >= 1,
{ frames: fade.samples.length, between: between.length }, { both: both.length },
); );
if (previous !== 'normal') {
const both = fade.samples.filter(
([out, next]) =>
out > 0.05 && out < 0.95 && next > 0.05 && next < 0.95,
);
check(
`${name}: ${previous} and ${name} draw together mid-crossfade`,
both.length >= 1,
{ both: both.length },
);
}
const others = Object.entries(fade.intensities).filter(
([style, value]) => style !== name && value !== 0,
);
check(
`${name}: alone at full intensity once settled`,
fade.intensities[name] === 1 && others.length === 0,
fade.intensities,
);
const order = fade.pipeline?.passes ?? [];
const at = order.indexOf('gevStyles');
check(
`${name}: the style pass shows, last before the final copy and after the overlays`,
fade.pipeline?.shown === true &&
at >= 0 &&
order[at + 1] === 'final' &&
['toneMapping', 'gevSharpen', 'gevOverlay', 'smaa', 'fxaa'].every(
(key) => !order.includes(key) || order.indexOf(key) < at,
),
fade.pipeline,
);
const pixels = await capture(page);
save(name, pixels);
const styled = stats(pixels);
const change = diff(normal, pixels);
const shift = histogramShift(normalStats, styled);
check(
`${name}: the frame changes against Normal`,
change.mean > 6 && change.changedShare > 0.25,
{ ...change, histogramShift: shift },
);
const signature = {
// Night vision: phosphor green over red and blue.
surveillance: styled.g > styled.r * 1.3 && styled.g > styled.b * 1.3,
// FLIR white-hot (palette 0): grey.
thermal: styled.saturation < normalStats.saturation * 0.5,
// Noir: greyer, with a slight sepia, and vignetted corners.
noir:
styled.saturation < normalStats.saturation &&
styled.vignette < normalStats.vignette * 0.85,
// Anime: cel bands, more saturated.
anime:
styled.saturation > normalStats.saturation * 1.05 || shift > 0.05,
// CRT: pixelated, dithered, scanlined.
retro: shift > 0.05 || change.mean > 12,
}[name];
check(`${name}: the style's own look`, signature, {
normal: {
r: normalStats.r,
g: normalStats.g,
b: normalStats.b,
saturation: normalStats.saturation,
vignette: normalStats.vignette,
},
styled: {
r: styled.r,
g: styled.g,
b: styled.b,
saturation: styled.saturation,
vignette: styled.vignette,
},
histogramShift: shift,
});
previous = name;
} }
const others = Object.entries(fade.intensities).filter(
// A style parameter reaches the Navara pass (FLIR's Ironbow palette). ([style, value]) => style !== name && value !== 0,
await pickStyle(page, 'thermal', previous);
previous = 'thermal';
const grey = await capture(page);
const ironbow = await page.evaluate(() => {
const a = window.__godsEyeView;
a.styleManager.stages.thermal.uniforms.palette = 1;
return a.engine.render.style('thermal').uniforms.palette;
});
const coloured = await capture(page);
check(
'FLIR Ironbow reaches the engine and colours the frame',
ironbow === 1 && stats(coloured).saturation > stats(grey).saturation + 10,
{
grey: stats(grey).saturation,
ironbow: stats(coloured).saturation,
},
); );
await page.evaluate(() => {
window.__godsEyeView.styleManager.stages.thermal.uniforms.palette = 0;
});
const back = await pickStyle(page, 'normal', previous);
check( check(
'Normal: every style back at 0 and the pass hidden', `${name}: alone at full intensity once settled`,
Object.values(back.intensities).every((v) => v === 0) && fade.intensities[name] === 1 && others.length === 0,
back.pipeline?.shown === false, fade.intensities,
back,
); );
await page.evaluate((sharpen) => { const order = fade.pipeline?.passes ?? [];
const { styleManager } = window.__godsEyeView; const at = order.indexOf('gevStyles');
styleManager._setDetectionMode?.('OFF');
styleManager.setSharpen(sharpen);
}, display);
const restored = await capture(page);
save('normal-restored', restored);
const residue = diff(normal, restored);
check( check(
'Normal restores the frame', `${name}: the style pass shows, last before the final copy and after the overlays`,
residue.mean < 1 && residue.changedShare < 0.01, fade.pipeline?.shown === true &&
residue, at >= 0 &&
); order[at + 1] === 'final' &&
['toneMapping', 'gevSharpen', 'gevOverlay', 'smaa', 'fxaa'].every(
// The retired Snow opens on Normal; a link's style opens on Navara. (key) => !order.includes(key) || order.indexOf(key) < at,
await page.goto(pageUrl('navara', '&style=snow'), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
const snow = await page.evaluate(() => {
const a = window.__godsEyeView;
return {
active: a.styleManager.activeStyle,
intensities: a.engine.render.visualStyles.map(
(s) => a.engine.render.style(s).intensity,
), ),
}; fade.pipeline,
}); );
const pixels = await capture(page);
save(name, pixels);
const styled = stats(pixels);
const change = diff(normal, pixels);
const shift = histogramShift(normalStats, styled);
check( check(
'style=snow opens on Normal', `${name}: the frame changes against Normal`,
snow.active === 'normal' && snow.intensities.every((v) => v === 0), change.mean > 6 && change.changedShare > 0.25,
snow, { ...change, histogramShift: shift },
); );
await page.goto(pageUrl('navara', `&style=${URL_NAMES.thermal}`), { const signature = {
waitUntil: 'domcontentloaded', // Night vision: phosphor green over red and blue.
surveillance: styled.g > styled.r * 1.3 && styled.g > styled.b * 1.3,
// FLIR white-hot (palette 0): grey.
thermal: styled.saturation < normalStats.saturation * 0.5,
// Noir: greyer, with a slight sepia, and vignetted corners.
noir:
styled.saturation < normalStats.saturation &&
styled.vignette < normalStats.vignette * 0.85,
// Anime: cel bands, more saturated.
anime: styled.saturation > normalStats.saturation * 1.05 || shift > 0.05,
// CRT: pixelated, dithered, scanlined.
retro: shift > 0.05 || change.mean > 12,
}[name];
check(`${name}: the style's own look`, signature, {
normal: {
r: normalStats.r,
g: normalStats.g,
b: normalStats.b,
saturation: normalStats.saturation,
vignette: normalStats.vignette,
},
styled: {
r: styled.r,
g: styled.g,
b: styled.b,
saturation: styled.saturation,
vignette: styled.vignette,
},
histogramShift: shift,
}); });
await waitForEngine(page, 'navara'); previous = name;
await page.waitForFunction( }
() => window.__godsEyeView.engine.render.style('thermal').intensity === 1,
{ timeout: 10000, polling: 50 },
);
check('a FLIR link opens filtered on Navara', true);
// Cesium keeps its own post stages; its engine draws no styles. The // A style parameter reaches the Navara pass (FLIR's Ironbow palette).
// switch is real input on the picker, in the same document. await pickStyle(page, 'thermal', previous);
await page.evaluate(() => previous = 'thermal';
window.__godsEyeView.styleManager.setStyle('normal'), const grey = await capture(page);
); const ironbow = await page.evaluate(() => {
await page.waitForFunction( const a = window.__godsEyeView;
() => window.__godsEyeView.styleManager.transitions.size === 0, a.styleManager.stages.thermal.uniforms.palette = 1;
); return a.engine.render.style('thermal').uniforms.palette;
await strict.switchTo(page, 'cesium'); });
await freeze(page); const coloured = await capture(page);
// The address may still carry the FLIR link: Normal first. check(
await pickStyle(page, 'normal', 'thermal'); 'FLIR Ironbow reaches the engine and colours the frame',
await page.evaluate((sharpen) => { ironbow === 1 && stats(coloured).saturation > stats(grey).saturation + 10,
const { styleManager } = window.__godsEyeView; {
styleManager._setDetectionMode?.('OFF'); grey: stats(grey).saturation,
styleManager.setSharpen(sharpen); ironbow: stats(coloured).saturation,
}, display); },
const cesiumNormal = await capture(page); );
const cesium = await pickStyle(page, 'thermal', 'normal'); await page.evaluate(() => {
const cesiumState = await page.evaluate(() => { window.__godsEyeView.styleManager.stages.thermal.uniforms.palette = 0;
const a = window.__godsEyeView; });
const stage = a.styleManager.stages.thermal;
return {
setStyle: typeof a.engine.render.setStyle,
stageName: stage.name,
enabled: stage.enabled,
intensity: stage.uniforms.intensity,
inScene: a.viewer?.scene?.postProcessStages?.contains?.(stage) ?? null,
};
});
const cesiumThermal = await capture(page);
save('cesium-normal', cesiumNormal);
save('cesium-thermal', cesiumThermal);
if (outDir)
await page.screenshot({
path: path.join(outDir, 'cesium-thermal-page.png'),
});
check(
'Cesium: FLIR is its own post stage, the engine draws none',
cesium.pending === 0 &&
cesiumState.setStyle === 'undefined' &&
cesiumState.enabled === true &&
cesiumState.intensity === 1 &&
cesiumState.inScene !== false,
cesiumState,
);
const cesiumChange = diff(cesiumNormal, cesiumThermal);
check(
'Cesium: FLIR changes its frame',
cesiumChange.mean > 6,
cesiumChange,
);
await pickStyle(page, 'normal', 'thermal');
await page.evaluate((sharpen) => {
const { styleManager } = window.__godsEyeView;
styleManager._setDetectionMode?.('OFF');
styleManager.setSharpen(sharpen);
}, display);
const cesiumBack = diff(cesiumNormal, await capture(page));
check(
'Cesium: Normal restores its frame',
cesiumBack.mean < 1.5,
cesiumBack,
);
check('no page errors', errors.length === 0, errors); const back = await pickStyle(page, 'normal', previous);
if (outDir) check(
fs.writeFileSync( 'Normal: every style back at 0 and the pass hidden',
path.join(outDir, 'visual-styles.json'), Object.values(back.intensities).every((v) => v === 0) &&
JSON.stringify(report, null, 2), back.pipeline?.shown === false,
); back,
}, );
); await page.evaluate((sharpen) => {
const { styleManager } = window.__godsEyeView;
styleManager._setDetectionMode?.('OFF');
styleManager.setSharpen(sharpen);
}, display);
const restored = await capture(page);
save('normal-restored', restored);
const residue = diff(normal, restored);
check(
'Normal restores the frame',
residue.mean < 1 && residue.changedShare < 0.01,
residue,
);
// The retired Snow opens on Normal; a link's style opens on Navara.
await page.goto(pageUrl('navara', '&style=snow'), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
const snow = await page.evaluate(() => {
const a = window.__godsEyeView;
return {
active: a.styleManager.activeStyle,
intensities: a.engine.render.visualStyles.map(
(s) => a.engine.render.style(s).intensity,
),
};
});
check(
'style=snow opens on Normal',
snow.active === 'normal' && snow.intensities.every((v) => v === 0),
snow,
);
await page.goto(pageUrl('navara', `&style=${URL_NAMES.thermal}`), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
await page.waitForFunction(
() => window.__godsEyeView.engine.render.style('thermal').intensity === 1,
{ timeout: 10000, polling: 50 },
);
check('a FLIR link opens filtered on Navara', true);
// Cesium keeps its own post stages; its engine draws no styles. The
// switch is real input on the picker, in the same document.
await page.evaluate(() =>
window.__godsEyeView.styleManager.setStyle('normal'),
);
await page.waitForFunction(
() => window.__godsEyeView.styleManager.transitions.size === 0,
);
await strict.switchTo(page, 'cesium');
await freeze(page);
// The address may still carry the FLIR link: Normal first.
await pickStyle(page, 'normal', 'thermal');
await page.evaluate((sharpen) => {
const { styleManager } = window.__godsEyeView;
styleManager._setDetectionMode?.('OFF');
styleManager.setSharpen(sharpen);
}, display);
const cesiumNormal = await capture(page);
const cesium = await pickStyle(page, 'thermal', 'normal');
const cesiumState = await page.evaluate(() => {
const a = window.__godsEyeView;
const stage = a.styleManager.stages.thermal;
return {
setStyle: typeof a.engine.render.setStyle,
stageName: stage.name,
enabled: stage.enabled,
intensity: stage.uniforms.intensity,
inScene: a.viewer?.scene?.postProcessStages?.contains?.(stage) ?? null,
};
});
const cesiumThermal = await capture(page);
save('cesium-normal', cesiumNormal);
save('cesium-thermal', cesiumThermal);
if (outDir)
await page.screenshot({
path: path.join(outDir, 'cesium-thermal-page.png'),
});
check(
'Cesium: FLIR is its own post stage, the engine draws none',
cesium.pending === 0 &&
cesiumState.setStyle === 'undefined' &&
cesiumState.enabled === true &&
cesiumState.intensity === 1 &&
cesiumState.inScene !== false,
cesiumState,
);
const cesiumChange = diff(cesiumNormal, cesiumThermal);
check('Cesium: FLIR changes its frame', cesiumChange.mean > 6, cesiumChange);
await pickStyle(page, 'normal', 'thermal');
await page.evaluate((sharpen) => {
const { styleManager } = window.__godsEyeView;
styleManager._setDetectionMode?.('OFF');
styleManager.setSharpen(sharpen);
}, display);
const cesiumBack = diff(cesiumNormal, await capture(page));
check('Cesium: Normal restores its frame', cesiumBack.mean < 1.5, cesiumBack);
check('no page errors', errors.length === 0, errors);
if (outDir)
fs.writeFileSync(
path.join(outDir, 'visual-styles.json'),
JSON.stringify(report, null, 2),
);
});
+11 -32
View File
@@ -1,6 +1,11 @@
import sharp from 'sharp'; import sharp from 'sharp';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic'; base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -54,18 +59,8 @@ const strict = createStrict({
}, },
], ],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b, { shaderErrors: true });
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
p.on('console', (msg) => {
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
errors.push(msg.text());
});
const assets = []; const assets = [];
p.on('request', (req) => { p.on('request', (req) => {
const path = new URL(req.url()).pathname; const path = new URL(req.url()).pathname;
@@ -77,21 +72,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
assets.push(path); assets.push(path);
}); });
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => { await waitForEngine(p, 'navara');
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await p.evaluate(async () => { await p.evaluate(async () => {
const { tileset, dataManager, sceneDirector } = window.__godsEyeView; const { tileset, dataManager, sceneDirector } = window.__godsEyeView;
if (!sceneDirector?.listScenes().length) if (!sceneDirector?.listScenes().length)
@@ -118,8 +99,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}; };
for (const kind of ['navara', 'cesium', 'navara']) { for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
} }
const state = await p.evaluate(() => { const state = await p.evaluate(() => {
const { dataManager } = window.__godsEyeView; const { dataManager } = window.__godsEyeView;
@@ -184,8 +164,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await p.waitForFunction( await p.waitForFunction(
() => window.qaEvent.getPlaybackState().progress > 0.53, () => window.qaEvent.getPlaybackState().progress > 0.53,
); );
await strict.pick(p, 'cesium'); await strict.switchTo(p, 'cesium');
await ready('cesium');
const live = await p.evaluate(() => window.qaEvent.getPlaybackState()); const live = await p.evaluate(() => window.qaEvent.getPlaybackState());
await strict.check( await strict.check(
p, p,
+10 -30
View File
@@ -1,7 +1,12 @@
import { fromDegrees } from '../src/geo/cartesian3.js'; import { fromDegrees } from '../src/geo/cartesian3.js';
import sharp from 'sharp'; import sharp from 'sharp';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic'; base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -11,34 +16,10 @@ const strict = createStrict({
origin: base.origin, origin: base.origin,
layers: ['bhote-koshi-locator'], layers: ['bhote-koshi-locator'],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b, { shaderErrors: true });
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
p.on('console', (msg) => {
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
errors.push(msg.text());
});
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => { await waitForEngine(p, 'navara');
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await p.evaluate(async () => { await p.evaluate(async () => {
const { tileset, dataManager } = window.__godsEyeView; const { tileset, dataManager } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__) if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -51,8 +32,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}); });
for (const kind of ['navara', 'cesium', 'navara']) { for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
} }
const { same, stats, params, at } = await p.evaluate( const { same, stats, params, at } = await p.evaluate(
(world) => { (world) => {
+10 -26
View File
@@ -1,6 +1,11 @@
import sharp from 'sharp'; import sharp from 'sharp';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
// Drone airspace, OSM places, sea marks and ski pistes on both engines, over // Drone airspace, OSM places, sea marks and ski pistes on both engines, over
// Navara → Cesium → Navara: each draws over the view centre, and the switch // Navara → Cesium → Navara: each draws over the view centre, and the switch
@@ -57,14 +62,8 @@ const strict = createStrict({
tileRoute('https://www.opensnowmap.org/*', 'ski-pistes'), tileRoute('https://www.opensnowmap.org/*', 'ski-pistes'),
], ],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b);
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await p.evaluateOnNewDocument((ring) => { await p.evaluateOnNewDocument((ring) => {
const fetch = window.fetch.bind(window); const fetch = window.fetch.bind(window);
window.qaZoneReads = 0; window.qaZoneReads = 0;
@@ -101,20 +100,6 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}; };
}, RING); }, RING);
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
const setEnabled = (id, on) => const setEnabled = (id, on) =>
p.evaluate( p.evaluate(
(layerId, show) => (layerId, show) =>
@@ -177,7 +162,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const greener = (on, off) => const greener = (on, off) =>
2 * on[1] - on[0] - on[2] > 2 * off[1] - off[0] - off[2] + 10; 2 * on[1] - on[0] - on[2] > 2 * off[1] - off[0] - off[2] + 10;
await ready('navara'); await waitForEngine(p, 'navara');
await p.evaluate(() => { await p.evaluate(() => {
const { dataManager } = window.__godsEyeView; const { dataManager } = window.__godsEyeView;
window.qaModels = Object.fromEntries( window.qaModels = Object.fromEntries(
@@ -190,8 +175,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
for (const [leg, kind] of ['navara', 'cesium', 'navara'].entries()) { for (const [leg, kind] of ['navara', 'cesium', 'navara'].entries()) {
const readsBeforeSwitch = await p.evaluate(() => window.qaZoneReads); const readsBeforeSwitch = await p.evaluate(() => window.qaZoneReads);
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
const same = await p.evaluate(() => const same = await p.evaluate(() =>
Object.entries(window.qaModels).every( Object.entries(window.qaModels).every(
([id, model]) => ([id, model]) =>
+77 -80
View File
@@ -6,86 +6,83 @@ const url = localPreview(process.argv[2]);
url.search = '?engine=navara&welcome=0'; url.search = '?engine=navara&welcome=0';
url.hash = 'v=2&lat=30.27&lon=-97.74&alt=12000&pitch=-90&map=sentinel2&l='; url.hash = 'v=2&lat=30.27&lon=-97.74&alt=12000&pitch=-90&map=sentinel2&l=';
const strict = createStrict({ origin: url.origin, layers: ['radio'] }); const strict = createStrict({ origin: url.origin, layers: ['radio'] });
await strict.run( await strict.run({}, async (browser) => {
{ args: ['--use-angle=metal', '--enable-gpu'] }, const page = await browser.newPage();
async (browser) => { const errors = [];
const page = await browser.newPage(); page.on('pageerror', (e) => errors.push(e.message));
const errors = []; await installRadioFixture(page);
page.on('pageerror', (e) => errors.push(e.message)); await page.goto(url.href);
await installRadioFixture(page); await page.waitForFunction(
await page.goto(url.href); () =>
await page.waitForFunction( window.__godsEyeView?.engine?.kind === 'navara' &&
() => document.querySelector('#loading-screen.hidden'),
window.__godsEyeView?.engine?.kind === 'navara' && );
document.querySelector('#loading-screen.hidden'), await page.evaluate(async (id) => {
); const { dataManager, styleManager, tileset } = window.__godsEyeView;
await page.evaluate(async (id) => { await styleManager.initialRestorePromise;
const { dataManager, styleManager, tileset } = window.__godsEyeView; if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
await styleManager.initialRestorePromise; throw Error('Expected keyless preview');
if (window.__GOOGLE_MAPS_API_KEY__ || tileset) window.qaLayer = dataManager.layers.get('radio').module;
throw Error('Expected keyless preview'); await dataManager.setEnabled('radio', true, { origin: 'user' });
window.qaLayer = dataManager.layers.get('radio').module; }, stations[0].id);
await dataManager.setEnabled('radio', true, { origin: 'user' }); await page.waitForFunction(() => window.qaLayer?.getStats().count === 750);
}, stations[0].id); await page.evaluate((id) => {
await page.waitForFunction(() => window.qaLayer?.getStats().count === 750); window.qaLayer.selectStation(id, {
await page.evaluate((id) => { autoplay: true,
window.qaLayer.selectStation(id, { focus: false,
autoplay: true, origin: 'user',
focus: false,
origin: 'user',
});
window.qaLayer.setRadioVolume(0.36);
}, stations[10].id);
await page.waitForFunction(
() =>
window.__qaRadio.plays.length === 1 &&
window.qaLayer.getUIState().audioState === 'playing',
);
const first = await page.evaluate(() => {
window.qaAudio = window.qaPlayer;
window.qaSnapshot = window.qaLayer.getAcceptedCatalogSnapshot();
return window.__qaRadio.reads;
}); });
for (const kind of ['cesium', 'navara']) { window.qaLayer.setRadioVolume(0.36);
await strict.switchTo(page, kind); }, stations[10].id);
const state = await page.evaluate(() => ({ await page.waitForFunction(
same: () =>
window.qaLayer === window.__qaRadio.plays.length === 1 &&
window.__godsEyeView.dataManager.layers.get('radio').module, window.qaLayer.getUIState().audioState === 'playing',
player: window.qaAudio === window.qaPlayer, );
snapshot: const first = await page.evaluate(() => {
window.qaSnapshot === window.qaLayer.getAcceptedCatalogSnapshot(), window.qaAudio = window.qaPlayer;
plays: window.__qaRadio.plays.length, window.qaSnapshot = window.qaLayer.getAcceptedCatalogSnapshot();
reads: window.__qaRadio.reads, return window.__qaRadio.reads;
state: window.qaLayer.getUIState(), });
count: window.qaLayer.getStats().count, for (const kind of ['cesium', 'navara']) {
})); await strict.switchTo(page, kind);
console.log( const state = await page.evaluate(() => ({
kind, same:
state.count, window.qaLayer ===
state.state.selected?.id, window.__godsEyeView.dataManager.layers.get('radio').module,
state.state.audioState, player: window.qaAudio === window.qaPlayer,
state.plays, snapshot:
state.reads, window.qaSnapshot === window.qaLayer.getAcceptedCatalogSnapshot(),
); plays: window.__qaRadio.plays.length,
assert.ok(state.same && state.player && state.snapshot); reads: window.__qaRadio.reads,
assert.equal(state.plays, 1); state: window.qaLayer.getUIState(),
assert.equal(state.reads, first); count: window.qaLayer.getStats().count,
assert.equal(state.count, 750); }));
assert.equal(state.state.audioState, 'playing');
assert.equal(state.state.selected.id, stations[10].id);
assert.equal(state.state.volume, 0.36);
// The fixture's HTMLMediaElement.prototype.play counts play() calls: the
// switch must not start the stream again.
await strict.passed(
page,
'radio',
'same model, player element and catalog snapshot; 750 stations, one catalog read, no second play(), same station playing at volume 0.36',
);
}
assert.deepEqual(errors, []);
console.log( console.log(
'PASS one live radio player, one station directory and the same selection', kind,
state.count,
state.state.selected?.id,
state.state.audioState,
state.plays,
state.reads,
); );
}, assert.ok(state.same && state.player && state.snapshot);
); assert.equal(state.plays, 1);
assert.equal(state.reads, first);
assert.equal(state.count, 750);
assert.equal(state.state.audioState, 'playing');
assert.equal(state.state.selected.id, stations[10].id);
assert.equal(state.state.volume, 0.36);
// The fixture's HTMLMediaElement.prototype.play counts play() calls: the
// switch must not start the stream again.
await strict.passed(
page,
'radio',
'same model, player element and catalog snapshot; 750 stations, one catalog read, no second play(), same station playing at volume 0.36',
);
}
assert.deepEqual(errors, []);
console.log(
'PASS one live radio player, one station directory and the same selection',
);
});
+1 -1
View File
@@ -14,7 +14,7 @@ const strict = createStrict({
origin: url.origin, origin: url.origin,
layers: [missions ? 'rocket-launches' : 'satellites'], layers: [missions ? 'rocket-launches' : 'satellites'],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const p = await b.newPage();
const errors = []; const errors = [];
p.on('pageerror', (e) => { p.on('pageerror', (e) => {
+1 -1
View File
@@ -54,7 +54,7 @@ const getOpt = (name, dflt) => {
}; };
const BASE_URL = getOpt('--url', 'http://localhost:4173'); const BASE_URL = getOpt('--url', 'http://localhost:4173');
// --engine=cesium|navara (phase 3): without a viewer the layer is read through // --engine=cesium|navara: without a viewer the layer is read through
// the QA probe (window.__godsEyeView.layers.drawn) and the military checks are // the QA probe (window.__godsEyeView.layers.drawn) and the military checks are
// skipped where the engine has no military layer. // skipped where the engine has no military layer.
const ENGINE = (argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9); const ENGINE = (argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
+155 -171
View File
@@ -1,5 +1,9 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
waitForEngine,
} from './qa-strict-harness.mjs';
import { serveFixtures, newFixture } from './qa-street-level.mjs'; import { serveFixtures, newFixture } from './qa-street-level.mjs';
import { KV_LINE, KV_SEQUENCE_ID } from './fixtures/street-level/kartaView.mjs'; import { KV_LINE, KV_SEQUENCE_ID } from './fixtures/street-level/kartaView.mjs';
import { import {
@@ -28,186 +32,166 @@ const strict = createStrict({
'https://panoramax.openstreetmap.fr/*', 'https://panoramax.openstreetmap.fr/*',
], ],
}); });
await strict.run( await strict.run({}, async (browser) => {
{ args: ['--use-angle=metal', '--enable-gpu'] }, const p = await browser.newPage();
async (browser) => { await p.setViewport({ width: 1280, height: 900 });
const p = await browser.newPage(); const errors = [],
await p.setViewport({ width: 1280, height: 900 }); fixture = newFixture({ configured: true });
const errors = [], p.on('pageerror', (e) => {
fixture = newFixture({ configured: true }); errors.push(e.message);
p.on('pageerror', (e) => { console.error(e.message.split('?')[0]);
errors.push(e.message); });
console.error(e.message.split('?')[0]); p.on('console', (m) => {
}); if (/shader|registerMeshPicker/i.test(m.text()) && m.type() === 'error')
p.on('console', (m) => { errors.push(m.text());
if (/shader|registerMeshPicker/i.test(m.text()) && m.type() === 'error') });
errors.push(m.text()); await serveFixtures(p, fixture);
}); let photoRequests = 0;
await serveFixtures(p, fixture); p.on('request', (req) => {
let photoRequests = 0; if (
p.on('request', (req) => { /\.jpg(?:$|\?)/i.test(new URL(req.url()).pathname) &&
if ( /openstreetcam|kartaview|panoramax/.test(new URL(req.url()).hostname)
/\.jpg(?:$|\?)/i.test(new URL(req.url()).pathname) && )
/openstreetcam|kartaview|panoramax/.test(new URL(req.url()).hostname) photoRequests++;
) });
photoRequests++; await p.goto(base.href);
}); await waitForEngine(p, 'navara');
await p.goto(base.href); await p.evaluate(async (provider) => {
const ready = async (k) => { const { dataManager, tileset } = window.__godsEyeView;
await p.waitForFunction( if (tileset || window.__GOOGLE_MAPS_API_KEY__)
(kind) => throw Error('Expected keyless preview');
window.__godsEyeView?.engine?.kind === kind && window.qaStreet = dataManager.layers.get('street-level').module;
!document.querySelector('#globe-engine')?.disabled && await dataManager.setEnabled('street-level', true, { origin: 'user' });
!document.querySelector('.engine-switch-frame') && window.qaStreet.setProviderEnabled(provider, true);
document.querySelector('#loading-screen.hidden'), }, provider);
{ timeout: 90000 }, const until = async (fn) => {
k, try {
); await p.waitForFunction(fn, { timeout: 20000 });
await p.evaluate( } catch (e) {
() => window.__godsEyeView.styleManager.initialRestorePromise, console.log(await p.evaluate(() => window.qaStreet.getUIState()));
); throw e;
};
await ready('navara');
await p.evaluate(async (provider) => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Expected keyless preview');
window.qaStreet = dataManager.layers.get('street-level').module;
await dataManager.setEnabled('street-level', true, { origin: 'user' });
window.qaStreet.setProviderEnabled(provider, true);
}, provider);
const until = async (fn) => {
try {
await p.waitForFunction(fn, { timeout: 20000 });
} catch (e) {
console.log(await p.evaluate(() => window.qaStreet.getUIState()));
throw e;
}
};
await until(() => window.qaStreet.getUIState().coverage.count > 0);
// The fixture drive passes under the middle of the canvas. Click its actual picked line.
const pickLine = async () => {
const xy = await p.evaluate(
(prefix) => {
const { engine } = window.__godsEyeView,
canvas = engine.host.canvas,
rect = canvas.getBoundingClientRect();
for (let dy = -30; dy <= 30; dy += 3)
for (let dx = -40; dx <= 40; dx++) {
const at = { x: rect.width / 2 + dx, y: rect.height / 2 + dy },
pick = engine.picker.pick(at),
id = pick?.id?.id ?? pick?.id;
if (typeof id === 'string' && id.startsWith(prefix))
return { x: at.x + rect.x, y: at.y + rect.y, id };
}
return null;
},
panorama ? 'pnx:seq:' : 'kv:line:',
);
assert.ok(xy, 'native ground line was pickable');
console.log('pick', xy);
await p.mouse.click(xy.x, xy.y);
await until(
() =>
window.qaStreet.getUIState().street.open &&
window.qaStreet.getUIState().sequence.images > 0 &&
(document.querySelector('#sl-viewer img')?.naturalWidth > 0 ||
document.querySelector('#sl-viewer canvas:not([hidden])')?.width >
0),
);
await strict.passed(
p,
'street-level',
'a click on the coverage line opens its sequence and photo',
xy,
);
return xy;
};
const xy = await pickLine();
if (panorama) {
const expected = panoramaxPictures().filter(
(p) => p.sequenceId === xy.id.slice('pnx:seq:'.length),
).length;
await p.waitForFunction(
(n) => window.qaStreet.getUIState().sequence.images === n,
{ timeout: 20000 },
expected,
);
} }
const first = await p.evaluate(() => { };
window.streetPhoto = await until(() => window.qaStreet.getUIState().coverage.count > 0);
document.querySelector('#sl-viewer canvas:not([hidden])') ?? // The fixture drive passes under the middle of the canvas. Click its actual picked line.
document.querySelector('#sl-viewer img'); const pickLine = async () => {
return window.qaStreet.getUIState(); const xy = await p.evaluate(
}); (prefix) => {
assert.ok( const { engine } = window.__godsEyeView,
(panorama ? PNX_SEQUENCES : [KV_SEQUENCE_ID]).includes( canvas = engine.host.canvas,
first.street.sequenceId, rect = canvas.getBoundingClientRect();
), for (let dy = -30; dy <= 30; dy += 3)
for (let dx = -40; dx <= 40; dx++) {
const at = { x: rect.width / 2 + dx, y: rect.height / 2 + dy },
pick = engine.picker.pick(at),
id = pick?.id?.id ?? pick?.id;
if (typeof id === 'string' && id.startsWith(prefix))
return { x: at.x + rect.x, y: at.y + rect.y, id };
}
return null;
},
panorama ? 'pnx:seq:' : 'kv:line:',
);
assert.ok(xy, 'native ground line was pickable');
console.log('pick', xy);
await p.mouse.click(xy.x, xy.y);
await until(
() =>
window.qaStreet.getUIState().street.open &&
window.qaStreet.getUIState().sequence.images > 0 &&
(document.querySelector('#sl-viewer img')?.naturalWidth > 0 ||
document.querySelector('#sl-viewer canvas:not([hidden])')?.width > 0),
); );
const reads = photoRequests;
for (const kind of ['cesium', 'navara']) {
await strict.pick(p, kind);
await ready(kind);
// The new globe reloads coverage right after it attaches: compare once
// that load is over.
await until(
() =>
!window.qaStreet.getUIState().coverage.loading &&
window.qaStreet.getUIState().coverage.count > 0,
);
const state = await p.evaluate(() => ({
model:
window.__godsEyeView.dataManager.layers.get('street-level').module ===
window.qaStreet,
samePhoto:
document.querySelector('#sl-viewer')?.contains(window.streetPhoto) &&
window.streetPhoto.isConnected,
ui: window.qaStreet.getUIState(),
}));
console.log(
kind,
state.model,
state.samePhoto,
state.ui.street.imageId,
state.ui.sequence.images,
state.ui.coverage.count,
);
assert.equal(state.model, true);
assert.equal(state.samePhoto, true);
assert.ok(state.ui.street.open);
assert.equal(state.ui.street.imageId, first.street.imageId);
assert.equal(state.ui.sequence.images, first.sequence.images);
assert.deepEqual(state.ui.filter, first.filter);
assert.ok(state.ui.coverage.count > 0);
await strict.passed(
p,
'street-level',
'same model, the same mounted photo, image and sequence; coverage kept',
);
}
assert.equal(photoRequests, reads);
await strict.passed( await strict.passed(
p, p,
'street-level', 'street-level',
'no photo requested again by the switches', 'a click on the coverage line opens its sequence and photo',
xy,
); );
// Click-picking on Cesium: close the viewer, switch, click the line again. return xy;
await p.evaluate(() => window.qaStreet.closeViewer()); };
await until(() => !window.qaStreet.getUIState().street.open); const xy = await pickLine();
await strict.pick(p, 'cesium'); if (panorama) {
await ready('cesium'); const expected = panoramaxPictures().filter(
(p) => p.sequenceId === xy.id.slice('pnx:seq:'.length),
).length;
await p.waitForFunction(
(n) => window.qaStreet.getUIState().sequence.images === n,
{ timeout: 20000 },
expected,
);
}
const first = await p.evaluate(() => {
window.streetPhoto =
document.querySelector('#sl-viewer canvas:not([hidden])') ??
document.querySelector('#sl-viewer img');
return window.qaStreet.getUIState();
});
assert.ok(
(panorama ? PNX_SEQUENCES : [KV_SEQUENCE_ID]).includes(
first.street.sequenceId,
),
);
const reads = photoRequests;
for (const kind of ['cesium', 'navara']) {
await strict.switchTo(p, kind);
// The new globe reloads coverage right after it attaches: compare once
// that load is over.
await until( await until(
() => () =>
!window.qaStreet.getUIState().coverage.loading && !window.qaStreet.getUIState().coverage.loading &&
window.qaStreet.getUIState().coverage.count > 0, window.qaStreet.getUIState().coverage.count > 0,
); );
await new Promise((resolve) => setTimeout(resolve, 1000)); const state = await p.evaluate(() => ({
await pickLine(); model:
assert.deepEqual(errors, []); window.__godsEyeView.dataManager.layers.get('street-level').module ===
assert.deepEqual(fixture.unknown, []); window.qaStreet,
samePhoto:
document.querySelector('#sl-viewer')?.contains(window.streetPhoto) &&
window.streetPhoto.isConnected,
ui: window.qaStreet.getUIState(),
}));
console.log( console.log(
'PASS native line pick, same mounted photo and sequence across two switches', kind,
state.model,
state.samePhoto,
state.ui.street.imageId,
state.ui.sequence.images,
state.ui.coverage.count,
); );
}, assert.equal(state.model, true);
); assert.equal(state.samePhoto, true);
assert.ok(state.ui.street.open);
assert.equal(state.ui.street.imageId, first.street.imageId);
assert.equal(state.ui.sequence.images, first.sequence.images);
assert.deepEqual(state.ui.filter, first.filter);
assert.ok(state.ui.coverage.count > 0);
await strict.passed(
p,
'street-level',
'same model, the same mounted photo, image and sequence; coverage kept',
);
}
assert.equal(photoRequests, reads);
await strict.passed(
p,
'street-level',
'no photo requested again by the switches',
);
// Click-picking on Cesium: close the viewer, switch, click the line again.
await p.evaluate(() => window.qaStreet.closeViewer());
await until(() => !window.qaStreet.getUIState().street.open);
await strict.switchTo(p, 'cesium');
await until(
() =>
!window.qaStreet.getUIState().coverage.loading &&
window.qaStreet.getUIState().coverage.count > 0,
);
await new Promise((resolve) => setTimeout(resolve, 1000));
await pickLine();
assert.deepEqual(errors, []);
assert.deepEqual(fixture.unknown, []);
console.log(
'PASS native line pick, same mounted photo and sequence across two switches',
);
});
+1 -1
View File
@@ -41,7 +41,7 @@ const navaraCode = async (url) => {
}; };
const results = []; const results = [];
strict.extra.contracts = results; strict.extra.contracts = results;
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
for (const start of ['navara', 'cesium']) { for (const start of ['navara', 'cesium']) {
const p = await b.newPage(); const p = await b.newPage();
await p.setViewport({ width: 1280, height: 800 }); await p.setViewport({ width: 1280, height: 800 });
+1 -1
View File
@@ -32,7 +32,7 @@ const other = (kind) => (kind === 'navara' ? 'cesium' : 'navara');
const strict = createStrict({ origin: base.origin, layers: [] }); const strict = createStrict({ origin: base.origin, layers: [] });
const results = []; const results = [];
strict.extra.contracts = results; strict.extra.contracts = results;
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
let p = null; let p = null;
const errors = []; const errors = [];
// A fresh page on `from`; warm visits the other engine first. // A fresh page on `from`; warm visits the other engine first.
+1 -1
View File
@@ -37,7 +37,7 @@ const other = (kind) => (kind === 'navara' ? 'cesium' : 'navara');
const strict = createStrict({ origin: base.origin, layers: [] }); const strict = createStrict({ origin: base.origin, layers: [] });
const results = []; const results = [];
strict.extra.contracts = results; strict.extra.contracts = results;
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
// A fresh page on `start` with the session; `warm` visits the other // A fresh page on `start` with the session; `warm` visits the other
// engine first so it is parked. // engine first so it is parked.
const open = async (start, warm) => { const open = async (start, warm) => {
+65 -79
View File
@@ -29,8 +29,21 @@ import {
import { createMissLoader, tileCacheFile } from './qa/tileCache.mjs'; import { createMissLoader, tileCacheFile } from './qa/tileCache.mjs';
export const ENGINES = Object.freeze(['cesium', 'navara']); export const ENGINES = Object.freeze(['cesium', 'navara']);
const RESERVED_PORTS = ['', '4173', '4185', '4186', '4187', '4188', '4189']; /** Local ports that belong to the user's own servers: never a QA preview. */
const RESERVED = [...RESERVED_PORTS, '4194', '4196']; export const RESERVED_PORTS = Object.freeze([
'',
'4173',
'4174',
'4185',
'4186',
'4187',
'4188',
'4189',
'4194',
'4196',
]);
/** Chrome flags every scenario's browser starts with (the GPU path). */
const CHROME_ARGS = Object.freeze(['--use-angle=metal', '--enable-gpu']);
/** The values of --layers=a,b and --evidence=FILE (null when absent). */ /** The values of --layers=a,b and --evidence=FILE (null when absent). */
export function strictArguments(argv = process.argv) { export function strictArguments(argv = process.argv) {
@@ -48,7 +61,7 @@ export function localPreview(href) {
const url = new URL(href); const url = new URL(href);
if ( if (
!['localhost', '127.0.0.1'].includes(url.hostname) || !['localhost', '127.0.0.1'].includes(url.hostname) ||
RESERVED.includes(url.port) RESERVED_PORTS.includes(url.port)
) )
throw Error('Supply an unreserved local keyless preview URL'); throw Error('Supply an unreserved local keyless preview URL');
return url; return url;
@@ -89,11 +102,18 @@ export async function installStrictNetwork(
// workers' own requests are never held. // workers' own requests are never held.
const intercept = async (session, type) => { const intercept = async (session, type) => {
session.on('Fetch.requestPaused', async ({ requestId, request }) => { session.on('Fetch.requestPaused', async ({ requestId, request }) => {
const fulfil = (status, headers, body) => // Every answer allows any origin; `noStore` keeps it out of the cache.
const fulfil = (status, contentType, body, { noStore = false } = {}) =>
session.send('Fetch.fulfillRequest', { session.send('Fetch.fulfillRequest', {
requestId, requestId,
responseCode: status, responseCode: status,
responseHeaders: headers, responseHeaders: [
...(contentType
? [{ name: 'Content-Type', value: contentType }]
: []),
...(noStore ? [{ name: 'Cache-Control', value: 'no-store' }] : []),
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
...(body == null ...(body == null
? {} ? {}
: { body: Buffer.from(body).toString('base64') }), : { body: Buffer.from(body).toString('base64') }),
@@ -106,12 +126,7 @@ export async function installStrictNetwork(
const answer = await route.respond(request); const answer = await route.respond(request);
return await fulfil( return await fulfil(
answer.status ?? 200, answer.status ?? 200,
[ answer.contentType,
...(answer.contentType
? [{ name: 'Content-Type', value: answer.contentType }]
: []),
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
answer.body ?? '', answer.body ?? '',
); );
} }
@@ -128,14 +143,9 @@ export async function installStrictNetwork(
(ledger.probes[probe.pattern] ?? 0) + 1; (ledger.probes[probe.pattern] ?? 0) + 1;
return await fulfil( return await fulfil(
probe.status, probe.status,
[ probe.contentType,
...(probe.contentType
? [{ name: 'Content-Type', value: probe.contentType }]
: []),
{ name: 'Cache-Control', value: 'no-store' },
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
probe.body ?? null, probe.body ?? null,
{ noStore: true },
); );
} }
const accept = Object.entries(request.headers ?? {}).find( const accept = Object.entries(request.headers ?? {}).find(
@@ -172,16 +182,7 @@ export async function installStrictNetwork(
const host = new URL(request.url).host; const host = new URL(request.url).host;
tiles.fetchedByHost[host] = (tiles.fetchedByHost[host] ?? 0) + 1; tiles.fetchedByHost[host] = (tiles.fetchedByHost[host] ?? 0) + 1;
} }
await fulfil( await fulfil(entry.status, entry.contentType, entry.body);
entry.status,
[
...(entry.contentType
? [{ name: 'Content-Type', value: entry.contentType }]
: []),
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
entry.body,
);
} catch (error) { } catch (error) {
if ( if (
/Invalid InterceptionId|closed|detached/i.test(String(error?.message)) /Invalid InterceptionId|closed|detached/i.test(String(error?.message))
@@ -326,21 +327,15 @@ export async function installStrictNetwork(
* each page gets an accessor in place of the global: reads before the script * each page gets an accessor in place of the global: reads before the script
* has assigned it are logged with their stack in `ledger.cesiumBeforeLoad` * has assigned it are logged with their stack in `ledger.cesiumBeforeLoad`
* and answer `undefined`, as production would. * and answer `undefined`, as production would.
*
* GEV_QA_CESIUM_TRAP=record (a survey, not a check) answers such a read with
* the SDK instead, loaded synchronously on the first one, and logs every
* distinct read site until the page itself loads Cesium.js, so one run lists
* all the Navara paths that still need Cesium.
*/ */
async function trapCesiumGlobal(session, ledger) { async function trapCesiumGlobal(session, ledger) {
const binding = '__gevStrictCesiumRead'; const binding = '__gevStrictCesiumRead';
const record = process.env.GEV_QA_CESIUM_TRAP === 'record';
session.on('Runtime.bindingCalled', (event) => { session.on('Runtime.bindingCalled', (event) => {
if (event.name !== binding) return; if (event.name !== binding) return;
const stack = String(event.payload).slice(0, 1500); const stack = String(event.payload).slice(0, 1500);
const known = ledger.cesiumBeforeLoad.find((read) => read.stack === stack); const known = ledger.cesiumBeforeLoad.find((read) => read.stack === stack);
if (known) known.count++; if (known) known.count++;
else if (ledger.cesiumBeforeLoad.length < (record ? 2000 : 50)) else if (ledger.cesiumBeforeLoad.length < 50)
ledger.cesiumBeforeLoad.push({ stack, count: 1 }); ledger.cesiumBeforeLoad.push({ stack, count: 1 });
}); });
await session.send('Runtime.enable'); await session.send('Runtime.enable');
@@ -349,10 +344,8 @@ async function trapCesiumGlobal(session, ledger) {
await session.send('Page.addScriptToEvaluateOnNewDocument', { await session.send('Page.addScriptToEvaluateOnNewDocument', {
source: `(() => { source: `(() => {
if (window.top !== window) return; if (window.top !== window) return;
const record = ${record};
const seen = new Set(); const seen = new Set();
let sdk; let sdk;
let pageLoaded = false;
const report = () => { const report = () => {
const limit = Error.stackTraceLimit; const limit = Error.stackTraceLimit;
Error.stackTraceLimit = 12; Error.stackTraceLimit = 12;
@@ -366,20 +359,7 @@ async function trapCesiumGlobal(session, ledger) {
Object.defineProperty(window, 'Cesium', { Object.defineProperty(window, 'Cesium', {
configurable: true, configurable: true,
get() { get() {
if (!record) { if (sdk === undefined) report();
if (sdk === undefined) report();
return sdk;
}
if (!pageLoaded) {
pageLoaded = !!document.querySelector('script[src$="/Cesium.js"]');
if (!pageLoaded) report();
}
if (sdk === undefined) {
const request = new XMLHttpRequest();
request.open('GET', '/cesium/Cesium.js', false);
request.send();
(0, eval)(request.responseText);
}
return sdk; return sdk;
}, },
set(value) { set(value) {
@@ -434,30 +414,8 @@ export async function pickEngine(
); );
const opened = !(await expanded()); const opened = !(await expanded());
if (opened) { if (opened) {
await page.click('#control-panel-toggle').catch(async (error) => { await page.click('#control-panel-toggle').catch((error) => {
const state = await page.evaluate(() => { throw Error(`cannot click the control panel toggle: ${error.message}`);
const el = document.querySelector('#control-panel-toggle');
const box = el?.getBoundingClientRect();
const style = el && getComputedStyle(el);
const hit =
box &&
document.elementFromPoint(
box.x + box.width / 2,
box.y + box.height / 2,
);
return {
box: box && [box.x, box.y, box.width, box.height],
display: style?.display,
visibility: style?.visibility,
hidden: el?.closest('[hidden]')?.id ?? null,
hit: hit?.id || hit?.className || null,
body: document.body.className,
};
});
throw Error(
`cannot click the control panel toggle (${error.message}): ` +
JSON.stringify(state),
);
}); });
await page.waitForFunction( await page.waitForFunction(
() => () =>
@@ -592,6 +550,27 @@ export async function waitForEngine(page, kind, { timeout = 120000 } = {}) {
); );
} }
/**
* A new 1024×768 page of `browser` whose uncaught errors (and, with
* `shaderErrors`, shader and WebGL program errors on the console) land in
* `errors`; each uncaught error is also logged without its query string.
*/
export async function openPage(browser, { shaderErrors = false } = {}) {
const page = await browser.newPage();
await page.setViewport({ width: 1024, height: 768 });
const errors = [];
page.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
if (shaderErrors)
page.on('console', (msg) => {
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
errors.push(msg.text());
});
return { page, errors };
}
/** /**
* The strict context of one scenario run. * The strict context of one scenario run.
* *
@@ -653,9 +632,16 @@ export function createStrict({
ledger, ledger,
extra, extra,
timings, timings,
/** Launch Chrome with the policy on every target. */ /**
async launch(options = {}) { * Launch headless Chrome with the policy on every target; `args` adds
const browser = await puppeteer.launch({ headless: true, ...options }); * flags to CHROME_ARGS.
*/
async launch({ args = [], ...options } = {}) {
const browser = await puppeteer.launch({
headless: true,
...options,
args: [...CHROME_ARGS, ...args],
});
await installStrictNetwork(browser, { await installStrictNetwork(browser, {
origin, origin,
fixtures, fixtures,
@@ -665,7 +651,7 @@ export function createStrict({
}); });
return browser; return browser;
}, },
/** Launch, run `body(browser)`, close, then finish(). */ /** Launch with `options`, run `body(browser)`, close, then finish(). */
async run(options, body) { async run(options, body) {
const browser = await strict.launch(options); const browser = await strict.launch(options);
let failed = null; let failed = null;
+1 -1
View File
@@ -20,7 +20,7 @@ import {
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
const strict = createStrict({ origin: base.origin, layers: [] }); const strict = createStrict({ origin: base.origin, layers: [] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const p = await b.newPage();
await p.setViewport({ width: 1280, height: 800 }); await p.setViewport({ width: 1280, height: 800 });
const errors = []; const errors = [];
+1 -1
View File
@@ -20,7 +20,7 @@ const base = localPreview(process.argv[2]);
const strict = createStrict({ origin: base.origin, layers: [] }); const strict = createStrict({ origin: base.origin, layers: [] });
const results = []; const results = [];
strict.extra.contracts = results; strict.extra.contracts = results;
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
for (const start of ENGINES) { for (const start of ENGINES) {
const page = await b.newPage(); const page = await b.newPage();
await page.setViewport({ width: 1280, height: 800 }); await page.setViewport({ width: 1280, height: 800 });
+1 -1
View File
@@ -24,7 +24,7 @@ import {
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
const strict = createStrict({ origin: base.origin, layers: [] }); const strict = createStrict({ origin: base.origin, layers: [] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const p = await b.newPage();
await p.setViewport({ width: 1280, height: 800 }); await p.setViewport({ width: 1280, height: 800 });
const errors = []; const errors = [];
+1 -3
View File
@@ -156,9 +156,7 @@ strict.extra.soak = report;
for (let run = 1; run <= runs; run++) { for (let run = 1; run <= runs; run++) {
const start = run % 2 ? 'navara' : 'cesium'; const start = run % 2 ? 'navara' : 'cesium';
const browser = await strict.launch({ const browser = await strict.launch();
args: ['--use-angle=metal', '--enable-gpu'],
});
const row = { run, start, swaps: [], checkpoints: [], errors: [] }; const row = { run, start, swaps: [], checkpoints: [], errors: [] };
report.runs.push(row); report.runs.push(row);
try { try {
+10 -30
View File
@@ -1,7 +1,12 @@
import { fromDegrees } from '../src/geo/cartesian3.js'; import { fromDegrees } from '../src/geo/cartesian3.js';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic'; base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -27,39 +32,15 @@ const strict = createStrict({
origin: base.origin, origin: base.origin,
layers: ['telegeography-submarine-cables'], layers: ['telegeography-submarine-cables'],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b, { shaderErrors: true });
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
const reads = []; const reads = [];
p.on('request', (req) => { p.on('request', (req) => {
const path = new URL(req.url()).pathname; const path = new URL(req.url()).pathname;
if (/\/(cable-geo|landing-point-geo).*\.json$/.test(path)) reads.push(path); if (/\/(cable-geo|landing-point-geo).*\.json$/.test(path)) reads.push(path);
}); });
p.on('console', (msg) => {
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
errors.push(msg.text());
});
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => { await waitForEngine(p, 'navara');
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await p.evaluate(async () => { await p.evaluate(async () => {
const { dataManager, tileset } = window.__godsEyeView; const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__) if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -71,8 +52,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}); });
for (const kind of ['navara', 'cesium', 'navara']) { for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
} }
await p.waitForFunction( await p.waitForFunction(
() => window.qaCables.getStats().referenceLabelCount > 0, () => window.qaCables.getStats().referenceLabelCount > 0,
+10 -30
View File
@@ -1,7 +1,12 @@
import { trafficFixtureResponse } from './traffic-fixtures.mjs'; import { trafficFixtureResponse } from './traffic-fixtures.mjs';
import sharp from 'sharp'; import sharp from 'sharp';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
import { STRICT_TILE_HOSTS } from './qa-map-policy.mjs'; import { STRICT_TILE_HOSTS } from './qa-map-policy.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
@@ -22,14 +27,8 @@ const strict = createStrict({
fixtures: ['https://tiles.maps.eox.at/*', 'https://tiles.openfreemap.org/*'], fixtures: ['https://tiles.maps.eox.at/*', 'https://tiles.openfreemap.org/*'],
tileHosts: STRICT_TILE_HOSTS.filter((host) => host !== 'tiles.maps.eox.at'), tileHosts: STRICT_TILE_HOSTS.filter((host) => host !== 'tiles.maps.eox.at'),
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b, { shaderErrors: true });
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
const keyless = process.argv.includes('--simulation'); const keyless = process.argv.includes('--simulation');
const reads = new Map(); const reads = new Map();
@@ -73,26 +72,8 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
} }
return req.continue(); return req.continue();
}); });
p.on('console', (msg) => {
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
errors.push(msg.text());
});
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => { await waitForEngine(p, 'navara');
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await p.evaluate(async () => { await p.evaluate(async () => {
const { tileset, dataManager } = window.__godsEyeView; const { tileset, dataManager } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__) if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -199,8 +180,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
); );
for (const kind of ['navara', 'cesium', 'navara']) { for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
} }
await waitTraffic(); await waitTraffic();
const check = await p.evaluate(() => { const check = await p.evaluate(() => {
+1 -1
View File
@@ -31,7 +31,7 @@ const strict = createStrict({
'*://*.earthdata.nasa.gov/*', '*://*.earthdata.nasa.gov/*',
], ],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const p = await b.newPage();
await p.setViewport({ width: 1280, height: 900 }); await p.setViewport({ width: 1280, height: 900 });
const errors = []; const errors = [];
+1 -1
View File
@@ -38,7 +38,7 @@ const getOpt = (name, dflt) => {
}; };
const APP_URL = getOpt('--url', 'http://localhost:4173'); const APP_URL = getOpt('--url', 'http://localhost:4173');
// --engine cesium|navara (phase 3): frame, settle and read the renderer // --engine cesium|navara: frame, settle and read the renderer
// through window.__godsEyeView.engine (either engine) instead of the Cesium // through window.__godsEyeView.engine (either engine) instead of the Cesium
// viewer; the URL gets ?engine=. Without it, as before. // viewer; the URL gets ?engine=. Without it, as before.
const ENGINE = getOpt('--engine', null); const ENGINE = getOpt('--engine', null);
+1 -1
View File
@@ -19,7 +19,7 @@
* node scripts/qa-vessel-datum.mjs --ports rotterdam --url http://localhost:4173 * node scripts/qa-vessel-datum.mjs --ports rotterdam --url http://localhost:4173
* node scripts/qa-vessel-datum.mjs --engine navara --url 'http://localhost:PORT/?qaFeed=vessels:fixture' * node scripts/qa-vessel-datum.mjs --engine navara --url 'http://localhost:PORT/?qaFeed=vessels:fixture'
* *
* --engine cesium|navara (phase 3): the engine-neutral path on either engine, * --engine cesium|navara: the engine-neutral path on either engine,
* through window.__godsEyeView.engine and the QA probe (layers.drawn: each * through window.__godsEyeView.engine and the QA probe (layers.drawn: each
* chevron's drawn position and depth-test flag) instead of the Cesium * chevron's drawn position and depth-test flag) instead of the Cesium
* viewer's primitives; the URL gets ?engine=. Without it, as before. * viewer's primitives; the URL gets ?engine=. Without it, as before.
+52 -47
View File
@@ -1,19 +1,28 @@
// Recent imagery on a keyless preview, starting on Navara: the NASA catalog
// and imagery tiles are fixtures, and the image, A/B, swap, divider, opacity,
// basemap, toggle, failure, box-drag and zoom-to-fit checks read the rendered
// pixels; both pinned dates survive a switch to Cesium and back. The report
// and its screenshots go next to the evidence file (or to the temp directory).
//
// node scripts/qa-w2-imagery.mjs http://127.0.0.1:<PORT>
// [--layers=recent-imagery] [--evidence=FILE]
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import sharp from 'sharp'; import sharp from 'sharp';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
strictArguments,
} from './qa-strict-harness.mjs';
import { isExpectedPreviewSetupError, redact } from './qa-browserEvidence.mjs'; import { isExpectedPreviewSetupError, redact } from './qa-browserEvidence.mjs';
const args = process.argv.slice(2); const { origin } = localPreview(process.argv[2]);
const option = (name, fallback) => const url = origin + '/?engine=navara';
args.includes(name) ? args[args.indexOf(name) + 1] : fallback; const { evidence } = strictArguments();
const url = option('--url', 'http://127.0.0.1:4193'); const output = evidence
const output = option('--report', '/private/tmp/gev-imagery.json'); ? path.join(path.dirname(evidence), 'imagery-report.json')
if ( : path.join(os.tmpdir(), 'gev-imagery.json');
['4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
new URL(url).port,
)
)
throw new Error('Reserved QA port');
const report = { const report = {
url, url,
checks: [], checks: [],
@@ -26,7 +35,7 @@ let failLandsat = false,
// The engine the page runs, for the strict assertion manifest. // The engine the page runs, for the strict assertion manifest.
let engine = null; let engine = null;
const strict = createStrict({ const strict = createStrict({
origin: localPreview(url).origin, origin,
layers: ['recent-imagery'], layers: ['recent-imagery'],
// The scenario answers the NASA catalog and imagery tiles itself. // The scenario answers the NASA catalog and imagery tiles itself.
fixtures: ['*://*.earthdata.nasa.gov/*'], fixtures: ['*://*.earthdata.nasa.gov/*'],
@@ -48,8 +57,6 @@ const browser = await strict.launch({
headless: true, headless: true,
args: [ args: [
'--no-sandbox', '--no-sandbox',
'--use-angle=metal',
'--enable-gpu',
'--disable-background-timer-throttling', '--disable-background-timer-throttling',
'--disable-renderer-backgrounding', '--disable-renderer-backgrounding',
], ],
@@ -63,7 +70,7 @@ try {
const text = redact(message.text()); const text = redact(message.text());
const expected = const expected =
isExpectedPreviewSetupError(message, { isExpectedPreviewSetupError(message, {
preview: args.includes('--preview'), preview: true,
baseUrl: url, baseUrl: url,
}) || }) ||
(failedTiles > 0 && (failedTiles > 0 &&
@@ -231,37 +238,35 @@ try {
await sleep(2500); await sleep(2500);
image = await capture('ab'); image = await capture('ab');
check('A/B dates on opposite sides', red(image.left) && green(image.right)); check('A/B dates on opposite sides', red(image.left) && green(image.right));
if (args.includes('--switch')) { const first = await page.evaluate(() => {
const first = await page.evaluate(() => { window.qaImageryOwner = window.__gevRecentImagery.layer;
window.qaImageryOwner = window.__gevRecentImagery.layer; return window.qaImageryOwner.diagnostics().pins;
return window.qaImageryOwner.diagnostics().pins; });
}); const reads = () =>
const reads = () => report.requests.filter((r) => r.host === 'cmr.earthdata.nasa.gov').length;
report.requests.filter((r) => r.host === 'cmr.earthdata.nasa.gov').length; const expectedReads = reads();
const expectedReads = reads(); const initial = await page.evaluate(() => window.__godsEyeView.engine.kind);
const initial = await page.evaluate(() => window.__godsEyeView.engine.kind); for (const kind of [initial === 'navara' ? 'cesium' : 'navara', initial]) {
for (const kind of [initial === 'navara' ? 'cesium' : 'navara', initial]) { await strict.switchTo(page, kind);
await strict.switchTo(page, kind); engine = kind;
engine = kind; const state = await page.evaluate(() => ({
const state = await page.evaluate(() => ({ same: window.__gevRecentImagery.layer === window.qaImageryOwner,
same: window.__gevRecentImagery.layer === window.qaImageryOwner, ...window.qaImageryOwner.diagnostics(),
...window.qaImageryOwner.diagnostics(), }));
})); check(
check( kind + ' holds both original pins',
kind + ' holds both original pins', state.same &&
state.same && state.mode === 'ab' &&
state.mode === 'ab' && state.pins.a === first.a &&
state.pins.a === first.a && state.pins.b === first.b,
state.pins.b === first.b, );
); await sleep(1800);
await sleep(1800); const pixels = await capture('switch-' + kind);
const pixels = await capture('switch-' + kind); check(
check( kind + ' renders both pinned dates',
kind + ' renders both pinned dates', red(pixels.left) && green(pixels.right),
red(pixels.left) && green(pixels.right), );
); check(kind + ' reuses the STAC search', reads() === expectedReads);
check(kind + ' reuses the STAC search', reads() === expectedReads);
}
} }
await page.evaluate(() => window.__gevRecentImagery.layer.swapSides()); await page.evaluate(() => window.__gevRecentImagery.layer.swapSides());
image = await capture('swap'); image = await capture('swap');
+11 -27
View File
@@ -1,7 +1,12 @@
import { fromDegrees } from '../src/geo/cartesian3.js'; import { fromDegrees } from '../src/geo/cartesian3.js';
import sharp from 'sharp'; import sharp from 'sharp';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic'; base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -16,14 +21,8 @@ const strict = createStrict({
'weather-cyclones', 'weather-cyclones',
], ],
}); });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b);
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
const times = ['2026-11-02T12:00:00.000Z', '2026-11-02T12:05:00.000Z']; const times = ['2026-11-02T12:00:00.000Z', '2026-11-02T12:05:00.000Z'];
const ids = ['weather-radar', 'weather-satellite', 'weather-lightning']; const ids = ['weather-radar', 'weather-satellite', 'weather-lightning'];
const images = new Map(), const images = new Map(),
@@ -168,21 +167,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
console.log('GPU', msg.text().slice(0, 3000)); console.log('GPU', msg.text().slice(0, 3000));
}); });
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => { await waitForEngine(p, 'navara');
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
const waitTime = (ids, time) => const waitTime = (ids, time) =>
p.waitForFunction( p.waitForFunction(
(ids, time) => (ids, time) =>
@@ -251,8 +236,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const polls = [...reads.entries()]; const polls = [...reads.entries()];
for (const kind of ['navara', 'cesium', 'navara']) { for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
} }
await waitTime(ids, times[0]); await waitTime(ids, times[0]);
const state = await p.evaluate( const state = await p.evaluate(
@@ -364,7 +348,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
for (const kind of ['navara', 'cesium', 'navara']) { for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await p.select('#globe-engine', kind); await p.select('#globe-engine', kind);
await ready(kind); await waitForEngine(p, kind);
} }
await p.waitForFunction( await p.waitForFunction(
() => () =>
+10 -26
View File
@@ -1,6 +1,11 @@
import sharp from 'sharp'; import sharp from 'sharp';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs'; import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]); const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic'; base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -9,14 +14,8 @@ base.hash =
// The fixture field is uniform, so the GPU seeds a fixed particle count. // The fixture field is uniform, so the GPU seeds a fixed particle count.
const WIND_PATHS = 7200; const WIND_PATHS = 7200;
const strict = createStrict({ origin: base.origin, layers: ['wind'] }); const strict = createStrict({ origin: base.origin, layers: ['wind'] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await strict.run({}, async (b) => {
const p = await b.newPage(); const { page: p, errors } = await openPage(b);
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
const reads = []; const reads = [];
await p.setRequestInterception(true); await p.setRequestInterception(true);
p.on('request', (req) => { p.on('request', (req) => {
@@ -70,21 +69,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
} }
}); });
await p.goto(base.href); await p.goto(base.href);
const ready = async (kind) => { await waitForEngine(p, 'navara');
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await p.evaluate(async () => { await p.evaluate(async () => {
const { dataManager, tileset } = window.__godsEyeView; const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__) if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -103,8 +88,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const baseline = reads.length; const baseline = reads.length;
for (const kind of ['navara', 'cesium', 'navara']) { for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) { if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind); await strict.switchTo(p, kind);
await ready(kind);
} }
await p.waitForFunction( await p.waitForFunction(
() => () =>
+4 -34
View File
@@ -5,19 +5,7 @@
// the session ids, listener counts and transport counters. // the session ids, listener counts and transport counters.
import puppeteer from 'puppeteer'; import puppeteer from 'puppeteer';
import { blockPaidQaMaps } from '../qa-map-policy.mjs'; import { blockPaidQaMaps } from '../qa-map-policy.mjs';
import { localPreview, waitForEngine } from '../qa-strict-harness.mjs';
const RESERVED_PORTS = new Set([
'',
'4173',
'4174',
'4185',
'4186',
'4187',
'4188',
'4189',
'4194',
'4196',
]);
/** Paid or live model endpoints no switch gate may reach. */ /** Paid or live model endpoints no switch gate may reach. */
export const BLOCKED_URLS = Object.freeze([ export const BLOCKED_URLS = Object.freeze([
@@ -36,13 +24,7 @@ export const BLOCKED_URLS = Object.freeze([
/** The preview origin from argv, refusing the ports that belong to the user. */ /** The preview origin from argv, refusing the ports that belong to the user. */
export function previewBase(argument) { export function previewBase(argument) {
const base = new URL(argument ?? ''); return localPreview(argument ?? '');
if (
!['localhost', '127.0.0.1'].includes(base.hostname) ||
RESERVED_PORTS.has(base.port)
)
throw new Error('Supply an unreserved local preview URL');
return base;
} }
/** The page address: Boston on OSM, no welcome, on the given engine. */ /** The page address: Boston on OSM, no welcome, on the given engine. */
@@ -247,20 +229,8 @@ export async function openSessionPage(browser, address) {
} }
/** The engine runs, its picker is idle and the loading screen is gone. */ /** The engine runs, its picker is idle and the loading screen is gone. */
export async function waitReady(page, kind) { export function waitReady(page, kind) {
await page.waitForFunction( return waitForEngine(page, kind, { timeout: 90_000 });
(kind) =>
window.__godsEyeView?.engine?.kind === kind &&
document.querySelector('#globe-engine')?.value === kind &&
!document.querySelector('#globe-engine').disabled &&
document.getElementById('loading-screen')?.classList.contains('hidden') &&
!document.querySelector('.engine-switch-frame'),
{ timeout: 90_000, polling: 100 },
kind,
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
} }
/** /**
+7 -88
View File
@@ -1,14 +1,12 @@
// Map tiles for headless QA: a disk cache shared by every run (in the system // Map tiles for headless QA: the disk cache entry of a tile request and one
// temp directory) and one fetch per missing tile, with backoff on HTTP 429, // fetch per missing tile, with backoff on HTTP 429, for the free tile hosts
// for the free tile hosts the QA pages load (OpenStreetMap imagery, the // the QA pages load (OpenStreetMap imagery, the keyless re:earth terrain).
// keyless re:earth terrain). Back-to-back QA runs otherwise reload the same // Back-to-back QA runs otherwise reload the same tiles until those hosts
// tiles until those hosts rate-limit them. Only requests to these hosts are // rate-limit them. The strict harness (scripts/qa-strict-harness.mjs) pauses
// paused (CDP Fetch with URL patterns); page-wide request interception also // only the requests to these hosts (CDP Fetch with URL patterns) and answers
// held the Navara workers' requests and kept workers alive past their globe. // them from the cache in the system temp directory.
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
export const CACHED_TILE_HOSTS = Object.freeze([ export const CACHED_TILE_HOSTS = Object.freeze([
@@ -119,82 +117,3 @@ export function createMissLoader({
}, },
}; };
} }
/**
* Serves the page's requests to CACHED_TILE_HOSTS (or `hosts`) from the
* cache (filling it on a miss). Returns the counts.
*/
export async function installTileCache(
page,
{
dir = path.join(os.tmpdir(), 'gev-qa-tiles'),
hosts = CACHED_TILE_HOSTS,
} = {},
) {
fs.mkdirSync(dir, { recursive: true });
const stats = { hits: 0, fetched: 0, cancelled: 0, failed: 0, errors: {} };
const client = await page.createCDPSession();
const misses = createMissLoader();
client.on('Fetch.requestPaused', async ({ requestId, request }) => {
const accept = Object.entries(request.headers ?? {}).find(
([name]) => name.toLowerCase() === 'accept',
)?.[1];
const file =
request.method === 'GET'
? tileCacheFile(request.url, accept, dir, hosts)
: null;
try {
if (!file)
return await client.send('Fetch.continueRequest', { requestId });
let entry = null;
if (fs.existsSync(`${file}.json`)) {
entry = {
...JSON.parse(fs.readFileSync(`${file}.json`, 'utf8')),
body: fs.readFileSync(file),
};
stats.hits++;
} else {
const tile = await misses.load(request.url, accept);
// Only successes are cached; a 404 (no tile there) is passed on.
if (tile.status === 200) {
fs.writeFileSync(file, tile.body);
fs.writeFileSync(
`${file}.json`,
JSON.stringify({ status: 200, contentType: tile.contentType }),
);
stats.fetched++;
} else if (tile.status === 429) stats.failed++;
entry = tile;
}
await client.send('Fetch.fulfillRequest', {
requestId,
responseCode: entry.status,
responseHeaders: [
...(entry.contentType
? [{ name: 'Content-Type', value: entry.contentType }]
: []),
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
body: entry.body.toString('base64'),
});
} catch (error) {
// The page dropped the request meanwhile (a tile no longer in view).
if (/Invalid InterceptionId/.test(String(error?.message))) {
stats.cancelled++;
return;
}
stats.failed++;
const reason = String(error?.message ?? error).slice(0, 80);
stats.errors[reason] = (stats.errors[reason] ?? 0) + 1;
await client
.send('Fetch.failRequest', { requestId, errorReason: 'Failed' })
.catch(() => {});
}
});
await client.send('Fetch.enable', {
patterns: hosts.map((host) => ({
urlPattern: `https://${host}/*`,
})),
});
return stats;
}
+1 -1
View File
@@ -14,7 +14,7 @@ const LINE_ID = 'gev-draw-preview';
const DOTS_ID = 'gev-draw-vertices'; const DOTS_ID = 'gev-draw-vertices';
/** '#rrggbb' → rgba in 0..1. */ /** '#rrggbb' → rgba in 0..1. */
export function cssRgba(css, alpha = 1) { function cssRgba(css, alpha = 1) {
const hex = Number.parseInt(String(css).slice(1), 16); const hex = Number.parseInt(String(css).slice(1), 16);
return [ return [
((hex >> 16) & 255) / 255, ((hex >> 16) & 255) / 255,
-11
View File
@@ -68,14 +68,3 @@ export function catalogControlServices(catalog) {
}), }),
); );
} }
/**
* Layer roles for controls on a page without data layers: one empty object
* per role, so the shell's optional calls
* (`layer.method?.()`) do nothing.
*/
export function absentLayerControlServices() {
return Object.fromEntries(
Object.keys(CONTROL_LAYER_IDS).map((role) => [role, {}]),
);
}
+1 -39
View File
@@ -1,10 +1,6 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { import { createLayerCatalog, catalogControlServices } from './catalog.js';
absentLayerControlServices,
createLayerCatalog,
catalogControlServices,
} from './catalog.js';
import { createApplicationData } from './data.js'; import { createApplicationData } from './data.js';
const metadata = (layers) => const metadata = (layers) =>
@@ -158,40 +154,6 @@ test('controls bind catalog instances rather than similarly named defaults', ()
); );
}); });
test('without data layers the controls get one empty stand-in per layer role', () => {
const ids = [
'traffic',
'flights',
'military',
'satellites',
'cctv',
'street-level',
'radio',
'bikeshare',
'transit',
'ais-live-vessels',
'military-awareness',
'military-installations',
'rocket-launches',
'local-adsb',
];
const layers = ids.map((id) => ({ id }));
const roles = Object.keys(
catalogControlServices(createLayerCatalog(layers, metadata(layers))),
);
const absent = absentLayerControlServices();
assert.deepEqual(Object.keys(absent), roles);
for (const role of roles) {
assert.deepEqual(Object.keys(absent[role]), [], role);
assert.equal(absent[role].subscribe, undefined);
}
assert.notEqual(absent.flightsLayer, absent.trafficLayer);
assert.notEqual(
absentLayerControlServices().flightsLayer,
absent.flightsLayer,
);
});
test('data setup without a viewer registers an empty catalog and no sonar scene', async (t) => { test('data setup without a viewer registers an empty catalog and no sonar scene', async (t) => {
const previous = globalThis.document; const previous = globalThis.document;
globalThis.document = { getElementById: () => null }; globalThis.document = { getElementById: () => null };
+9 -9
View File
@@ -6,17 +6,17 @@ import { createAirQualityCesiumRenderer } from '../../layers/airQuality/render/c
import { createFirmsCesiumRenderer } from '../../layers/firms/render/cesium.js'; import { createFirmsCesiumRenderer } from '../../layers/firms/render/cesium.js';
import { createEarthquakesCesiumRenderer } from '../../layers/earthquakes/render/cesium.js'; import { createEarthquakesCesiumRenderer } from '../../layers/earthquakes/render/cesium.js';
import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesium.js'; import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesium.js';
// Which engine draws which layer: the layer renderer registry (plan, "layer // Which engine draws which layer: the layer renderer registry and the layer
// renderer interface") and the layer availability API built on it. // availability API built on it.
// //
// A renderer factory `(engine, options) => renderer` draws one layer on one // A renderer factory `(engine, options) => renderer` draws one layer on one
// engine kind (`render/cesium.js`, `render/navara.js` of a layer). Cesium // engine kind (`render/cesium.js`, `render/navara.js` of a layer). Cesium
// layers still draw themselves until their render/cesium.js is extracted // layers without a render/cesium.js draw themselves: they are registered as
// (the *a tasks): they are registered as CESIUM_BUILTIN. Cesium entries are // CESIUM_BUILTIN. Cesium entries are registered here, statically (main
// registered here, statically (main chunk); Navara factories are registered // chunk); Navara factories are registered by the Navara mode after its
// by the Navara mode after its import() (src/app/navaraMode.js). // import() (src/app/navaraMode.js).
// //
// ## Layer availability (stable API, used by phase 4's layer list) // ## Layer availability (stable API, used by the layer list)
// //
// import { createLayerAvailability } from './app/layers/renderers.js'; // import { createLayerAvailability } from './app/layers/renderers.js';
// const availability = createLayerAvailability(engine.kind); // const availability = createLayerAvailability(engine.kind);
@@ -28,8 +28,8 @@ import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesiu
// The object is frozen; available() and unavailable() return fresh arrays. // The object is frozen; available() and unavailable() return fresh arrays.
// It reads the registry when called, so a Navara factory registered later // It reads the registry when called, so a Navara factory registered later
// (after the Navara mode loads) shows up. `name` is the layer's display name // (after the Navara mode loads) shows up. `name` is the layer's display name
// (src/app/layers/directory.js). Phase 4 injects it as its `availability` // (src/app/layers/directory.js). The layer list takes it as its
// provider (`{ engine, unavailable() }` is the part it reads). // `availability` provider (`{ engine, unavailable() }` is the part it reads).
import { APPLICATION_LAYER_DIRECTORY } from './directory.js'; import { APPLICATION_LAYER_DIRECTORY } from './directory.js';
import { createFlightsCesiumRenderer } from '../../layers/flights/render/cesium.js'; import { createFlightsCesiumRenderer } from '../../layers/flights/render/cesium.js';
+2 -2
View File
@@ -14,7 +14,7 @@
// drawn(id) what the layer draws per object (glyph, model), // drawn(id) what the layer draws per object (glyph, model),
// as plain data, or null (layers with qaDrawn) // as plain data, or null (layers with qaDrawn)
// following() { label, native } of engine.camera.following() // following() { label, native } of engine.camera.following()
// movers() engine.movers.list() // movers() engine.movers.list() ([] without movers: Cesium)
// framesDrawn() frames drawn since the probe started // framesDrawn() frames drawn since the probe started
const plain = (position) => const plain = (position) =>
@@ -83,7 +83,7 @@ export function createQaProbe({ engine, dataManager, availability }) {
native: handle.native?.id ?? null, native: handle.native?.id ?? null,
}; };
}, },
movers: () => engine.movers.list(), movers: () => engine.movers?.list() ?? [],
framesDrawn: () => frames, framesDrawn: () => frames,
destroy: () => removeFrames(), destroy: () => removeFrames(),
}; };
+5 -5
View File
@@ -217,11 +217,11 @@ async function createRenderer({
googleTileset: tileset, googleTileset: tileset,
cesiumToken, cesiumToken,
initialStack: tileset ? 'photoreal' : 'esri-imagery', initialStack: tileset ? 'photoreal' : 'esri-imagery',
// Task 5 (height-datum fix): rebroadcast stack changes as a window // Rebroadcast stack changes as a window CustomEvent so data layers (CCTV
// CustomEvent so data layers (CCTV per-regime ground resolution) can // per-regime ground resolution) can react without coupling
// react without coupling MapStackController to layer modules. Fires on // MapStackController to layer modules. Fires on 'switching'/'ready'/
// 'switching'/'ready'/'error'; listeners derive the surface regime from // 'error'; listeners derive the surface regime from live scene state, so
// live scene state, so intermediate emissions are harmless. // intermediate emissions are harmless.
onChange: (state) => { onChange: (state) => {
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('gev:map-stack-changed', { detail: state }), new CustomEvent('gev:map-stack-changed', { detail: state }),
-9
View File
@@ -9,13 +9,6 @@
// a release that runs, in reverse, when the part detaches (before its // a release that runs, in reverse, when the part detaches (before its
// detachScene). // detachScene).
const STATES = Object.freeze([
'detached',
'attaching',
'attached',
'detaching',
]);
export class SceneSwitchingError extends Error { export class SceneSwitchingError extends Error {
constructor(message = 'The globe is switching engines') { constructor(message = 'The globe is switching engines') {
super(message); super(message);
@@ -208,5 +201,3 @@ export function createSceneHost({ onError = null } = {}) {
}, },
}); });
} }
export const SCENE_HOST_STATES = STATES;
+1 -1
View File
@@ -327,7 +327,7 @@ function attachViewerTools(
); );
// Install the explicit scope mask used by the DISPLAY controls. // Install the explicit scope mask used by the DISPLAY controls.
installScopeMask(viewer); installScopeMask(engine);
defer(() => destroyScopeMask()); defer(() => destroyScopeMask());
// The follow camera recomputes the tracked target's dead-reckon position // The follow camera recomputes the tracked target's dead-reckon position
+1 -1
View File
@@ -9,7 +9,7 @@ export const PITCH_CLAMP_MIN_HEIGHT_M = 300_000;
// with the first Cesium globe, after this module (src/app/loadCesium.js). // with the first Cesium globe, after this module (src/app/loadCesium.js).
const RADIANS_PER_DEGREE = Math.PI / 180; const RADIANS_PER_DEGREE = Math.PI / 180;
/** Look this far below the Earth's limb, so the globe stays on screen. */ /** Look this far below the Earth's limb, so the globe stays on screen. */
export const LIMB_MARGIN_RAD = 8 * RADIANS_PER_DEGREE; const LIMB_MARGIN_RAD = 8 * RADIANS_PER_DEGREE;
const PITCH_EASE = 0.35; const PITCH_EASE = 0.35;
const PITCH_SNAP_RAD = 0.1 * RADIANS_PER_DEGREE; const PITCH_SNAP_RAD = 0.1 * RADIANS_PER_DEGREE;
const HEIGHT_EPSILON_M = 0.0001; const HEIGHT_EPSILON_M = 0.0001;
+1 -1
View File
@@ -1,6 +1,6 @@
// Deterministic Launch Library 2 records for phase-3 mission/replay acceptance. // Deterministic Launch Library 2 records for phase-3 mission/replay acceptance.
import { SATELLITE_FIXTURE_EPOCH } from './satelliteFixture.js'; import { SATELLITE_FIXTURE_EPOCH } from './satelliteFixture.js';
export const LAUNCH_FIXTURE_EPOCH = SATELLITE_FIXTURE_EPOCH; const LAUNCH_FIXTURE_EPOCH = SATELLITE_FIXTURE_EPOCH;
export function launchFixture() { export function launchFixture() {
const definitions = [ const definitions = [
[ [
+1 -3
View File
@@ -1,4 +1,4 @@
// Deterministic TLE catalogs for phase 3 browser QA; no live feed requests. // Deterministic TLE catalogs for browser QA; no live feed requests.
import { W1_FROZEN_MS } from './w1Feeds.js'; import { W1_FROZEN_MS } from './w1Feeds.js';
const checksum = (line) => { const checksum = (line) => {
@@ -30,8 +30,6 @@ const GROUPS = {
starlink: [50000, 6000, 53, 15.2, 'QA STARLINK'], starlink: [50000, 6000, 53, 15.2, 'QA STARLINK'],
}; };
export const SATELLITE_FIXTURE_EPOCH = W1_FROZEN_MS; export const SATELLITE_FIXTURE_EPOCH = W1_FROZEN_MS;
export const SATELLITE_FIXTURE_CORE_COUNT = 839;
export const SATELLITE_FIXTURE_DENSE_COUNT = 6839;
export function satelliteFixture(group) { export function satelliteFixture(group) {
const spec = GROUPS[group]; const spec = GROUPS[group];
if (!spec) throw new TypeError('Unknown fixture group'); if (!spec) throw new TypeError('Unknown fixture group');
+1
View File
@@ -57,6 +57,7 @@ function fakeViewer({ cameraHeightM, surfaceM, canvas = null }) {
const position = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, cameraHeightM); const position = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, cameraHeightM);
const viewer = { const viewer = {
flights, flights,
canvas,
scene: { scene: {
canvas, canvas,
sampleHeightSupported: true, sampleHeightSupported: true,
+1 -1
View File
@@ -400,7 +400,7 @@ test('vessel and fire layers announce valid clicks and never fly cameras', () =>
assert.doesNotMatch(source, /camera\.flyTo/); assert.doesNotMatch(source, /camera\.flyTo/);
} }
// Vessels' click rule is a function the engine's selection dispatcher // Vessels' click rule is a function the engine's selection dispatcher
// calls (phase 3, 13a): the same body, out of the old LEFT_CLICK handler. // calls: the same body, out of the old LEFT_CLICK handler.
const vesselClick = body( const vesselClick = body(
vessels, vessels,
/function handleClick\(click\) \{([\s\S]*?)\n \}/, /function handleClick\(click\) \{([\s\S]*?)\n \}/,
-14
View File
@@ -2370,17 +2370,3 @@ export function flyRoute(
}; };
return typeof runNavigation === 'function' ? runNavigation(start) : start(); return typeof runNavigation === 'function' ? runNavigation(start) : start();
} }
// QA (development builds only; production builds drop it): the verbs and
// their diagnostics, so a browser scenario can drive them on an engine whose
// voice tools do not offer them yet.
if (import.meta.env?.DEV && typeof window !== 'undefined')
window.__gevCameraVerbs = Object.freeze({
cameraVerbsSupported,
flyRoute,
getActiveCameraMotion,
initCameraVerbs,
interruptCameraMotion,
moveCamera,
releaseCameraVerbs,
});
+1 -3
View File
@@ -261,10 +261,8 @@ const TRACKED_RASTER_PX = 192;
* Default size serves the fleet; pass `aircraftIcon(kind, TRACKED_ICON_PX)` * Default size serves the fleet; pass `aircraftIcon(kind, TRACKED_ICON_PX)`
* (re-exported below) for the tracked billboard. */ * (re-exported below) for the tracked billboard. */
export const TRACKED_ICON_PX = TRACKED_RASTER_PX; export const TRACKED_ICON_PX = TRACKED_RASTER_PX;
/** The fleet raster size (aircraftIcon's default). */
export const FLEET_ICON_PX = FLEET_RASTER_PX;
/** Every silhouette kind aircraftIcon draws (an engine's sprite atlas). */ /** Every silhouette kind aircraftIcon draws (an engine's sprite atlas). */
export const AIRCRAFT_ICON_KINDS = Object.freeze(Object.keys(BODIES)); const AIRCRAFT_ICON_KINDS = Object.freeze(Object.keys(BODIES));
export function aircraftIcon(kind, px = FLEET_RASTER_PX) { export function aircraftIcon(kind, px = FLEET_RASTER_PX) {
const k = BODIES[kind] ? kind : 'airliner'; const k = BODIES[kind] ? kind : 'airliner';
const key = `${k}@${px}`; const key = `${k}@${px}`;
+2 -2
View File
@@ -2,8 +2,8 @@ import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
/** /**
* Arrival corrections belong to one globe engine and yield to its next camera * Arrival corrections belong to one globe engine and yield to its next camera
* owner. Callers may name the engine or (layers until phase 1c) the viewer it * owner. Callers may name the engine or (layers that still hold one) the
* is registered for; both reach the same correction. * viewer it is registered for; both reach the same correction.
*/ */
const arrivals = new WeakMap(); const arrivals = new WeakMap();
+1 -1
View File
@@ -29,7 +29,7 @@ for (const layer of LAYERS) {
test(`${layer.name}: every GLB creation bypasses the tile-contended frame-spread queue`, () => { test(`${layer.name}: every GLB creation bypasses the tile-contended frame-spread queue`, () => {
// The aircraft renderer loads through its `loadModel`, Cesium.Model.fromGltfAsync // The aircraft renderer loads through its `loadModel`, Cesium.Model.fromGltfAsync
// unless a test injects a loader (phase 3). // unless a test injects a loader.
assert.match(source, /loadModel = \(options\) => Cesium\.Model\.fromGltfAsync\(options\)/); assert.match(source, /loadModel = \(options\) => Cesium\.Model\.fromGltfAsync\(options\)/);
const calls = [...source.matchAll(/(?:Cesium\.Model\.fromGltfAsync|\bloadModel)\(\{([\s\S]*?)\}\)/g)]; const calls = [...source.matchAll(/(?:Cesium\.Model\.fromGltfAsync|\bloadModel)\(\{([\s\S]*?)\}\)/g)];
assert.ok(calls.length >= 3, `expected fleet, tracked, and preload model calls; found ${calls.length}`); assert.ok(calls.length >= 3, `expected fleet, tracked, and preload model calls; found ${calls.length}`);
+2 -2
View File
@@ -34,7 +34,7 @@ export const OPENMAPTILES_CREDIT = {
key: 'openfreemap', key: 'openfreemap',
html: 'Vector tiles: <a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a> © <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>', html: 'Vector tiles: <a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a> © <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>',
}; };
export const GOOGLE_PLACES_CREDIT = { const GOOGLE_PLACES_CREDIT = {
key: 'google-maps-places', key: 'google-maps-places',
html: 'Google Maps Places: <a href="https://developers.google.com/maps/documentation/places/web-service/policies" target="_blank" rel="noopener">Google Maps</a>', html: 'Google Maps Places: <a href="https://developers.google.com/maps/documentation/places/web-service/policies" target="_blank" rel="noopener">Google Maps</a>',
}; };
@@ -45,7 +45,7 @@ const viewerCredits = new WeakMap();
/** /**
* Credits of a globe engine, or of a Cesium viewer's credit display for * Credits of a globe engine, or of a Cesium viewer's credit display for
* callers that still hold a viewer (layers until phase 1c); null without one. * callers that still hold a viewer (Cesium layers); null without one.
* @returns {{ addStatic: Function, request: () => void } | null} * @returns {{ addStatic: Function, request: () => void } | null}
*/ */
function creditsOf(target) { function creditsOf(target) {
+1 -1
View File
@@ -143,7 +143,7 @@ test('every scene click handler consults ownership before it picks', () => {
'src/layers/submarineCables/interaction.js', 'src/layers/submarineCables/interaction.js',
'viewer.scene.pick(click.position)', 'viewer.scene.pick(click.position)',
], ],
// Vessels take the engine dispatcher's pick (phase 3, 13a): the guard // Vessels take the engine dispatcher's pick: the guard
// comes before the click rule reads it. // comes before the click rule reads it.
['src/layers/vessels/selection.js', 'resolvePickId(picked)'], ['src/layers/vessels/selection.js', 'resolvePickId(picked)'],
]; ];
+1 -1
View File
@@ -140,7 +140,7 @@ test('sampler on a globe engine: samples engine.surface and latches against the
assert.equal(cachedMeshFloor(cell.lat, cell.lon), 137.0); assert.equal(cachedMeshFloor(cell.lat, cell.lon), 137.0);
const [call] = engine.fake.callsOf('surface.sampleHeightDetailed'); const [call] = engine.fake.callsOf('surface.sampleHeightDetailed');
assert.ok(Math.abs(Cesium.Math.toDegrees(call.args[0].latitude) - cell.lat) < 1e-9); assert.ok(Math.abs(Cesium.Math.toDegrees(call.args[0].latitude) - cell.lat) < 1e-9);
assert.deepEqual(call.args[1], { exclude: ['own'], lod: 'visible' }); assert.deepEqual(call.args[1], { exclude: ['own'] });
}); });
test('sampler: a coarse tile answer is a miss; a fine one latches', async () => { test('sampler: a coarse tile answer is a miss; a fine one latches', async () => {
+1 -2
View File
@@ -1,7 +1,6 @@
// Native constants shared by the W1 adapters; keep the SDK import here. // Native constants shared by the W1 adapters; keep the SDK import here.
// Read on call: the production build's Cesium global loads with the first // Read on call: the production build's Cesium global loads with the first
// Cesium globe, after this module has loaded on a Navara start. // Cesium globe, after this module has loaded on a Navara start.
import { Axis, ScreenSpaceEventType } from 'cesium'; import { ScreenSpaceEventType } from 'cesium';
export const modelAxis = () => Axis;
export const trackingEventTypes = () => ScreenSpaceEventType; export const trackingEventTypes = () => ScreenSpaceEventType;
+3 -3
View File
@@ -1,6 +1,6 @@
import { nearbyContact } from './pickRegistry.js'; import { nearbyContact } from './pickRegistry.js';
// One selection dispatcher for the layers that have moved onto the globe // One selection dispatcher for the layers that draw through the globe
// engine (phase 3 Task 4): one click subscription with picks // engine: one click subscription with picks
// (engine.input.on('click', …, { pick: true, drill: 8 })), the press // (engine.input.on('click', …, { pick: true, drill: 8 })), the press
// gesture and one Escape listener, instead of a ScreenSpaceEventHandler // gesture and one Escape listener, instead of a ScreenSpaceEventHandler
// per layer. Owners register as with pickRegistry (registerPickOwner, so // per layer. Owners register as with pickRegistry (registerPickOwner, so
@@ -38,7 +38,7 @@ import {
isTrackingSelectionGesture, isTrackingSelectionGesture,
} from './trackingClickGesture.js'; } from './trackingClickGesture.js';
export const SELECTION_DRILL = 8; const SELECTION_DRILL = 8;
/** /**
* @param {import('../globe/contract.js').GlobeEngine} engine * @param {import('../globe/contract.js').GlobeEngine} engine
+1 -1
View File
@@ -109,7 +109,7 @@ test('flights, AIS, and FIRMS enable paths are wired through the shared sprite r
assert.deepEqual(calls, [viewer, viewer, viewer]); assert.deepEqual(calls, [viewer, viewer, viewer]);
const firmsLayer = createFirmsHeatmapLayer({ id: 'firms', name: 'FIRMS' }); const firmsLayer = createFirmsHeatmapLayer({ id: 'firms', name: 'FIRMS' });
// Flights draws through its renderer (phase 3): enable asks the fleet to // Flights draws through its renderer: enable asks the fleet to
// restore its order, which the Cesium aircraft renderer wires to the shared // restore its order, which the Cesium aircraft renderer wires to the shared
// restorer under the flights' sprite slot. // restorer under the flights' sprite slot.
assert.match(flightsLayer.enable.toString(), /fleet\.restoreOrderOnEnable\(\)/); assert.match(flightsLayer.enable.toString(), /fleet\.restoreOrderOnEnable\(\)/);
+1 -1
View File
@@ -114,7 +114,7 @@ test('civilian and military click handlers apply duration only at the deselect b
readLayerSource(new URL('./flights.js', import.meta.url)), readLayerSource(new URL('./flights.js', import.meta.url)),
readLayerSource(new URL('./militaryFlights.js', import.meta.url)), readLayerSource(new URL('./militaryFlights.js', import.meta.url)),
]; ];
// Flights and military moved to the selection dispatcher (phase 3), whose // Flights and military use the selection dispatcher, whose
// source applies the same two rules: the selection gesture before a // source applies the same two rules: the selection gesture before a
// select, the full click classifier before the empty-space deselect, which // select, the full click classifier before the empty-space deselect, which
// each layer answers with a user-origin clear. // each layer answers with a user-origin clear.
+7 -9
View File
@@ -1,12 +1,15 @@
// Seeded, deterministic bit-for-bit comparison of src/geo against Cesium. // Seeded, deterministic bit-for-bit comparison of src/geo against Cesium's
// release build (Build/Cesium, debug checks stripped, as production ships it).
// Every number is compared with Object.is (so -0 and NaN count), a throw on one // Every number is compared with Object.is (so -0 and NaN count), a throw on one
// side must be a throw on the other, and `result` aliasing is exercised the // side must be a throw on the other, and `result` aliasing is exercised the
// same way on both sides. // same way on both sides.
import { test } from 'node:test'; import { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import * as Cesium from 'cesium'; import { createRequire } from 'node:module';
import * as geo from './index.js'; import * as geo from './index.js';
const Cesium = createRequire(import.meta.url)('cesium/Build/Cesium/index.cjs');
const { const {
cartesian3: c3, cartesian3: c3,
cartographic, cartographic,
@@ -108,7 +111,7 @@ const cesium = (v) => new C3(v.x, v.y, v.z);
test('vector operations, including -0 inputs and aliased results, equal Cesium bit for bit', () => { test('vector operations, including -0 inputs and aliased results, equal Cesium bit for bit', () => {
const { scalar, vector } = inputs(1); const { scalar, vector } = inputs(1);
// Directionless vectors first: normalize must throw on both sides. // Directionless vectors first: normalize returns NaN on both sides.
const DEGENERATE = [ const DEGENERATE = [
{ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 },
{ x: -0, y: 0, z: -0 }, { x: -0, y: 0, z: -0 },
@@ -397,12 +400,7 @@ test('matrices and quaternions, including aliased results and every fromRotation
][i % 4]; ][i % 4];
const angle = const angle =
i % 8 < 4 ? Math.PI - 0.5 * random() : (random() - 0.5) * 2 * Math.PI; i % 8 < 4 ? Math.PI - 0.5 * random() : (random() - 0.5) * 2 * Math.PI;
let m; const m = rotation(axis, angle);
try {
m = rotation(axis, angle);
} catch {
continue; // a zero axis: Cesium throws, nothing to compare
}
const trace = m[0] + m[4] + m[8]; const trace = m[0] + m[4] + m[8];
branches.add( branches.add(
trace > 0 trace > 0
-7
View File
@@ -1,8 +1,6 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — BoundingSphere.js subset. // Ported from CesiumJS 1.138 (Apache-2.0) — BoundingSphere.js subset.
// Exact mission framing: Ritter/box fit and the union of two spheres. // Exact mission framing: Ritter/box fit and the union of two spheres.
import * as Cartesian3 from './cartesian3.js'; import * as Cartesian3 from './cartesian3.js';
import { Check } from './check.js';
import { DEBUG } from './debug.js';
const defined = (value) => value !== null && value !== undefined; const defined = (value) => value !== null && value !== undefined;
export function create(center = Cartesian3.ZERO, radius = 0) { export function create(center = Cartesian3.ZERO, radius = 0) {
return { center: Cartesian3.clone(center), radius }; return { center: Cartesian3.clone(center), radius };
@@ -184,11 +182,6 @@ export function fromPoints(positions, result) {
return result; return result;
} }
export function union(left, right, result) { export function union(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
if (!defined(result)) { if (!defined(result)) {
result = create(); result = create();
} }
+1 -11
View File
@@ -1,17 +1,12 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — Cartesian2.js // Ported from CesiumJS 1.138 (Apache-2.0) — Cartesian2.js
// //
// 2D vectors (screen coordinates) as plain-data {x, y} objects. With DEBUG on // 2D vectors (screen coordinates) as plain-data {x, y} objects.
// (development and tests; see debug.js) invalid arguments throw the same
// DeveloperError as Cesium's debug checks.
// //
// Geo values are not Object.prototype objects, so src/app/stateChannel.js // Geo values are not Object.prototype objects, so src/app/stateChannel.js
// copyState and src/director/documentFields.js jsonTree reject them: copy // copyState and src/director/documentFields.js jsonTree reject them: copy
// the fields into a literal before putting one into product state or a // the fields into a literal before putting one into product state or a
// scene document. // scene document.
import { DEBUG } from './debug.js';
import { Check } from './check.js';
// Values are instances of a module-private constructor rather than object // Values are instances of a module-private constructor rather than object
// literals: every `{x, y}` literal in the process shares one V8 hidden // literals: every `{x, y}` literal in the process shares one V8 hidden
// class, and code elsewhere that builds such literals would make geo's hot // class, and code elsewhere that builds such literals would make geo's hot
@@ -38,11 +33,6 @@ export function clone(cartesian, result) {
/** The distance between two points. */ /** The distance between two points. */
export function distance(left, right) { export function distance(left, right) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
const x = left.x - right.x; const x = left.x - right.x;
const y = left.y - right.y; const y = left.y - right.y;
return Math.sqrt(x * x + y * y); return Math.sqrt(x * x + y * y);
+3 -144
View File
@@ -3,19 +3,14 @@
// 3D vectors as plain-data {x, y, z} objects. Arithmetic takes a required `result` // 3D vectors as plain-data {x, y, z} objects. Arithmetic takes a required `result`
// (as Cesium does) that may be one of the inputs; creators take an optional // (as Cesium does) that may be one of the inputs; creators take an optional
// one. The operation order matches Cesium so the outputs are bit-identical. // one. The operation order matches Cesium so the outputs are bit-identical.
// With DEBUG on (development and tests; see debug.js) invalid arguments throw // As in release Cesium, arguments are not checked and `normalize` returns NaN
// the same DeveloperError as Cesium's debug checks, and `normalize` throws on a // components for a zero or non-finite vector.
// zero or non-finite vector; production builds skip the checks and return NaN
// components there, as release Cesium does.
// //
// Geo values are not Object.prototype objects, so src/app/stateChannel.js // Geo values are not Object.prototype objects, so src/app/stateChannel.js
// copyState and src/director/documentFields.js jsonTree reject them: copy // copyState and src/director/documentFields.js jsonTree reject them: copy
// the fields into a literal before putting one into product state or a // the fields into a literal before putting one into product state or a
// scene document. // scene document.
import { DEBUG } from './debug.js';
import { Check } from './check.js';
import { DeveloperError } from './developerError.js';
import { equalsEpsilon as scalarEqualsEpsilon, toRadians } from './math.js'; import { equalsEpsilon as scalarEqualsEpsilon, toRadians } from './math.js';
// Values are instances of a module-private constructor rather than object // Values are instances of a module-private constructor rather than object
@@ -59,10 +54,6 @@ export const UNIT_Y = Object.freeze(create(0.0, 1.0, 0.0));
export const UNIT_Z = Object.freeze(create(0.0, 0.0, 1.0)); export const UNIT_Z = Object.freeze(create(0.0, 0.0, 1.0));
export function magnitudeSquared(cartesian) { export function magnitudeSquared(cartesian) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
}
return ( return (
cartesian.x * cartesian.x + cartesian.x * cartesian.x +
cartesian.y * cartesian.y + cartesian.y * cartesian.y +
@@ -77,60 +68,29 @@ export function magnitude(cartesian) {
const distanceScratch = create(); const distanceScratch = create();
export function distance(left, right) { export function distance(left, right) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
subtract(left, right, distanceScratch); subtract(left, right, distanceScratch);
return magnitude(distanceScratch); return magnitude(distanceScratch);
} }
export function distanceSquared(left, right) { export function distanceSquared(left, right) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
subtract(left, right, distanceScratch); subtract(left, right, distanceScratch);
return magnitudeSquared(distanceScratch); return magnitudeSquared(distanceScratch);
} }
/** The unit vector; with DEBUG on, throws when the vector has no direction (zero or non-finite). */ /** The unit vector; NaN components when the vector has no direction (zero or non-finite). */
export function normalize(cartesian, result) { export function normalize(cartesian, result) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
Check.typeOf.object('result', result);
}
const length = magnitude(cartesian); const length = magnitude(cartesian);
result.x = cartesian.x / length; result.x = cartesian.x / length;
result.y = cartesian.y / length; result.y = cartesian.y / length;
result.z = cartesian.z / length; result.z = cartesian.z / length;
if (DEBUG) {
if (isNaN(result.x) || isNaN(result.y) || isNaN(result.z)) {
throw new DeveloperError('normalized result is not a number');
}
}
return result; return result;
} }
export function dot(left, right) { export function dot(left, right) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
return left.x * right.x + left.y * right.y + left.z * right.z; return left.x * right.x + left.y * right.y + left.z * right.z;
} }
export function multiplyComponents(left, right, result) { export function multiplyComponents(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
Check.typeOf.object('result', result);
}
result.x = left.x * right.x; result.x = left.x * right.x;
result.y = left.y * right.y; result.y = left.y * right.y;
result.z = left.z * right.z; result.z = left.z * right.z;
@@ -138,12 +98,6 @@ export function multiplyComponents(left, right, result) {
} }
export function add(left, right, result) { export function add(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
Check.typeOf.object('result', result);
}
result.x = left.x + right.x; result.x = left.x + right.x;
result.y = left.y + right.y; result.y = left.y + right.y;
result.z = left.z + right.z; result.z = left.z + right.z;
@@ -151,12 +105,6 @@ export function add(left, right, result) {
} }
export function subtract(left, right, result) { export function subtract(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
Check.typeOf.object('result', result);
}
result.x = left.x - right.x; result.x = left.x - right.x;
result.y = left.y - right.y; result.y = left.y - right.y;
result.z = left.z - right.z; result.z = left.z - right.z;
@@ -164,12 +112,6 @@ export function subtract(left, right, result) {
} }
export function multiplyByScalar(cartesian, scalar, result) { export function multiplyByScalar(cartesian, scalar, result) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
Check.typeOf.number('scalar', scalar);
Check.typeOf.object('result', result);
}
result.x = cartesian.x * scalar; result.x = cartesian.x * scalar;
result.y = cartesian.y * scalar; result.y = cartesian.y * scalar;
result.z = cartesian.z * scalar; result.z = cartesian.z * scalar;
@@ -177,12 +119,6 @@ export function multiplyByScalar(cartesian, scalar, result) {
} }
export function divideByScalar(cartesian, scalar, result) { export function divideByScalar(cartesian, scalar, result) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
Check.typeOf.number('scalar', scalar);
Check.typeOf.object('result', result);
}
result.x = cartesian.x / scalar; result.x = cartesian.x / scalar;
result.y = cartesian.y / scalar; result.y = cartesian.y / scalar;
result.z = cartesian.z / scalar; result.z = cartesian.z / scalar;
@@ -190,11 +126,6 @@ export function divideByScalar(cartesian, scalar, result) {
} }
export function negate(cartesian, result) { export function negate(cartesian, result) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
Check.typeOf.object('result', result);
}
result.x = -cartesian.x; result.x = -cartesian.x;
result.y = -cartesian.y; result.y = -cartesian.y;
result.z = -cartesian.z; result.z = -cartesian.z;
@@ -204,25 +135,12 @@ export function negate(cartesian, result) {
const lerpScratch = create(); const lerpScratch = create();
export function lerp(start, end, t, result) { export function lerp(start, end, t, result) {
if (DEBUG) {
Check.typeOf.object('start', start);
Check.typeOf.object('end', end);
Check.typeOf.number('t', t);
Check.typeOf.object('result', result);
}
multiplyByScalar(end, t, lerpScratch); multiplyByScalar(end, t, lerpScratch);
result = multiplyByScalar(start, 1.0 - t, result); result = multiplyByScalar(start, 1.0 - t, result);
return add(lerpScratch, result, result); return add(lerpScratch, result, result);
} }
export function cross(left, right, result) { export function cross(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
Check.typeOf.object('result', result);
}
const leftX = left.x; const leftX = left.x;
const leftY = left.y; const leftY = left.y;
const leftZ = left.z; const leftZ = left.z;
@@ -241,12 +159,6 @@ export function cross(left, right, result) {
} }
export function midpoint(left, right, result) { export function midpoint(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
Check.typeOf.object('result', result);
}
result.x = (left.x + right.x) * 0.5; result.x = (left.x + right.x) * 0.5;
result.y = (left.y + right.y) * 0.5; result.y = (left.y + right.y) * 0.5;
result.z = (left.z + right.z) * 0.5; result.z = (left.z + right.z) * 0.5;
@@ -292,11 +204,6 @@ let scratchK = create();
/** The Earth-fixed position of a geodetic one in radians on the ellipsoid (WGS84 by default). */ /** The Earth-fixed position of a geodetic one in radians on the ellipsoid (WGS84 by default). */
export function fromRadians(longitude, latitude, height, ellipsoid, result) { export function fromRadians(longitude, latitude, height, ellipsoid, result) {
if (DEBUG) {
Check.typeOf.number('longitude', longitude);
Check.typeOf.number('latitude', latitude);
}
height = height ?? 0.0; height = height ?? 0.0;
const radiiSquared = const radiiSquared =
ellipsoid == null ? WGS84_RADII_SQUARED : ellipsoid.radiiSquared; ellipsoid == null ? WGS84_RADII_SQUARED : ellipsoid.radiiSquared;
@@ -318,11 +225,6 @@ export function fromRadians(longitude, latitude, height, ellipsoid, result) {
/** The Earth-fixed position of a geodetic one in degrees on the ellipsoid (WGS84 by default). */ /** The Earth-fixed position of a geodetic one in degrees on the ellipsoid (WGS84 by default). */
export function fromDegrees(longitude, latitude, height, ellipsoid, result) { export function fromDegrees(longitude, latitude, height, ellipsoid, result) {
if (DEBUG) {
Check.typeOf.number('longitude', longitude);
Check.typeOf.number('latitude', latitude);
}
longitude = toRadians(longitude); longitude = toRadians(longitude);
latitude = toRadians(latitude); latitude = toRadians(latitude);
return fromRadians(longitude, latitude, height, ellipsoid, result); return fromRadians(longitude, latitude, height, ellipsoid, result);
@@ -330,15 +232,6 @@ export function fromDegrees(longitude, latitude, height, ellipsoid, result) {
/** Positions from a flat [lon, lat, lon, lat, …] list in degrees, at height 0. */ /** Positions from a flat [lon, lat, lon, lat, …] list in degrees, at height 0. */
export function fromDegreesArray(coordinates, ellipsoid, result) { export function fromDegreesArray(coordinates, ellipsoid, result) {
if (DEBUG) {
Check.defined('coordinates', coordinates);
if (coordinates.length < 2 || coordinates.length % 2 !== 0) {
throw new DeveloperError(
'the number of coordinates must be a multiple of 2 and at least 2',
);
}
}
const length = coordinates.length; const length = coordinates.length;
if (result == null) { if (result == null) {
result = new Array(length / 2); result = new Array(length / 2);
@@ -358,43 +251,9 @@ export function fromDegreesArray(coordinates, ellipsoid, result) {
return result; return result;
} }
/** Positions from a flat [lon, lat, height, …] list in degrees and metres. */
export function fromDegreesArrayHeights(coordinates, ellipsoid, result) {
if (DEBUG) {
Check.defined('coordinates', coordinates);
if (coordinates.length < 3 || coordinates.length % 3 !== 0) {
throw new DeveloperError(
'the number of coordinates must be a multiple of 3 and at least 3',
);
}
}
const length = coordinates.length;
if (result == null) {
result = new Array(length / 3);
} else {
result.length = length / 3;
}
for (let i = 0; i < length; i += 3) {
const index = i / 3;
result[index] = fromDegrees(
coordinates[i],
coordinates[i + 1],
coordinates[i + 2],
ellipsoid,
result[index],
);
}
return result;
}
const angleBetweenScratch = create(); const angleBetweenScratch = create();
const angleBetweenScratch2 = create(); const angleBetweenScratch2 = create();
export function angleBetween(left, right) { export function angleBetween(left, right) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
normalize(left, angleBetweenScratch); normalize(left, angleBetweenScratch);
normalize(right, angleBetweenScratch2); normalize(right, angleBetweenScratch2);
const cosine = dot(angleBetweenScratch, angleBetweenScratch2); const cosine = dot(angleBetweenScratch, angleBetweenScratch2);
+2 -90
View File
@@ -21,78 +21,16 @@ const same = (ours, theirs, label) =>
label, label,
); );
test('binary vector ops equal Cesium bit for bit', () => { test('magnitudeSquared equals Cesium', () => {
for (const a of SAMPLES)
for (const b of SAMPLES) {
for (const op of ['add', 'subtract', 'cross', 'multiplyComponents']) {
same(
c3[op](asPlain(a), asPlain(b), c3.create()),
C[op](asCesium(a), asCesium(b), new C()),
`${op}`,
);
}
assert.equal(
c3.dot(asPlain(a), asPlain(b)),
C.dot(asCesium(a), asCesium(b)),
);
assert.equal(
c3.distance(asPlain(a), asPlain(b)),
C.distance(asCesium(a), asCesium(b)),
);
assert.equal(
c3.distanceSquared(asPlain(a), asPlain(b)),
C.distanceSquared(asCesium(a), asCesium(b)),
);
same(
c3.lerp(asPlain(a), asPlain(b), 0.3, c3.create()),
C.lerp(asCesium(a), asCesium(b), 0.3, new C()),
'lerp',
);
same(
c3.midpoint(asPlain(a), asPlain(b), c3.create()),
C.midpoint(asCesium(a), asCesium(b), new C()),
'midpoint',
);
assert.equal(
c3.equalsEpsilon(asPlain(a), asPlain(b), 1e-7),
C.equalsEpsilon(asCesium(a), asCesium(b), 1e-7),
);
}
});
test('unary ops and scalars equal Cesium', () => {
for (const a of SAMPLES) { for (const a of SAMPLES) {
assert.equal(c3.magnitude(asPlain(a)), C.magnitude(asCesium(a)));
assert.equal( assert.equal(
c3.magnitudeSquared(asPlain(a)), c3.magnitudeSquared(asPlain(a)),
C.magnitudeSquared(asCesium(a)), C.magnitudeSquared(asCesium(a)),
); );
same(
c3.negate(asPlain(a), c3.create()),
C.negate(asCesium(a), new C()),
'negate',
);
same(
c3.multiplyByScalar(asPlain(a), 2.5, c3.create()),
C.multiplyByScalar(asCesium(a), 2.5, new C()),
'mul',
);
same(
c3.divideByScalar(asPlain(a), 4, c3.create()),
C.divideByScalar(asCesium(a), 4, new C()),
'div',
);
if (C.magnitude(asCesium(a)) !== 0) {
same(
c3.normalize(asPlain(a), c3.create()),
C.normalize(asCesium(a), new C()),
'normalize',
);
}
} }
}); });
test('geodetic constructors equal Cesium', () => { test('fromRadians and fromDegreesArray equal Cesium', () => {
for (const [lon, lat, h] of [ for (const [lon, lat, h] of [
[0, 0, 0], [0, 0, 0],
[-73.9857, 40.7484, 381], [-73.9857, 40.7484, 381],
@@ -100,11 +38,6 @@ test('geodetic constructors equal Cesium', () => {
[180, -90, 1e4], [180, -90, 1e4],
[12.5, 89.999999, 1], [12.5, 89.999999, 1],
]) { ]) {
same(
c3.fromDegrees(lon, lat, h),
C.fromDegrees(lon, lat, h),
`fromDegrees ${lon},${lat}`,
);
const lr = [Cesium.Math.toRadians(lon), Cesium.Math.toRadians(lat), h]; const lr = [Cesium.Math.toRadians(lon), Cesium.Math.toRadians(lat), h];
same(c3.fromRadians(...lr), C.fromRadians(...lr), 'fromRadians'); same(c3.fromRadians(...lr), C.fromRadians(...lr), 'fromRadians');
} }
@@ -112,10 +45,6 @@ test('geodetic constructors equal Cesium', () => {
c3.fromDegreesArray(flat).forEach((p, i) => c3.fromDegreesArray(flat).forEach((p, i) =>
same(p, C.fromDegreesArray(flat)[i], `array ${i}`), same(p, C.fromDegreesArray(flat)[i], `array ${i}`),
); );
const heights = [-74, 40.7, 10, -73.9, 40.8, 20];
c3.fromDegreesArrayHeights(heights).forEach((p, i) =>
same(p, C.fromDegreesArrayHeights(heights)[i], `heights ${i}`),
);
}); });
test('result aliasing works like Cesium and creators reuse result', () => { test('result aliasing works like Cesium and creators reuse result', () => {
@@ -138,20 +67,3 @@ test('plain results are accepted by Cesium functions (duck typing)', () => {
), ),
); );
}); });
test('normalize throws on a zero or NaN vector, as Cesium does when run from source', () => {
for (const [x, y, z] of [
[0, 0, 0],
[-0, 0, -0],
[Number.NaN, 1, 2],
]) {
assert.throws(
() => C.normalize(new C(x, y, z), new C()),
/normalized result is not a number/,
);
assert.throws(
() => c3.normalize({ x, y, z }, c3.create()),
/normalized result is not a number/,
);
}
});
+1 -19
View File
@@ -1,17 +1,13 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — Cartographic.js // Ported from CesiumJS 1.138 (Apache-2.0) — Cartographic.js
// //
// Geodetic positions as plain-data {longitude, latitude, height} objects (radians and // Geodetic positions as plain-data {longitude, latitude, height} objects (radians and
// metres). Every function takes an optional `result` and returns it. With // metres). Every function takes an optional `result` and returns it.
// DEBUG on (development and tests; see debug.js) invalid arguments throw the
// same DeveloperError as Cesium's debug checks.
// //
// Geo values are not Object.prototype objects, so src/app/stateChannel.js // Geo values are not Object.prototype objects, so src/app/stateChannel.js
// copyState and src/director/documentFields.js jsonTree reject them: copy // copyState and src/director/documentFields.js jsonTree reject them: copy
// the fields into a literal before putting one into product state or a // the fields into a literal before putting one into product state or a
// scene document. // scene document.
import { DEBUG } from './debug.js';
import { Check } from './check.js';
import { toRadians } from './math.js'; import { toRadians } from './math.js';
import * as cartesian3 from './cartesian3.js'; import * as cartesian3 from './cartesian3.js';
import { scaleToGeodeticSurface } from './scaleToGeodeticSurface.js'; import { scaleToGeodeticSurface } from './scaleToGeodeticSurface.js';
@@ -51,11 +47,6 @@ const WGS84_CENTER_TOLERANCE_SQUARED = 0.1;
/** A geodetic position from radians. */ /** A geodetic position from radians. */
export function fromRadians(longitude, latitude, height, result) { export function fromRadians(longitude, latitude, height, result) {
if (DEBUG) {
Check.typeOf.number('longitude', longitude);
Check.typeOf.number('latitude', latitude);
}
height = height ?? 0.0; height = height ?? 0.0;
if (result == null) return create(longitude, latitude, height); if (result == null) return create(longitude, latitude, height);
result.longitude = longitude; result.longitude = longitude;
@@ -66,11 +57,6 @@ export function fromRadians(longitude, latitude, height, result) {
/** A geodetic position from degrees; the result is in radians. */ /** A geodetic position from degrees; the result is in radians. */
export function fromDegrees(longitude, latitude, height, result) { export function fromDegrees(longitude, latitude, height, result) {
if (DEBUG) {
Check.typeOf.number('longitude', longitude);
Check.typeOf.number('latitude', latitude);
}
longitude = toRadians(longitude); longitude = toRadians(longitude);
latitude = toRadians(latitude); latitude = toRadians(latitude);
return fromRadians(longitude, latitude, height, result); return fromRadians(longitude, latitude, height, result);
@@ -133,10 +119,6 @@ export function fromCartesian(cartesian, ellipsoid, result) {
* where Cesium uses Ellipsoid.default; gev never changes it). * where Cesium uses Ellipsoid.default; gev never changes it).
*/ */
export function toCartesian(cartographic, ellipsoid, result) { export function toCartesian(cartographic, ellipsoid, result) {
if (DEBUG) {
Check.defined('cartographic', cartographic);
}
return cartesian3.fromRadians( return cartesian3.fromRadians(
cartographic.longitude, cartographic.longitude,
cartographic.latitude, cartographic.latitude,
-29
View File
@@ -4,12 +4,6 @@ import assert from 'node:assert/strict';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as cartographic from './cartographic.js'; import * as cartographic from './cartographic.js';
const close = (a, b, tol, label) =>
assert.ok(
Math.abs(a - b) <= tol * Math.max(1, Math.abs(b)),
`${label}: ${a} vs ${b}`,
);
test('fromDegrees and fromRadians equal Cesium', () => { test('fromDegrees and fromRadians equal Cesium', () => {
for (const [lon, lat, h] of [ for (const [lon, lat, h] of [
[0, 0, 0], [0, 0, 0],
@@ -30,29 +24,6 @@ test('fromDegrees and fromRadians equal Cesium', () => {
} }
}); });
test('fromCartesian and toCartesian equal Cesium, including the centre case', () => {
for (const [x, y, z] of [
[1334762.2, -4654869.1, 4137287.9],
[6378137, 0, 0],
[0.01, 0, 0],
]) {
const ours = cartographic.fromCartesian({ x, y, z });
const theirs = Cesium.Cartographic.fromCartesian(
new Cesium.Cartesian3(x, y, z),
);
if (theirs === undefined) {
assert.equal(ours, undefined);
continue;
}
close(ours.longitude, theirs.longitude, 1e-12, 'lon');
close(ours.latitude, theirs.latitude, 1e-12, 'lat');
close(ours.height, theirs.height, 1e-9, 'h');
const back = cartographic.toCartesian(ours);
const cBack = Cesium.Cartographic.toCartesian(theirs);
for (const k of ['x', 'y', 'z']) close(back[k], cBack[k], 1e-12, k);
}
});
test('result objects are reused', () => { test('result objects are reused', () => {
const out = cartographic.create(); const out = cartographic.create();
assert.equal(cartographic.fromDegrees(1, 2, 3, out), out); assert.equal(cartographic.fromDegrees(1, 2, 3, out), out);
-61
View File
@@ -1,61 +0,0 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — Check.js
//
// Cesium's argument checks with the same conditions and message text. Only the
// checks src/geo needs are ported. Callers run them inside `if (DEBUG)` blocks
// (see debug.js), as Cesium runs them inside its debug pragmas, so production
// builds drop the calls and this module with them.
import { DeveloperError } from './developerError.js';
const undefinedMessage = (name) =>
`${name} is required, actual value was undefined`;
const typeMessage = (actual, expected, name) =>
`Expected ${name} to be typeof ${expected}, actual typeof was ${actual}`;
/** Throws unless `test` is neither undefined nor null. */
function defined(name, test) {
if (test === undefined || test === null) {
throw new DeveloperError(undefinedMessage(name));
}
}
/** Throws unless `typeof test` is 'number' (NaN passes, as in Cesium). */
function number(name, test) {
if (typeof test !== 'number') {
throw new DeveloperError(typeMessage(typeof test, 'number', name));
}
}
// Attached in a function marked pure, not by top-level assignments, so that a
// production build, where no caller runs a check, drops this module entirely.
function withBounds(check) {
check.lessThanOrEquals = function (name, test, limit) {
check(name, test);
if (test > limit) {
throw new DeveloperError(
`Expected ${name} to be less than or equal to ${limit}, actual value was ${test}`,
);
}
};
check.greaterThanOrEquals = function (name, test, limit) {
check(name, test);
if (test < limit) {
throw new DeveloperError(
`Expected ${name} to be greater than or equal to ${limit}, actual value was ${test}`,
);
}
};
return check;
}
/** Throws unless `typeof test` is 'object' (null passes, as in Cesium). */
function object(name, test) {
if (typeof test !== 'object') {
throw new DeveloperError(typeMessage(typeof test, 'object', name));
}
}
export const Check = /* @__PURE__ */ Object.freeze({
defined,
typeOf: Object.freeze({ number: /* @__PURE__ */ withBounds(number), object }),
});
-486
View File
@@ -1,486 +0,0 @@
// Argument-check parity with Cesium. gev runs Cesium from source, where its
// debug checks are live, so for any input both sides must throw an error with
// the same name and message, or neither may throw. The inputs are seeded and
// deterministic: each case builds its arguments twice from the same seed, once
// for geo and once for Cesium, so no object is shared between the two sides.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import * as geo from './index.js';
import { DeveloperError } from './developerError.js';
const TRIALS = 150;
function generator(seed) {
return () => {
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Invalid values every argument may receive. NaN is a number to Check.typeOf.number
// and null an object to Check.typeOf.object, so both reach the arithmetic.
const INVALID = [undefined, null, Number.NaN, 'abc', 5, true, {}];
const vectorValues = { x: 1.5, y: -2, z: 3 };
const VALID = {
num: (side, random) => [0, -0, 0.7, -2.5, 1e6][Math.floor(random() * 5)],
index3: (side, random) => Math.floor(random() * 5) - 1,
index4: (side, random) => Math.floor(random() * 6) - 1,
nonNegative: (side, random) => [0, 3, 6378137, -1][Math.floor(random() * 4)],
vec: (side) =>
side === 'geo' ? { ...vectorValues } : new Cesium.Cartesian3(1.5, -2, 3),
zeroVec: (side) =>
side === 'geo' ? { x: 0, y: 0, z: 0 } : new Cesium.Cartesian3(),
vec4: (side) =>
side === 'geo' ? { x: 0, y: 0, z: 0, w: 0 } : new Cesium.Cartesian4(),
carto: (side) =>
side === 'geo'
? { longitude: 0.3, latitude: -0.2, height: 10 }
: new Cesium.Cartographic(0.3, -0.2, 10),
partialCarto: (side) =>
side === 'geo'
? { longitude: 0.3, latitude: -0.2 }
: { longitude: 0.3, latitude: -0.2 },
quat: (side) =>
side === 'geo'
? { x: 0.1, y: 0.2, z: 0.3, w: 0.9 }
: new Cesium.Quaternion(0.1, 0.2, 0.3, 0.9),
mat3: (side) =>
side === 'geo'
? geo.matrix3.fromRotationZ(0.4)
: Cesium.Matrix3.fromRotationZ(0.4, new Cesium.Matrix3()),
mat4: (side) =>
side === 'geo'
? geo.transforms.eastNorthUpToFixedFrame({ x: 6378137, y: 1, z: 2 })
: Cesium.Transforms.eastNorthUpToFixedFrame(
new Cesium.Cartesian3(6378137, 1, 2),
),
hpr: (side) =>
side === 'geo'
? { heading: 0.3, pitch: -0.2, roll: 0.1 }
: new Cesium.HeadingPitchRoll(0.3, -0.2, 0.1),
partialHpr: () => ({ heading: 0.3 }),
ellipsoid: (side) =>
side === 'geo' ? geo.ellipsoid.WGS84 : Cesium.Ellipsoid.WGS84,
coords2: (side, random) =>
[[1, 2], [1, 2, 3], [], [1, 2, 3, 4]][Math.floor(random() * 4)],
coords3: (side, random) =>
[[1, 2, 3], [1, 2], [], [1, 2, 3, 4, 5, 6], [1, 2, 'h']][
Math.floor(random() * 5)
],
nanVec: (side) =>
side === 'geo'
? { x: 1, y: Number.NaN, z: 2 }
: new Cesium.Cartesian3(1, Number.NaN, 2),
partialVec: () => ({ x: 1, y: 2 }),
stringHeight: () => '5',
};
/** One argument: usually a valid value of its kind, sometimes an invalid one. */
function argument(kinds, side, random) {
if (random() < 0.4) return INVALID[Math.floor(random() * INVALID.length)];
const kind = kinds[Math.floor(random() * kinds.length)];
return VALID[kind](side, random);
}
const W = Cesium.Ellipsoid.WGS84;
const C3 = Cesium.Cartesian3;
const V = ['vec', 'zeroVec', 'nanVec', 'partialVec'];
const N = ['num'];
const R = ['vec'];
// [name, geo function, Cesium function, argument kinds per position]
const CASES = [
['math.toRadians', geo.math.toRadians, Cesium.Math.toRadians, [N]],
['math.toDegrees', geo.math.toDegrees, Cesium.Math.toDegrees, [N]],
['math.clamp', geo.math.clamp, Cesium.Math.clamp, [N, N, N]],
['math.lerp', geo.math.lerp, Cesium.Math.lerp, [N, N, N]],
['math.mod', geo.math.mod, Cesium.Math.mod, [N, ['num', 'nonNegative']]],
['math.zeroToTwoPi', geo.math.zeroToTwoPi, Cesium.Math.zeroToTwoPi, [N]],
[
'math.negativePiToPi',
geo.math.negativePiToPi,
Cesium.Math.negativePiToPi,
[N],
],
[
'math.equalsEpsilon',
geo.math.equalsEpsilon,
Cesium.Math.equalsEpsilon,
[N, N, N, N],
],
['cartesian2.clone', geo.cartesian2.clone, Cesium.Cartesian2.clone, [R, R]],
[
'cartesian2.distance',
geo.cartesian2.distance,
Cesium.Cartesian2.distance,
[R, R],
],
['cartesian3.clone', geo.cartesian3.clone, C3.clone, [V, R]],
[
'cartesian3.fromElements',
geo.cartesian3.fromElements,
C3.fromElements,
[N, N, N, R],
],
['cartesian3.magnitude', geo.cartesian3.magnitude, C3.magnitude, [V]],
[
'cartesian3.magnitudeSquared',
geo.cartesian3.magnitudeSquared,
C3.magnitudeSquared,
[V],
],
['cartesian3.distance', geo.cartesian3.distance, C3.distance, [V, V]],
[
'cartesian3.distanceSquared',
geo.cartesian3.distanceSquared,
C3.distanceSquared,
[V, V],
],
['cartesian3.normalize', geo.cartesian3.normalize, C3.normalize, [V, R]],
['cartesian3.dot', geo.cartesian3.dot, C3.dot, [V, V]],
[
'cartesian3.multiplyComponents',
geo.cartesian3.multiplyComponents,
C3.multiplyComponents,
[V, V, R],
],
['cartesian3.add', geo.cartesian3.add, C3.add, [V, V, R]],
['cartesian3.subtract', geo.cartesian3.subtract, C3.subtract, [V, V, R]],
[
'cartesian3.multiplyByScalar',
geo.cartesian3.multiplyByScalar,
C3.multiplyByScalar,
[V, N, R],
],
[
'cartesian3.divideByScalar',
geo.cartesian3.divideByScalar,
C3.divideByScalar,
[V, N, R],
],
['cartesian3.negate', geo.cartesian3.negate, C3.negate, [V, R]],
['cartesian3.lerp', geo.cartesian3.lerp, C3.lerp, [V, V, N, R]],
['cartesian3.cross', geo.cartesian3.cross, C3.cross, [V, V, R]],
['cartesian3.midpoint', geo.cartesian3.midpoint, C3.midpoint, [V, V, R]],
['cartesian3.equals', geo.cartesian3.equals, C3.equals, [V, V]],
[
'cartesian3.equalsEpsilon',
geo.cartesian3.equalsEpsilon,
C3.equalsEpsilon,
[V, V, N, N],
],
[
'cartesian3.fromRadians',
geo.cartesian3.fromRadians,
C3.fromRadians,
[N, N, ['num', 'stringHeight'], ['ellipsoid'], R],
],
[
'cartesian3.fromDegrees',
geo.cartesian3.fromDegrees,
C3.fromDegrees,
[N, N, ['num', 'stringHeight'], ['ellipsoid'], R],
],
[
'cartesian3.fromDegreesArray',
geo.cartesian3.fromDegreesArray,
C3.fromDegreesArray,
[['coords2'], ['ellipsoid']],
],
[
'cartesian3.fromDegreesArrayHeights',
geo.cartesian3.fromDegreesArrayHeights,
C3.fromDegreesArrayHeights,
[['coords3'], ['ellipsoid']],
],
[
'cartographic.fromRadians',
geo.cartographic.fromRadians,
Cesium.Cartographic.fromRadians,
[N, N, N, ['carto']],
],
[
'cartographic.fromDegrees',
geo.cartographic.fromDegrees,
Cesium.Cartographic.fromDegrees,
[N, N, N, ['carto']],
],
[
'cartographic.fromCartesian',
geo.cartographic.fromCartesian,
Cesium.Cartographic.fromCartesian,
[V, ['ellipsoid'], ['carto']],
],
[
'cartographic.toCartesian',
geo.cartographic.toCartesian,
Cesium.Cartographic.toCartesian,
[['carto', 'partialCarto'], ['ellipsoid'], R],
],
[
'cartographic.clone',
geo.cartographic.clone,
Cesium.Cartographic.clone,
[['carto'], ['carto']],
],
[
'ellipsoid.create',
geo.ellipsoid.create,
(x, y, z) => new Cesium.Ellipsoid(x, y, z),
[['nonNegative'], ['nonNegative'], ['nonNegative']],
],
[
'WGS84.geodeticSurfaceNormal',
geo.ellipsoid.WGS84.geodeticSurfaceNormal,
(c, r) => W.geodeticSurfaceNormal(c, r),
[V, R],
],
[
'WGS84.geodeticSurfaceNormalCartographic',
geo.ellipsoid.WGS84.geodeticSurfaceNormalCartographic,
(c, r) => W.geodeticSurfaceNormalCartographic(c, r),
[['carto', 'partialCarto'], R],
],
[
'WGS84.scaleToGeodeticSurface',
geo.ellipsoid.WGS84.scaleToGeodeticSurface,
(c, r) => W.scaleToGeodeticSurface(c, r),
[V, R],
],
[
'WGS84.cartographicToCartesian',
geo.ellipsoid.WGS84.cartographicToCartesian,
(c, r) => W.cartographicToCartesian(c, r),
[['carto', 'partialCarto'], R],
],
[
'WGS84.cartesianToCartographic',
geo.ellipsoid.WGS84.cartesianToCartographic,
(c, r) => W.cartesianToCartographic(c, r),
[V, ['carto']],
],
[
'matrix3.fromQuaternion',
geo.matrix3.fromQuaternion,
Cesium.Matrix3.fromQuaternion,
[['quat'], ['mat3']],
],
[
'matrix3.fromRotationZ',
geo.matrix3.fromRotationZ,
Cesium.Matrix3.fromRotationZ,
[N, ['mat3']],
],
[
'matrix3.multiplyByVector',
geo.matrix3.multiplyByVector,
Cesium.Matrix3.multiplyByVector,
[['mat3'], V, R],
],
[
'matrix3.getColumn',
geo.matrix3.getColumn,
Cesium.Matrix3.getColumn,
[['mat3'], ['index3'], R],
],
[
'matrix4.clone',
geo.matrix4.clone,
Cesium.Matrix4.clone,
[['mat4'], ['mat4']],
],
[
'matrix4.equals',
geo.matrix4.equals,
Cesium.Matrix4.equals,
[['mat4'], ['mat4']],
],
[
'matrix4.multiply',
geo.matrix4.multiply,
Cesium.Matrix4.multiply,
[['mat4'], ['mat4'], ['mat4']],
],
[
'matrix4.multiplyTransformation',
geo.matrix4.multiplyTransformation,
Cesium.Matrix4.multiplyTransformation,
[['mat4'], ['mat4'], ['mat4']],
],
[
'matrix4.multiplyByPoint',
geo.matrix4.multiplyByPoint,
Cesium.Matrix4.multiplyByPoint,
[['mat4'], V, R],
],
[
'matrix4.multiplyByPointAsVector',
geo.matrix4.multiplyByPointAsVector,
Cesium.Matrix4.multiplyByPointAsVector,
[['mat4'], V, R],
],
[
'matrix4.getTranslation',
geo.matrix4.getTranslation,
Cesium.Matrix4.getTranslation,
[['mat4'], R],
],
[
'matrix4.getMatrix3',
geo.matrix4.getMatrix3,
Cesium.Matrix4.getMatrix3,
[['mat4'], ['mat3']],
],
[
'matrix4.getColumn',
geo.matrix4.getColumn,
Cesium.Matrix4.getColumn,
[['mat4'], ['index4'], ['vec4']],
],
[
'matrix4.inverseTransformation',
geo.matrix4.inverseTransformation,
Cesium.Matrix4.inverseTransformation,
[['mat4'], ['mat4']],
],
[
'matrix4.fromRotationTranslation',
geo.matrix4.fromRotationTranslation,
Cesium.Matrix4.fromRotationTranslation,
[['mat3'], R, ['mat4']],
],
[
'matrix4.fromUniformScale',
geo.matrix4.fromUniformScale,
Cesium.Matrix4.fromUniformScale,
[N, ['mat4']],
],
[
'quaternion.fromAxisAngle',
geo.quaternion.fromAxisAngle,
Cesium.Quaternion.fromAxisAngle,
[V, N, ['quat']],
],
[
'quaternion.fromRotationMatrix',
geo.quaternion.fromRotationMatrix,
Cesium.Quaternion.fromRotationMatrix,
[['mat3'], ['quat']],
],
[
'transforms.eastNorthUpToFixedFrame',
geo.transforms.eastNorthUpToFixedFrame,
Cesium.Transforms.eastNorthUpToFixedFrame,
[V, ['ellipsoid'], ['mat4']],
],
[
'transforms.headingPitchRollToFixedFrame',
geo.transforms.headingPitchRollToFixedFrame,
Cesium.Transforms.headingPitchRollToFixedFrame,
[V, ['hpr', 'partialHpr'], ['ellipsoid'], [], ['mat4']],
],
];
function outcome(fn, args) {
try {
fn(...args);
return 'returned';
} catch (error) {
return `${error?.name}: ${error?.message}`;
}
}
test('every ported function throws exactly where Cesium does, with the same name and message', () => {
let thrown = 0;
let checked = 0;
const mismatches = [];
CASES.forEach(([name, ours, theirs, kinds], caseIndex) => {
for (let trial = 0; trial < TRIALS; trial++) {
const seed = caseIndex * 100003 + trial;
// Trial 0 passes no arguments at all; the rest draw from the pools.
const build = (side) => {
if (trial === 0) return [];
const random = generator(seed);
return kinds.map((k) =>
k.length ? argument(k, side, random) : undefined,
);
};
const a = outcome(ours, build('geo'));
const b = outcome(theirs, build('cesium'));
if (a !== b)
mismatches.push(`${name} (seed ${seed}): geo ${a} | Cesium ${b}`);
checked++;
if (b !== 'returned') thrown++;
}
});
assert.deepEqual(mismatches, []);
// The pools must actually reach the checks.
assert.ok(thrown > checked / 4, `only ${thrown} of ${checked} cases threw`);
});
test('the checks throw a DeveloperError with Cesium message text', () => {
const cases = [
[() => geo.math.toRadians(undefined), 'degrees is required.'],
[() => geo.math.mod(5, 0), 'divisor cannot be 0.'],
[
() => geo.math.clamp('1', 0, 1),
'Expected value to be typeof number, actual typeof was string',
],
[
() => geo.cartesian3.fromDegrees(undefined, 1),
'Expected longitude to be typeof number, actual typeof was undefined',
],
[
() => geo.cartesian3.add({ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }),
'Expected result to be typeof object, actual typeof was undefined',
],
[
() => geo.cartesian3.fromDegreesArray([1]),
'the number of coordinates must be a multiple of 2 and at least 2',
],
[
() => geo.cartesian3.fromDegreesArray(),
'coordinates is required, actual value was undefined',
],
[
() => geo.ellipsoid.create(-1, 1, 1),
'Expected x to be greater than or equal to 0, actual value was -1',
],
[
() => geo.matrix3.getColumn(geo.matrix3.create(), 3, {}),
'Expected index to be less than or equal to 2, actual value was 3',
],
[
() =>
geo.transforms.eastNorthUpToFixedFrame({ x: 1, y: Number.NaN, z: 0 }),
'origin has a NaN component',
],
[
() => geo.cartographic.fromCartesian({ x: 1, y: 2, z: 3 }, {}),
'oneOverRadii is required.',
],
];
for (const [fn, message] of cases) {
assert.throws(fn, (error) => {
assert.ok(error instanceof DeveloperError);
assert.equal(error.name, 'DeveloperError');
assert.equal(error.message, message);
return true;
});
}
// NaN is typeof number, so Check.typeOf.number lets it through (as in Cesium).
assert.ok(Number.isNaN(geo.math.clamp(Number.NaN, 0, 1)));
assert.ok(Number.isNaN(Cesium.Math.clamp(Number.NaN, 0, 1)));
});
-12
View File
@@ -1,12 +0,0 @@
// Whether src/geo runs Cesium's debug argument checks.
//
// Cesium wraps its argument checks in `//>>includeStart('debug')` pragmas: they
// are live in its Source modules (Node tests, the Vite dev server) and stripped
// from Build/Cesium, which vite-plugin-cesium copies into production builds.
// src/geo does the same: every Check call and every debug throw sits in an
// `if (DEBUG)` block. DEBUG is true in Node (no import.meta.env) and in Vite
// dev; `vite build` folds it to false and the minifier drops the blocks, so a
// production build returns what release Cesium returns (NaN, not a throw, for
// a degenerate input).
export const DEBUG = import.meta.env?.PROD !== true;
-13
View File
@@ -1,13 +0,0 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — DeveloperError.js
//
// The error Cesium throws for invalid arguments. Its debug argument checks are
// live in Cesium's Source modules (Node tests, the Vite dev server) and
// stripped from production builds; src/geo throws the same name and message
// wherever Cesium does while DEBUG is on (see debug.js).
export class DeveloperError extends Error {
constructor(message) {
super(message);
this.name = 'DeveloperError';
}
}
+1 -22
View File
@@ -3,14 +3,9 @@
// A quadratic surface x²/a² + y²/b² + z²/c² = 1 as a plain, frozen object. // A quadratic surface x²/a² + y²/b² + z²/c² = 1 as a plain, frozen object.
// Every method takes an optional `result` and returns it (a new object when it // Every method takes an optional `result` and returns it (a new object when it
// is absent); `result` may be the input. The algorithms and the operation order // is absent); `result` may be the input. The algorithms and the operation order
// match Cesium so the outputs are bit-identical. With DEBUG on (development // match Cesium so the outputs are bit-identical.
// and tests; see debug.js) invalid arguments throw the same DeveloperError as
// Cesium's debug checks.
import { DEBUG } from './debug.js';
import { Check } from './check.js';
import { create as createCartographic } from './cartographic.js'; import { create as createCartographic } from './cartographic.js';
import { DeveloperError } from './developerError.js';
import { EPSILON14 } from './math.js'; import { EPSILON14 } from './math.js';
import { scaleToGeodeticSurface } from './scaleToGeodeticSurface.js'; import { scaleToGeodeticSurface } from './scaleToGeodeticSurface.js';
import { import {
@@ -39,11 +34,6 @@ export function create(x, y, z) {
x = x ?? 0.0; x = x ?? 0.0;
y = y ?? 0.0; y = y ?? 0.0;
z = z ?? 0.0; z = z ?? 0.0;
if (DEBUG) {
Check.typeOf.number.greaterThanOrEquals('x', x, 0.0);
Check.typeOf.number.greaterThanOrEquals('y', y, 0.0);
Check.typeOf.number.greaterThanOrEquals('z', z, 0.0);
}
const radii = Object.freeze(vec(x, y, z)); const radii = Object.freeze(vec(x, y, z));
const radiiSquared = Object.freeze(vec(x * x, y * y, z * z)); const radiiSquared = Object.freeze(vec(x * x, y * y, z * z));
@@ -68,10 +58,6 @@ export function create(x, y, z) {
/** The normal to the ellipsoid surface at a geodetic position. */ /** The normal to the ellipsoid surface at a geodetic position. */
function geodeticSurfaceNormalCartographic(cartographic, result) { function geodeticSurfaceNormalCartographic(cartographic, result) {
if (DEBUG) {
Check.typeOf.object('cartographic', cartographic);
}
const longitude = cartographic.longitude; const longitude = cartographic.longitude;
const latitude = cartographic.latitude; const latitude = cartographic.latitude;
const cosLatitude = Math.cos(latitude); const cosLatitude = Math.cos(latitude);
@@ -89,13 +75,6 @@ export function create(x, y, z) {
/** The normal to the ellipsoid surface at a position, or `undefined` at the centre. */ /** The normal to the ellipsoid surface at a position, or `undefined` at the centre. */
function geodeticSurfaceNormal(cartesian, result) { function geodeticSurfaceNormal(cartesian, result) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
if (isNaN(cartesian.x) || isNaN(cartesian.y) || isNaN(cartesian.z)) {
throw new DeveloperError('cartesian has a NaN component');
}
}
if (equalsEpsilon(cartesian, ZERO, EPSILON14)) { if (equalsEpsilon(cartesian, ZERO, EPSILON14)) {
return undefined; return undefined;
} }
-45
View File
@@ -12,18 +12,6 @@ const close = (a, b, tol, label) =>
const closeVec = (a, b, tol, label) => { const closeVec = (a, b, tol, label) => {
for (const k of ['x', 'y', 'z']) close(a[k], b[k], tol, `${label}.${k}`); for (const k of ['x', 'y', 'z']) close(a[k], b[k], tol, `${label}.${k}`);
}; };
const POINTS = [
[6378137, 0, 0],
[0, 6378137, 0],
[0, 0, 6356752.314245],
[1334762.2, -4654869.1, 4137287.9],
[-2694045, -4293642, 3857878],
[1e3, 1e3, 1e3],
[0.01, 0.01, 0.01],
[7e6, -2e6, 1e6],
[4510000, 4510000, 1],
[1, 1, 6400000],
];
test('WGS84 constants equal Cesium', () => { test('WGS84 constants equal Cesium', () => {
const c = Cesium.Ellipsoid.WGS84; const c = Cesium.Ellipsoid.WGS84;
@@ -40,39 +28,6 @@ test('WGS84 constants equal Cesium', () => {
assert.equal(WGS84.maximumRadius, c.maximumRadius); assert.equal(WGS84.maximumRadius, c.maximumRadius);
}); });
test('surface normals, scaling and conversions equal Cesium', () => {
const c = Cesium.Ellipsoid.WGS84;
for (const [x, y, z] of POINTS) {
const p = { x, y, z };
const cp = new Cesium.Cartesian3(x, y, z);
closeVec(
WGS84.geodeticSurfaceNormal(p),
c.geodeticSurfaceNormal(cp),
1e-12,
'normal',
);
const ours = WGS84.scaleToGeodeticSurface(p);
const theirs = c.scaleToGeodeticSurface(cp);
if (theirs === undefined) assert.equal(ours, undefined);
else closeVec(ours, theirs, 1e-12, 'scale');
const carto = WGS84.cartesianToCartographic(p);
const cCarto = c.cartesianToCartographic(cp);
if (cCarto === undefined) {
assert.equal(carto, undefined);
} else {
close(carto.longitude, cCarto.longitude, 1e-12, 'lon');
close(carto.latitude, cCarto.latitude, 1e-12, 'lat');
close(carto.height, cCarto.height, 1e-9, 'height');
closeVec(
WGS84.cartographicToCartesian(carto),
c.cartographicToCartesian(cCarto),
1e-12,
'roundtrip',
);
}
}
});
test('result parameters are filled in place and may alias the input', () => { test('result parameters are filled in place and may alias the input', () => {
const p = { x: 1334762.2, y: -4654869.1, z: 4137287.9 }; const p = { x: 1334762.2, y: -4654869.1, z: 4137287.9 };
const out = { x: 0, y: 0, z: 0 }; const out = { x: 0, y: 0, z: 0 };
-48
View File
@@ -1,11 +1,8 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — EllipsoidGeodesic.js. // Ported from CesiumJS 1.138 (Apache-2.0) — EllipsoidGeodesic.js.
// Vincenty inverse and surface interpolation, preserving operation order. // Vincenty inverse and surface interpolation, preserving operation order.
import * as Cartesian3 from './cartesian3.js';
import * as Cartographic from './cartographic.js'; import * as Cartographic from './cartographic.js';
import * as Ellipsoid from './ellipsoid.js'; import * as Ellipsoid from './ellipsoid.js';
import * as CesiumMath from './math.js'; import * as CesiumMath from './math.js';
import { Check } from './check.js';
import { DEBUG } from './debug.js';
const defined = (value) => value !== null && value !== undefined; const defined = (value) => value !== null && value !== undefined;
function setConstants(ellipsoidGeodesic) { function setConstants(ellipsoidGeodesic) {
const uSquared = ellipsoidGeodesic._uSquared; const uSquared = ellipsoidGeodesic._uSquared;
@@ -230,29 +227,7 @@ function vincentyInverseFormula(
ellipsoidGeodesic._uSquared = uSquared; ellipsoidGeodesic._uSquared = uSquared;
} }
const scratchCart1 = Cartesian3.create();
const scratchCart2 = Cartesian3.create();
function computeProperties(ellipsoidGeodesic, start, end, ellipsoid) { function computeProperties(ellipsoidGeodesic, start, end, ellipsoid) {
const firstCartesian = Cartesian3.normalize(
ellipsoid.cartographicToCartesian(start, scratchCart2),
scratchCart1,
);
const lastCartesian = Cartesian3.normalize(
ellipsoid.cartographicToCartesian(end, scratchCart2),
scratchCart2,
);
if (DEBUG) {
Check.typeOf.number.greaterThanOrEquals(
'value',
Math.abs(
Math.abs(Cartesian3.angleBetween(firstCartesian, lastCartesian)) -
Math.PI,
),
0.0125,
);
}
vincentyInverseFormula( vincentyInverseFormula(
ellipsoidGeodesic, ellipsoidGeodesic,
ellipsoid.maximumRadius, ellipsoid.maximumRadius,
@@ -322,10 +297,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
*/ */
surfaceDistance: { surfaceDistance: {
get: function () { get: function () {
if (DEBUG) {
Check.defined('distance', this._distance);
}
return this._distance; return this._distance;
}, },
}, },
@@ -362,10 +333,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
*/ */
startHeading: { startHeading: {
get: function () { get: function () {
if (DEBUG) {
Check.defined('distance', this._distance);
}
return this._startHeading; return this._startHeading;
}, },
}, },
@@ -378,10 +345,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
*/ */
endHeading: { endHeading: {
get: function () { get: function () {
if (DEBUG) {
Check.defined('distance', this._distance);
}
return this._endHeading; return this._endHeading;
}, },
}, },
@@ -394,11 +357,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
* @param {Cartographic} end The final planetodetic point on the path. * @param {Cartographic} end The final planetodetic point on the path.
*/ */
EllipsoidGeodesic.prototype.setEndPoints = function (start, end) { EllipsoidGeodesic.prototype.setEndPoints = function (start, end) {
if (DEBUG) {
Check.defined('start', start);
Check.defined('end', end);
}
computeProperties(this, start, end, this._ellipsoid); computeProperties(this, start, end, this._ellipsoid);
}; };
@@ -425,17 +383,11 @@ EllipsoidGeodesic.prototype.interpolateUsingFraction = function (
* @param {number} distance The distance from the initial point to the point of interest along the geodesic * @param {number} distance The distance from the initial point to the point of interest along the geodesic
* @param {Cartographic} [result] The object in which to store the result. * @param {Cartographic} [result] The object in which to store the result.
* @returns {Cartographic} The location of the point along the geodesic. * @returns {Cartographic} The location of the point along the geodesic.
*
* @exception {DeveloperError} start and end must be set before calling function interpolateUsingSurfaceDistance
*/ */
EllipsoidGeodesic.prototype.interpolateUsingSurfaceDistance = function ( EllipsoidGeodesic.prototype.interpolateUsingSurfaceDistance = function (
distance, distance,
result, result,
) { ) {
if (DEBUG) {
Check.defined('distance', this._distance);
}
const constants = this._constants; const constants = this._constants;
const s = constants.distanceRatio + distance / constants.b; const s = constants.distanceRatio + distance / constants.b;
-14
View File
@@ -66,17 +66,3 @@ test('endpoint reset, result reuse, custom sphere and dateline/polar paths retai
); );
} }
}); });
test('unset and near-antipodal geodesics reject the same debug inputs', () => {
assert.throws(() => geo.ellipsoidGeodesic.create().surfaceDistance, {
name: 'DeveloperError',
});
const a = geo.cartographic.fromDegrees(0, 0),
b = geo.cartographic.fromDegrees(180, 0);
assert.throws(() => geo.ellipsoidGeodesic.create(a, b), {
name: 'DeveloperError',
});
assert.throws(
() => geo.ellipsoidGeodesic.create().setEndPoints(undefined, b),
{ name: 'DeveloperError' },
);
});
Loaded 100 of 272 files, more files were not shown because too many files have changed in this diff. Show more