mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-08 04:14:02 +00:00
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:
1 parent
c502404e1d
commit
7abf74518f
272 files changed
+4828
-10213
No files matched your search
@@ -59,7 +59,8 @@ jobs:
|
||||
- name: Build production bundle
|
||||
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
|
||||
|
||||
street-level-browser:
|
||||
@@ -90,9 +91,6 @@ jobs:
|
||||
MAPILLARY_CLIENT_TOKEN: 'MLY|0|qa-fixture'
|
||||
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
|
||||
env:
|
||||
MAPILLARY_CLIENT_TOKEN: 'MLY|0|qa-fixture'
|
||||
@@ -154,6 +152,3 @@ jobs:
|
||||
|
||||
- name: Build production bundle
|
||||
run: npm run build
|
||||
|
||||
- name: Main chunk (no three or Navara in Cesium mode; size budget when recorded)
|
||||
run: npm run check:main-chunk
|
||||
@@ -52,14 +52,13 @@ resolved dependency inventory remains in `package-lock.json`.
|
||||
are adapted from `@cesium/engine/Source/Core` (`Math.js`, `Ellipsoid.js`,
|
||||
`scaleToGeodeticSurface.js`, `Cartographic.js`, `Cartesian2.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,
|
||||
vectors, geodetic positions and quaternions became plain-data instances of
|
||||
module-private constructors (own enumerable fields only, no methods),
|
||||
matrices plain arrays, and the ellipsoid a frozen object; the debug
|
||||
argument checks were ported with Cesium's conditions and messages and, like
|
||||
Cesium's, are active in development and tests and stripped from production
|
||||
builds; only the subset gev uses was kept.
|
||||
argument checks were left out, as in Cesium's release build; only the subset
|
||||
gev uses was kept.
|
||||
- Author: Copyright 2011-2024 CesiumJS Contributors
|
||||
- Source: <https://github.com/CesiumGS/cesium>
|
||||
- License: Apache License 2.0
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// initial chunks are every entry chunk and what it imports statically;
|
||||
// navaraMode.js reaches Navara only through import(), so three and
|
||||
// @navaramap/* belong to lazy chunks. This checks the bundle's module ids
|
||||
// (what Rollup put in each chunk), and fails the build otherwise;
|
||||
// scripts/check-main-chunk.mjs checks the built files' text as well.
|
||||
// (what Rollup put in each chunk), and fails the build otherwise.
|
||||
|
||||
/**
|
||||
* Module ids that only the Navara engine may load, when no package list is
|
||||
|
||||
@@ -55,7 +55,8 @@ See [application construction](APPLICATION.md) and the
|
||||
2. `scripts/check-package-boundaries.mjs` builds every declared export without
|
||||
app Vite configuration or environment files. `scripts/package-boundaries.json`
|
||||
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.
|
||||
|
||||
Portable source graphs cannot reach application/rendering, Node, Cesium or
|
||||
|
||||
+7
-7
@@ -61,21 +61,21 @@
|
||||
"@jtarrio/webrtlsdr": "^3.0.6",
|
||||
"@mapbox/vector-tile": "^3.0.0",
|
||||
"@meri-imperiumi/eccodes-wasm": "^2.48.2",
|
||||
"@navaramap/three": "0.2.1",
|
||||
"@navaramap/three-default-plugin": "0.2.1",
|
||||
"cesium": "^1.124.0",
|
||||
"egm96-universal": "^1.1.1",
|
||||
"hls.js": "^1.7.3",
|
||||
"kdbush": "4.0.2",
|
||||
"mapillary-js": "^4.1.2",
|
||||
"mgrs": "^2.1.0",
|
||||
"pbf": "^5.1.2",
|
||||
"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",
|
||||
"three": "0.185.1"
|
||||
"satellite.js": "^6.0.2",
|
||||
"three": "0.185.1",
|
||||
"vue": "^3.5.44",
|
||||
"vue-i18n": "^11.4.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "^20.14.6",
|
||||
|
||||
@@ -30,9 +30,9 @@ const globeEngine = (file) => /^src\/globe\/(?:cesium|navara)\//.test(file);
|
||||
const cesiumPackage = /^(?:cesium(?:\/|$)|@cesium\/)/;
|
||||
// Navara renders through three.js and its postprocessing peer.
|
||||
const navaraPackage = /^(?:(?:three|postprocessing)(?:\/|$)|@navaramap\/)/;
|
||||
// Phase 3: the layers moved onto layer renderers (the *a tasks) keep each
|
||||
// engine in their renderers: cesium only in render/cesium*.js, three and
|
||||
// Navara only in render/navara*.js. The list grows with each moved layer.
|
||||
// Layers built on layer renderers keep each engine in their renderers:
|
||||
// cesium only in render/cesium*.js, three and Navara only in
|
||||
// render/navara*.js. The list grows with each layer moved onto renderers.
|
||||
const RENDERED_LAYER_DIRS = [
|
||||
'src/layers/alpr/',
|
||||
'src/layers/airQuality/',
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
#!/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
|
||||
* 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
|
||||
* Prints the size. `--json` prints the whole result; `--dist=DIR` reads
|
||||
* another build.
|
||||
*
|
||||
* npm run build && npm run check:main-chunk
|
||||
@@ -20,12 +15,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { gzipSync } from 'node:zlib';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import {
|
||||
entryFromHtml,
|
||||
entryFromManifest,
|
||||
forbiddenMarkersIn,
|
||||
initialChunks,
|
||||
} from '../src/tooling/mainChunk.js';
|
||||
import { entryFromHtml, entryFromManifest } from '../src/tooling/mainChunk.js';
|
||||
|
||||
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
|
||||
* loads (`entry`, or found from dist/index.html or the Vite manifest), else
|
||||
* (phase 4's rule, for a dist without a page) the largest
|
||||
* dist/assets/index-*.js.
|
||||
* loads (`entry`, else found from the Vite manifest or dist/index.html).
|
||||
*/
|
||||
export function measureMainChunk(distDir, { entry = null } = {}) {
|
||||
let file = entry;
|
||||
if (!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;
|
||||
const file = entry ?? findEntry(distDir).entry;
|
||||
const bytes = fs.readFileSync(path.join(distDir, file));
|
||||
return {
|
||||
name: main.name,
|
||||
gzipBytes: gzipSync(main.bytes, { level: 9 }).length,
|
||||
name: path.basename(file),
|
||||
gzipBytes: gzipSync(bytes, { level: 9 }).length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,44 +62,15 @@ function findEntry(dist) {
|
||||
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() {
|
||||
const option = (name) =>
|
||||
process.argv.find((arg) => arg.startsWith(`--${name}=`))?.split('=')[1];
|
||||
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');
|
||||
let budget = null;
|
||||
if (fs.existsSync(baselinePath)) {
|
||||
const measured = measureMainChunk(dist, { entry: composition.entry });
|
||||
const measured = measureMainChunk(dist, { entry });
|
||||
budget = {
|
||||
...measured,
|
||||
...checkMainChunk(
|
||||
@@ -142,28 +79,11 @@ function main() {
|
||||
),
|
||||
};
|
||||
}
|
||||
const ok = composition.ok && (budget?.ok ?? true);
|
||||
const ok = budget?.ok ?? true;
|
||||
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 {
|
||||
const kb = (bytes) => `${(bytes / 1000).toFixed(2)} kB`;
|
||||
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(`[check:main-chunk] entry ${entry} (via ${via})`);
|
||||
console.log(
|
||||
budget
|
||||
? `[check:main-chunk] ${budget.name}: ${budget.message}`
|
||||
|
||||
@@ -37,7 +37,11 @@ export async function checkPackageBoundaries(root) {
|
||||
) {
|
||||
throw new Error(`Invalid package boundary: ${name}`);
|
||||
}
|
||||
// A module entry ending in "/" owns every file under that directory.
|
||||
const allowed = new Set();
|
||||
const allowedDirectories = [];
|
||||
const owns = (id) =>
|
||||
allowed.has(id) || allowedDirectories.some((dir) => id.startsWith(dir));
|
||||
for (const module of group.modules) {
|
||||
if (
|
||||
typeof module !== 'string' ||
|
||||
@@ -56,7 +60,11 @@ export async function checkPackageBoundaries(root) {
|
||||
) {
|
||||
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) {
|
||||
if (
|
||||
@@ -82,7 +90,7 @@ export async function checkPackageBoundaries(root) {
|
||||
if (
|
||||
typeof target !== 'string' ||
|
||||
!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}`);
|
||||
}
|
||||
@@ -109,7 +117,7 @@ export async function checkPackageBoundaries(root) {
|
||||
// template blocks belong to the component file itself.
|
||||
if (info.id.startsWith('\0')) return;
|
||||
const id = info.id.split('?')[0];
|
||||
if (!allowed.has(id)) {
|
||||
if (!owns(id)) {
|
||||
throw new Error(
|
||||
`Package boundary ${name} imports an unowned module: ${path.relative(root, id)}`,
|
||||
);
|
||||
|
||||
@@ -589,6 +589,8 @@
|
||||
"src/layers/alpr/model.js",
|
||||
"src/layers/alpr/policy.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/source.js",
|
||||
"src/layers/alpr/source.test.mjs",
|
||||
@@ -1217,6 +1219,7 @@
|
||||
"src/layers/aircraft/render/contract.js",
|
||||
"src/layers/shared/color.test.mjs",
|
||||
"src/layers/shared/color.js",
|
||||
"src/layers/shared/viewRectangle.js",
|
||||
"src/layers/military/render/cesium.js",
|
||||
"src/layers/military/render/navara.js",
|
||||
"src/layers/aircraft/qaDrawn.js",
|
||||
@@ -1225,13 +1228,18 @@
|
||||
"src/layers/vessels/render/cesium.js",
|
||||
"src/layers/vessels/render/contract.js",
|
||||
"src/layers/vessels/renderer.test.mjs",
|
||||
"src/layers/vessels/testFixtures.mjs",
|
||||
"src/layers/vessels/render/navara.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/rendering.js",
|
||||
"src/layers/installations/render/cesium/selection.js",
|
||||
"src/layers/bikeshare/render/cesium/lifecycle.js",
|
||||
"src/layers/bikeshare/presentation.js",
|
||||
"src/layers/bikeshare/stationPoints.js",
|
||||
"src/layers/bikeshare/presentation.test.mjs",
|
||||
"src/layers/bikeshare/selectionModel.js",
|
||||
"src/layers/bikeshare/render/cesium.js",
|
||||
@@ -1329,8 +1337,8 @@
|
||||
"src/globe/cesium/render.js",
|
||||
"src/globe/cesium/surface.js",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/navara/cameraMath.js",
|
||||
"src/globe/navara/cameraMath.test.mjs",
|
||||
"src/globe/cameraMath.js",
|
||||
"src/globe/cameraMath.test.mjs",
|
||||
"src/globe/navara/cameraModel.js",
|
||||
"src/globe/navara/cameraModel.test.mjs",
|
||||
"src/globe/navara/createNavaraEngine.js",
|
||||
@@ -1364,14 +1372,12 @@
|
||||
"src/tooling/mainChunkComposition.test.mjs",
|
||||
"src/tooling/initialChunkGuard.test.mjs",
|
||||
"src/globe/movers.test.mjs",
|
||||
"src/globe/cesium/movers.test.mjs",
|
||||
"src/globe/navara/movers/pack.test.mjs",
|
||||
"src/globe/navara/movers/pickIds.test.mjs",
|
||||
"src/globe/navara/movers/moverMesh.test.mjs",
|
||||
"src/globe/navara/movers/modelMover.test.mjs",
|
||||
"src/bench/syntheticFlights.test.mjs",
|
||||
"src/globe/lines.test.mjs",
|
||||
"src/globe/cesium/lines.test.mjs",
|
||||
"src/globe/navara/lines/lineMesh.test.mjs",
|
||||
"src/globe/pickDelivery.test.mjs",
|
||||
"src/globe/navara/picking.test.mjs",
|
||||
|
||||
+47
-534
@@ -897,24 +897,7 @@
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/ui/unitLabels.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1176,24 +1159,7 @@
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
"src/cameraAngles.js",
|
||||
"src/globe/contract.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/app/layers/directory.js",
|
||||
"src/globe/follow.js",
|
||||
"src/ui/kit/UiSegmented.vue"
|
||||
@@ -1333,24 +1299,7 @@
|
||||
"src/ui/i18n.js",
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
"src/globe/contract.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/"
|
||||
],
|
||||
"external": ["cesium", "vue", "pinia", "vue-i18n"]
|
||||
},
|
||||
@@ -1432,24 +1381,7 @@
|
||||
"src/ui/unitLabels.js",
|
||||
"src/data/render/cesiumProjection.js",
|
||||
"src/data/render/cesiumConstants.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/time/clock.js",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
@@ -1516,24 +1448,7 @@
|
||||
"src/data/render/cesiumProjection.js",
|
||||
"src/data/render/cesiumConstants.js",
|
||||
"src/layers/shared/color.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/time/clock.js",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
@@ -1571,24 +1486,7 @@
|
||||
"src/data/inputOwnership.js",
|
||||
"src/data/render/cesiumProjection.js",
|
||||
"src/data/render/cesiumConstants.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js"
|
||||
],
|
||||
@@ -1635,24 +1533,7 @@
|
||||
"src/layers/vessels/icons.js",
|
||||
"src/layers/shared/color.js",
|
||||
"src/data/render/cesiumProjection.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js"
|
||||
],
|
||||
@@ -1665,24 +1546,7 @@
|
||||
"src/layers/earthquakes/model.js",
|
||||
"src/layers/earthquakes/source.js",
|
||||
"src/layers/earthquakes/records.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"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/geo/",
|
||||
"src/layers/shared/color.js"
|
||||
],
|
||||
"external": []
|
||||
@@ -1751,24 +1615,7 @@
|
||||
"src/i18n/locales/uk.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/layers/firms/rectangle.js",
|
||||
"src/layers/shared/color.js"
|
||||
],
|
||||
@@ -1810,24 +1657,7 @@
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/ui/unitLabels.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/time/clock.js",
|
||||
"src/layers/shared/color.js"
|
||||
],
|
||||
@@ -1872,24 +1702,7 @@
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/layers/shared/color.js",
|
||||
"src/layers/launches/render/cesium.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/time/clock.js",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js"
|
||||
@@ -1946,24 +1759,7 @@
|
||||
"src/layers/installations/render/cesium/selection.js",
|
||||
"src/layers/shared/color.js",
|
||||
"src/layers/installations/render/cesium/namedMarkers.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js"
|
||||
],
|
||||
@@ -2015,24 +1811,7 @@
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/ui/unitLabels.js",
|
||||
"src/data/render/cesiumProjection.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js"
|
||||
],
|
||||
@@ -2094,24 +1873,7 @@
|
||||
"src/i18n/locales/uk.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js"
|
||||
],
|
||||
@@ -2151,24 +1913,7 @@
|
||||
"src/layers/bikeshare/selectionModel.js",
|
||||
"src/layers/bikeshare/presentation.js",
|
||||
"src/layers/shared/color.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js"
|
||||
],
|
||||
@@ -2227,24 +1972,7 @@
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/layers/shared/color.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/"
|
||||
],
|
||||
"external": ["hls.js"]
|
||||
},
|
||||
@@ -2318,24 +2046,7 @@
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/layers/streetLevel/providers/mapillary/policy.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/"
|
||||
],
|
||||
"external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"]
|
||||
},
|
||||
@@ -2371,24 +2082,7 @@
|
||||
"src/i18n/locales/uk.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/geo/index.js",
|
||||
"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/geo/"
|
||||
],
|
||||
"external": ["@mapbox/vector-tile", "pbf"]
|
||||
},
|
||||
@@ -2437,24 +2131,7 @@
|
||||
"src/i18n/locales/uk.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/geo/index.js",
|
||||
"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/geo/"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -2486,24 +2163,7 @@
|
||||
"src/i18n/locales/uk.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/geo/index.js",
|
||||
"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/geo/"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -3692,7 +3352,7 @@
|
||||
"src/globe/cesium/render.js",
|
||||
"src/globe/cesium/surface.js",
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
"src/globe/navara/cameraMath.js",
|
||||
"src/globe/cameraMath.js",
|
||||
"src/globe/navara/cameraModel.js",
|
||||
"src/globe/navara/createNavaraEngine.js",
|
||||
"src/globe/navara/credits.js",
|
||||
@@ -3722,6 +3382,8 @@
|
||||
"src/data/render/cesiumTrailRenderer.js",
|
||||
"src/layers/alpr/render/cesiumPresentation.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/bikeshare/selectionModel.js",
|
||||
"src/layers/bikeshare/presentation.js",
|
||||
@@ -3742,35 +3404,16 @@
|
||||
"src/layers/localAdsb/qaDrawn.js",
|
||||
"src/ui/imageryBoxNavara.js",
|
||||
"src/layers/installations/render/cesium/namedMarkers.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/time/clock.js",
|
||||
"src/globe/navara/overlay.js",
|
||||
"src/globe/navara/frameDescriptor.js",
|
||||
"src/globe/cesium/clock.js",
|
||||
"src/globe/pickDelivery.js",
|
||||
"src/globe/navara/clock.js",
|
||||
"src/globe/cesium/lines.js",
|
||||
"src/globe/follow.js",
|
||||
"src/globe/lines.js",
|
||||
"src/globe/navara/movers/pickIds.js",
|
||||
"src/globe/cesium/movers.js",
|
||||
"src/globe/movers.js",
|
||||
"src/globe/cesium/follow.js",
|
||||
"src/globe/cpuPicks.js",
|
||||
@@ -3847,6 +3490,8 @@
|
||||
"src/layers/cyclones/render/navara.js",
|
||||
"src/layers/shared/render/groundRing.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/navaraOutlineBatch.js",
|
||||
"src/layers/installations/render/navaraStencilBatch.js",
|
||||
@@ -3857,6 +3502,7 @@
|
||||
"src/layers/radio/render/navara.js",
|
||||
"src/layers/transit/render/navara.js",
|
||||
"src/layers/transit/render/navaraSprites.js",
|
||||
"src/layers/shared/render/navaraMoverBank.js",
|
||||
"src/layers/transit/render/navaraView.js",
|
||||
"src/layers/transit/render/navaraTrail.js",
|
||||
"src/layers/recentImagery/render/navara.js",
|
||||
@@ -3868,14 +3514,18 @@
|
||||
"src/layers/cctv/render/navaraLine.js",
|
||||
"src/layers/alpr/render/navara.js",
|
||||
"src/layers/alpr/render/navaraOverlay.js",
|
||||
"src/layers/alpr/overlay.js",
|
||||
"src/layers/alpr/presentation.js",
|
||||
"src/layers/radio/screenClusters.js",
|
||||
"src/layers/shared/render/navaraCellGeometry.js",
|
||||
"src/layers/shared/render/navaraGroundCells.js",
|
||||
"src/layers/firms/render/navaraSprites.js",
|
||||
"src/layers/shared/render/imageAtlas.js",
|
||||
"src/layers/firms/render/navaraHeat.js",
|
||||
"src/layers/shared/render/navaraGroundGeometry.js",
|
||||
"src/layers/shared/render/navaraGroundGrid.js",
|
||||
"src/layers/shared/render/navaraGroundSurface.js",
|
||||
"src/layers/shared/render/navaraGroundEffect.js",
|
||||
"src/annotations/outlineRungs.js",
|
||||
"src/sources/openFreeMapOutlines.js",
|
||||
"src/sources/nominatimOutlines.js",
|
||||
@@ -4364,6 +4014,7 @@
|
||||
"src/layers/transit/state.js",
|
||||
"src/layers/transit/testing.js",
|
||||
"src/layers/transit/viewport.js",
|
||||
"src/layers/shared/viewRectangle.js",
|
||||
"src/data/cameraSensitivity.js",
|
||||
"src/data/contactPlayback.js",
|
||||
"src/data/contactTrailRenderer.js",
|
||||
@@ -4528,6 +4179,7 @@
|
||||
"src/globe/cesium/input.js",
|
||||
"src/globe/cesium/convert.js",
|
||||
"src/globe/cesium/camera.js",
|
||||
"src/globe/cameraMath.js",
|
||||
"src/globe/cesium/frames.js",
|
||||
"src/globe/cesium/picker.js",
|
||||
"src/globe/cesium/events.js",
|
||||
@@ -4543,6 +4195,8 @@
|
||||
"src/data/render/cesiumTrailRenderer.js",
|
||||
"src/layers/alpr/render/cesiumPresentation.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/bikeshare/selectionModel.js",
|
||||
"src/layers/bikeshare/presentation.js",
|
||||
@@ -4550,6 +4204,7 @@
|
||||
"src/layers/bikeshare/render/cesium/viewport.js",
|
||||
"src/layers/bikeshare/render/cesium/lifecycle.js",
|
||||
"src/layers/bikeshare/render/cesium/rendering.js",
|
||||
"src/layers/bikeshare/stationPoints.js",
|
||||
"src/layers/bikeshare/render/cesium/selection.js",
|
||||
"src/layers/installations/render/cesium/lifecycle.js",
|
||||
"src/layers/installations/render/cesium.js",
|
||||
@@ -4561,24 +4216,7 @@
|
||||
"src/data/render/cesiumContactTrailRenderer.js",
|
||||
"src/layers/localAdsb/qaDrawn.js",
|
||||
"src/layers/installations/render/cesium/namedMarkers.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/time/clock.js",
|
||||
"src/globe/follow.js",
|
||||
"src/globe/cesium/follow.js",
|
||||
@@ -4687,24 +4325,7 @@
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
"src/globe/cesium/credits.js",
|
||||
"src/globe/contract.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/globe/surfaceDetail.js",
|
||||
"src/sources/openFreeMap.js",
|
||||
"src/sources/vectorTiles.js",
|
||||
@@ -4985,24 +4606,7 @@
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/ui/unitLabels.js",
|
||||
"src/geo/julianDate.js",
|
||||
"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"
|
||||
"src/geo/"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -5315,6 +4919,7 @@
|
||||
"src/layers/transit/testing.js",
|
||||
"src/layers/transit/trails.js",
|
||||
"src/layers/transit/viewport.js",
|
||||
"src/layers/shared/viewRectangle.js",
|
||||
"src/overlays/worldOverlay.js",
|
||||
"src/overlays/worldOverlayDraw.js",
|
||||
"src/overlays/worldOverlayTokens.js",
|
||||
@@ -5349,6 +4954,7 @@
|
||||
"src/globe/cesium/picker.js",
|
||||
"src/globe/cesium/frames.js",
|
||||
"src/globe/cesium/camera.js",
|
||||
"src/globe/cameraMath.js",
|
||||
"src/globe/cesium/convert.js",
|
||||
"src/globe/cesium/input.js",
|
||||
"src/maps/google3d.js",
|
||||
@@ -5373,24 +4979,7 @@
|
||||
"src/data/render/cesiumProjection.js",
|
||||
"src/layers/shared/color.js",
|
||||
"src/layers/transit/render/cesium.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/time/clock.js",
|
||||
"src/globe/surfaceDetail.js",
|
||||
"src/celestialRing/render/cesium.js"
|
||||
@@ -5418,6 +5007,7 @@
|
||||
"src/layers/transit/state.js",
|
||||
"src/layers/transit/testing.js",
|
||||
"src/layers/transit/viewport.js",
|
||||
"src/layers/shared/viewRectangle.js",
|
||||
"src/data/contactPlayback.js",
|
||||
"src/data/contactTrailRenderer.js",
|
||||
"src/data/transitProxy.js",
|
||||
@@ -5445,24 +5035,7 @@
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/layers/shared/color.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/globe/contract.js"
|
||||
],
|
||||
"external": ["cesium", "pbf"]
|
||||
@@ -5506,9 +5079,6 @@
|
||||
"exports": ["./layers/perimeters"],
|
||||
"modules": [
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/check.js",
|
||||
"src/geo/debug.js",
|
||||
"src/geo/developerError.js",
|
||||
"src/geo/math.js",
|
||||
"src/layers/perimeters/index.js",
|
||||
"src/layers/perimeters/render/cesium.js",
|
||||
@@ -5611,9 +5181,6 @@
|
||||
"src/layers/ghostMaps/kml.js",
|
||||
"src/layers/ghostMaps/source.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/check.js",
|
||||
"src/geo/debug.js",
|
||||
"src/geo/developerError.js",
|
||||
"src/geo/math.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/data/geoFileImport.js",
|
||||
@@ -5663,24 +5230,7 @@
|
||||
"exports": ["./layers/drone-airspace"],
|
||||
"modules": [
|
||||
"src/layers/droneAirspace/index.js",
|
||||
"src/geo/index.js",
|
||||
"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/geo/",
|
||||
"src/layers/shared/render/cesiumZones.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/layers/droneAirspace/source.js",
|
||||
@@ -5769,24 +5319,7 @@
|
||||
"src/tools/queries/panelRequest.js",
|
||||
"src/geoDistance.js",
|
||||
"src/visualStylePolicy.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"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"
|
||||
"src/geo/"
|
||||
],
|
||||
"external": ["satellite.js"]
|
||||
},
|
||||
@@ -5898,9 +5431,6 @@
|
||||
"exports": ["./layers/air-quality"],
|
||||
"modules": [
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/check.js",
|
||||
"src/geo/debug.js",
|
||||
"src/geo/developerError.js",
|
||||
"src/geo/math.js",
|
||||
"src/layers/airQuality/index.js",
|
||||
"src/layers/airQuality/render/cesium.js",
|
||||
@@ -5946,24 +5476,7 @@
|
||||
"src/loadingFeedback.js",
|
||||
"src/data/installationFeedback.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"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"
|
||||
"src/geo/"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
}
|
||||
|
||||
@@ -83,42 +83,28 @@ try {
|
||||
// Every name the checkout's .env files define, plus key-like names in this
|
||||
// environment: all blank for the build and the preview.
|
||||
const KEY_LIKE = /(_KEY|_TOKEN|_SECRET|_CLIENT_ID|PASSWORD)$/;
|
||||
const dotenvNames = () => {
|
||||
const names = new Set();
|
||||
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 = line.match(/^\s*([A-Z0-9_]+)\s*=/)?.[1];
|
||||
if (name) names.add(name);
|
||||
}
|
||||
return [...names].sort();
|
||||
};
|
||||
const envNames = () => {
|
||||
const names = new Set();
|
||||
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 = line.match(/^\s*([A-Z0-9_]+)\s*=/)?.[1];
|
||||
if (name) names.add(name);
|
||||
}
|
||||
for (const name of Object.keys(process.env))
|
||||
if (KEY_LIKE.test(name)) names.add(name);
|
||||
return [...names].sort();
|
||||
};
|
||||
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;
|
||||
};
|
||||
/** The [name, value] pairs of the checkout's .env and .env.local, in order. */
|
||||
const dotenv = () =>
|
||||
['.env', '.env.local']
|
||||
.filter((file) => existsSync(join(root, file)))
|
||||
.flatMap((file) => readFileSync(join(root, file), 'utf8').split('\n'))
|
||||
.map((line) => line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)/))
|
||||
.filter(Boolean)
|
||||
.map(([, name, raw]) => [name, raw.trim().replace(/^["']|["']$/g, '')]);
|
||||
const dotenvNames = () => [...new Set(dotenv().map(([name]) => name))].sort();
|
||||
const envNames = () =>
|
||||
[
|
||||
...new Set([
|
||||
...dotenvNames(),
|
||||
...Object.keys(process.env).filter((name) => KEY_LIKE.test(name)),
|
||||
]),
|
||||
].sort();
|
||||
// Credentials only (model names and URLs are settings the code may also
|
||||
// carry as defaults).
|
||||
const envValues = () =>
|
||||
dotenv()
|
||||
.filter(([name, value]) => KEY_LIKE.test(name) && value.length >= 8)
|
||||
.map(([, value]) => value);
|
||||
const scrubbed = () => ({
|
||||
...process.env,
|
||||
...Object.fromEntries(envNames().map((name) => [name, ''])),
|
||||
@@ -368,9 +354,9 @@ const runs = [
|
||||
['rocket-launches'],
|
||||
['--missions'],
|
||||
],
|
||||
// Stage 3's own checks (one application per page, voice across a switch).
|
||||
// They block paid routes themselves but do not run under the strict
|
||||
// harness, so they add no assertion manifest and no network ledger.
|
||||
// One application per page, and voice across a switch: these block paid
|
||||
// routes themselves but do not run under the strict harness, so they add
|
||||
// no assertion manifest and no network ledger.
|
||||
['app-once', 'app-once', [], [], 600, true],
|
||||
['voice-engine-switch', 'voice-engine-switch', [], [], 300, true],
|
||||
['strict-rapid-switch', 'strict-rapid-switch', []],
|
||||
@@ -379,14 +365,18 @@ const runs = [
|
||||
['strict-failure-matrix', 'strict-failure-matrix', [], [], 600],
|
||||
['strict-cold-chunks', 'strict-cold-chunks', []],
|
||||
['strict-prod-smoke', 'strict-prod-smoke', []],
|
||||
// Stage 6: the app's visual styles, bloom, contact sonar, cockpit clouds
|
||||
// and annotations drawn on Navara (annotations is a tool, not a catalog
|
||||
// layer: TOOL_IDS).
|
||||
// The app's visual styles, bloom, contact sonar, cockpit clouds, and the
|
||||
// annotations, Draw, Import and the scope mask with the celestial ring on
|
||||
// Navara (those are tools and display overlays, not catalog layers:
|
||||
// TOOL_IDS).
|
||||
['navara-visual-styles', 'navara-visual-styles', []],
|
||||
['navara-bloom', 'navara-bloom', []],
|
||||
['navara-sonar', 'navara-sonar', []],
|
||||
['navara-cockpit-clouds', 'navara-cockpit-clouds', []],
|
||||
['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--long',
|
||||
@@ -398,7 +388,13 @@ const runs = [
|
||||
];
|
||||
// Ids a scenario covers that are tools, not catalog layers: their
|
||||
// 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 = [
|
||||
...new Set(
|
||||
runs.flatMap(([, , ids]) => ids).filter((id) => !TOOL_IDS.has(id)),
|
||||
@@ -414,9 +410,7 @@ const liveCatalog = {};
|
||||
layers: [],
|
||||
name: 'catalog',
|
||||
});
|
||||
const browser = await strict.launch({
|
||||
args: ['--use-angle=metal', '--enable-gpu'],
|
||||
});
|
||||
const browser = await strict.launch();
|
||||
try {
|
||||
for (const engine of ENGINES) {
|
||||
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 evidence = join(output, 'evidence', label + '.json');
|
||||
rmSync(evidence, { force: true });
|
||||
const args =
|
||||
name === 'w2-imagery'
|
||||
? [
|
||||
'--url',
|
||||
base.origin + '/?engine=navara',
|
||||
'--report',
|
||||
join(output, 'evidence', 'imagery-report.json'),
|
||||
'--switch',
|
||||
'--preview',
|
||||
]
|
||||
: [base.origin, ...extra];
|
||||
const args = [base.origin, ...extra];
|
||||
if (external) args.splice(0, 1, base.origin + '/');
|
||||
else args.push(`--layers=${layers.join(',')}`, `--evidence=${evidence}`);
|
||||
const start = Date.now();
|
||||
|
||||
+1
-18
@@ -221,7 +221,7 @@ try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
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(
|
||||
() => window.__gevGeoImport.diagnostics().drawn === 1,
|
||||
{ timeout: 30_000 },
|
||||
@@ -412,23 +412,6 @@ try {
|
||||
report.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));
|
||||
if (!reportOnly) console.log('PASS application created once; session kept');
|
||||
} finally {
|
||||
|
||||
@@ -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.
|
||||
export function watchW1Page(page, report, { name, preview = false, baseUrl }) {
|
||||
const errors = (report.browserErrors ??= []);
|
||||
|
||||
@@ -37,7 +37,7 @@ const { stdout: video } = await promisify(execFile)(
|
||||
{ encoding: 'buffer', maxBuffer: 16000000 },
|
||||
);
|
||||
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 errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
*
|
||||
* This legacy harness is the geometry, lifecycle and real-input oracle.
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
// media pixels are a separate mandatory W2 companion gate.
|
||||
// media pixels are a separate mandatory pixel gate.
|
||||
// -----------------------------------------------------------------------
|
||||
await sleep(1000);
|
||||
const shot = path.join(SHOTS_DIR, 'cctv-v2-active.png');
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import assert from 'node:assert/strict';
|
||||
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]);
|
||||
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||
@@ -10,14 +15,8 @@ const strict = createStrict({
|
||||
origin: base.origin,
|
||||
layers: ['military-awareness', 'military-installations'],
|
||||
});
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b);
|
||||
await p.evaluateOnNewDocument(() => {
|
||||
const fetch = window.fetch.bind(window);
|
||||
window.fetch = (input, options) => {
|
||||
@@ -89,21 +88,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
);
|
||||
});
|
||||
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,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
await p.evaluate(async () => {
|
||||
const { dataManager, styleManager, tileset } = window.__godsEyeView;
|
||||
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
|
||||
@@ -235,8 +220,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
baseline,
|
||||
);
|
||||
for (const kind of ['cesium', 'navara']) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
const current = await sample();
|
||||
console.log('handoff', kind, JSON.stringify(current));
|
||||
assert.deepEqual(current, {
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
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]);
|
||||
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||
base.hash =
|
||||
'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'] });
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b);
|
||||
let routeReads = 0,
|
||||
release;
|
||||
await p.setRequestInterception(true);
|
||||
@@ -55,21 +54,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
});
|
||||
});
|
||||
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,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
await p.evaluate(async () => {
|
||||
const { tileset, dataManager } = window.__godsEyeView;
|
||||
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
|
||||
@@ -91,8 +76,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
'directions',
|
||||
'A and B set by clicks on the globe; one route request',
|
||||
);
|
||||
await strict.pick(p, 'cesium');
|
||||
await ready('cesium');
|
||||
await strict.switchTo(p, 'cesium');
|
||||
release();
|
||||
await p.waitForFunction(() => window.qaRoute.getStats().count === 3);
|
||||
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',
|
||||
);
|
||||
const transfer = async (kind) => {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
assert.equal(
|
||||
await p.evaluate(
|
||||
() =>
|
||||
|
||||
@@ -135,7 +135,7 @@ const strict = createStrict({
|
||||
layers: [],
|
||||
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();
|
||||
await p.setViewport({ width: 1280, height: 900 });
|
||||
const errors = [];
|
||||
|
||||
+444
-460
@@ -7,478 +7,462 @@ base.search = '?welcome=0&look=cinematic';
|
||||
base.hash =
|
||||
'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'] });
|
||||
await strict.run(
|
||||
{ args: ['--use-angle=metal', '--enable-gpu'] },
|
||||
async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
||||
const fetch = window.fetch.bind(window);
|
||||
// Two Boston vessels for the Contacts session; empty air and site feeds.
|
||||
const fixtures = {
|
||||
'/api/vessels': () => ({
|
||||
status: 'live',
|
||||
rows: ['123456789', '223456789'].map((mmsi, i) => ({
|
||||
mmsi,
|
||||
name: 'Boston ' + i,
|
||||
lat: 42.3554,
|
||||
lon: -71.0605 + i * 0.05,
|
||||
last_position_epoch: Date.now() / 1000,
|
||||
})),
|
||||
}),
|
||||
'/api/vessels/track': () => ({ samples: [] }),
|
||||
'/api/flights': () => ({ time: Date.now() / 1000, states: [] }),
|
||||
'/api/military': () => ({ now: Date.now(), ac: [] }),
|
||||
'/api/military-installations': () => ({ elements: [], status: 'ok' }),
|
||||
};
|
||||
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) {
|
||||
await strict.run({}, async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
||||
const fetch = window.fetch.bind(window);
|
||||
// Two Boston vessels for the Contacts session; empty air and site feeds.
|
||||
const fixtures = {
|
||||
'/api/vessels': () => ({
|
||||
status: 'live',
|
||||
rows: ['123456789', '223456789'].map((mmsi, i) => ({
|
||||
mmsi,
|
||||
name: 'Boston ' + i,
|
||||
lat: 42.3554,
|
||||
lon: -71.0605 + i * 0.05,
|
||||
last_position_epoch: Date.now() / 1000,
|
||||
})),
|
||||
}),
|
||||
'/api/vessels/track': () => ({ samples: [] }),
|
||||
'/api/flights': () => ({ time: Date.now() / 1000, states: [] }),
|
||||
'/api/military': () => ({ now: Date.now(), ac: [] }),
|
||||
'/api/military-installations': () => ({ elements: [], status: 'ok' }),
|
||||
};
|
||||
window.fetch = (url, options) => {
|
||||
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 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',
|
||||
})
|
||||
String(url).startsWith('https://earthquake.usgs.gov/earthquakes/feed/')
|
||||
)
|
||||
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 = () =>
|
||||
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, {
|
||||
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',
|
||||
});
|
||||
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.
|
||||
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(
|
||||
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;
|
||||
engine.camera.zoomOut(2_000_000);
|
||||
await new Promise((resolve) => {
|
||||
const remove = engine.frames.postRender.addEventListener(() => {
|
||||
remove();
|
||||
resolve();
|
||||
});
|
||||
engine.render.request('qa-orbital-daylight');
|
||||
});
|
||||
const previous = window.__renderers.get(engine.kind);
|
||||
return (
|
||||
engine.camera.height() > 1_500_000 &&
|
||||
engine.host.container.hasAttribute('data-terrain-daylight')
|
||||
!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')
|
||||
);
|
||||
}),
|
||||
'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(
|
||||
() => !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' &&
|
||||
!window.__godsEyeView.engine.host.container.hasAttribute(
|
||||
'data-terrain-daylight',
|
||||
),
|
||||
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'),
|
||||
);
|
||||
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',
|
||||
};
|
||||
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');
|
||||
});
|
||||
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',
|
||||
);
|
||||
});
|
||||
@@ -67,7 +67,7 @@ const getOpt = (name, dflt) => {
|
||||
};
|
||||
|
||||
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.
|
||||
const ENGINE = (argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
|
||||
const APP_URL =
|
||||
|
||||
@@ -21,7 +21,7 @@ const strict = createStrict({
|
||||
// installAlprFixture answers the camera tiles.
|
||||
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();
|
||||
await p.setViewport({ width: 1280, height: 900 });
|
||||
const errors = [];
|
||||
|
||||
@@ -11,7 +11,7 @@ import fs from 'node:fs';
|
||||
// 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.
|
||||
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.
|
||||
const ENGINE = (process.argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
|
||||
const APP_URL =
|
||||
@@ -125,7 +125,7 @@ const report = await page.evaluate(async () => {
|
||||
drawnBy: modelAt ? 'model' : 'sprite',
|
||||
meshM: meshH != null ? +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).
|
||||
probe: answer ? { source: answer.source, tileSizeM: answer.tileSizeM ?? null } : null,
|
||||
demFloorM: cachedGroundFloor(c.latitude * 180 / Math.PI, c.longitude * 180 / Math.PI) ?? null,
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
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]);
|
||||
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||
base.hash =
|
||||
'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'] });
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b);
|
||||
const file = await readFile(
|
||||
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],
|
||||
);
|
||||
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,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
await p.evaluate(async () => {
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
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(() => {
|
||||
window.ghostStats.titles = [];
|
||||
});
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
await p.waitForFunction(() =>
|
||||
window.ghostStats.titles.some((v) => /QA zone/i.test(v)),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
import sharp from 'sharp';
|
||||
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]);
|
||||
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||
@@ -16,14 +21,8 @@ const strict = createStrict({
|
||||
'fire-perimeters',
|
||||
],
|
||||
});
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b);
|
||||
await p.evaluateOnNewDocument(() => {
|
||||
const fetch = window.fetch.bind(window);
|
||||
window.fetch = (input, o) => {
|
||||
@@ -122,20 +121,6 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
};
|
||||
});
|
||||
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') => {
|
||||
await p.evaluate(
|
||||
async (show, id) => {
|
||||
@@ -230,7 +215,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
}
|
||||
return on;
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
await p.evaluate(() => {
|
||||
const d = window.__godsEyeView;
|
||||
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']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
const same = await p.evaluate(
|
||||
() =>
|
||||
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']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
await card(true);
|
||||
await strict.check(
|
||||
p,
|
||||
@@ -478,8 +461,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
let histories = await p.evaluate(() => window.qaVesselHistories);
|
||||
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
}
|
||||
const sample = await p.evaluate(() => {
|
||||
const entry =
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
import assert from 'node:assert/strict';
|
||||
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]);
|
||||
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||
@@ -26,14 +31,8 @@ const strict = createStrict({
|
||||
origin: base.origin,
|
||||
layers: ['local-dams', 'local-datacenters'],
|
||||
});
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b);
|
||||
const reads = { datacenters: 0, dams: 0 };
|
||||
p.on('request', (req) => {
|
||||
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);
|
||||
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,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
const ids = ['local-datacenters', 'local-dams'];
|
||||
const dataset = await p.evaluate(async (ids) => {
|
||||
const { dataManager, tileset } = window.__godsEyeView;
|
||||
@@ -184,8 +169,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
await p.evaluate(() => {
|
||||
window.qaSelections = [];
|
||||
});
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
await p.waitForFunction(
|
||||
() => window.qaSelections.includes('local-datacenters:342664739'),
|
||||
{ timeout: 15000 },
|
||||
|
||||
@@ -314,8 +314,7 @@ async function main() {
|
||||
(ids) => ids.filter((id) => !window.__godsEyeView.layers.available(id)),
|
||||
fieldLayers,
|
||||
);
|
||||
// An engine with flights but not the other field layers (Navara, W1 so
|
||||
// far): the same 12,000 / 5,200 observations, all as aircraft, so the
|
||||
// An engine with flights but not the other field layers: the same 12,000 / 5,200 observations, all as aircraft, so the
|
||||
// detection lane is measured on the layers the engine has.
|
||||
let fieldCounts = FIELD_COUNTS;
|
||||
let normalCounts = NORMAL_COUNTS;
|
||||
|
||||
+482
-493
@@ -15,514 +15,503 @@ url.hash =
|
||||
// left.
|
||||
const HIDDEN_PICKER = 'the Globe Engine picker is hidden in cockpit mode';
|
||||
const strict = createStrict({ origin: url.origin, layers: [layerId] });
|
||||
await strict.run(
|
||||
{ args: ['--use-angle=metal', '--enable-gpu'] },
|
||||
async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
await page.setViewport({ width: 1280, height: 720 });
|
||||
await page.evaluateOnNewDocument(
|
||||
(layerId, aircraftId) => {
|
||||
window.__qaAircraft = { layerId, aircraftId };
|
||||
const fetch = window.fetch.bind(window);
|
||||
window.__flightPolls = 0;
|
||||
const interval = window.setInterval.bind(window);
|
||||
window.setInterval = function (callback, delay, ...args) {
|
||||
if (typeof callback !== 'function')
|
||||
return interval(callback, delay, ...args);
|
||||
const id = interval(
|
||||
function (...values) {
|
||||
if (window.__handoff?.timer === id) window.__handoff.ticks++;
|
||||
return callback.apply(this, values);
|
||||
},
|
||||
delay,
|
||||
...args,
|
||||
);
|
||||
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,
|
||||
await strict.run({}, async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
await page.setViewport({ width: 1280, height: 720 });
|
||||
await page.evaluateOnNewDocument(
|
||||
(layerId, aircraftId) => {
|
||||
window.__qaAircraft = { layerId, aircraftId };
|
||||
const fetch = window.fetch.bind(window);
|
||||
window.__flightPolls = 0;
|
||||
const interval = window.setInterval.bind(window);
|
||||
window.setInterval = function (callback, delay, ...args) {
|
||||
if (typeof callback !== 'function')
|
||||
return interval(callback, delay, ...args);
|
||||
const id = interval(
|
||||
function (...values) {
|
||||
if (window.__handoff?.timer === id) window.__handoff.ticks++;
|
||||
return callback.apply(this, values);
|
||||
},
|
||||
delay,
|
||||
...args,
|
||||
);
|
||||
return id;
|
||||
};
|
||||
});
|
||||
// Refuse the first Cesium canvas. The application must roll back with the
|
||||
// original aircraft still in the cockpit, then allow an ordinary retry.
|
||||
await page.evaluate(() => {
|
||||
const original = HTMLCanvasElement.prototype.getContext;
|
||||
HTMLCanvasElement.prototype.getContext = function (type, ...args) {
|
||||
if (String(type).includes('webgl')) {
|
||||
window.__refusedContext = true;
|
||||
throw new Error('QA renderer startup failure');
|
||||
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,
|
||||
],
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
return original.call(this, type, ...args);
|
||||
};
|
||||
window.__restoreContext = () => {
|
||||
HTMLCanvasElement.prototype.getContext = original;
|
||||
};
|
||||
});
|
||||
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 (
|
||||
engine.kind === kind &&
|
||||
new URL(location.href).searchParams.get('engine') === kind &&
|
||||
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
|
||||
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',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
},
|
||||
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');
|
||||
}
|
||||
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);
|
||||
};
|
||||
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 ===
|
||||
window.__qaAircraft.aircraftId &&
|
||||
new URL(location.href).searchParams.get('engine') === 'cesium'
|
||||
);
|
||||
})),
|
||||
'a failed warm Navara resume must leave Cesium usable without a history entry',
|
||||
},
|
||||
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 strict.passed(
|
||||
page,
|
||||
layerId,
|
||||
'warm Navara failure rolls back to Cesium: same aircraft and cockpit, no history entry',
|
||||
);
|
||||
console.log(
|
||||
'PASS warm Navara failure rolled back to Cesium with the same flight and cockpit',
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
window.__parkedCesium = window.__godsEyeView.engine;
|
||||
});
|
||||
await strict.selectEngine(page, 'navara', HIDDEN_PICKER);
|
||||
await ready('navara');
|
||||
assert.ok(
|
||||
await page.evaluate(() => {
|
||||
const { engine, styleManager } = window.__godsEyeView;
|
||||
return (
|
||||
engine !== window.__parkedNavara &&
|
||||
styleManager.getCockpitState().active &&
|
||||
styleManager.getAircraftTrackingTarget()?.id ===
|
||||
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,
|
||||
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`,
|
||||
),
|
||||
'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;
|
||||
timer: entry.intervalId,
|
||||
ticks: 0,
|
||||
model: entry.module,
|
||||
pollCount: window.__flightPolls,
|
||||
timestamp: entry.module.getStats().lastUpdate,
|
||||
};
|
||||
});
|
||||
// Refuse the first Cesium canvas. The application must roll back with the
|
||||
// original aircraft still in the cockpit, then allow an ordinary retry.
|
||||
await page.evaluate(() => {
|
||||
const original = HTMLCanvasElement.prototype.getContext;
|
||||
HTMLCanvasElement.prototype.getContext = function (type, ...args) {
|
||||
if (String(type).includes('webgl')) {
|
||||
window.__refusedContext = true;
|
||||
throw new Error('QA renderer startup failure');
|
||||
}
|
||||
return original.call(this, type, ...args);
|
||||
};
|
||||
window.__restoreContext = () => {
|
||||
HTMLCanvasElement.prototype.getContext = original;
|
||||
};
|
||||
});
|
||||
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 (
|
||||
window.__startDocument === document &&
|
||||
!document.querySelector('.engine-switch-retry') &&
|
||||
engine.kind === kind &&
|
||||
new URL(location.href).searchParams.get('engine') === kind &&
|
||||
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,
|
||||
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 ===
|
||||
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.
|
||||
})),
|
||||
'a failed warm Navara resume must leave Cesium usable without a history entry',
|
||||
);
|
||||
await strict.passed(
|
||||
page,
|
||||
layerId,
|
||||
'warm Navara failure rolls back to Cesium: same aircraft and cockpit, no history entry',
|
||||
);
|
||||
console.log(
|
||||
'PASS warm Navara failure rolled back to Cesium with the same flight and cockpit',
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
window.__parkedCesium = window.__godsEyeView.engine;
|
||||
});
|
||||
await strict.selectEngine(page, 'navara', HIDDEN_PICKER);
|
||||
await ready('navara');
|
||||
assert.ok(
|
||||
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,
|
||||
const { engine, styleManager } = window.__godsEyeView;
|
||||
return (
|
||||
engine !== window.__parkedNavara &&
|
||||
styleManager.getCockpitState().active &&
|
||||
styleManager.getAircraftTrackingTarget()?.id ===
|
||||
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;
|
||||
});
|
||||
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, []);
|
||||
},
|
||||
);
|
||||
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 ' +
|
||||
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, []);
|
||||
});
|
||||
@@ -1,19 +1,18 @@
|
||||
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]);
|
||||
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||
base.hash =
|
||||
'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'] });
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b);
|
||||
await p.evaluateOnNewDocument(() => {
|
||||
const raw = window.fetch;
|
||||
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);
|
||||
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,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
await p.evaluate(async () => {
|
||||
const { dataManager, tileset } = window.__godsEyeView;
|
||||
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(() => {
|
||||
window.qaSelections = [];
|
||||
});
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
await p.waitForFunction(() =>
|
||||
window.qaSelections.includes('local-adsb:abc123'),
|
||||
);
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
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 LON = -122.4194;
|
||||
@@ -31,8 +35,6 @@ const RING = [
|
||||
await strict.run(
|
||||
{
|
||||
args: [
|
||||
'--use-angle=metal',
|
||||
'--enable-gpu',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
],
|
||||
@@ -43,21 +45,7 @@ await strict.run(
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
await page.goto(base.href);
|
||||
const ready = async (kind) => {
|
||||
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');
|
||||
await waitForEngine(page, 'navara');
|
||||
assert.equal(
|
||||
await page.evaluate(() =>
|
||||
Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__),
|
||||
@@ -172,8 +160,7 @@ await strict.run(
|
||||
if (
|
||||
(await page.evaluate(() => window.__godsEyeView.engine.kind)) !== kind
|
||||
) {
|
||||
await strict.pick(page, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(page, kind);
|
||||
}
|
||||
await wait(2500);
|
||||
const state = await page.evaluate(() => ({
|
||||
|
||||
+140
-143
@@ -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
|
||||
// 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
|
||||
@@ -53,153 +53,150 @@ const setBloom = (page, enabled, intensityPct) =>
|
||||
intensityPct,
|
||||
);
|
||||
|
||||
await strict.run(
|
||||
{ args: ['--use-angle=metal', '--enable-gpu'] },
|
||||
async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
// The same sun on every run: the frame's brightness decides what blooms
|
||||
// and what each style's signature reads.
|
||||
await pinDaytime(page);
|
||||
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'), {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await waitForEngine(page, 'navara');
|
||||
await freeze(page);
|
||||
await strict.run({}, async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
// The same sun on every run: the frame's brightness decides what blooms
|
||||
// and what each style's signature reads.
|
||||
await pinDaytime(page);
|
||||
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'), {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await waitForEngine(page, 'navara');
|
||||
await freeze(page);
|
||||
|
||||
const initial = await page.evaluate(() => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
return { bloom: engine.render.bloom(), pipeline: engine.pipeline() };
|
||||
});
|
||||
check(
|
||||
'Navara blooms itself and starts with bloom off',
|
||||
initial.bloom.enabled === false && !initial.pipeline?.bloomShown,
|
||||
initial,
|
||||
);
|
||||
const normal = await capture(page);
|
||||
save('normal', normal);
|
||||
const normalStats = stats(normal);
|
||||
const initial = await page.evaluate(() => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
return { bloom: engine.render.bloom(), pipeline: engine.pipeline() };
|
||||
});
|
||||
check(
|
||||
'Navara blooms itself and starts with bloom off',
|
||||
initial.bloom.enabled === false && !initial.pipeline?.bloomShown,
|
||||
initial,
|
||||
);
|
||||
const normal = await capture(page);
|
||||
save('normal', normal);
|
||||
const normalStats = stats(normal);
|
||||
|
||||
const full = await setBloom(page, true, 200);
|
||||
check(
|
||||
'the slider at 200 reaches the engine at full strength',
|
||||
full.engine.enabled === true &&
|
||||
full.engine.strength === 1 &&
|
||||
full.pipeline.bloomShown === true,
|
||||
full,
|
||||
);
|
||||
const order = full.pipeline.passes ?? [];
|
||||
const at = order.indexOf('gevBloom');
|
||||
check(
|
||||
'bloom runs after the scene and overlays, just before the final copy',
|
||||
at >= 0 &&
|
||||
order[at + 1] === 'final' &&
|
||||
['toneMapping', 'gevSharpen', 'gevOverlay', 'smaa', 'fxaa'].every(
|
||||
(key) => !order.includes(key) || order.indexOf(key) < at,
|
||||
),
|
||||
order,
|
||||
);
|
||||
const glowFull = await capture(page);
|
||||
save('bloom-200', glowFull);
|
||||
const fullChange = diff(normal, glowFull);
|
||||
const fullStats = stats(glowFull);
|
||||
check(
|
||||
'bloom at 200 glows the frame brighter',
|
||||
fullChange.mean > 3 && luma(fullStats) > luma(normalStats) + 2,
|
||||
{
|
||||
...fullChange,
|
||||
normalLuma: luma(normalStats),
|
||||
bloomLuma: luma(fullStats),
|
||||
},
|
||||
);
|
||||
const full = await setBloom(page, true, 200);
|
||||
check(
|
||||
'the slider at 200 reaches the engine at full strength',
|
||||
full.engine.enabled === true &&
|
||||
full.engine.strength === 1 &&
|
||||
full.pipeline.bloomShown === true,
|
||||
full,
|
||||
);
|
||||
const order = full.pipeline.passes ?? [];
|
||||
const at = order.indexOf('gevBloom');
|
||||
check(
|
||||
'bloom runs after the scene and overlays, just before the final copy',
|
||||
at >= 0 &&
|
||||
order[at + 1] === 'final' &&
|
||||
['toneMapping', 'gevSharpen', 'gevOverlay', 'smaa', 'fxaa'].every(
|
||||
(key) => !order.includes(key) || order.indexOf(key) < at,
|
||||
),
|
||||
order,
|
||||
);
|
||||
const glowFull = await capture(page);
|
||||
save('bloom-200', glowFull);
|
||||
const fullChange = diff(normal, glowFull);
|
||||
const fullStats = stats(glowFull);
|
||||
check(
|
||||
'bloom at 200 glows the frame brighter',
|
||||
fullChange.mean > 3 && luma(fullStats) > luma(normalStats) + 2,
|
||||
{
|
||||
...fullChange,
|
||||
normalLuma: luma(normalStats),
|
||||
bloomLuma: luma(fullStats),
|
||||
},
|
||||
);
|
||||
|
||||
await setBloom(page, true, 100);
|
||||
const glowHalf = await capture(page);
|
||||
save('bloom-100', glowHalf);
|
||||
const halfChange = diff(normal, glowHalf);
|
||||
check(
|
||||
'a lower slider glows less',
|
||||
halfChange.mean > 0.5 && halfChange.mean < fullChange.mean,
|
||||
{ half: halfChange.mean, full: fullChange.mean },
|
||||
);
|
||||
await setBloom(page, true, 100);
|
||||
const glowHalf = await capture(page);
|
||||
save('bloom-100', glowHalf);
|
||||
const halfChange = diff(normal, glowHalf);
|
||||
check(
|
||||
'a lower slider glows less',
|
||||
halfChange.mean > 0.5 && halfChange.mean < fullChange.mean,
|
||||
{ half: halfChange.mean, full: fullChange.mean },
|
||||
);
|
||||
|
||||
const dead = await setBloom(page, true, 10);
|
||||
const deadFrame = await capture(page);
|
||||
check(
|
||||
"Cesium's dead zone: the toggle on, nothing glows",
|
||||
dead.engine.enabled === false &&
|
||||
dead.pipeline.bloomShown === false &&
|
||||
diff(normal, deadFrame).mean === 0,
|
||||
{ engine: dead.engine, diff: diff(normal, deadFrame) },
|
||||
);
|
||||
const dead = await setBloom(page, true, 10);
|
||||
const deadFrame = await capture(page);
|
||||
check(
|
||||
"Cesium's dead zone: the toggle on, nothing glows",
|
||||
dead.engine.enabled === false &&
|
||||
dead.pipeline.bloomShown === false &&
|
||||
diff(normal, deadFrame).mean === 0,
|
||||
{ engine: dead.engine, diff: diff(normal, deadFrame) },
|
||||
);
|
||||
|
||||
// With a style: bloom first, then the style, then the final copy.
|
||||
await setBloom(page, true, 200);
|
||||
const styled = await pickStyle(page, 'noir', 'normal');
|
||||
const styledOrder = styled.pipeline?.passes ?? [];
|
||||
check(
|
||||
'bloom runs before the visual styles',
|
||||
styledOrder.indexOf('gevBloom') >= 0 &&
|
||||
styledOrder.indexOf('gevBloom') + 1 ===
|
||||
styledOrder.indexOf('gevStyles') &&
|
||||
styledOrder.indexOf('gevStyles') + 1 === styledOrder.indexOf('final'),
|
||||
styledOrder,
|
||||
);
|
||||
await pickStyle(page, 'normal', 'noir');
|
||||
// With a style: bloom first, then the style, then the final copy.
|
||||
await setBloom(page, true, 200);
|
||||
const styled = await pickStyle(page, 'noir', 'normal');
|
||||
const styledOrder = styled.pipeline?.passes ?? [];
|
||||
check(
|
||||
'bloom runs before the visual styles',
|
||||
styledOrder.indexOf('gevBloom') >= 0 &&
|
||||
styledOrder.indexOf('gevBloom') + 1 ===
|
||||
styledOrder.indexOf('gevStyles') &&
|
||||
styledOrder.indexOf('gevStyles') + 1 === styledOrder.indexOf('final'),
|
||||
styledOrder,
|
||||
);
|
||||
await pickStyle(page, 'normal', 'noir');
|
||||
|
||||
const off = await setBloom(page, false, 200);
|
||||
const offFrame = await capture(page);
|
||||
check(
|
||||
'turning bloom off restores the frame',
|
||||
off.engine.enabled === false &&
|
||||
off.pipeline.bloomShown === false &&
|
||||
diff(normal, offFrame).mean === 0,
|
||||
{ engine: off.engine, diff: diff(normal, offFrame) },
|
||||
);
|
||||
const off = await setBloom(page, false, 200);
|
||||
const offFrame = await capture(page);
|
||||
check(
|
||||
'turning bloom off restores the frame',
|
||||
off.engine.enabled === false &&
|
||||
off.pipeline.bloomShown === false &&
|
||||
diff(normal, offFrame).mean === 0,
|
||||
{ engine: off.engine, diff: diff(normal, offFrame) },
|
||||
);
|
||||
|
||||
// Cesium keeps its scene bloom stage; its engine has no bloom.
|
||||
await strict.switchTo(page, 'cesium');
|
||||
await freeze(page);
|
||||
await setBloom(page, false, 200);
|
||||
const cesiumNormal = await capture(page);
|
||||
const cesium = await setBloom(page, true, 200);
|
||||
const cesiumStage = await page.evaluate(() => {
|
||||
const a = window.__godsEyeView;
|
||||
const stage = a.viewer?.scene?.postProcessStages?.bloom;
|
||||
return {
|
||||
setBloom: typeof a.engine.render.setBloom,
|
||||
enabled: stage?.enabled ?? null,
|
||||
contrast: stage?.uniforms?.contrast ?? null,
|
||||
};
|
||||
});
|
||||
const cesiumGlow = await capture(page);
|
||||
save('cesium-bloom-200', cesiumGlow);
|
||||
check(
|
||||
"Cesium: its scene's bloom stage glows, the engine draws none",
|
||||
cesium.engine === null &&
|
||||
cesiumStage.setBloom === 'undefined' &&
|
||||
cesiumStage.enabled !== false &&
|
||||
diff(cesiumNormal, cesiumGlow).mean > 1,
|
||||
{ stage: cesiumStage, diff: diff(cesiumNormal, cesiumGlow) },
|
||||
);
|
||||
await setBloom(page, false, 200);
|
||||
const cesiumBack = diff(cesiumNormal, await capture(page));
|
||||
check(
|
||||
'Cesium: bloom off restores its frame',
|
||||
cesiumBack.mean < 0.5,
|
||||
cesiumBack,
|
||||
);
|
||||
// Cesium keeps its scene bloom stage; its engine has no bloom.
|
||||
await strict.switchTo(page, 'cesium');
|
||||
await freeze(page);
|
||||
await setBloom(page, false, 200);
|
||||
const cesiumNormal = await capture(page);
|
||||
const cesium = await setBloom(page, true, 200);
|
||||
const cesiumStage = await page.evaluate(() => {
|
||||
const a = window.__godsEyeView;
|
||||
const stage = a.viewer?.scene?.postProcessStages?.bloom;
|
||||
return {
|
||||
setBloom: typeof a.engine.render.setBloom,
|
||||
enabled: stage?.enabled ?? null,
|
||||
contrast: stage?.uniforms?.contrast ?? null,
|
||||
};
|
||||
});
|
||||
const cesiumGlow = await capture(page);
|
||||
save('cesium-bloom-200', cesiumGlow);
|
||||
check(
|
||||
"Cesium: its scene's bloom stage glows, the engine draws none",
|
||||
cesium.engine === null &&
|
||||
cesiumStage.setBloom === 'undefined' &&
|
||||
cesiumStage.enabled !== false &&
|
||||
diff(cesiumNormal, cesiumGlow).mean > 1,
|
||||
{ stage: cesiumStage, diff: diff(cesiumNormal, cesiumGlow) },
|
||||
);
|
||||
await setBloom(page, false, 200);
|
||||
const cesiumBack = diff(cesiumNormal, await capture(page));
|
||||
check(
|
||||
'Cesium: bloom off restores its frame',
|
||||
cesiumBack.mean < 0.5,
|
||||
cesiumBack,
|
||||
);
|
||||
|
||||
check('no page errors', errors.length === 0, errors);
|
||||
if (outDir)
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, 'bloom.json'),
|
||||
JSON.stringify(report, null, 2),
|
||||
);
|
||||
},
|
||||
);
|
||||
check('no page errors', errors.length === 0, errors);
|
||||
if (outDir)
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, 'bloom.json'),
|
||||
JSON.stringify(report, null, 2),
|
||||
);
|
||||
});
|
||||
@@ -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
|
||||
// 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
|
||||
@@ -101,152 +101,147 @@ const coverage = (page) =>
|
||||
() => window.__godsEyeView.engine.render.cloudCover?.().coverage,
|
||||
);
|
||||
|
||||
await strict.run(
|
||||
{ args: ['--use-angle=metal', '--enable-gpu'] },
|
||||
async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
await page.evaluateOnNewDocument((utc) => {
|
||||
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
||||
const offset = Date.parse(utc) - Date.now();
|
||||
const RealDate = Date;
|
||||
class ShiftedDate extends RealDate {
|
||||
constructor(...args) {
|
||||
if (args.length === 0) super(RealDate.now() + offset);
|
||||
else super(...args);
|
||||
}
|
||||
static now() {
|
||||
return RealDate.now() + offset;
|
||||
}
|
||||
await strict.run({}, async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
await page.evaluateOnNewDocument((utc) => {
|
||||
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
||||
const offset = Date.parse(utc) - Date.now();
|
||||
const RealDate = Date;
|
||||
class ShiftedDate extends RealDate {
|
||||
constructor(...args) {
|
||||
if (args.length === 0) super(RealDate.now() + offset);
|
||||
else super(...args);
|
||||
}
|
||||
globalThis.Date = ShiftedDate;
|
||||
}, MIDDAY_UTC);
|
||||
await page.setViewport({ width: 1280, height: 800 });
|
||||
await page.goto(visualPageUrl(base.origin, 'navara', '', LOW_VIEW), {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await waitForEngine(page, 'navara');
|
||||
await freeze(page);
|
||||
static now() {
|
||||
return RealDate.now() + offset;
|
||||
}
|
||||
}
|
||||
globalThis.Date = ShiftedDate;
|
||||
}, MIDDAY_UTC);
|
||||
await page.setViewport({ width: 1280, height: 800 });
|
||||
await page.goto(visualPageUrl(base.origin, 'navara', '', LOW_VIEW), {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await waitForEngine(page, 'navara');
|
||||
await freeze(page);
|
||||
|
||||
const setup = await page.evaluate(() => {
|
||||
const a = window.__godsEyeView;
|
||||
return {
|
||||
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(() => ({
|
||||
const setup = await page.evaluate(() => {
|
||||
const a = window.__godsEyeView;
|
||||
return {
|
||||
cover: a.engine.render.cloudCover?.() ?? null,
|
||||
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(
|
||||
'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 cockpit(page, false);
|
||||
check(
|
||||
'leaving the cockpit gives the look its own clouds back',
|
||||
(await coverage(page)) === null,
|
||||
);
|
||||
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');
|
||||
|
||||
// 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,
|
||||
};
|
||||
// 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 },
|
||||
});
|
||||
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);
|
||||
});
|
||||
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')),
|
||||
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);
|
||||
if (outDir)
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, 'cockpit-clouds.json'),
|
||||
JSON.stringify(report, null, 2),
|
||||
);
|
||||
},
|
||||
);
|
||||
await cockpit(page, false);
|
||||
check(
|
||||
'leaving the cockpit gives the look its own clouds back',
|
||||
(await coverage(page)) === null,
|
||||
);
|
||||
|
||||
// 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),
|
||||
);
|
||||
});
|
||||
@@ -32,8 +32,6 @@ const AREA = [
|
||||
await strict.run(
|
||||
{
|
||||
args: [
|
||||
'--use-angle=metal',
|
||||
'--enable-gpu',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
],
|
||||
|
||||
@@ -136,8 +136,6 @@ const FILES = [
|
||||
await strict.run(
|
||||
{
|
||||
args: [
|
||||
'--use-angle=metal',
|
||||
'--enable-gpu',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
],
|
||||
|
||||
@@ -10,7 +10,11 @@ import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import sharp from 'sharp';
|
||||
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]);
|
||||
base.search = '?engine=navara&welcome=0';
|
||||
@@ -31,8 +35,6 @@ const EDGE = { x: 1250, y: 560, width: 26, height: 90 };
|
||||
await strict.run(
|
||||
{
|
||||
args: [
|
||||
'--use-angle=metal',
|
||||
'--enable-gpu',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
],
|
||||
@@ -43,21 +45,7 @@ await strict.run(
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
await page.goto(base.href);
|
||||
const ready = async (kind) => {
|
||||
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');
|
||||
await waitForEngine(page, 'navara');
|
||||
assert.equal(
|
||||
await page.evaluate(() =>
|
||||
Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__),
|
||||
@@ -92,8 +80,7 @@ await strict.run(
|
||||
if (
|
||||
(await page.evaluate(() => window.__godsEyeView.engine.kind)) !== kind
|
||||
) {
|
||||
await strict.pick(page, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(page, kind);
|
||||
}
|
||||
await wait(2500);
|
||||
const mask = await page.evaluate(() => {
|
||||
|
||||
+175
-179
@@ -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
|
||||
// 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
|
||||
@@ -98,193 +98,189 @@ const framesIn = (page, ms) =>
|
||||
const mean = (values) =>
|
||||
values.reduce((sum, value) => sum + value, 0) / Math.max(1, values.length);
|
||||
|
||||
await strict.run(
|
||||
{ args: ['--use-angle=metal', '--enable-gpu'] },
|
||||
async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
||||
await strict.run({}, async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
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 });
|
||||
await page.goto(visualPageUrl(base.origin, 'navara'), {
|
||||
waitUntil: 'domcontentloaded',
|
||||
window.__qaSonarSet = set;
|
||||
let missing = 0;
|
||||
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');
|
||||
await freeze(page);
|
||||
set.setCount(places.length);
|
||||
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();
|
||||
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 },
|
||||
});
|
||||
window.__qaSonarSet = set;
|
||||
let missing = 0;
|
||||
places.forEach((place, index) => {
|
||||
const position = engine.camera.pickEllipsoid(place);
|
||||
if (!position) missing++;
|
||||
set.set(index, { position, color: [1, 1, 1, 1] });
|
||||
});
|
||||
set.setCount(places.length);
|
||||
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,
|
||||
await page.evaluate(() => {
|
||||
const { styleManager } = window.__godsEyeView;
|
||||
styleManager._setHudVariant('cyber');
|
||||
// Normal keeps the contacts' own colours; the HUD stays on (in Normal
|
||||
// it would hide itself, and the sweep with it).
|
||||
styleManager.setStyle('normal');
|
||||
styleManager.hud.setMode('on');
|
||||
styleManager._setCyberSonarEnabled(true);
|
||||
styleManager._setCyberSonarSetting('opacity', 35);
|
||||
});
|
||||
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(
|
||||
'the Cyber HUD runs with the contact adapter',
|
||||
theme.theme === 'cyber' && theme.gpu === 'supported' && theme.hud,
|
||||
theme,
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
await page.evaluate(() => {
|
||||
const { styleManager } = window.__godsEyeView;
|
||||
styleManager._setHudVariant('cyber');
|
||||
// Normal keeps the contacts' own colours; the HUD stays on (in Normal
|
||||
// it would hide itself, and the sweep with it).
|
||||
styleManager.setStyle('normal');
|
||||
styleManager.hud.setMode('on');
|
||||
styleManager._setCyberSonarEnabled(true);
|
||||
styleManager._setCyberSonarSetting('opacity', 35);
|
||||
const lit = luma.filter((_, i) => expected[i] > 0.9);
|
||||
const dim = luma.filter((_, i) => expected[i] <= sonar.opacity / 100);
|
||||
sweeps.push({
|
||||
spread: Math.max(...luma.slice(0, 32)) - Math.min(...luma.slice(0, 32)),
|
||||
angle: sonar.angleDeg,
|
||||
lit: lit.length,
|
||||
litLuma: mean(lit),
|
||||
dimLuma: mean(dim),
|
||||
brightest: luma.indexOf(Math.max(...luma)),
|
||||
expectedBrightest: expected.indexOf(Math.max(...expected)),
|
||||
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(
|
||||
'the Cyber HUD runs with the contact adapter',
|
||||
theme.theme === 'cyber' && theme.gpu === 'supported' && theme.hud,
|
||||
theme,
|
||||
`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 },
|
||||
);
|
||||
|
||||
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 lit = luma.filter((_, i) => expected[i] > 0.9);
|
||||
const dim = luma.filter((_, i) => expected[i] <= sonar.opacity / 100);
|
||||
sweeps.push({
|
||||
spread: Math.max(...luma.slice(0, 32)) - Math.min(...luma.slice(0, 32)),
|
||||
angle: sonar.angleDeg,
|
||||
lit: lit.length,
|
||||
litLuma: mean(lit),
|
||||
dimLuma: mean(dim),
|
||||
brightest: luma.indexOf(Math.max(...luma)),
|
||||
expectedBrightest: expected.indexOf(Math.max(...expected)),
|
||||
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(() =>
|
||||
window.__godsEyeView.styleManager._setCyberSonarEnabled(false),
|
||||
);
|
||||
const neutral = await read(page, places);
|
||||
const spread =
|
||||
Math.max(...neutral.luma.slice(0, 32)) -
|
||||
Math.min(...neutral.luma.slice(0, 32));
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
const idle = await framesIn(page, 1500);
|
||||
check(
|
||||
'with the sweep off every contact takes the neutral factor and the scene rests',
|
||||
neutral.sonar.enabled &&
|
||||
!neutral.sonar.active &&
|
||||
// What still differs is the map under the translucent contacts.
|
||||
spread < Math.min(...sweeps.map((sweep) => sweep.spread)) * 0.6 &&
|
||||
idle <= 3,
|
||||
{ spread, sweeping: sweeps.map((sweep) => sweep.spread), frames: idle },
|
||||
);
|
||||
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.styleManager._setCyberSonarEnabled(false),
|
||||
);
|
||||
const neutral = await read(page, places);
|
||||
const spread =
|
||||
Math.max(...neutral.luma.slice(0, 32)) -
|
||||
Math.min(...neutral.luma.slice(0, 32));
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
const idle = await framesIn(page, 1500);
|
||||
check(
|
||||
'with the sweep off every contact takes the neutral factor and the scene rests',
|
||||
neutral.sonar.enabled &&
|
||||
!neutral.sonar.active &&
|
||||
// What still differs is the map under the translucent contacts.
|
||||
spread < Math.min(...sweeps.map((sweep) => sweep.spread)) * 0.6 &&
|
||||
idle <= 3,
|
||||
{ spread, sweeping: sweeps.map((sweep) => sweep.spread), frames: idle },
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
const { styleManager } = window.__godsEyeView;
|
||||
styleManager._setCyberSonarEnabled(true);
|
||||
styleManager._setHudVariant('tactical');
|
||||
styleManager.setStyle('normal');
|
||||
styleManager.hud.setMode('auto');
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView.styleManager.transitions.size === 0,
|
||||
);
|
||||
const after = await read(page, places);
|
||||
const drift = Math.max(
|
||||
...after.luma.map((value, i) => Math.abs(value - baseline.luma[i])),
|
||||
);
|
||||
check(
|
||||
'leaving the Cyber HUD gives the contacts back unchanged',
|
||||
after.sonar.enabled === false && drift < 3,
|
||||
{ drift },
|
||||
);
|
||||
await page.evaluate(() => window.__qaSonarSet.destroy());
|
||||
|
||||
await page.evaluate(() => {
|
||||
const { styleManager } = window.__godsEyeView;
|
||||
styleManager._setCyberSonarEnabled(true);
|
||||
styleManager._setHudVariant('tactical');
|
||||
styleManager.setStyle('normal');
|
||||
styleManager.hud.setMode('auto');
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView.styleManager.transitions.size === 0,
|
||||
);
|
||||
const after = await read(page, places);
|
||||
const drift = Math.max(
|
||||
...after.luma.map((value, i) => Math.abs(value - baseline.luma[i])),
|
||||
);
|
||||
check(
|
||||
'leaving the Cyber HUD gives the contacts back unchanged',
|
||||
after.sonar.enabled === false && drift < 3,
|
||||
{ drift },
|
||||
);
|
||||
await page.evaluate(() => window.__qaSonarSet.destroy());
|
||||
// Cesium keeps its command adapter; its engine draws no sonar.
|
||||
await strict.switchTo(page, 'cesium');
|
||||
await page.evaluate(() => {
|
||||
const { styleManager } = window.__godsEyeView;
|
||||
styleManager._setHudVariant('cyber');
|
||||
});
|
||||
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'),
|
||||
);
|
||||
|
||||
// Cesium keeps its command adapter; its engine draws no sonar.
|
||||
await strict.switchTo(page, 'cesium');
|
||||
await page.evaluate(() => {
|
||||
const { styleManager } = window.__godsEyeView;
|
||||
styleManager._setHudVariant('cyber');
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => document.documentElement.dataset.cyberSonarGpu,
|
||||
{ timeout: 10000 },
|
||||
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),
|
||||
);
|
||||
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
@@ -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
|
||||
// signature against Normal), switching styles crossfades (both styles drawn
|
||||
// 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);
|
||||
|
||||
await strict.run(
|
||||
{ args: ['--use-angle=metal', '--enable-gpu'] },
|
||||
async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
// The same sun on every run: the frame's brightness decides what blooms
|
||||
// and what each style's signature reads.
|
||||
await pinDaytime(page);
|
||||
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 waitForEngine(page, 'navara');
|
||||
await freeze(page);
|
||||
await strict.run({}, async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
// The same sun on every run: the frame's brightness decides what blooms
|
||||
// and what each style's signature reads.
|
||||
await pinDaytime(page);
|
||||
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 waitForEngine(page, 'navara');
|
||||
await freeze(page);
|
||||
|
||||
const capabilities = await page.evaluate(() => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
return {
|
||||
styles: engine.render.visualStyles,
|
||||
pipeline: engine.pipeline(),
|
||||
};
|
||||
});
|
||||
const capabilities = await page.evaluate(() => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
return {
|
||||
styles: engine.render.visualStyles,
|
||||
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(
|
||||
'Navara draws every app style itself',
|
||||
JSON.stringify(capabilities.styles) === JSON.stringify(STYLES),
|
||||
capabilities,
|
||||
`${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(
|
||||
'no style pass shows on Normal',
|
||||
capabilities.pipeline?.shown === false,
|
||||
capabilities.pipeline,
|
||||
`${name}: the engine fades it in over several frames`,
|
||||
between.length >= 1,
|
||||
{ frames: fade.samples.length, between: between.length },
|
||||
);
|
||||
|
||||
// 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(
|
||||
`${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,
|
||||
if (previous !== 'normal') {
|
||||
const both = fade.samples.filter(
|
||||
([out, next]) => out > 0.05 && out < 0.95 && next > 0.05 && next < 0.95,
|
||||
);
|
||||
check(
|
||||
`${name}: the engine fades it in over several frames`,
|
||||
between.length >= 1,
|
||||
{ frames: fade.samples.length, between: between.length },
|
||||
`${name}: ${previous} and ${name} draw together mid-crossfade`,
|
||||
both.length >= 1,
|
||||
{ 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;
|
||||
}
|
||||
|
||||
// A style parameter reaches the Navara pass (FLIR's Ironbow palette).
|
||||
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,
|
||||
},
|
||||
const others = Object.entries(fade.intensities).filter(
|
||||
([style, value]) => style !== name && value !== 0,
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
window.__godsEyeView.styleManager.stages.thermal.uniforms.palette = 0;
|
||||
});
|
||||
|
||||
const back = await pickStyle(page, 'normal', previous);
|
||||
check(
|
||||
'Normal: every style back at 0 and the pass hidden',
|
||||
Object.values(back.intensities).every((v) => v === 0) &&
|
||||
back.pipeline?.shown === false,
|
||||
back,
|
||||
`${name}: alone at full intensity once settled`,
|
||||
fade.intensities[name] === 1 && others.length === 0,
|
||||
fade.intensities,
|
||||
);
|
||||
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);
|
||||
const order = fade.pipeline?.passes ?? [];
|
||||
const at = order.indexOf('gevStyles');
|
||||
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,
|
||||
`${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(
|
||||
'style=snow opens on Normal',
|
||||
snow.active === 'normal' && snow.intensities.every((v) => v === 0),
|
||||
snow,
|
||||
`${name}: the frame changes against Normal`,
|
||||
change.mean > 6 && change.changedShare > 0.25,
|
||||
{ ...change, histogramShift: shift },
|
||||
);
|
||||
await page.goto(pageUrl('navara', `&style=${URL_NAMES.thermal}`), {
|
||||
waitUntil: 'domcontentloaded',
|
||||
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,
|
||||
});
|
||||
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);
|
||||
previous = name;
|
||||
}
|
||||
|
||||
// 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,
|
||||
);
|
||||
// A style parameter reaches the Navara pass (FLIR's Ironbow palette).
|
||||
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;
|
||||
});
|
||||
|
||||
check('no page errors', errors.length === 0, errors);
|
||||
if (outDir)
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, 'visual-styles.json'),
|
||||
JSON.stringify(report, null, 2),
|
||||
);
|
||||
},
|
||||
);
|
||||
const back = await pickStyle(page, 'normal', previous);
|
||||
check(
|
||||
'Normal: every style back at 0 and the pass hidden',
|
||||
Object.values(back.intensities).every((v) => v === 0) &&
|
||||
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),
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,11 @@
|
||||
import sharp from 'sharp';
|
||||
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]);
|
||||
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) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
p.on('console', (msg) => {
|
||||
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
|
||||
errors.push(msg.text());
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b, { shaderErrors: true });
|
||||
const assets = [];
|
||||
p.on('request', (req) => {
|
||||
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);
|
||||
});
|
||||
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,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
await p.evaluate(async () => {
|
||||
const { tileset, dataManager, sceneDirector } = window.__godsEyeView;
|
||||
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']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
}
|
||||
const state = await p.evaluate(() => {
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
@@ -184,8 +164,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
await p.waitForFunction(
|
||||
() => window.qaEvent.getPlaybackState().progress > 0.53,
|
||||
);
|
||||
await strict.pick(p, 'cesium');
|
||||
await ready('cesium');
|
||||
await strict.switchTo(p, 'cesium');
|
||||
const live = await p.evaluate(() => window.qaEvent.getPlaybackState());
|
||||
await strict.check(
|
||||
p,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
import sharp from 'sharp';
|
||||
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]);
|
||||
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||
@@ -11,34 +16,10 @@ const strict = createStrict({
|
||||
origin: base.origin,
|
||||
layers: ['bhote-koshi-locator'],
|
||||
});
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
p.on('console', (msg) => {
|
||||
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
|
||||
errors.push(msg.text());
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b, { shaderErrors: true });
|
||||
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,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
await p.evaluate(async () => {
|
||||
const { tileset, dataManager } = window.__godsEyeView;
|
||||
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']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
}
|
||||
const { same, stats, params, at } = await p.evaluate(
|
||||
(world) => {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import sharp from 'sharp';
|
||||
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
|
||||
// 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'),
|
||||
],
|
||||
});
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b);
|
||||
await p.evaluateOnNewDocument((ring) => {
|
||||
const fetch = window.fetch.bind(window);
|
||||
window.qaZoneReads = 0;
|
||||
@@ -101,20 +100,6 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
};
|
||||
}, RING);
|
||||
await p.goto(base.href);
|
||||
const ready = async (kind) => {
|
||||
await p.waitForFunction(
|
||||
(k) =>
|
||||
window.__godsEyeView?.engine?.kind === k &&
|
||||
!document.querySelector('#globe-engine')?.disabled &&
|
||||
!document.querySelector('.engine-switch-frame') &&
|
||||
document.querySelector('#loading-screen.hidden'),
|
||||
{ timeout: 90000 },
|
||||
kind,
|
||||
);
|
||||
await p.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
};
|
||||
const setEnabled = (id, on) =>
|
||||
p.evaluate(
|
||||
(layerId, show) =>
|
||||
@@ -177,7 +162,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const greener = (on, off) =>
|
||||
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(() => {
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
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()) {
|
||||
const readsBeforeSwitch = await p.evaluate(() => window.qaZoneReads);
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
const same = await p.evaluate(() =>
|
||||
Object.entries(window.qaModels).every(
|
||||
([id, model]) =>
|
||||
|
||||
@@ -6,86 +6,83 @@ const url = localPreview(process.argv[2]);
|
||||
url.search = '?engine=navara&welcome=0';
|
||||
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'] });
|
||||
await strict.run(
|
||||
{ args: ['--use-angle=metal', '--enable-gpu'] },
|
||||
async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
await installRadioFixture(page);
|
||||
await page.goto(url.href);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.engine?.kind === 'navara' &&
|
||||
document.querySelector('#loading-screen.hidden'),
|
||||
);
|
||||
await page.evaluate(async (id) => {
|
||||
const { dataManager, styleManager, tileset } = window.__godsEyeView;
|
||||
await styleManager.initialRestorePromise;
|
||||
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
|
||||
throw Error('Expected keyless preview');
|
||||
window.qaLayer = dataManager.layers.get('radio').module;
|
||||
await dataManager.setEnabled('radio', true, { origin: 'user' });
|
||||
}, stations[0].id);
|
||||
await page.waitForFunction(() => window.qaLayer?.getStats().count === 750);
|
||||
await page.evaluate((id) => {
|
||||
window.qaLayer.selectStation(id, {
|
||||
autoplay: true,
|
||||
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;
|
||||
await strict.run({}, async (browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
await installRadioFixture(page);
|
||||
await page.goto(url.href);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.engine?.kind === 'navara' &&
|
||||
document.querySelector('#loading-screen.hidden'),
|
||||
);
|
||||
await page.evaluate(async (id) => {
|
||||
const { dataManager, styleManager, tileset } = window.__godsEyeView;
|
||||
await styleManager.initialRestorePromise;
|
||||
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
|
||||
throw Error('Expected keyless preview');
|
||||
window.qaLayer = dataManager.layers.get('radio').module;
|
||||
await dataManager.setEnabled('radio', true, { origin: 'user' });
|
||||
}, stations[0].id);
|
||||
await page.waitForFunction(() => window.qaLayer?.getStats().count === 750);
|
||||
await page.evaluate((id) => {
|
||||
window.qaLayer.selectStation(id, {
|
||||
autoplay: true,
|
||||
focus: false,
|
||||
origin: 'user',
|
||||
});
|
||||
for (const kind of ['cesium', 'navara']) {
|
||||
await strict.switchTo(page, kind);
|
||||
const state = await page.evaluate(() => ({
|
||||
same:
|
||||
window.qaLayer ===
|
||||
window.__godsEyeView.dataManager.layers.get('radio').module,
|
||||
player: window.qaAudio === window.qaPlayer,
|
||||
snapshot:
|
||||
window.qaSnapshot === window.qaLayer.getAcceptedCatalogSnapshot(),
|
||||
plays: window.__qaRadio.plays.length,
|
||||
reads: window.__qaRadio.reads,
|
||||
state: window.qaLayer.getUIState(),
|
||||
count: window.qaLayer.getStats().count,
|
||||
}));
|
||||
console.log(
|
||||
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, []);
|
||||
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']) {
|
||||
await strict.switchTo(page, kind);
|
||||
const state = await page.evaluate(() => ({
|
||||
same:
|
||||
window.qaLayer ===
|
||||
window.__godsEyeView.dataManager.layers.get('radio').module,
|
||||
player: window.qaAudio === window.qaPlayer,
|
||||
snapshot:
|
||||
window.qaSnapshot === window.qaLayer.getAcceptedCatalogSnapshot(),
|
||||
plays: window.__qaRadio.plays.length,
|
||||
reads: window.__qaRadio.reads,
|
||||
state: window.qaLayer.getUIState(),
|
||||
count: window.qaLayer.getStats().count,
|
||||
}));
|
||||
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',
|
||||
);
|
||||
});
|
||||
@@ -14,7 +14,7 @@ const strict = createStrict({
|
||||
origin: url.origin,
|
||||
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 errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
|
||||
@@ -54,7 +54,7 @@ const getOpt = (name, dflt) => {
|
||||
};
|
||||
|
||||
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
|
||||
// skipped where the engine has no military layer.
|
||||
const ENGINE = (argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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 { KV_LINE, KV_SEQUENCE_ID } from './fixtures/street-level/kartaView.mjs';
|
||||
import {
|
||||
@@ -28,186 +32,166 @@ const strict = createStrict({
|
||||
'https://panoramax.openstreetmap.fr/*',
|
||||
],
|
||||
});
|
||||
await strict.run(
|
||||
{ args: ['--use-angle=metal', '--enable-gpu'] },
|
||||
async (browser) => {
|
||||
const p = await browser.newPage();
|
||||
await p.setViewport({ width: 1280, height: 900 });
|
||||
const errors = [],
|
||||
fixture = newFixture({ configured: true });
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.error(e.message.split('?')[0]);
|
||||
});
|
||||
p.on('console', (m) => {
|
||||
if (/shader|registerMeshPicker/i.test(m.text()) && m.type() === 'error')
|
||||
errors.push(m.text());
|
||||
});
|
||||
await serveFixtures(p, fixture);
|
||||
let photoRequests = 0;
|
||||
p.on('request', (req) => {
|
||||
if (
|
||||
/\.jpg(?:$|\?)/i.test(new URL(req.url()).pathname) &&
|
||||
/openstreetcam|kartaview|panoramax/.test(new URL(req.url()).hostname)
|
||||
)
|
||||
photoRequests++;
|
||||
});
|
||||
await p.goto(base.href);
|
||||
const ready = async (k) => {
|
||||
await p.waitForFunction(
|
||||
(kind) =>
|
||||
window.__godsEyeView?.engine?.kind === kind &&
|
||||
!document.querySelector('#globe-engine')?.disabled &&
|
||||
!document.querySelector('.engine-switch-frame') &&
|
||||
document.querySelector('#loading-screen.hidden'),
|
||||
{ timeout: 90000 },
|
||||
k,
|
||||
);
|
||||
await p.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
};
|
||||
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,
|
||||
);
|
||||
await strict.run({}, async (browser) => {
|
||||
const p = await browser.newPage();
|
||||
await p.setViewport({ width: 1280, height: 900 });
|
||||
const errors = [],
|
||||
fixture = newFixture({ configured: true });
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.error(e.message.split('?')[0]);
|
||||
});
|
||||
p.on('console', (m) => {
|
||||
if (/shader|registerMeshPicker/i.test(m.text()) && m.type() === 'error')
|
||||
errors.push(m.text());
|
||||
});
|
||||
await serveFixtures(p, fixture);
|
||||
let photoRequests = 0;
|
||||
p.on('request', (req) => {
|
||||
if (
|
||||
/\.jpg(?:$|\?)/i.test(new URL(req.url()).pathname) &&
|
||||
/openstreetcam|kartaview|panoramax/.test(new URL(req.url()).hostname)
|
||||
)
|
||||
photoRequests++;
|
||||
});
|
||||
await p.goto(base.href);
|
||||
await waitForEngine(p, '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;
|
||||
}
|
||||
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,
|
||||
),
|
||||
};
|
||||
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),
|
||||
);
|
||||
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(
|
||||
p,
|
||||
'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.
|
||||
await p.evaluate(() => window.qaStreet.closeViewer());
|
||||
await until(() => !window.qaStreet.getUIState().street.open);
|
||||
await strict.pick(p, 'cesium');
|
||||
await ready('cesium');
|
||||
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 =
|
||||
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(
|
||||
() =>
|
||||
!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, []);
|
||||
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(
|
||||
'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',
|
||||
);
|
||||
});
|
||||
@@ -41,7 +41,7 @@ const navaraCode = async (url) => {
|
||||
};
|
||||
const 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']) {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1280, height: 800 });
|
||||
|
||||
@@ -32,7 +32,7 @@ const other = (kind) => (kind === 'navara' ? 'cesium' : 'navara');
|
||||
const strict = createStrict({ origin: base.origin, layers: [] });
|
||||
const results = [];
|
||||
strict.extra.contracts = results;
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
await strict.run({}, async (b) => {
|
||||
let p = null;
|
||||
const errors = [];
|
||||
// A fresh page on `from`; warm visits the other engine first.
|
||||
|
||||
@@ -37,7 +37,7 @@ const other = (kind) => (kind === 'navara' ? 'cesium' : 'navara');
|
||||
const strict = createStrict({ origin: base.origin, layers: [] });
|
||||
const 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
|
||||
// engine first so it is parked.
|
||||
const open = async (start, warm) => {
|
||||
|
||||
@@ -29,8 +29,21 @@ import {
|
||||
import { createMissLoader, tileCacheFile } from './qa/tileCache.mjs';
|
||||
|
||||
export const ENGINES = Object.freeze(['cesium', 'navara']);
|
||||
const RESERVED_PORTS = ['', '4173', '4185', '4186', '4187', '4188', '4189'];
|
||||
const RESERVED = [...RESERVED_PORTS, '4194', '4196'];
|
||||
/** Local ports that belong to the user's own servers: never a QA preview. */
|
||||
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). */
|
||||
export function strictArguments(argv = process.argv) {
|
||||
@@ -48,7 +61,7 @@ export function localPreview(href) {
|
||||
const url = new URL(href);
|
||||
if (
|
||||
!['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');
|
||||
return url;
|
||||
@@ -89,11 +102,18 @@ export async function installStrictNetwork(
|
||||
// workers' own requests are never held.
|
||||
const intercept = async (session, type) => {
|
||||
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', {
|
||||
requestId,
|
||||
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: Buffer.from(body).toString('base64') }),
|
||||
@@ -106,12 +126,7 @@ export async function installStrictNetwork(
|
||||
const answer = await route.respond(request);
|
||||
return await fulfil(
|
||||
answer.status ?? 200,
|
||||
[
|
||||
...(answer.contentType
|
||||
? [{ name: 'Content-Type', value: answer.contentType }]
|
||||
: []),
|
||||
{ name: 'Access-Control-Allow-Origin', value: '*' },
|
||||
],
|
||||
answer.contentType,
|
||||
answer.body ?? '',
|
||||
);
|
||||
}
|
||||
@@ -128,14 +143,9 @@ export async function installStrictNetwork(
|
||||
(ledger.probes[probe.pattern] ?? 0) + 1;
|
||||
return await fulfil(
|
||||
probe.status,
|
||||
[
|
||||
...(probe.contentType
|
||||
? [{ name: 'Content-Type', value: probe.contentType }]
|
||||
: []),
|
||||
{ name: 'Cache-Control', value: 'no-store' },
|
||||
{ name: 'Access-Control-Allow-Origin', value: '*' },
|
||||
],
|
||||
probe.contentType,
|
||||
probe.body ?? null,
|
||||
{ noStore: true },
|
||||
);
|
||||
}
|
||||
const accept = Object.entries(request.headers ?? {}).find(
|
||||
@@ -172,16 +182,7 @@ export async function installStrictNetwork(
|
||||
const host = new URL(request.url).host;
|
||||
tiles.fetchedByHost[host] = (tiles.fetchedByHost[host] ?? 0) + 1;
|
||||
}
|
||||
await fulfil(
|
||||
entry.status,
|
||||
[
|
||||
...(entry.contentType
|
||||
? [{ name: 'Content-Type', value: entry.contentType }]
|
||||
: []),
|
||||
{ name: 'Access-Control-Allow-Origin', value: '*' },
|
||||
],
|
||||
entry.body,
|
||||
);
|
||||
await fulfil(entry.status, entry.contentType, entry.body);
|
||||
} catch (error) {
|
||||
if (
|
||||
/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
|
||||
* has assigned it are logged with their stack in `ledger.cesiumBeforeLoad`
|
||||
* 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) {
|
||||
const binding = '__gevStrictCesiumRead';
|
||||
const record = process.env.GEV_QA_CESIUM_TRAP === 'record';
|
||||
session.on('Runtime.bindingCalled', (event) => {
|
||||
if (event.name !== binding) return;
|
||||
const stack = String(event.payload).slice(0, 1500);
|
||||
const known = ledger.cesiumBeforeLoad.find((read) => read.stack === stack);
|
||||
if (known) known.count++;
|
||||
else if (ledger.cesiumBeforeLoad.length < (record ? 2000 : 50))
|
||||
else if (ledger.cesiumBeforeLoad.length < 50)
|
||||
ledger.cesiumBeforeLoad.push({ stack, count: 1 });
|
||||
});
|
||||
await session.send('Runtime.enable');
|
||||
@@ -349,10 +344,8 @@ async function trapCesiumGlobal(session, ledger) {
|
||||
await session.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||
source: `(() => {
|
||||
if (window.top !== window) return;
|
||||
const record = ${record};
|
||||
const seen = new Set();
|
||||
let sdk;
|
||||
let pageLoaded = false;
|
||||
const report = () => {
|
||||
const limit = Error.stackTraceLimit;
|
||||
Error.stackTraceLimit = 12;
|
||||
@@ -366,20 +359,7 @@ async function trapCesiumGlobal(session, ledger) {
|
||||
Object.defineProperty(window, 'Cesium', {
|
||||
configurable: true,
|
||||
get() {
|
||||
if (!record) {
|
||||
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);
|
||||
}
|
||||
if (sdk === undefined) report();
|
||||
return sdk;
|
||||
},
|
||||
set(value) {
|
||||
@@ -434,30 +414,8 @@ export async function pickEngine(
|
||||
);
|
||||
const opened = !(await expanded());
|
||||
if (opened) {
|
||||
await page.click('#control-panel-toggle').catch(async (error) => {
|
||||
const state = await page.evaluate(() => {
|
||||
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.click('#control-panel-toggle').catch((error) => {
|
||||
throw Error(`cannot click the control panel toggle: ${error.message}`);
|
||||
});
|
||||
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.
|
||||
*
|
||||
@@ -653,9 +632,16 @@ export function createStrict({
|
||||
ledger,
|
||||
extra,
|
||||
timings,
|
||||
/** Launch Chrome with the policy on every target. */
|
||||
async launch(options = {}) {
|
||||
const browser = await puppeteer.launch({ headless: true, ...options });
|
||||
/**
|
||||
* Launch headless Chrome with the policy on every target; `args` adds
|
||||
* flags to CHROME_ARGS.
|
||||
*/
|
||||
async launch({ args = [], ...options } = {}) {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
...options,
|
||||
args: [...CHROME_ARGS, ...args],
|
||||
});
|
||||
await installStrictNetwork(browser, {
|
||||
origin,
|
||||
fixtures,
|
||||
@@ -665,7 +651,7 @@ export function createStrict({
|
||||
});
|
||||
return browser;
|
||||
},
|
||||
/** Launch, run `body(browser)`, close, then finish(). */
|
||||
/** Launch with `options`, run `body(browser)`, close, then finish(). */
|
||||
async run(options, body) {
|
||||
const browser = await strict.launch(options);
|
||||
let failed = null;
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
|
||||
const base = localPreview(process.argv[2]);
|
||||
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();
|
||||
await p.setViewport({ width: 1280, height: 800 });
|
||||
const errors = [];
|
||||
|
||||
@@ -20,7 +20,7 @@ const base = localPreview(process.argv[2]);
|
||||
const strict = createStrict({ origin: base.origin, layers: [] });
|
||||
const 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) {
|
||||
const page = await b.newPage();
|
||||
await page.setViewport({ width: 1280, height: 800 });
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
|
||||
const base = localPreview(process.argv[2]);
|
||||
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();
|
||||
await p.setViewport({ width: 1280, height: 800 });
|
||||
const errors = [];
|
||||
|
||||
@@ -156,9 +156,7 @@ strict.extra.soak = report;
|
||||
|
||||
for (let run = 1; run <= runs; run++) {
|
||||
const start = run % 2 ? 'navara' : 'cesium';
|
||||
const browser = await strict.launch({
|
||||
args: ['--use-angle=metal', '--enable-gpu'],
|
||||
});
|
||||
const browser = await strict.launch();
|
||||
const row = { run, start, swaps: [], checkpoints: [], errors: [] };
|
||||
report.runs.push(row);
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
import assert from 'node:assert/strict';
|
||||
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]);
|
||||
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||
@@ -27,39 +32,15 @@ const strict = createStrict({
|
||||
origin: base.origin,
|
||||
layers: ['telegeography-submarine-cables'],
|
||||
});
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b, { shaderErrors: true });
|
||||
const reads = [];
|
||||
p.on('request', (req) => {
|
||||
const path = new URL(req.url()).pathname;
|
||||
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);
|
||||
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,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
await p.evaluate(async () => {
|
||||
const { dataManager, tileset } = window.__godsEyeView;
|
||||
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']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
}
|
||||
await p.waitForFunction(
|
||||
() => window.qaCables.getStats().referenceLabelCount > 0,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { trafficFixtureResponse } from './traffic-fixtures.mjs';
|
||||
import sharp from 'sharp';
|
||||
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';
|
||||
|
||||
const base = localPreview(process.argv[2]);
|
||||
@@ -22,14 +27,8 @@ const strict = createStrict({
|
||||
fixtures: ['https://tiles.maps.eox.at/*', 'https://tiles.openfreemap.org/*'],
|
||||
tileHosts: STRICT_TILE_HOSTS.filter((host) => host !== 'tiles.maps.eox.at'),
|
||||
});
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b, { shaderErrors: true });
|
||||
|
||||
const keyless = process.argv.includes('--simulation');
|
||||
const reads = new Map();
|
||||
@@ -73,26 +72,8 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
}
|
||||
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);
|
||||
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,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
await p.evaluate(async () => {
|
||||
const { tileset, dataManager } = window.__godsEyeView;
|
||||
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']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
}
|
||||
await waitTraffic();
|
||||
const check = await p.evaluate(() => {
|
||||
|
||||
@@ -31,7 +31,7 @@ const strict = createStrict({
|
||||
'*://*.earthdata.nasa.gov/*',
|
||||
],
|
||||
});
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
await strict.run({}, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1280, height: 900 });
|
||||
const errors = [];
|
||||
|
||||
@@ -38,7 +38,7 @@ const getOpt = (name, dflt) => {
|
||||
};
|
||||
|
||||
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
|
||||
// viewer; the URL gets ?engine=. Without it, as before.
|
||||
const ENGINE = getOpt('--engine', null);
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* 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'
|
||||
*
|
||||
* --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
|
||||
* chevron's drawn position and depth-test flag) instead of the Cesium
|
||||
* viewer's primitives; the URL gets ?engine=. Without it, as before.
|
||||
|
||||
+52
-47
@@ -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 os from 'node:os';
|
||||
import path from 'node:path';
|
||||
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';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const option = (name, fallback) =>
|
||||
args.includes(name) ? args[args.indexOf(name) + 1] : fallback;
|
||||
const url = option('--url', 'http://127.0.0.1:4193');
|
||||
const output = option('--report', '/private/tmp/gev-imagery.json');
|
||||
if (
|
||||
['4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||
new URL(url).port,
|
||||
)
|
||||
)
|
||||
throw new Error('Reserved QA port');
|
||||
const { origin } = localPreview(process.argv[2]);
|
||||
const url = origin + '/?engine=navara';
|
||||
const { evidence } = strictArguments();
|
||||
const output = evidence
|
||||
? path.join(path.dirname(evidence), 'imagery-report.json')
|
||||
: path.join(os.tmpdir(), 'gev-imagery.json');
|
||||
const report = {
|
||||
url,
|
||||
checks: [],
|
||||
@@ -26,7 +35,7 @@ let failLandsat = false,
|
||||
// The engine the page runs, for the strict assertion manifest.
|
||||
let engine = null;
|
||||
const strict = createStrict({
|
||||
origin: localPreview(url).origin,
|
||||
origin,
|
||||
layers: ['recent-imagery'],
|
||||
// The scenario answers the NASA catalog and imagery tiles itself.
|
||||
fixtures: ['*://*.earthdata.nasa.gov/*'],
|
||||
@@ -48,8 +57,6 @@ const browser = await strict.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--use-angle=metal',
|
||||
'--enable-gpu',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
],
|
||||
@@ -63,7 +70,7 @@ try {
|
||||
const text = redact(message.text());
|
||||
const expected =
|
||||
isExpectedPreviewSetupError(message, {
|
||||
preview: args.includes('--preview'),
|
||||
preview: true,
|
||||
baseUrl: url,
|
||||
}) ||
|
||||
(failedTiles > 0 &&
|
||||
@@ -231,37 +238,35 @@ try {
|
||||
await sleep(2500);
|
||||
image = await capture('ab');
|
||||
check('A/B dates on opposite sides', red(image.left) && green(image.right));
|
||||
if (args.includes('--switch')) {
|
||||
const first = await page.evaluate(() => {
|
||||
window.qaImageryOwner = window.__gevRecentImagery.layer;
|
||||
return window.qaImageryOwner.diagnostics().pins;
|
||||
});
|
||||
const reads = () =>
|
||||
report.requests.filter((r) => r.host === 'cmr.earthdata.nasa.gov').length;
|
||||
const expectedReads = reads();
|
||||
const initial = await page.evaluate(() => window.__godsEyeView.engine.kind);
|
||||
for (const kind of [initial === 'navara' ? 'cesium' : 'navara', initial]) {
|
||||
await strict.switchTo(page, kind);
|
||||
engine = kind;
|
||||
const state = await page.evaluate(() => ({
|
||||
same: window.__gevRecentImagery.layer === window.qaImageryOwner,
|
||||
...window.qaImageryOwner.diagnostics(),
|
||||
}));
|
||||
check(
|
||||
kind + ' holds both original pins',
|
||||
state.same &&
|
||||
state.mode === 'ab' &&
|
||||
state.pins.a === first.a &&
|
||||
state.pins.b === first.b,
|
||||
);
|
||||
await sleep(1800);
|
||||
const pixels = await capture('switch-' + kind);
|
||||
check(
|
||||
kind + ' renders both pinned dates',
|
||||
red(pixels.left) && green(pixels.right),
|
||||
);
|
||||
check(kind + ' reuses the STAC search', reads() === expectedReads);
|
||||
}
|
||||
const first = await page.evaluate(() => {
|
||||
window.qaImageryOwner = window.__gevRecentImagery.layer;
|
||||
return window.qaImageryOwner.diagnostics().pins;
|
||||
});
|
||||
const reads = () =>
|
||||
report.requests.filter((r) => r.host === 'cmr.earthdata.nasa.gov').length;
|
||||
const expectedReads = reads();
|
||||
const initial = await page.evaluate(() => window.__godsEyeView.engine.kind);
|
||||
for (const kind of [initial === 'navara' ? 'cesium' : 'navara', initial]) {
|
||||
await strict.switchTo(page, kind);
|
||||
engine = kind;
|
||||
const state = await page.evaluate(() => ({
|
||||
same: window.__gevRecentImagery.layer === window.qaImageryOwner,
|
||||
...window.qaImageryOwner.diagnostics(),
|
||||
}));
|
||||
check(
|
||||
kind + ' holds both original pins',
|
||||
state.same &&
|
||||
state.mode === 'ab' &&
|
||||
state.pins.a === first.a &&
|
||||
state.pins.b === first.b,
|
||||
);
|
||||
await sleep(1800);
|
||||
const pixels = await capture('switch-' + kind);
|
||||
check(
|
||||
kind + ' renders both pinned dates',
|
||||
red(pixels.left) && green(pixels.right),
|
||||
);
|
||||
check(kind + ' reuses the STAC search', reads() === expectedReads);
|
||||
}
|
||||
await page.evaluate(() => window.__gevRecentImagery.layer.swapSides());
|
||||
image = await capture('swap');
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
import sharp from 'sharp';
|
||||
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]);
|
||||
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||
@@ -16,14 +21,8 @@ const strict = createStrict({
|
||||
'weather-cyclones',
|
||||
],
|
||||
});
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b);
|
||||
const times = ['2026-11-02T12:00:00.000Z', '2026-11-02T12:05:00.000Z'];
|
||||
const ids = ['weather-radar', 'weather-satellite', 'weather-lightning'];
|
||||
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));
|
||||
});
|
||||
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,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
const waitTime = (ids, time) =>
|
||||
p.waitForFunction(
|
||||
(ids, time) =>
|
||||
@@ -251,8 +236,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const polls = [...reads.entries()];
|
||||
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
}
|
||||
await waitTime(ids, times[0]);
|
||||
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']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await p.select('#globe-engine', kind);
|
||||
await ready(kind);
|
||||
await waitForEngine(p, kind);
|
||||
}
|
||||
await p.waitForFunction(
|
||||
() =>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import sharp from 'sharp';
|
||||
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]);
|
||||
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.
|
||||
const WIND_PATHS = 7200;
|
||||
const strict = createStrict({ origin: base.origin, layers: ['wind'] });
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
await strict.run({}, async (b) => {
|
||||
const { page: p, errors } = await openPage(b);
|
||||
const reads = [];
|
||||
await p.setRequestInterception(true);
|
||||
p.on('request', (req) => {
|
||||
@@ -70,21 +69,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
}
|
||||
});
|
||||
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,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await waitForEngine(p, 'navara');
|
||||
await p.evaluate(async () => {
|
||||
const { dataManager, tileset } = window.__godsEyeView;
|
||||
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;
|
||||
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await strict.switchTo(p, kind);
|
||||
}
|
||||
await p.waitForFunction(
|
||||
() =>
|
||||
|
||||
@@ -5,19 +5,7 @@
|
||||
// the session ids, listener counts and transport counters.
|
||||
import puppeteer from 'puppeteer';
|
||||
import { blockPaidQaMaps } from '../qa-map-policy.mjs';
|
||||
|
||||
const RESERVED_PORTS = new Set([
|
||||
'',
|
||||
'4173',
|
||||
'4174',
|
||||
'4185',
|
||||
'4186',
|
||||
'4187',
|
||||
'4188',
|
||||
'4189',
|
||||
'4194',
|
||||
'4196',
|
||||
]);
|
||||
import { localPreview, waitForEngine } from '../qa-strict-harness.mjs';
|
||||
|
||||
/** Paid or live model endpoints no switch gate may reach. */
|
||||
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. */
|
||||
export function previewBase(argument) {
|
||||
const base = new URL(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;
|
||||
return localPreview(argument ?? '');
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
export async function waitReady(page, 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') &&
|
||||
!document.querySelector('.engine-switch-frame'),
|
||||
{ timeout: 90_000, polling: 100 },
|
||||
kind,
|
||||
);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
export function waitReady(page, kind) {
|
||||
return waitForEngine(page, kind, { timeout: 90_000 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Map tiles for headless QA: a disk cache shared by every run (in the system
|
||||
// temp directory) and one fetch per missing tile, with backoff on HTTP 429,
|
||||
// for the free tile hosts the QA pages load (OpenStreetMap imagery, the
|
||||
// keyless re:earth terrain). Back-to-back QA runs otherwise reload the same
|
||||
// tiles until those hosts rate-limit them. Only requests to these hosts are
|
||||
// paused (CDP Fetch with URL patterns); page-wide request interception also
|
||||
// held the Navara workers' requests and kept workers alive past their globe.
|
||||
// Map tiles for headless QA: the disk cache entry of a tile request and one
|
||||
// fetch per missing tile, with backoff on HTTP 429, for the free tile hosts
|
||||
// the QA pages load (OpenStreetMap imagery, the keyless re:earth terrain).
|
||||
// Back-to-back QA runs otherwise reload the same tiles until those hosts
|
||||
// rate-limit them. The strict harness (scripts/qa-strict-harness.mjs) pauses
|
||||
// only the requests to these hosts (CDP Fetch with URL patterns) and answers
|
||||
// them from the cache in the system temp directory.
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ const LINE_ID = 'gev-draw-preview';
|
||||
const DOTS_ID = 'gev-draw-vertices';
|
||||
|
||||
/** '#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);
|
||||
return [
|
||||
((hex >> 16) & 255) / 255,
|
||||
|
||||
@@ -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,10 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
absentLayerControlServices,
|
||||
createLayerCatalog,
|
||||
catalogControlServices,
|
||||
} from './catalog.js';
|
||||
import { createLayerCatalog, catalogControlServices } from './catalog.js';
|
||||
import { createApplicationData } from './data.js';
|
||||
|
||||
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) => {
|
||||
const previous = globalThis.document;
|
||||
globalThis.document = { getElementById: () => null };
|
||||
|
||||
@@ -6,17 +6,17 @@ import { createAirQualityCesiumRenderer } from '../../layers/airQuality/render/c
|
||||
import { createFirmsCesiumRenderer } from '../../layers/firms/render/cesium.js';
|
||||
import { createEarthquakesCesiumRenderer } from '../../layers/earthquakes/render/cesium.js';
|
||||
import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesium.js';
|
||||
// Which engine draws which layer: the layer renderer registry (plan, "layer
|
||||
// renderer interface") and the layer availability API built on it.
|
||||
// Which engine draws which layer: the layer renderer registry and the layer
|
||||
// availability API built on it.
|
||||
//
|
||||
// A renderer factory `(engine, options) => renderer` draws one layer on one
|
||||
// engine kind (`render/cesium.js`, `render/navara.js` of a layer). Cesium
|
||||
// layers still draw themselves until their render/cesium.js is extracted
|
||||
// (the *a tasks): they are registered as CESIUM_BUILTIN. Cesium entries are
|
||||
// registered here, statically (main chunk); Navara factories are registered
|
||||
// by the Navara mode after its import() (src/app/navaraMode.js).
|
||||
// layers without a render/cesium.js draw themselves: they are registered as
|
||||
// CESIUM_BUILTIN. Cesium entries are registered here, statically (main
|
||||
// chunk); Navara factories are registered by the Navara mode after its
|
||||
// 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';
|
||||
// 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.
|
||||
// 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
|
||||
// (src/app/layers/directory.js). Phase 4 injects it as its `availability`
|
||||
// provider (`{ engine, unavailable() }` is the part it reads).
|
||||
// (src/app/layers/directory.js). The layer list takes it as its
|
||||
// `availability` provider (`{ engine, unavailable() }` is the part it reads).
|
||||
|
||||
import { APPLICATION_LAYER_DIRECTORY } from './directory.js';
|
||||
import { createFlightsCesiumRenderer } from '../../layers/flights/render/cesium.js';
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
// drawn(id) what the layer draws per object (glyph, model),
|
||||
// as plain data, or null (layers with qaDrawn)
|
||||
// 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
|
||||
|
||||
const plain = (position) =>
|
||||
@@ -83,7 +83,7 @@ export function createQaProbe({ engine, dataManager, availability }) {
|
||||
native: handle.native?.id ?? null,
|
||||
};
|
||||
},
|
||||
movers: () => engine.movers.list(),
|
||||
movers: () => engine.movers?.list() ?? [],
|
||||
framesDrawn: () => frames,
|
||||
destroy: () => removeFrames(),
|
||||
};
|
||||
|
||||
+5
-5
@@ -217,11 +217,11 @@ async function createRenderer({
|
||||
googleTileset: tileset,
|
||||
cesiumToken,
|
||||
initialStack: tileset ? 'photoreal' : 'esri-imagery',
|
||||
// Task 5 (height-datum fix): rebroadcast stack changes as a window
|
||||
// CustomEvent so data layers (CCTV per-regime ground resolution) can
|
||||
// react without coupling MapStackController to layer modules. Fires on
|
||||
// 'switching'/'ready'/'error'; listeners derive the surface regime from
|
||||
// live scene state, so intermediate emissions are harmless.
|
||||
// Rebroadcast stack changes as a window CustomEvent so data layers (CCTV
|
||||
// per-regime ground resolution) can react without coupling
|
||||
// MapStackController to layer modules. Fires on 'switching'/'ready'/
|
||||
// 'error'; listeners derive the surface regime from live scene state, so
|
||||
// intermediate emissions are harmless.
|
||||
onChange: (state) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('gev:map-stack-changed', { detail: state }),
|
||||
|
||||
@@ -9,13 +9,6 @@
|
||||
// a release that runs, in reverse, when the part detaches (before its
|
||||
// detachScene).
|
||||
|
||||
const STATES = Object.freeze([
|
||||
'detached',
|
||||
'attaching',
|
||||
'attached',
|
||||
'detaching',
|
||||
]);
|
||||
|
||||
export class SceneSwitchingError extends Error {
|
||||
constructor(message = 'The globe is switching engines') {
|
||||
super(message);
|
||||
@@ -208,5 +201,3 @@ export function createSceneHost({ onError = null } = {}) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const SCENE_HOST_STATES = STATES;
|
||||
+1
-1
@@ -327,7 +327,7 @@ function attachViewerTools(
|
||||
);
|
||||
|
||||
// Install the explicit scope mask used by the DISPLAY controls.
|
||||
installScopeMask(viewer);
|
||||
installScopeMask(engine);
|
||||
defer(() => destroyScopeMask());
|
||||
|
||||
// The follow camera recomputes the tracked target's dead-reckon position
|
||||
|
||||
@@ -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).
|
||||
const RADIANS_PER_DEGREE = Math.PI / 180;
|
||||
/** 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_SNAP_RAD = 0.1 * RADIANS_PER_DEGREE;
|
||||
const HEIGHT_EPSILON_M = 0.0001;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Deterministic Launch Library 2 records for phase-3 mission/replay acceptance.
|
||||
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() {
|
||||
const definitions = [
|
||||
[
|
||||
|
||||
@@ -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';
|
||||
|
||||
const checksum = (line) => {
|
||||
@@ -30,8 +30,6 @@ const GROUPS = {
|
||||
starlink: [50000, 6000, 53, 15.2, 'QA STARLINK'],
|
||||
};
|
||||
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) {
|
||||
const spec = GROUPS[group];
|
||||
if (!spec) throw new TypeError('Unknown fixture group');
|
||||
|
||||
@@ -57,6 +57,7 @@ function fakeViewer({ cameraHeightM, surfaceM, canvas = null }) {
|
||||
const position = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, cameraHeightM);
|
||||
const viewer = {
|
||||
flights,
|
||||
canvas,
|
||||
scene: {
|
||||
canvas,
|
||||
sampleHeightSupported: true,
|
||||
|
||||
@@ -400,7 +400,7 @@ test('vessel and fire layers announce valid clicks and never fly cameras', () =>
|
||||
assert.doesNotMatch(source, /camera\.flyTo/);
|
||||
}
|
||||
// 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(
|
||||
vessels,
|
||||
/function handleClick\(click\) \{([\s\S]*?)\n \}/,
|
||||
|
||||
@@ -2370,17 +2370,3 @@ export function flyRoute(
|
||||
};
|
||||
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,
|
||||
});
|
||||
@@ -261,10 +261,8 @@ const TRACKED_RASTER_PX = 192;
|
||||
* Default size serves the fleet; pass `aircraftIcon(kind, TRACKED_ICON_PX)`
|
||||
* (re-exported below) for the tracked billboard. */
|
||||
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). */
|
||||
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) {
|
||||
const k = BODIES[kind] ? kind : 'airliner';
|
||||
const key = `${k}@${px}`;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* is registered for; both reach the same correction.
|
||||
* owner. Callers may name the engine or (layers that still hold one) the
|
||||
* viewer it is registered for; both reach the same correction.
|
||||
*/
|
||||
const arrivals = new WeakMap();
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ for (const layer of LAYERS) {
|
||||
|
||||
test(`${layer.name}: every GLB creation bypasses the tile-contended frame-spread queue`, () => {
|
||||
// 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\)/);
|
||||
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}`);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const OPENMAPTILES_CREDIT = {
|
||||
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>',
|
||||
};
|
||||
export const GOOGLE_PLACES_CREDIT = {
|
||||
const GOOGLE_PLACES_CREDIT = {
|
||||
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>',
|
||||
};
|
||||
@@ -45,7 +45,7 @@ const viewerCredits = new WeakMap();
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
function creditsOf(target) {
|
||||
|
||||
@@ -143,7 +143,7 @@ test('every scene click handler consults ownership before it picks', () => {
|
||||
'src/layers/submarineCables/interaction.js',
|
||||
'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.
|
||||
['src/layers/vessels/selection.js', 'resolvePickId(picked)'],
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
const [call] = engine.fake.callsOf('surface.sampleHeightDetailed');
|
||||
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 () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 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
|
||||
// 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;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { nearbyContact } from './pickRegistry.js';
|
||||
// One selection dispatcher for the layers that have moved onto the globe
|
||||
// engine (phase 3 Task 4): one click subscription with picks
|
||||
// One selection dispatcher for the layers that draw through the globe
|
||||
// engine: one click subscription with picks
|
||||
// (engine.input.on('click', …, { pick: true, drill: 8 })), the press
|
||||
// gesture and one Escape listener, instead of a ScreenSpaceEventHandler
|
||||
// per layer. Owners register as with pickRegistry (registerPickOwner, so
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
isTrackingSelectionGesture,
|
||||
} from './trackingClickGesture.js';
|
||||
|
||||
export const SELECTION_DRILL = 8;
|
||||
const SELECTION_DRILL = 8;
|
||||
|
||||
/**
|
||||
* @param {import('../globe/contract.js').GlobeEngine} engine
|
||||
|
||||
@@ -109,7 +109,7 @@ test('flights, AIS, and FIRMS enable paths are wired through the shared sprite r
|
||||
assert.deepEqual(calls, [viewer, viewer, viewer]);
|
||||
|
||||
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
|
||||
// restorer under the flights' sprite slot.
|
||||
assert.match(flightsLayer.enable.toString(), /fleet\.restoreOrderOnEnable\(\)/);
|
||||
|
||||
@@ -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('./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
|
||||
// select, the full click classifier before the empty-space deselect, which
|
||||
// each layer answers with a user-origin clear.
|
||||
|
||||
@@ -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
|
||||
// side must be a throw on the other, and `result` aliasing is exercised the
|
||||
// same way on both sides.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { createRequire } from 'node:module';
|
||||
import * as geo from './index.js';
|
||||
|
||||
const Cesium = createRequire(import.meta.url)('cesium/Build/Cesium/index.cjs');
|
||||
|
||||
const {
|
||||
cartesian3: c3,
|
||||
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', () => {
|
||||
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 = [
|
||||
{ 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];
|
||||
const angle =
|
||||
i % 8 < 4 ? Math.PI - 0.5 * random() : (random() - 0.5) * 2 * Math.PI;
|
||||
let m;
|
||||
try {
|
||||
m = rotation(axis, angle);
|
||||
} catch {
|
||||
continue; // a zero axis: Cesium throws, nothing to compare
|
||||
}
|
||||
const m = rotation(axis, angle);
|
||||
const trace = m[0] + m[4] + m[8];
|
||||
branches.add(
|
||||
trace > 0
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — BoundingSphere.js subset.
|
||||
// Exact mission framing: Ritter/box fit and the union of two spheres.
|
||||
import * as Cartesian3 from './cartesian3.js';
|
||||
import { Check } from './check.js';
|
||||
import { DEBUG } from './debug.js';
|
||||
const defined = (value) => value !== null && value !== undefined;
|
||||
export function create(center = Cartesian3.ZERO, radius = 0) {
|
||||
return { center: Cartesian3.clone(center), radius };
|
||||
@@ -184,11 +182,6 @@ export function fromPoints(positions, result) {
|
||||
return result;
|
||||
}
|
||||
export function union(left, right, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
|
||||
if (!defined(result)) {
|
||||
result = create();
|
||||
}
|
||||
|
||||
+1
-11
@@ -1,17 +1,12 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — Cartesian2.js
|
||||
//
|
||||
// 2D vectors (screen coordinates) as plain-data {x, y} objects. With DEBUG on
|
||||
// (development and tests; see debug.js) invalid arguments throw the same
|
||||
// DeveloperError as Cesium's debug checks.
|
||||
// 2D vectors (screen coordinates) as plain-data {x, y} objects.
|
||||
//
|
||||
// Geo values are not Object.prototype objects, so src/app/stateChannel.js
|
||||
// copyState and src/director/documentFields.js jsonTree reject them: copy
|
||||
// the fields into a literal before putting one into product state or a
|
||||
// scene document.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
|
||||
// Values are instances of a module-private constructor rather than object
|
||||
// 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
|
||||
@@ -38,11 +33,6 @@ export function clone(cartesian, result) {
|
||||
|
||||
/** The distance between two points. */
|
||||
export function distance(left, right) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
|
||||
const x = left.x - right.x;
|
||||
const y = left.y - right.y;
|
||||
return Math.sqrt(x * x + y * y);
|
||||
|
||||
+3
-144
@@ -3,19 +3,14 @@
|
||||
// 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
|
||||
// one. The operation order matches Cesium so the outputs are bit-identical.
|
||||
// With DEBUG on (development and tests; see debug.js) invalid arguments throw
|
||||
// the same DeveloperError as Cesium's debug checks, and `normalize` throws on a
|
||||
// zero or non-finite vector; production builds skip the checks and return NaN
|
||||
// components there, as release Cesium does.
|
||||
// As in release Cesium, arguments are not checked and `normalize` returns NaN
|
||||
// components for a zero or non-finite vector.
|
||||
//
|
||||
// Geo values are not Object.prototype objects, so src/app/stateChannel.js
|
||||
// copyState and src/director/documentFields.js jsonTree reject them: copy
|
||||
// the fields into a literal before putting one into product state or a
|
||||
// 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';
|
||||
|
||||
// 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 function magnitudeSquared(cartesian) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
}
|
||||
|
||||
return (
|
||||
cartesian.x * cartesian.x +
|
||||
cartesian.y * cartesian.y +
|
||||
@@ -77,60 +68,29 @@ export function magnitude(cartesian) {
|
||||
const distanceScratch = create();
|
||||
|
||||
export function distance(left, right) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
|
||||
subtract(left, right, distanceScratch);
|
||||
return magnitude(distanceScratch);
|
||||
}
|
||||
|
||||
export function distanceSquared(left, right) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
|
||||
subtract(left, right, 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) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
const length = magnitude(cartesian);
|
||||
result.x = cartesian.x / length;
|
||||
result.y = cartesian.y / 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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.y = left.y * right.y;
|
||||
result.z = left.z * right.z;
|
||||
@@ -138,12 +98,6 @@ export function multiplyComponents(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.y = left.y + right.y;
|
||||
result.z = left.z + right.z;
|
||||
@@ -151,12 +105,6 @@ export function add(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.y = left.y - right.y;
|
||||
result.z = left.z - right.z;
|
||||
@@ -164,12 +112,6 @@ export function subtract(left, right, 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.y = cartesian.y * scalar;
|
||||
result.z = cartesian.z * scalar;
|
||||
@@ -177,12 +119,6 @@ export function multiplyByScalar(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.y = cartesian.y / scalar;
|
||||
result.z = cartesian.z / scalar;
|
||||
@@ -190,11 +126,6 @@ export function divideByScalar(cartesian, scalar, result) {
|
||||
}
|
||||
|
||||
export function negate(cartesian, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
result.x = -cartesian.x;
|
||||
result.y = -cartesian.y;
|
||||
result.z = -cartesian.z;
|
||||
@@ -204,25 +135,12 @@ export function negate(cartesian, result) {
|
||||
const lerpScratch = create();
|
||||
|
||||
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);
|
||||
result = multiplyByScalar(start, 1.0 - t, result);
|
||||
return add(lerpScratch, result, 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 leftY = left.y;
|
||||
const leftZ = left.z;
|
||||
@@ -241,12 +159,6 @@ export function cross(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.y = (left.y + right.y) * 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). */
|
||||
export function fromRadians(longitude, latitude, height, ellipsoid, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number('longitude', longitude);
|
||||
Check.typeOf.number('latitude', latitude);
|
||||
}
|
||||
|
||||
height = height ?? 0.0;
|
||||
const 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). */
|
||||
export function fromDegrees(longitude, latitude, height, ellipsoid, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number('longitude', longitude);
|
||||
Check.typeOf.number('latitude', latitude);
|
||||
}
|
||||
|
||||
longitude = toRadians(longitude);
|
||||
latitude = toRadians(latitude);
|
||||
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. */
|
||||
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;
|
||||
if (result == null) {
|
||||
result = new Array(length / 2);
|
||||
@@ -358,43 +251,9 @@ export function fromDegreesArray(coordinates, ellipsoid, 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 angleBetweenScratch2 = create();
|
||||
export function angleBetween(left, right) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
normalize(left, angleBetweenScratch);
|
||||
normalize(right, angleBetweenScratch2);
|
||||
const cosine = dot(angleBetweenScratch, angleBetweenScratch2);
|
||||
|
||||
@@ -21,78 +21,16 @@ const same = (ours, theirs, label) =>
|
||||
label,
|
||||
);
|
||||
|
||||
test('binary vector ops equal Cesium bit for bit', () => {
|
||||
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', () => {
|
||||
test('magnitudeSquared equals Cesium', () => {
|
||||
for (const a of SAMPLES) {
|
||||
assert.equal(c3.magnitude(asPlain(a)), C.magnitude(asCesium(a)));
|
||||
assert.equal(
|
||||
c3.magnitudeSquared(asPlain(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 [
|
||||
[0, 0, 0],
|
||||
[-73.9857, 40.7484, 381],
|
||||
@@ -100,11 +38,6 @@ test('geodetic constructors equal Cesium', () => {
|
||||
[180, -90, 1e4],
|
||||
[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];
|
||||
same(c3.fromRadians(...lr), C.fromRadians(...lr), 'fromRadians');
|
||||
}
|
||||
@@ -112,10 +45,6 @@ test('geodetic constructors equal Cesium', () => {
|
||||
c3.fromDegreesArray(flat).forEach((p, 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', () => {
|
||||
@@ -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
@@ -1,17 +1,13 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — Cartographic.js
|
||||
//
|
||||
// Geodetic positions as plain-data {longitude, latitude, height} objects (radians and
|
||||
// metres). Every function takes an optional `result` and returns it. With
|
||||
// DEBUG on (development and tests; see debug.js) invalid arguments throw the
|
||||
// same DeveloperError as Cesium's debug checks.
|
||||
// metres). Every function takes an optional `result` and returns it.
|
||||
//
|
||||
// Geo values are not Object.prototype objects, so src/app/stateChannel.js
|
||||
// copyState and src/director/documentFields.js jsonTree reject them: copy
|
||||
// the fields into a literal before putting one into product state or a
|
||||
// scene document.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
import { toRadians } from './math.js';
|
||||
import * as cartesian3 from './cartesian3.js';
|
||||
import { scaleToGeodeticSurface } from './scaleToGeodeticSurface.js';
|
||||
@@ -51,11 +47,6 @@ const WGS84_CENTER_TOLERANCE_SQUARED = 0.1;
|
||||
|
||||
/** A geodetic position from radians. */
|
||||
export function fromRadians(longitude, latitude, height, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number('longitude', longitude);
|
||||
Check.typeOf.number('latitude', latitude);
|
||||
}
|
||||
|
||||
height = height ?? 0.0;
|
||||
if (result == null) return create(longitude, latitude, height);
|
||||
result.longitude = longitude;
|
||||
@@ -66,11 +57,6 @@ export function fromRadians(longitude, latitude, height, result) {
|
||||
|
||||
/** A geodetic position from degrees; the result is in radians. */
|
||||
export function fromDegrees(longitude, latitude, height, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number('longitude', longitude);
|
||||
Check.typeOf.number('latitude', latitude);
|
||||
}
|
||||
|
||||
longitude = toRadians(longitude);
|
||||
latitude = toRadians(latitude);
|
||||
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).
|
||||
*/
|
||||
export function toCartesian(cartographic, ellipsoid, result) {
|
||||
if (DEBUG) {
|
||||
Check.defined('cartographic', cartographic);
|
||||
}
|
||||
|
||||
return cartesian3.fromRadians(
|
||||
cartographic.longitude,
|
||||
cartographic.latitude,
|
||||
|
||||
@@ -4,12 +4,6 @@ import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
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', () => {
|
||||
for (const [lon, lat, h] of [
|
||||
[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', () => {
|
||||
const out = cartographic.create();
|
||||
assert.equal(cartographic.fromDegrees(1, 2, 3, out), out);
|
||||
|
||||
@@ -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 }),
|
||||
});
|
||||
@@ -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)));
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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
@@ -3,14 +3,9 @@
|
||||
// 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
|
||||
// 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
|
||||
// and tests; see debug.js) invalid arguments throw the same DeveloperError as
|
||||
// Cesium's debug checks.
|
||||
// match Cesium so the outputs are bit-identical.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
import { create as createCartographic } from './cartographic.js';
|
||||
import { DeveloperError } from './developerError.js';
|
||||
import { EPSILON14 } from './math.js';
|
||||
import { scaleToGeodeticSurface } from './scaleToGeodeticSurface.js';
|
||||
import {
|
||||
@@ -39,11 +34,6 @@ export function create(x, y, z) {
|
||||
x = x ?? 0.0;
|
||||
y = y ?? 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 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. */
|
||||
function geodeticSurfaceNormalCartographic(cartographic, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('cartographic', cartographic);
|
||||
}
|
||||
|
||||
const longitude = cartographic.longitude;
|
||||
const latitude = cartographic.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. */
|
||||
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)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -12,18 +12,6 @@ const close = (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}`);
|
||||
};
|
||||
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', () => {
|
||||
const c = Cesium.Ellipsoid.WGS84;
|
||||
@@ -40,39 +28,6 @@ test('WGS84 constants equal Cesium', () => {
|
||||
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', () => {
|
||||
const p = { x: 1334762.2, y: -4654869.1, z: 4137287.9 };
|
||||
const out = { x: 0, y: 0, z: 0 };
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — EllipsoidGeodesic.js.
|
||||
// Vincenty inverse and surface interpolation, preserving operation order.
|
||||
import * as Cartesian3 from './cartesian3.js';
|
||||
import * as Cartographic from './cartographic.js';
|
||||
import * as Ellipsoid from './ellipsoid.js';
|
||||
import * as CesiumMath from './math.js';
|
||||
import { Check } from './check.js';
|
||||
import { DEBUG } from './debug.js';
|
||||
const defined = (value) => value !== null && value !== undefined;
|
||||
function setConstants(ellipsoidGeodesic) {
|
||||
const uSquared = ellipsoidGeodesic._uSquared;
|
||||
@@ -230,29 +227,7 @@ function vincentyInverseFormula(
|
||||
ellipsoidGeodesic._uSquared = uSquared;
|
||||
}
|
||||
|
||||
const scratchCart1 = Cartesian3.create();
|
||||
const scratchCart2 = Cartesian3.create();
|
||||
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(
|
||||
ellipsoidGeodesic,
|
||||
ellipsoid.maximumRadius,
|
||||
@@ -322,10 +297,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
|
||||
*/
|
||||
surfaceDistance: {
|
||||
get: function () {
|
||||
if (DEBUG) {
|
||||
Check.defined('distance', this._distance);
|
||||
}
|
||||
|
||||
return this._distance;
|
||||
},
|
||||
},
|
||||
@@ -362,10 +333,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
|
||||
*/
|
||||
startHeading: {
|
||||
get: function () {
|
||||
if (DEBUG) {
|
||||
Check.defined('distance', this._distance);
|
||||
}
|
||||
|
||||
return this._startHeading;
|
||||
},
|
||||
},
|
||||
@@ -378,10 +345,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
|
||||
*/
|
||||
endHeading: {
|
||||
get: function () {
|
||||
if (DEBUG) {
|
||||
Check.defined('distance', this._distance);
|
||||
}
|
||||
|
||||
return this._endHeading;
|
||||
},
|
||||
},
|
||||
@@ -394,11 +357,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
|
||||
* @param {Cartographic} end The final planetodetic point on the path.
|
||||
*/
|
||||
EllipsoidGeodesic.prototype.setEndPoints = function (start, end) {
|
||||
if (DEBUG) {
|
||||
Check.defined('start', start);
|
||||
Check.defined('end', end);
|
||||
}
|
||||
|
||||
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 {Cartographic} [result] The object in which to store the result.
|
||||
* @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 (
|
||||
distance,
|
||||
result,
|
||||
) {
|
||||
if (DEBUG) {
|
||||
Check.defined('distance', this._distance);
|
||||
}
|
||||
|
||||
const constants = this._constants;
|
||||
|
||||
const s = constants.distanceRatio + distance / constants.b;
|
||||
|
||||
@@ -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
Reference in new issue
Block a user