mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Run the Navara paths without the Cesium SDK, and load it on demand for the features that still build on it
Every Cesium read that a Navara page made was surveyed with the strict acceptance (GEV_QA_CESIUM_TRAP=record). Math on those paths now uses src/geo: screen annotations, weather and wind center readings, cyclone picks, scene data pack polygons and images (new navaraGeometry.js), the awareness subject, the orbit offset of camera verbs, voice frame_overhead, the agent world packet and Directions colours and ground picks. The celestial ring's Sun and Moon come from a bit-identical port of Cesium's Simon1994 model, JulianDate and TEME rotation (src/geo/simon1994.js, julianDate.js, transforms.js); a Cesium globe still applies its ICRF rotation once loaded. Traffic, Street Level, the Nepal event and locator and geo import build on Cesium's math or entity model on either globe; their enable (or the import) now loads the SDK first through src/app/loadCesium.js, synchronously when it is already there. A production page guards the global so a missed read is reported and starts the load. No module reads Cesium while it loads (scripts/check-cesium-load-time.mjs, run by npm test). Cesium.js is still loaded by index.html here.
This commit is contained in:
1 parent
f87da8523e
commit
b8305de14b
58 files changed
+2130
-325
No files matched your search
@@ -0,0 +1,179 @@
|
||||
// The production build replaces every `cesium` import with the global that
|
||||
// /cesium/Cesium.js defines, and that script loads only when a Cesium globe
|
||||
// first starts (src/app/loadCesium.js). A module that reads a Cesium binding
|
||||
// while it loads would therefore throw on a page that starts on Navara. This
|
||||
// check lists such reads: a reference to a binding imported from `cesium`, or
|
||||
// to the `Cesium` global, that runs when its module is evaluated (top level,
|
||||
// class heritage, static members, immediately invoked functions) rather than
|
||||
// inside a function that runs later.
|
||||
//
|
||||
// node scripts/check-cesium-load-time.mjs [--json]
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { parse } from '@babel/parser';
|
||||
|
||||
const root = new URL('..', import.meta.url);
|
||||
|
||||
/** Files under src/ that the app ships (tests and test support excluded). */
|
||||
export function sourceFiles() {
|
||||
return execFileSync('git', ['ls-files', 'src'], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.split('\n')
|
||||
.filter(
|
||||
(path) =>
|
||||
/\.(?:js|mjs)$/.test(path) &&
|
||||
!/\.test\.m?js$/.test(path) &&
|
||||
!path.startsWith('src/testSupport/') &&
|
||||
!path.includes('/testing/'),
|
||||
);
|
||||
}
|
||||
|
||||
const FUNCTION_TYPES = new Set([
|
||||
'FunctionDeclaration',
|
||||
'FunctionExpression',
|
||||
'ArrowFunctionExpression',
|
||||
'ObjectMethod',
|
||||
'ClassMethod',
|
||||
'ClassPrivateMethod',
|
||||
]);
|
||||
|
||||
/** Names a module binds to `cesium` imports. */
|
||||
function cesiumBindings(program) {
|
||||
const names = new Set();
|
||||
for (const node of program.body) {
|
||||
if (node.type !== 'ImportDeclaration' || node.source.value !== 'cesium')
|
||||
continue;
|
||||
for (const specifier of node.specifiers) names.add(specifier.local.name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function isReference(node, parent, key) {
|
||||
if (node.type !== 'Identifier') return false;
|
||||
if (!parent) return true;
|
||||
if (
|
||||
(parent.type === 'MemberExpression' ||
|
||||
parent.type === 'OptionalMemberExpression') &&
|
||||
key === 'property' &&
|
||||
!parent.computed
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
(parent.type === 'ObjectProperty' ||
|
||||
parent.type === 'ClassProperty' ||
|
||||
parent.type === 'ObjectMethod' ||
|
||||
parent.type === 'ClassMethod') &&
|
||||
key === 'key' &&
|
||||
!parent.computed
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
parent.type === 'ImportSpecifier' ||
|
||||
parent.type === 'ImportNamespaceSpecifier' ||
|
||||
parent.type === 'ImportDefaultSpecifier' ||
|
||||
parent.type === 'ExportSpecifier'
|
||||
)
|
||||
return false;
|
||||
if (parent.type === 'LabeledStatement' || parent.type === 'BreakStatement')
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load-time references in one module. `deferred` is true below a function
|
||||
* body that runs later; an immediately invoked function and a class's static
|
||||
* members and heritage run at load and stay not deferred.
|
||||
*/
|
||||
export function loadTimeReferences(code, path = '<module>') {
|
||||
const program = parse(code, {
|
||||
sourceType: 'module',
|
||||
plugins: ['classProperties', 'classStaticBlock', 'importAttributes'],
|
||||
}).program;
|
||||
const names = cesiumBindings(program);
|
||||
const found = [];
|
||||
// A re-export from `cesium` becomes a read of the global's property when
|
||||
// the module loads.
|
||||
for (const node of program.body)
|
||||
if (
|
||||
(node.type === 'ExportNamedDeclaration' ||
|
||||
node.type === 'ExportAllDeclaration') &&
|
||||
node.source?.value === 'cesium'
|
||||
)
|
||||
found.push({
|
||||
path,
|
||||
line: node.loc.start.line,
|
||||
name: 'export from cesium',
|
||||
});
|
||||
const hasGlobal = /\bCesium\b/.test(code);
|
||||
if (!names.size && !hasGlobal) return found;
|
||||
// A `Cesium` global reference counts when the module does not bind it.
|
||||
const watched = (name) =>
|
||||
names.has(name) || (name === 'Cesium' && !names.has('Cesium'));
|
||||
const visit = (node, parent, key, deferred) => {
|
||||
if (!node || typeof node.type !== 'string') return;
|
||||
if (node.type === 'ImportDeclaration') return;
|
||||
if (!deferred && isReference(node, parent, key) && watched(node.name))
|
||||
found.push({ path, line: node.loc.start.line, name: node.name });
|
||||
let childDeferred = deferred;
|
||||
if (FUNCTION_TYPES.has(node.type)) {
|
||||
const invoked =
|
||||
parent &&
|
||||
(parent.type === 'CallExpression' || parent.type === 'NewExpression') &&
|
||||
key === 'callee';
|
||||
childDeferred = deferred || !invoked;
|
||||
}
|
||||
if (
|
||||
(node.type === 'ClassProperty' || node.type === 'ClassPrivateProperty') &&
|
||||
!node.static
|
||||
)
|
||||
childDeferred = true;
|
||||
for (const [childKey, value] of Object.entries(node)) {
|
||||
if (
|
||||
childKey === 'loc' ||
|
||||
childKey === 'leadingComments' ||
|
||||
childKey === 'trailingComments' ||
|
||||
childKey === 'innerComments'
|
||||
)
|
||||
continue;
|
||||
// Computed keys and parameter defaults of a deferred function: its key
|
||||
// is evaluated with the enclosing scope, its body later.
|
||||
const keyDeferred =
|
||||
FUNCTION_TYPES.has(node.type) && childKey === 'key'
|
||||
? deferred
|
||||
: childDeferred;
|
||||
if (Array.isArray(value))
|
||||
for (const child of value) visit(child, node, childKey, keyDeferred);
|
||||
else if (value && typeof value === 'object')
|
||||
visit(value, node, childKey, keyDeferred);
|
||||
}
|
||||
};
|
||||
visit(program, null, null, false);
|
||||
return found;
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const found = [];
|
||||
for (const path of sourceFiles()) {
|
||||
const code = readFileSync(new URL(path, root), 'utf8');
|
||||
try {
|
||||
found.push(...loadTimeReferences(code, path));
|
||||
} catch (error) {
|
||||
console.error(`${path}: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
if (process.argv.includes('--json'))
|
||||
console.log(JSON.stringify(found, null, 2));
|
||||
else
|
||||
for (const { path, line, name } of found)
|
||||
console.log(`${path}:${line} reads ${name} while the module loads`);
|
||||
if (found.length) {
|
||||
console.error(
|
||||
`${found.length} load-time Cesium read(s); a page that starts on Navara has no Cesium global yet.`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
} else console.log('No module reads Cesium while it loads.');
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { loadTimeReferences, sourceFiles } from './check-cesium-load-time.mjs';
|
||||
|
||||
const reads = (code) =>
|
||||
loadTimeReferences(code).map(({ line, name }) => `${line}:${name}`);
|
||||
|
||||
test('reads that run while a module loads are found; reads inside later code are not', () => {
|
||||
assert.deepEqual(
|
||||
reads(`import * as Cesium from 'cesium';
|
||||
const color = Cesium.Color.RED;
|
||||
export function later() { return Cesium.Color.BLUE; }
|
||||
const lazy = () => Cesium.Math.PI;
|
||||
class Shape { static axis = Cesium.Axis.Y; field = Cesium.Axis.Z; draw() { return Cesium; } }
|
||||
(() => Cesium.VERSION)();
|
||||
export const fallback = (sdk = Cesium) => sdk;`),
|
||||
['2:Cesium', '5:Cesium', '6:Cesium'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
reads(`import { Cartesian3 as C3, Color } from 'cesium';
|
||||
export const ORIGIN = C3.ZERO;
|
||||
export function white() { return Color.WHITE; }`),
|
||||
['2:C3'],
|
||||
);
|
||||
assert.deepEqual(reads(`export { Axis } from 'cesium';`), [
|
||||
'1:export from cesium',
|
||||
]);
|
||||
assert.deepEqual(
|
||||
reads(`window.__CESIUM__ = Cesium;`),
|
||||
['1:Cesium'],
|
||||
'the global counts where the module does not import it',
|
||||
);
|
||||
assert.deepEqual(reads(`const geo = { Cesium: 1 }; geo.Cesium;`), []);
|
||||
});
|
||||
|
||||
test('no shipped module reads Cesium while it loads', () => {
|
||||
const found = sourceFiles().flatMap((path) =>
|
||||
loadTimeReferences(readFileSync(path, 'utf8'), path),
|
||||
);
|
||||
assert.deepEqual(found, []);
|
||||
});
|
||||
@@ -889,6 +889,8 @@
|
||||
"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",
|
||||
@@ -1163,6 +1165,8 @@
|
||||
"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",
|
||||
@@ -1317,6 +1321,8 @@
|
||||
"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",
|
||||
@@ -1413,6 +1419,8 @@
|
||||
"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",
|
||||
@@ -1494,6 +1502,8 @@
|
||||
"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",
|
||||
@@ -1546,6 +1556,8 @@
|
||||
"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",
|
||||
@@ -1608,6 +1620,8 @@
|
||||
"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",
|
||||
@@ -1640,6 +1654,8 @@
|
||||
"src/geo/quaternion.js",
|
||||
"src/geo/scaleToGeodeticSurface.js",
|
||||
"src/geo/transforms.js",
|
||||
"src/geo/simon1994.js",
|
||||
"src/geo/julianDate.js",
|
||||
"src/layers/shared/color.js"
|
||||
],
|
||||
"external": []
|
||||
@@ -1717,6 +1733,8 @@
|
||||
"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",
|
||||
@@ -1774,6 +1792,8 @@
|
||||
"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",
|
||||
@@ -1834,6 +1854,8 @@
|
||||
"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",
|
||||
@@ -1906,6 +1928,8 @@
|
||||
"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",
|
||||
@@ -1972,6 +1996,8 @@
|
||||
"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",
|
||||
@@ -1984,6 +2010,7 @@
|
||||
"traffic-layer": {
|
||||
"exports": ["./layers/traffic"],
|
||||
"modules": [
|
||||
"src/layers/shared/color.js",
|
||||
"src/data/cameraSensitivity.js",
|
||||
"src/data/flowMatch.js",
|
||||
"src/data/tomtomTiles.js",
|
||||
@@ -2002,6 +2029,7 @@
|
||||
"src/layers/traffic/ingestion.js",
|
||||
"src/sources/requestDeadline.js",
|
||||
"src/layers/traffic/lifecycle.js",
|
||||
"src/app/loadCesium.js",
|
||||
"src/layers/traffic/model.js",
|
||||
"src/layers/traffic/policy.js",
|
||||
"src/layers/traffic/rendering.js",
|
||||
@@ -2047,6 +2075,8 @@
|
||||
"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",
|
||||
@@ -2102,6 +2132,8 @@
|
||||
"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",
|
||||
@@ -2176,6 +2208,8 @@
|
||||
"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",
|
||||
@@ -2194,6 +2228,7 @@
|
||||
],
|
||||
"modules": [
|
||||
"src/layers/streetLevel/index.js",
|
||||
"src/app/loadCesium.js",
|
||||
"src/layers/streetLevel/credits.js",
|
||||
"src/layers/streetLevel/providers/mapillary/index.js",
|
||||
"src/layers/streetLevel/viewerHost.js",
|
||||
@@ -2264,6 +2299,8 @@
|
||||
"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",
|
||||
@@ -2315,6 +2352,8 @@
|
||||
"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",
|
||||
@@ -2378,6 +2417,8 @@
|
||||
"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",
|
||||
@@ -2425,6 +2466,8 @@
|
||||
"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",
|
||||
@@ -2657,6 +2700,7 @@
|
||||
"src/app/layers/traffic.js",
|
||||
"src/app/operations.js",
|
||||
"src/app/scene.js",
|
||||
"src/app/loadCesium.js",
|
||||
"src/sources/sourceSlot.js",
|
||||
"src/app/sources.js",
|
||||
"src/app/startupChrome.js",
|
||||
@@ -2678,6 +2722,7 @@
|
||||
"src/cctvFocusPolicy.js",
|
||||
"src/cctvFocusRequest.js",
|
||||
"src/celestialRing.js",
|
||||
"src/celestialRing/ephemeris.js",
|
||||
"src/cockpitCloudEffects.js",
|
||||
"src/cockpitMath.js",
|
||||
"src/cockpitTracking.js",
|
||||
@@ -3646,6 +3691,8 @@
|
||||
"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",
|
||||
@@ -3721,6 +3768,7 @@
|
||||
"src/layers/traffic/render/navara.js",
|
||||
"src/layers/streetLevel/render/navara.js",
|
||||
"src/scenes/dataPacks/navara.js",
|
||||
"src/scenes/dataPacks/navaraGeometry.js",
|
||||
"src/layers/shared/render/navaraStory.js",
|
||||
"src/layers/bhoteKoshi/render/navara.js",
|
||||
"src/layers/weather/render/navara.js",
|
||||
@@ -3845,6 +3893,7 @@
|
||||
"src/sources/sourceSlot.js",
|
||||
"src/cctvFocusRequest.js",
|
||||
"src/celestialRing.js",
|
||||
"src/celestialRing/ephemeris.js",
|
||||
"src/cockpitMath.js",
|
||||
"src/data/aircraftClass.js",
|
||||
"src/data/aircraftEmergency.js",
|
||||
@@ -4108,6 +4157,7 @@
|
||||
"src/layers/traffic/ingestion.js",
|
||||
"src/sources/requestDeadline.js",
|
||||
"src/layers/traffic/lifecycle.js",
|
||||
"src/app/loadCesium.js",
|
||||
"src/layers/traffic/model.js",
|
||||
"src/layers/traffic/policy.js",
|
||||
"src/layers/traffic/rendering.js",
|
||||
@@ -4431,6 +4481,8 @@
|
||||
"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",
|
||||
@@ -4552,6 +4604,8 @@
|
||||
"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",
|
||||
@@ -4817,6 +4871,7 @@
|
||||
"src/data/distanceFormat.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/layers/directions/index.js",
|
||||
"src/layers/shared/color.js",
|
||||
"src/data/pointerOwnerNames.js",
|
||||
"src/layers/directions/source.js",
|
||||
"src/data/routeSteps.js",
|
||||
@@ -4833,7 +4888,25 @@
|
||||
"src/i18n/locales/uk.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/ui/unitLabels.js"
|
||||
"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"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -5094,6 +5167,7 @@
|
||||
"src/data/headingDegrees.js",
|
||||
"src/cyberSonar.js",
|
||||
"src/celestialRing.js",
|
||||
"src/celestialRing/ephemeris.js",
|
||||
"src/data/approxDistance.js",
|
||||
"src/data/cameraSensitivity.js",
|
||||
"src/data/contactPlayback.js",
|
||||
@@ -5203,6 +5277,8 @@
|
||||
"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",
|
||||
@@ -5273,6 +5349,8 @@
|
||||
"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",
|
||||
@@ -5429,6 +5507,7 @@
|
||||
"src/geo/math.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/data/geoFileImport.js",
|
||||
"src/app/loadCesium.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
@@ -5523,7 +5602,25 @@
|
||||
"src/tools/globePanel.js",
|
||||
"src/tools/queries/panelRequest.js",
|
||||
"src/geoDistance.js",
|
||||
"src/visualStylePolicy.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"
|
||||
],
|
||||
"external": ["satellite.js"]
|
||||
},
|
||||
@@ -5681,7 +5778,25 @@
|
||||
"src/contextModePolicy.js",
|
||||
"src/loadingFeedback.js",
|
||||
"src/data/installationFeedback.js",
|
||||
"src/i18n/marker.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"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../geo/index.js';
|
||||
import {
|
||||
feedProvenanceEnvelope,
|
||||
layerSnapshot,
|
||||
@@ -110,7 +110,7 @@ export function getWorldPacket(options = {}) {
|
||||
if (!dataManager || typeof dataManager.getAll !== 'function')
|
||||
throw new TypeError('World Packet requires a data manager');
|
||||
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(
|
||||
const cartographic = geo.cartographic.fromCartesian(
|
||||
engine?.camera.livePosition ?? viewer.camera.positionWC,
|
||||
);
|
||||
// Read the roster once. Voice must not rewalk disabled layers: preserving the
|
||||
@@ -127,8 +127,8 @@ export function getWorldPacket(options = {}) {
|
||||
ok: true,
|
||||
action: 'get_current_view_state',
|
||||
camera: {
|
||||
latitude: Cesium.Math.toDegrees(cartographic.latitude),
|
||||
longitude: Cesium.Math.toDegrees(cartographic.longitude),
|
||||
latitude: geo.math.toDegrees(cartographic.latitude),
|
||||
longitude: geo.math.toDegrees(cartographic.longitude),
|
||||
heightM: cartographic.height,
|
||||
},
|
||||
style: styleManager?.activeStyle || 'normal',
|
||||
|
||||
@@ -24,8 +24,13 @@ import { formatDistanceText } from '../data/distanceFormat.js';
|
||||
|
||||
// Dev convenience: expose the app's Cesium instance for console/preview probing
|
||||
// (single shared module instance — avoids dual-Cesium state bugs when testing).
|
||||
if (typeof window !== 'undefined' && !window.__CESIUM__)
|
||||
window.__CESIUM__ = Cesium;
|
||||
// A getter, because the production build's Cesium global loads only with the
|
||||
// first Cesium globe.
|
||||
if (typeof window !== 'undefined' && !('__CESIUM__' in window))
|
||||
Object.defineProperty(window, '__CESIUM__', {
|
||||
configurable: true,
|
||||
get: () => Cesium,
|
||||
});
|
||||
|
||||
/**
|
||||
* Annotation engine — the voice agent's "whiteboard" over the 3D world.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../geo/index.js';
|
||||
import { getOverlayPaintRect } from '../overlays/worldOverlay.js';
|
||||
import {
|
||||
getActiveTrackedReadoutId,
|
||||
@@ -90,8 +91,8 @@ export function createScreenAnnotationRenderer(
|
||||
let published = '';
|
||||
// Only the renderer whose board the store shows may change or remove it.
|
||||
const shown = () => store.board?.owner === owner;
|
||||
const scratch = new Cesium.Cartesian2();
|
||||
const scratchDir = new Cesium.Cartesian3();
|
||||
const scratch = geo.cartesian2.create();
|
||||
const scratchDir = geo.cartesian3.create();
|
||||
// Per-point ground height cache. Marks must sit on the real surface, not at
|
||||
// sea level — otherwise in elevated cities (Austin ~150 m) they project
|
||||
// underground and parallax sinks them at oblique angles. Sampled once the
|
||||
@@ -141,11 +142,9 @@ export function createScreenAnnotationRenderer(
|
||||
}
|
||||
try {
|
||||
if (view.clampToHeightSupported()) {
|
||||
const c = view.clampToHeight(
|
||||
Cesium.Cartesian3.fromDegrees(lon, lat, 0),
|
||||
);
|
||||
const c = view.clampToHeight(geo.cartesian3.fromDegrees(lon, lat, 0));
|
||||
if (c) {
|
||||
const h = Cesium.Cartographic.fromCartesian(c).height;
|
||||
const h = geo.cartographic.fromCartesian(c).height;
|
||||
if (Number.isFinite(h) && h > -430 && h < 9000) {
|
||||
heightCache.set(key, { h, settled: true, gen: projGen });
|
||||
trimHeightCache();
|
||||
@@ -359,14 +358,10 @@ export function createScreenAnnotationRenderer(
|
||||
// Anchor at the real surface height (cached + validated), so marks sit on
|
||||
// the ground/building instead of at sea level. The settle-once cache keeps
|
||||
// it stable (no per-frame jitter that earlier broke nearby projections).
|
||||
const world = Cesium.Cartesian3.fromDegrees(
|
||||
lon,
|
||||
lat,
|
||||
groundHeight(lon, lat),
|
||||
);
|
||||
const world = geo.cartesian3.fromDegrees(lon, lat, groundHeight(lon, lat));
|
||||
// Reject points behind the camera (they produce wrapped/extreme coords).
|
||||
Cesium.Cartesian3.subtract(world, view.cameraPosition(), scratchDir);
|
||||
if (Cesium.Cartesian3.dot(scratchDir, view.cameraDirection()) <= 0)
|
||||
geo.cartesian3.subtract(world, view.cameraPosition(), scratchDir);
|
||||
if (geo.cartesian3.dot(scratchDir, view.cameraDirection()) <= 0)
|
||||
return null;
|
||||
// Reject points beyond the globe horizon.
|
||||
if (!view.horizonVisible(world)) return null;
|
||||
@@ -392,7 +387,7 @@ export function createScreenAnnotationRenderer(
|
||||
const TRACKED_BBOX_MARGIN = 8; // px buffer added around the footprint
|
||||
const TRACKED_FADE_EASE = 0.22; // per-frame ease toward hidden(0) / visible(1)
|
||||
const TRACKED_HYSTERESIS_PX = 18; // dead-band so the overlap test can't flip-flop
|
||||
const _scratchTrackedWin = new Cesium.Cartesian2();
|
||||
const _scratchTrackedWin = geo.cartesian2.create();
|
||||
|
||||
// Evaluate a NearFarScalar (billboard scaleByDistance) at a camera distance.
|
||||
function nearFarValue(nfs, dist) {
|
||||
@@ -428,7 +423,7 @@ export function createScreenAnnotationRenderer(
|
||||
if (sbd)
|
||||
scale = nearFarValue(
|
||||
sbd,
|
||||
Cesium.Cartesian3.distance(view.cameraPosition(), world),
|
||||
geo.cartesian3.distance(view.cameraPosition(), world),
|
||||
);
|
||||
const hw = (baseW * scale) / 2;
|
||||
const hh = (baseH * scale) / 2;
|
||||
|
||||
@@ -46,10 +46,10 @@ const PALETTE = {
|
||||
*
|
||||
* BOTH paints onto terrain AND onto 3D tiles, so one value covers both worlds.
|
||||
*/
|
||||
const CLASSIFY = Cesium.ClassificationType.BOTH;
|
||||
const CLAMP = Cesium.HeightReference.CLAMP_TO_GROUND;
|
||||
|
||||
export function createWorldAnnotationRenderer(viewer) {
|
||||
const CLASSIFY = Cesium.ClassificationType.BOTH;
|
||||
const CLAMP = Cesium.HeightReference.CLAMP_TO_GROUND;
|
||||
const dataSource = new Cesium.CustomDataSource('gev-annotations');
|
||||
viewer.dataSources.add(dataSource);
|
||||
const osmAnnotations = new Set();
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// The production build externalizes Cesium: every `cesium` import reads the
|
||||
// global that /cesium/Cesium.js defines, and the page loads that script only
|
||||
// when something needs it. The dev server and Node import Cesium's ES module
|
||||
// package instead, so there the SDK is always there and only the widget
|
||||
// styles load. A Navara page that never needs Cesium never loads either.
|
||||
//
|
||||
// - `viewer()`: a Cesium globe is about to start: the SDK and the widget
|
||||
// styles;
|
||||
// - `sdk()`: a feature that still builds on Cesium's entity model (the Nepal
|
||||
// event and locator, imported KML) is used on Navara: the SDK alone.
|
||||
export function createCesiumLoader({
|
||||
document = globalThis.document,
|
||||
globals = globalThis,
|
||||
base = `${import.meta.env?.BASE_URL ?? '/'}cesium/`,
|
||||
script = import.meta.env?.PROD === true,
|
||||
} = {}) {
|
||||
let sdkPending = null;
|
||||
let stylesPending = null;
|
||||
const root = () => {
|
||||
globals.CESIUM_BASE_URL ??= base;
|
||||
return globals.CESIUM_BASE_URL.replace(/\/?$/, '/');
|
||||
};
|
||||
// A failed load leaves no element behind and can be retried.
|
||||
const load = (tag, url) => {
|
||||
const element = document.createElement(tag);
|
||||
if (tag === 'script') element.src = url;
|
||||
else {
|
||||
element.rel = 'stylesheet';
|
||||
element.href = url;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
element.onload = () => {
|
||||
element.onload = element.onerror = null;
|
||||
resolve();
|
||||
};
|
||||
element.onerror = () => {
|
||||
element.onload = element.onerror = null;
|
||||
element.remove();
|
||||
reject(new Error(`Could not load ${url}`));
|
||||
};
|
||||
document.head.append(element);
|
||||
});
|
||||
};
|
||||
// Read without running a getter: a page may define the global as one.
|
||||
const ready = () =>
|
||||
!script ||
|
||||
Object.getOwnPropertyDescriptor(globals, 'Cesium')?.value != null;
|
||||
const sdk = () => {
|
||||
if (ready()) return Promise.resolve();
|
||||
sdkPending ??= load('script', `${root()}Cesium.js`).catch((error) => {
|
||||
sdkPending = null;
|
||||
throw error;
|
||||
});
|
||||
return sdkPending;
|
||||
};
|
||||
const styles = () => {
|
||||
stylesPending ??= load('link', `${root()}Widgets/widgets.css`).catch(
|
||||
(error) => {
|
||||
stylesPending = null;
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
return stylesPending;
|
||||
};
|
||||
return {
|
||||
ready,
|
||||
sdk,
|
||||
viewer: () => Promise.all([styles(), sdk()]).then(() => undefined),
|
||||
};
|
||||
}
|
||||
|
||||
let loader;
|
||||
const shared = () => (loader ??= createCesiumLoader());
|
||||
|
||||
/** Before a Cesium globe starts: the SDK and its widget styles. */
|
||||
export function loadCesium() {
|
||||
return shared().viewer();
|
||||
}
|
||||
|
||||
/** Before a feature that still builds on Cesium's entity model runs on Navara. */
|
||||
export function loadCesiumSdk() {
|
||||
return shared().sdk();
|
||||
}
|
||||
|
||||
/** Whether `cesium` imports can be read now, without waiting. */
|
||||
export function cesiumSdkReady() {
|
||||
return shared().ready();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `start` now when the SDK is there, else once it has loaded (a promise,
|
||||
* nothing when `signal` aborted meanwhile): a layer enable that is
|
||||
* synchronous everywhere but on a Navara page without the SDK yet.
|
||||
*/
|
||||
export function whenCesiumSdk(start, signal) {
|
||||
if (cesiumSdkReady()) return start();
|
||||
return loadCesiumSdk().then(() => (signal?.aborted ? undefined : start()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Production page, once at boot: until Cesium.js has run, a read of the
|
||||
* `Cesium` global (what every `cesium` import reads in this build) logs where
|
||||
* it came from and starts loading the SDK. A path that still needs Cesium on
|
||||
* a Navara page then fails once, visibly, and works when it runs again,
|
||||
* instead of failing for the rest of the session. Cesium.js's own assignment
|
||||
* turns the global back into a plain value. A global the page already has
|
||||
* (the strict QA trap, a host's own SDK) is left alone.
|
||||
*/
|
||||
export function guardCesiumGlobal({
|
||||
globals = globalThis,
|
||||
script = import.meta.env?.PROD === true,
|
||||
report = (stack) =>
|
||||
console.error('[Cesium] read before the SDK loaded', stack),
|
||||
load = loadCesiumSdk,
|
||||
} = {}) {
|
||||
if (!script || Object.getOwnPropertyDescriptor(globals, 'Cesium')) return;
|
||||
let reports = 0;
|
||||
Object.defineProperty(globals, 'Cesium', {
|
||||
configurable: true,
|
||||
get() {
|
||||
if (reports++ < 10) report(new Error('Cesium read').stack);
|
||||
load().catch(() => {});
|
||||
return undefined;
|
||||
},
|
||||
set(value) {
|
||||
Object.defineProperty(globals, 'Cesium', {
|
||||
value,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createCesiumLoader } from './loadCesium.js';
|
||||
|
||||
function fixture(globals = {}, { script = true } = {}) {
|
||||
const elements = [];
|
||||
const document = {
|
||||
createElement: (tag) => ({
|
||||
tag,
|
||||
remove() {
|
||||
elements.splice(elements.indexOf(this), 1);
|
||||
},
|
||||
}),
|
||||
head: { append: (element) => elements.push(element) },
|
||||
};
|
||||
return {
|
||||
elements,
|
||||
globals,
|
||||
loader: createCesiumLoader({
|
||||
document,
|
||||
globals,
|
||||
base: '/app/cesium/',
|
||||
script,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
test('a Cesium globe loads the SDK and its styles together, once, on demand', async () => {
|
||||
const { elements, globals, loader } = fixture();
|
||||
assert.equal(elements.length, 0);
|
||||
const first = loader.viewer();
|
||||
assert.equal(globals.CESIUM_BASE_URL, '/app/cesium/');
|
||||
assert.deepEqual(
|
||||
elements.map((e) => e.href ?? e.src),
|
||||
['/app/cesium/Widgets/widgets.css', '/app/cesium/Cesium.js'],
|
||||
);
|
||||
const again = loader.viewer();
|
||||
assert.equal(elements.length, 2, 'a second request shares the first');
|
||||
elements.forEach((e) => e.onload());
|
||||
await first;
|
||||
await again;
|
||||
await loader.viewer();
|
||||
assert.equal(elements.length, 2);
|
||||
});
|
||||
|
||||
test('a feature on Navara loads the SDK alone, and a later globe adds only the styles', async () => {
|
||||
const { elements, loader } = fixture();
|
||||
const sdk = loader.sdk();
|
||||
assert.deepEqual(
|
||||
elements.map((e) => e.src ?? e.href),
|
||||
['/app/cesium/Cesium.js'],
|
||||
);
|
||||
elements[0].onload();
|
||||
await sdk;
|
||||
const viewer = loader.viewer();
|
||||
assert.deepEqual(
|
||||
elements.map((e) => e.src ?? e.href),
|
||||
['/app/cesium/Cesium.js', '/app/cesium/Widgets/widgets.css'],
|
||||
);
|
||||
elements[1].onload();
|
||||
await viewer;
|
||||
});
|
||||
|
||||
test('the SDK is not loaded where the page already has it or imports its module', async () => {
|
||||
const present = fixture({ Cesium: {} });
|
||||
await present.loader.sdk();
|
||||
assert.equal(present.elements.length, 0);
|
||||
const getter = {};
|
||||
Object.defineProperty(getter, 'Cesium', { get: () => ({}) });
|
||||
const trapped = fixture(getter);
|
||||
trapped.loader.sdk();
|
||||
assert.equal(trapped.elements.length, 1, 'a getter is not read as the SDK');
|
||||
const dev = fixture({}, { script: false });
|
||||
await dev.loader.sdk();
|
||||
assert.equal(dev.elements.length, 0);
|
||||
});
|
||||
|
||||
test('a failed Cesium asset load can be retried and preserves the configured base', async () => {
|
||||
const { elements, globals, loader } = fixture({
|
||||
CESIUM_BASE_URL: '/custom/',
|
||||
});
|
||||
const failed = loader.viewer();
|
||||
elements[1].onerror();
|
||||
await assert.rejects(failed, /Could not load/);
|
||||
assert.deepEqual(
|
||||
elements.map((e) => e.href),
|
||||
['/custom/Widgets/widgets.css'],
|
||||
'the failed script is removed',
|
||||
);
|
||||
const retried = loader.viewer();
|
||||
assert.equal(globals.CESIUM_BASE_URL, '/custom/');
|
||||
assert.equal(elements[1].src, '/custom/Cesium.js');
|
||||
elements.forEach((e) => e.onload());
|
||||
await retried;
|
||||
});
|
||||
|
||||
test('the standalone startup graph can be imported before the Cesium SDK exists', () => {
|
||||
// Empty SDK exports make any boot-time native constructor or enum read fail.
|
||||
// mgrs is CJS in Node; its browser named export is unrelated to this gate.
|
||||
const script = `
|
||||
import { registerHooks } from 'node:module';
|
||||
registerHooks({ resolve(specifier, context, next) {
|
||||
const code = specifier === 'cesium'
|
||||
? 'export const Axis=undefined,ScreenSpaceEventType=undefined,BoundingSphere=undefined,Color=undefined,PostProcessStage=undefined;'
|
||||
: specifier === 'mgrs' ? 'export function forward() {}' : null;
|
||||
return code ? { url: 'data:text/javascript,' + encodeURIComponent(code), shortCircuit: true } : next(specifier, context);
|
||||
}});
|
||||
await import('./src/standalone/application.js');
|
||||
const { registerNavaraLayerRenderers } = await import('./src/app/navaraMode.js');
|
||||
await registerNavaraLayerRenderers();
|
||||
const { createStandaloneCatalog } = await import('./src/standalone/catalog.js');
|
||||
const lifetime = new AbortController();
|
||||
const catalog = createStandaloneCatalog({ engineKind: 'navara', signal: lifetime.signal });
|
||||
if (catalog.layers.length !== 33) throw new Error('Incomplete cold Navara catalog');
|
||||
lifetime.abort();
|
||||
`;
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
['--import=./build/registerVue.js', '--input-type=module', '-e', script],
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
});
|
||||
|
||||
test('a read before the SDK loads is reported and starts the load; the SDK then reads plainly', async () => {
|
||||
const { guardCesiumGlobal } = await import('./loadCesium.js');
|
||||
const globals = {};
|
||||
const reports = [];
|
||||
let loads = 0;
|
||||
guardCesiumGlobal({
|
||||
globals,
|
||||
script: true,
|
||||
report: (stack) => reports.push(stack),
|
||||
load: async () => loads++,
|
||||
});
|
||||
assert.equal(globals.Cesium, undefined);
|
||||
assert.equal(reports.length, 1);
|
||||
assert.equal(loads, 1);
|
||||
const sdk = { Cartesian3: {} };
|
||||
globals.Cesium = sdk;
|
||||
assert.equal(globals.Cesium, sdk);
|
||||
assert.equal(Object.getOwnPropertyDescriptor(globals, 'Cesium').value, sdk);
|
||||
assert.equal(reports.length, 1);
|
||||
const trapped = { Cesium: 'host' };
|
||||
guardCesiumGlobal({ globals: trapped, script: true });
|
||||
assert.equal(trapped.Cesium, 'host', 'an existing global is left alone');
|
||||
const dev = {};
|
||||
guardCesiumGlobal({ globals: dev, script: false });
|
||||
assert.equal(Object.getOwnPropertyDescriptor(dev, 'Cesium'), undefined);
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import { LayerBindings } from './ui/layerBindings.js';
|
||||
import { ScreenSpaceEventType } from 'cesium';
|
||||
import { FIRMS_LEFT_CLICK } from './layers/firms/render/cesium.js';
|
||||
import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs';
|
||||
import { readLayerSource } from './testSupport/readLayerSource.mjs';
|
||||
import { enter as cockpitEnter, navigateContext } from './ui/cockpitTrackingController.js';
|
||||
@@ -416,7 +418,7 @@ test('vessel and fire layers announce valid clicks and never fly cameras', () =>
|
||||
);
|
||||
assert.match(vesselFocus, /requestWorldFocus\(\{/);
|
||||
assert.match(firms, /const clickType\s*=\s*layerState\._renderer\?\.clickType\s*\?\?\s*config\.leftClickEventType/);
|
||||
assert.match(firms, /FIRMS_LEFT_CLICK = Cesium\.ScreenSpaceEventType\.LEFT_CLICK/);
|
||||
assert.equal(FIRMS_LEFT_CLICK, ScreenSpaceEventType.LEFT_CLICK);
|
||||
const fireClick = body(
|
||||
firms,
|
||||
/_clickHandler\.setInputAction\(\(click\) => \{([\s\S]*?)\n \}, clickType\);/,
|
||||
|
||||
+5
-3
@@ -1786,11 +1786,13 @@ function orbitOffset(position, heading, target) {
|
||||
const carto = geo.cartographic.fromCartesian(position);
|
||||
const tCarto = geo.cartographic.fromCartesian(target);
|
||||
const dh = carto.height - tCarto.height;
|
||||
return new Cesium.HeadingPitchRange(
|
||||
// A plain heading/pitch/range: Cesium's lookAt reads the fields, and the
|
||||
// Cesium SDK is not loaded on a Navara page.
|
||||
return {
|
||||
heading,
|
||||
-Math.asin(Math.min(1, Math.max(-1, dh / Math.max(1, range)))),
|
||||
pitch: -Math.asin(Math.min(1, Math.max(-1, dh / Math.max(1, range)))),
|
||||
range,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/** Rigid origin-centred rotation stays outside the actual ellipsoid's bounding sphere. */
|
||||
|
||||
@@ -2,10 +2,11 @@ import * as geo from './geo/index.js';
|
||||
import { createCelestialView } from './celestialRing/view.js';
|
||||
import { engineFromViewer } from './globe/cesium/engineFromViewer.js';
|
||||
import {
|
||||
createCelestialEphemeris,
|
||||
cesiumIcrfToFixed,
|
||||
projectCelestialPoint,
|
||||
flyCelestialViewer,
|
||||
} from './celestialRing/render/cesium.js';
|
||||
import { createCelestialEphemeris } from './celestialRing/ephemeris.js';
|
||||
import { appClock } from './time/clock.js';
|
||||
import { governorRequestRender } from './renderGovernor.js';
|
||||
|
||||
@@ -431,7 +432,10 @@ export class CelestialRing {
|
||||
this._moonFixed = geo.cartesian3.create();
|
||||
this._toCenter = geo.cartesian3.create();
|
||||
this._screenCenter = geo.cartesian2.create();
|
||||
this._ephemeris = createCelestialEphemeris();
|
||||
// A Cesium viewer's ICRF rotation; an engine (Navara) takes the TEME one.
|
||||
this._ephemeris = createCelestialEphemeris(
|
||||
viewer?.kind ? {} : { icrfToFixed: cesiumIcrfToFixed },
|
||||
);
|
||||
|
||||
this._initView(store);
|
||||
this._removePostRender = this.viewer.scene.postRender.addEventListener(() =>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as geo from '../geo/index.js';
|
||||
|
||||
/**
|
||||
* Earth-fixed unit directions to the Sun and the Moon at an epoch: Cesium's
|
||||
* planetary model (geo's bit-identical port), rotated to the Earth with
|
||||
* `icrfToFixed(epochMs, result)` where it answers (a Cesium globe once its
|
||||
* ICRF data has loaded) and with the TEME rotation otherwise, as Cesium's
|
||||
* own fallback does. Needs no Cesium SDK, so it runs on a Navara page.
|
||||
*/
|
||||
export function createCelestialEphemeris({ icrfToFixed = null } = {}) {
|
||||
const sunInertial = geo.cartesian3.create();
|
||||
const moonInertial = geo.cartesian3.create();
|
||||
const fixedMatrix = geo.matrix3.create();
|
||||
return (epochMs, sunFixed, moonFixed) => {
|
||||
const time = geo.julianDate.fromDate(new Date(epochMs));
|
||||
geo.simon1994.computeSunPositionInEarthInertialFrame(time, sunInertial);
|
||||
geo.simon1994.computeMoonPositionInEarthInertialFrame(time, moonInertial);
|
||||
const matrix =
|
||||
icrfToFixed?.(epochMs, fixedMatrix) ||
|
||||
geo.transforms.computeTemeToPseudoFixedMatrix(time, fixedMatrix);
|
||||
if (!matrix) return false;
|
||||
geo.matrix3.multiplyByVector(matrix, sunInertial, sunFixed);
|
||||
geo.matrix3.multiplyByVector(matrix, moonInertial, moonFixed);
|
||||
geo.cartesian3.normalize(sunFixed, sunFixed);
|
||||
geo.cartesian3.normalize(moonFixed, moonFixed);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -1,30 +1,11 @@
|
||||
// Exact legacy planetary model and bare-viewer fallback for CelestialRing.
|
||||
// Cesium's ICRF rotation and the bare-viewer fallback for CelestialRing.
|
||||
import * as Cesium from 'cesium';
|
||||
export function createCelestialEphemeris() {
|
||||
const sunInertial = new Cesium.Cartesian3();
|
||||
const moonInertial = new Cesium.Cartesian3();
|
||||
const fixedMatrix = new Cesium.Matrix3();
|
||||
return (epochMs, sunFixed, moonFixed) => {
|
||||
const time = Cesium.JulianDate.fromDate(new Date(epochMs));
|
||||
Cesium.Simon1994PlanetaryPositions.computeSunPositionInEarthInertialFrame(
|
||||
time,
|
||||
sunInertial,
|
||||
);
|
||||
Cesium.Simon1994PlanetaryPositions.computeMoonPositionInEarthInertialFrame(
|
||||
time,
|
||||
moonInertial,
|
||||
);
|
||||
const matrix =
|
||||
Cesium.Transforms.computeIcrfToFixedMatrix(time, fixedMatrix) ||
|
||||
Cesium.Transforms.computeTemeToPseudoFixedMatrix(time, fixedMatrix);
|
||||
if (!matrix) return false;
|
||||
Cesium.Matrix3.multiplyByVector(matrix, sunInertial, sunFixed);
|
||||
Cesium.Matrix3.multiplyByVector(matrix, moonInertial, moonFixed);
|
||||
Cesium.Cartesian3.normalize(sunFixed, sunFixed);
|
||||
Cesium.Cartesian3.normalize(moonFixed, moonFixed);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
/** Cesium's ICRF-to-fixed rotation, once its data has loaded; else undefined. */
|
||||
export const cesiumIcrfToFixed = (epochMs, result) =>
|
||||
Cesium.Transforms.computeIcrfToFixedMatrix(
|
||||
Cesium.JulianDate.fromDate(new Date(epochMs)),
|
||||
result,
|
||||
);
|
||||
export const projectCelestialPoint = (scene, position, result) =>
|
||||
Cesium.SceneTransforms.worldToWindowCoordinates(scene, position, result);
|
||||
export const flyCelestialViewer = (viewer, options) =>
|
||||
|
||||
@@ -9,14 +9,7 @@ export const CYBER_SONAR_CESIUM_VERSION = '1.138.0';
|
||||
// Follow every public DrawCommand setter, including pick/shadow/culling flags.
|
||||
// Copying native.dirty would keep derived pipelines dirty forever: the scene
|
||||
// consumes our command, so it never clears that flag on the native command.
|
||||
const commandProperties = Object.entries(
|
||||
Object.getOwnPropertyDescriptors(Cesium.DrawCommand.prototype),
|
||||
)
|
||||
.filter(
|
||||
([key, descriptor]) =>
|
||||
descriptor.set && key !== 'shaderProgram' && key !== 'uniformMap',
|
||||
)
|
||||
.map(([key]) => key);
|
||||
let commandProperties;
|
||||
|
||||
export function syncCyberSonarCommand(
|
||||
native,
|
||||
@@ -25,6 +18,14 @@ export function syncCyberSonarCommand(
|
||||
uniforms,
|
||||
labelBackground,
|
||||
) {
|
||||
commandProperties ??= Object.entries(
|
||||
Object.getOwnPropertyDescriptors(Cesium.DrawCommand.prototype),
|
||||
)
|
||||
.filter(
|
||||
([key, descriptor]) =>
|
||||
descriptor.set && key !== 'shaderProgram' && key !== 'uniformMap',
|
||||
)
|
||||
.map(([key]) => key);
|
||||
if (!record.command) {
|
||||
record.command = new Cesium.DrawCommand();
|
||||
record.uniformMap = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { waitForSignal } from '../sources/requestDeadline.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { cesiumSdkReady, loadCesiumSdk } from '../app/loadCesium.js';
|
||||
import { isPointerFree } from './inputOwnership.js';
|
||||
import {
|
||||
clearOverlaySource,
|
||||
@@ -47,8 +48,9 @@ const SCENE_PATH_RENDER_HOLD = 'bhote-koshi-scene-path-travel';
|
||||
const SCENE_MEDIA_MIN_PLAYBACK_SECONDS = 6;
|
||||
const SCENE_MEDIA_EXIT_SECONDS = 0.65;
|
||||
const BHOTE_EVENT_ACCENT = WORLD_OVERLAY_STYLE.accent;
|
||||
const FLOOD_COLOR = Cesium.Color.fromCssColorString('#d8954e').withAlpha(0.68);
|
||||
const SURGE_COLOR = Cesium.Color.fromCssColorString('#e8b47c');
|
||||
const FLOOD_COLOR = () =>
|
||||
Cesium.Color.fromCssColorString('#d8954e').withAlpha(0.68);
|
||||
const SURGE_COLOR = () => Cesium.Color.fromCssColorString('#e8b47c');
|
||||
const CINEMATIC_INPUT_EVENTS = Object.freeze(['pointerdown', 'wheel']);
|
||||
const CINEMATIC_VIEW_SPECS = Object.freeze([
|
||||
{
|
||||
@@ -1077,8 +1079,8 @@ export function createBhoteKoshiEventLayer({
|
||||
let _cinematicKeyframes = [];
|
||||
let _cinematicActive = false;
|
||||
let _cinematicReplayArmed = false;
|
||||
let _cinematicPose = { target: new Cesium.Cartesian3() };
|
||||
let _cinematicOffset = new Cesium.HeadingPitchRange();
|
||||
let _cinematicPose = { target: { x: 0, y: 0, z: 0 } };
|
||||
let _cinematicOffset = { heading: 0, pitch: 0, range: 0 };
|
||||
let _lastCinematicProgress = null;
|
||||
let _cameraInputTarget = null;
|
||||
let _evidenceClickTarget = null;
|
||||
@@ -2570,11 +2572,11 @@ export function createBhoteKoshiEventLayer({
|
||||
const tileSafe = usesPhotorealFloodSurface();
|
||||
_floodSurfaceMode = tileSafe ? 'tiles' : 'terrain';
|
||||
const haloMaterial = new Cesium.PolylineDashMaterialProperty({
|
||||
color: FLOOD_COLOR.withAlpha(0.38),
|
||||
color: FLOOD_COLOR().withAlpha(0.38),
|
||||
dashLength: 18,
|
||||
});
|
||||
const coreMaterial = new Cesium.PolylineGlowMaterialProperty({
|
||||
color: FLOOD_COLOR.withAlpha(0.96),
|
||||
color: FLOOD_COLOR().withAlpha(0.96),
|
||||
glowPower: 0.24,
|
||||
taperPower: 0.7,
|
||||
});
|
||||
@@ -2619,7 +2621,7 @@ export function createBhoteKoshiEventLayer({
|
||||
position: Cesium.Cartesian3.ZERO,
|
||||
point: {
|
||||
pixelSize: 11,
|
||||
color: SURGE_COLOR,
|
||||
color: SURGE_COLOR(),
|
||||
outlineColor: Cesium.Color.BLACK.withAlpha(0.85),
|
||||
outlineWidth: 3,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
@@ -3540,6 +3542,11 @@ export function createBhoteKoshiEventLayer({
|
||||
if (!_viewer && _engine) _native = createNativeRenderer(engine);
|
||||
_embeddedMedia?.attachScene?.(viewer, engine);
|
||||
if (!_enabled) return;
|
||||
// Its enable has loaded the SDK; a restored attachment waits for it too.
|
||||
if (!cesiumSdkReady()) {
|
||||
await untilSceneReleased(loadCesiumSdk(), signal);
|
||||
if (!current()) return;
|
||||
}
|
||||
// The catalog has rebound the incoming map controller before this call.
|
||||
if (_mapStackController) {
|
||||
_comparisonLease = acquireImageryComparison(_mapStackController, {
|
||||
@@ -3618,6 +3625,12 @@ export function createBhoteKoshiEventLayer({
|
||||
}
|
||||
|
||||
async function enable(viewer, { origin = 'local-restore', signal } = {}) {
|
||||
// The imagery, flood and corridor are Cesium entities on either globe, so
|
||||
// a Navara page loads the Cesium SDK for them first.
|
||||
if (!cesiumSdkReady()) {
|
||||
await loadCesiumSdk();
|
||||
throwIfEnableCancelled(signal, true);
|
||||
}
|
||||
_viewer = viewer;
|
||||
_enabled = true;
|
||||
// Public lifecycle passes signal, not intent origin, to enable. Its atomic
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { cesiumSdkReady, loadCesiumSdk } from '../app/loadCesium.js';
|
||||
import {
|
||||
clearOverlaySource,
|
||||
setOverlayEntries,
|
||||
@@ -144,10 +145,10 @@ const NEARBY_CITIES = Object.freeze([
|
||||
placement: 'right-lower',
|
||||
}),
|
||||
]);
|
||||
const BORDER_COLOR = Cesium.Color.fromCssColorString('#20e7f2');
|
||||
const BORDER_COLOR = () => Cesium.Color.fromCssColorString('#20e7f2');
|
||||
const BORDER_ACCENT = '#20e7f2';
|
||||
const INCIDENT_ACCENT = '#ffad55';
|
||||
const INCIDENT_COLOR = Cesium.Color.fromCssColorString(INCIDENT_ACCENT);
|
||||
const INCIDENT_COLOR = () => Cesium.Color.fromCssColorString(INCIDENT_ACCENT);
|
||||
|
||||
const DEFAULT_OVERLAY_HOST = Object.freeze({
|
||||
setEntries: setOverlayEntries,
|
||||
@@ -521,7 +522,7 @@ export function createBhoteKoshiLocatorLayer({
|
||||
sequenceStartedAt,
|
||||
visibleAfterMs = 0,
|
||||
) {
|
||||
addCallout(dataSource, INCIDENT, INCIDENT_COLOR, {
|
||||
addCallout(dataSource, INCIDENT, INCIDENT_COLOR(), {
|
||||
sequenceStartedAt,
|
||||
visibleAfterMs,
|
||||
});
|
||||
@@ -555,12 +556,12 @@ export function createBhoteKoshiLocatorLayer({
|
||||
false,
|
||||
),
|
||||
color: new Cesium.CallbackProperty((_time, result) => {
|
||||
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
||||
const color = Cesium.Color.clone(INCIDENT_COLOR(), result);
|
||||
color.alpha = 0.16 * (1 - pulseProgress());
|
||||
return color;
|
||||
}, false),
|
||||
outlineColor: new Cesium.CallbackProperty((_time, result) => {
|
||||
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
||||
const color = Cesium.Color.clone(INCIDENT_COLOR(), result);
|
||||
color.alpha = isVisible() ? 0.08 + 0.78 * (1 - pulseProgress()) : 0;
|
||||
return color;
|
||||
}, false),
|
||||
@@ -705,7 +706,7 @@ export function createBhoteKoshiLocatorLayer({
|
||||
labelPlacement = 'right',
|
||||
{ showAnchor = true } = {},
|
||||
) {
|
||||
addCallout(dataSource, NEPAL, BORDER_COLOR, {
|
||||
addCallout(dataSource, NEPAL, BORDER_COLOR(), {
|
||||
labelPlacement,
|
||||
sequenceStartedAt,
|
||||
showAnchor,
|
||||
@@ -820,12 +821,12 @@ export function createBhoteKoshiLocatorLayer({
|
||||
false,
|
||||
),
|
||||
color: new Cesium.CallbackProperty((_time, result) => {
|
||||
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
||||
const color = Cesium.Color.clone(INCIDENT_COLOR(), result);
|
||||
color.alpha = 0.16 * (1 - pulseProgress());
|
||||
return color;
|
||||
}, false),
|
||||
outlineColor: new Cesium.CallbackProperty((_time, result) => {
|
||||
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
||||
const color = Cesium.Color.clone(INCIDENT_COLOR(), result);
|
||||
color.alpha = 0.08 + 0.78 * (1 - pulseProgress());
|
||||
return color;
|
||||
}, false),
|
||||
@@ -890,7 +891,7 @@ export function createBhoteKoshiLocatorLayer({
|
||||
positions: animatedPositions,
|
||||
width: 3.5,
|
||||
clampToGround: true,
|
||||
material: BORDER_COLOR.withAlpha(0.96),
|
||||
material: BORDER_COLOR().withAlpha(0.96),
|
||||
classificationType: Cesium.ClassificationType.BOTH,
|
||||
},
|
||||
});
|
||||
@@ -915,7 +916,7 @@ export function createBhoteKoshiLocatorLayer({
|
||||
show: new Cesium.CallbackProperty(() => now() >= drawStartedAt, false),
|
||||
polygon: {
|
||||
hierarchy,
|
||||
material: BORDER_COLOR.withAlpha(0.055),
|
||||
material: BORDER_COLOR().withAlpha(0.055),
|
||||
classificationType: Cesium.ClassificationType.BOTH,
|
||||
},
|
||||
});
|
||||
@@ -925,7 +926,7 @@ export function createBhoteKoshiLocatorLayer({
|
||||
positions: animatedPositions,
|
||||
width: 5,
|
||||
clampToGround: true,
|
||||
material: BORDER_COLOR.withAlpha(0.12),
|
||||
material: BORDER_COLOR().withAlpha(0.12),
|
||||
classificationType: Cesium.ClassificationType.BOTH,
|
||||
},
|
||||
});
|
||||
@@ -935,7 +936,7 @@ export function createBhoteKoshiLocatorLayer({
|
||||
positions: animatedPositions,
|
||||
width: 2.5,
|
||||
clampToGround: true,
|
||||
material: BORDER_COLOR.withAlpha(0.96),
|
||||
material: BORDER_COLOR().withAlpha(0.96),
|
||||
classificationType: Cesium.ClassificationType.BOTH,
|
||||
},
|
||||
});
|
||||
@@ -1084,6 +1085,12 @@ export function createBhoteKoshiLocatorLayer({
|
||||
}
|
||||
|
||||
async function enable(viewer, { signal } = {}) {
|
||||
// The callouts and flood path are Cesium entities on either globe, so a
|
||||
// Navara page loads the Cesium SDK for them first.
|
||||
if (!cesiumSdkReady()) {
|
||||
await loadCesiumSdk();
|
||||
if (signal?.aborted) throw abortError();
|
||||
}
|
||||
_viewer = viewer;
|
||||
_enabled = true;
|
||||
_boundaryReady = false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Session-local geodata loading with bounded geometry and local-only file references. */
|
||||
import * as Cesium from 'cesium';
|
||||
import { cesiumSdkReady, loadCesiumSdk } from '../app/loadCesium.js';
|
||||
import { createImportImageBudget } from './geoImportImages.js';
|
||||
import { createImportedPins } from './geoImportPins.js';
|
||||
import { msg } from '../i18n/marker.js';
|
||||
@@ -1416,6 +1417,9 @@ export async function loadGeoFile(
|
||||
throw new GeoImportError(
|
||||
msg('Only GeoJSON, KML, KMZ and GPX files can be imported.'),
|
||||
);
|
||||
// The import builds Cesium data sources on either globe: a Navara page
|
||||
// loads the Cesium SDK first.
|
||||
if (!cesiumSdkReady()) await boundedImport(loadCesiumSdk(), signal);
|
||||
const stroke = Cesium.Color.fromCssColorString(color);
|
||||
const urls = [];
|
||||
let pins;
|
||||
|
||||
@@ -21,17 +21,17 @@ const STYLE = [
|
||||
'imageSubRegion',
|
||||
];
|
||||
|
||||
class ImportedBillboards extends Cesium.BillboardCollection {
|
||||
update(frameState) {
|
||||
// Height queries should find the map, never another imported marker.
|
||||
// Ordinary click picks and rendered screenshots still include the pins.
|
||||
if (frameState.passes.pick && frameState.passes.offscreen) return;
|
||||
super.update(frameState);
|
||||
}
|
||||
}
|
||||
|
||||
/** Own static imported pins without a per-frame Entity visualizer walk. */
|
||||
export function createImportedPins(dataSource, viewer) {
|
||||
class ImportedBillboards extends Cesium.BillboardCollection {
|
||||
update(frameState) {
|
||||
// Height queries should find the map, never another imported marker.
|
||||
// Ordinary click picks and rendered screenshots still include the pins.
|
||||
if (frameState.passes.pick && frameState.passes.offscreen) return;
|
||||
super.update(frameState);
|
||||
}
|
||||
}
|
||||
|
||||
const all = dataSource.entities.values.slice();
|
||||
const now = Cesium.JulianDate.now();
|
||||
const pins = all.filter((entity) => entity.billboard && entity.position);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { modelAxis } from './render/cesiumConstants.js';
|
||||
import * as geo from '../geo/index.js';
|
||||
/**
|
||||
* Model-space visual centres measured from the shipped GLBs' scene-space AABBs.
|
||||
@@ -71,17 +70,9 @@ export const MODEL_TRAIL_ANCHOR_NATIVE = Object.freeze({
|
||||
'/models/atr72.glb': Object.freeze([12.9785, -1.2935, 0]),
|
||||
});
|
||||
|
||||
/** @constant Cesium's OWN axis correction for a glTF loaded with the
|
||||
* `Model.fromGltfAsync` defaults (upAxis Y, forwardAxis Z), assembled from its
|
||||
* exported constants rather than restated as numbers here. `ModelSceneGraph`
|
||||
* renders with `modelMatrix x components.transform x axisCorrection`, and the
|
||||
* trail anchor has to ride the same chain or it lands on a different axis than
|
||||
* the aircraft it is supposed to be attached to. */
|
||||
const AXIS_CORRECTION = geo.matrix4.multiplyTransformation(
|
||||
modelAxis.Y_UP_TO_Z_UP,
|
||||
modelAxis.Z_UP_TO_X_UP,
|
||||
geo.matrix4.create(),
|
||||
);
|
||||
// glTF Y-up/Z-forward to the model frame: Cesium's Y_UP_TO_Z_UP ×
|
||||
// Z_UP_TO_X_UP, shared with Navara before the Cesium SDK is loaded.
|
||||
const AXIS_CORRECTION = [0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1];
|
||||
const _anchorScratch = geo.cartesian3.create();
|
||||
const _chainScratch = geo.matrix4.create();
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Native constants shared by the W1 adapters; keep the SDK import here.
|
||||
export {
|
||||
Axis as modelAxis,
|
||||
ScreenSpaceEventType as trackingEventTypes,
|
||||
} from 'cesium';
|
||||
// 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';
|
||||
|
||||
export const modelAxis = () => Axis;
|
||||
export const trackingEventTypes = () => ScreenSpaceEventType;
|
||||
@@ -6,11 +6,12 @@ export function worldToWindowCoordinates(scene, position, result) {
|
||||
result,
|
||||
);
|
||||
}
|
||||
const occluder = new Cesium.EllipsoidalOccluder(
|
||||
Cesium.Ellipsoid.WGS84,
|
||||
new Cesium.Cartesian3(),
|
||||
);
|
||||
let occluder;
|
||||
export function horizonOccluder(camera) {
|
||||
occluder ??= new Cesium.EllipsoidalOccluder(
|
||||
Cesium.Ellipsoid.WGS84,
|
||||
new Cesium.Cartesian3(),
|
||||
);
|
||||
occluder.cameraPosition = camera.positionWC;
|
||||
return occluder;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export function isTrackingClickGesture(gesture = {}) {
|
||||
*/
|
||||
export function bindTrackingClickGesture(handler, onClick, options = {}) {
|
||||
const now = options.now || (() => performance.now());
|
||||
const eventTypes = options.eventTypes || trackingEventTypes;
|
||||
const eventTypes = options.eventTypes || trackingEventTypes();
|
||||
let pressActive = false;
|
||||
let pressStartedAt = 0;
|
||||
let previousPosition = null;
|
||||
|
||||
@@ -16,3 +16,5 @@ export * as transforms from './transforms.js';
|
||||
|
||||
export * as ellipsoidGeodesic from './ellipsoidGeodesic.js';
|
||||
export * as boundingSphere from './boundingSphere.js';
|
||||
export * as julianDate from './julianDate.js';
|
||||
export * as simon1994 from './simon1994.js';
|
||||
@@ -0,0 +1,193 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — JulianDate.js
|
||||
//
|
||||
// The part of Cesium's JulianDate that the planetary positions need: a date
|
||||
// as {dayNumber, secondsOfDay} in International Atomic Time (TAI), made from
|
||||
// a JavaScript Date through Cesium's leap second table. The operation order
|
||||
// matches Cesium so the values are bit-identical.
|
||||
|
||||
const SECONDS_PER_DAY = 86400.0;
|
||||
|
||||
// Cesium's JulianDate.leapSeconds: [TAI day, TAI seconds of day, TAI - UTC].
|
||||
const LEAP_SECONDS = [
|
||||
[2441317, 43210.0, 10], // January 1, 1972 00:00:00 UTC
|
||||
[2441499, 43211.0, 11], // July 1, 1972 00:00:00 UTC
|
||||
[2441683, 43212.0, 12], // January 1, 1973 00:00:00 UTC
|
||||
[2442048, 43213.0, 13], // January 1, 1974 00:00:00 UTC
|
||||
[2442413, 43214.0, 14], // January 1, 1975 00:00:00 UTC
|
||||
[2442778, 43215.0, 15], // January 1, 1976 00:00:00 UTC
|
||||
[2443144, 43216.0, 16], // January 1, 1977 00:00:00 UTC
|
||||
[2443509, 43217.0, 17], // January 1, 1978 00:00:00 UTC
|
||||
[2443874, 43218.0, 18], // January 1, 1979 00:00:00 UTC
|
||||
[2444239, 43219.0, 19], // January 1, 1980 00:00:00 UTC
|
||||
[2444786, 43220.0, 20], // July 1, 1981 00:00:00 UTC
|
||||
[2445151, 43221.0, 21], // July 1, 1982 00:00:00 UTC
|
||||
[2445516, 43222.0, 22], // July 1, 1983 00:00:00 UTC
|
||||
[2446247, 43223.0, 23], // July 1, 1985 00:00:00 UTC
|
||||
[2447161, 43224.0, 24], // January 1, 1988 00:00:00 UTC
|
||||
[2447892, 43225.0, 25], // January 1, 1990 00:00:00 UTC
|
||||
[2448257, 43226.0, 26], // January 1, 1991 00:00:00 UTC
|
||||
[2448804, 43227.0, 27], // July 1, 1992 00:00:00 UTC
|
||||
[2449169, 43228.0, 28], // July 1, 1993 00:00:00 UTC
|
||||
[2449534, 43229.0, 29], // July 1, 1994 00:00:00 UTC
|
||||
[2450083, 43230.0, 30], // January 1, 1996 00:00:00 UTC
|
||||
[2450630, 43231.0, 31], // July 1, 1997 00:00:00 UTC
|
||||
[2451179, 43232.0, 32], // January 1, 1999 00:00:00 UTC
|
||||
[2453736, 43233.0, 33], // January 1, 2006 00:00:00 UTC
|
||||
[2454832, 43234.0, 34], // January 1, 2009 00:00:00 UTC
|
||||
[2456109, 43235.0, 35], // July 1, 2012 00:00:00 UTC
|
||||
[2457204, 43236.0, 36], // July 1, 2015 00:00:00 UTC
|
||||
[2457754, 43237.0, 37], // January 1, 2017 00:00:00 UTC
|
||||
].map(([dayNumber, secondsOfDay, offset]) => ({
|
||||
julianDate: { dayNumber, secondsOfDay },
|
||||
offset,
|
||||
}));
|
||||
|
||||
function setComponents(wholeDays, secondsOfDay, julianDate) {
|
||||
const extraDays = (secondsOfDay / SECONDS_PER_DAY) | 0;
|
||||
wholeDays += extraDays;
|
||||
secondsOfDay -= SECONDS_PER_DAY * extraDays;
|
||||
|
||||
if (secondsOfDay < 0) {
|
||||
wholeDays--;
|
||||
secondsOfDay += SECONDS_PER_DAY;
|
||||
}
|
||||
|
||||
julianDate.dayNumber = wholeDays;
|
||||
julianDate.secondsOfDay = secondsOfDay;
|
||||
return julianDate;
|
||||
}
|
||||
|
||||
function compare(left, right) {
|
||||
const julianDayNumberDifference = left.dayNumber - right.dayNumber;
|
||||
if (julianDayNumberDifference !== 0) {
|
||||
return julianDayNumberDifference;
|
||||
}
|
||||
return left.secondsOfDay - right.secondsOfDay;
|
||||
}
|
||||
|
||||
// Cesium's binarySearch over the leap second table.
|
||||
function searchLeapSeconds(julianDate) {
|
||||
let low = 0;
|
||||
let high = LEAP_SECONDS.length - 1;
|
||||
while (low <= high) {
|
||||
const i = ~~((low + high) / 2);
|
||||
const comparison = compare(LEAP_SECONDS[i].julianDate, julianDate);
|
||||
if (comparison < 0) {
|
||||
low = i + 1;
|
||||
continue;
|
||||
}
|
||||
if (comparison > 0) {
|
||||
high = i - 1;
|
||||
continue;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
return ~(high + 1);
|
||||
}
|
||||
|
||||
function secondsDifference(left, right) {
|
||||
const dayDifference = (left.dayNumber - right.dayNumber) * SECONDS_PER_DAY;
|
||||
return dayDifference + (left.secondsOfDay - right.secondsOfDay);
|
||||
}
|
||||
|
||||
function convertUtcToTai(julianDate) {
|
||||
let index = searchLeapSeconds(julianDate);
|
||||
if (index < 0) {
|
||||
index = ~index;
|
||||
}
|
||||
if (index >= LEAP_SECONDS.length) {
|
||||
index = LEAP_SECONDS.length - 1;
|
||||
}
|
||||
let offset = LEAP_SECONDS[index].offset;
|
||||
if (index > 0) {
|
||||
const difference = secondsDifference(
|
||||
LEAP_SECONDS[index].julianDate,
|
||||
julianDate,
|
||||
);
|
||||
if (difference > offset) {
|
||||
index--;
|
||||
offset = LEAP_SECONDS[index].offset;
|
||||
}
|
||||
}
|
||||
addSeconds(julianDate, offset, julianDate);
|
||||
}
|
||||
|
||||
/** A TAI date from its whole Julian day number and seconds into that day. */
|
||||
export function fromTai(julianDayNumber, secondsOfDay) {
|
||||
const wholeDays = julianDayNumber | 0;
|
||||
secondsOfDay = secondsOfDay + (julianDayNumber - wholeDays) * SECONDS_PER_DAY;
|
||||
return setComponents(wholeDays, secondsOfDay, {
|
||||
dayNumber: 0,
|
||||
secondsOfDay: 0,
|
||||
});
|
||||
}
|
||||
|
||||
/** Cesium's JulianDate.fromDate: a valid JavaScript Date as a TAI date. */
|
||||
export function fromDate(date, result) {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = date.getUTCMonth() + 1;
|
||||
const day = date.getUTCDate();
|
||||
let hour = date.getUTCHours();
|
||||
const minute = date.getUTCMinutes();
|
||||
const second = date.getUTCSeconds();
|
||||
const millisecond = date.getUTCMilliseconds();
|
||||
|
||||
// Algorithm from page 604 of the Explanatory Supplement to the
|
||||
// Astronomical Almanac (Seidelmann 1992).
|
||||
const a = ((month - 14) / 12) | 0;
|
||||
const b = year + 4800 + a;
|
||||
let dayNumber =
|
||||
(((1461 * b) / 4) | 0) +
|
||||
(((367 * (month - 2 - 12 * a)) / 12) | 0) -
|
||||
(((3 * (((b + 100) / 100) | 0)) / 4) | 0) +
|
||||
day -
|
||||
32075;
|
||||
|
||||
// JulianDates are noon-based
|
||||
hour = hour - 12;
|
||||
if (hour < 0) {
|
||||
hour += 24;
|
||||
}
|
||||
|
||||
const secondsOfDay =
|
||||
second + (hour * 3600.0 + minute * 60.0 + millisecond * 0.001);
|
||||
|
||||
if (secondsOfDay >= 43200.0) {
|
||||
dayNumber -= 1;
|
||||
}
|
||||
|
||||
result = setComponents(
|
||||
dayNumber,
|
||||
secondsOfDay,
|
||||
result ?? { dayNumber: 0, secondsOfDay: 0 },
|
||||
);
|
||||
convertUtcToTai(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The date `seconds` later (earlier when negative); `result` is required. */
|
||||
export function addSeconds(julianDate, seconds, result) {
|
||||
return setComponents(
|
||||
julianDate.dayNumber,
|
||||
julianDate.secondsOfDay + seconds,
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
/** The date as a fractional Julian day number. */
|
||||
export function totalDays(julianDate) {
|
||||
return julianDate.dayNumber + julianDate.secondsOfDay / SECONDS_PER_DAY;
|
||||
}
|
||||
|
||||
/** How many seconds TAI is ahead of UTC at the date. */
|
||||
export function computeTaiMinusUtc(julianDate) {
|
||||
let index = searchLeapSeconds(julianDate);
|
||||
if (index < 0) {
|
||||
index = ~index;
|
||||
--index;
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
return LEAP_SECONDS[index].offset;
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — Simon1994PlanetaryPositions.js
|
||||
//
|
||||
// The Sun's and the Moon's positions in the Earth-centred inertial frame
|
||||
// (Simon et al. 1994), for the celestial ring on a page without the Cesium
|
||||
// SDK. The operation order matches Cesium so the outputs are bit-identical;
|
||||
// Cesium's debug range checks are left out (its inputs are its own orbit
|
||||
// constants). Dates are geo/julianDate.js values.
|
||||
|
||||
import * as Cartesian3 from './cartesian3.js';
|
||||
import * as Matrix3 from './matrix3.js';
|
||||
import * as CesiumMath from './math.js';
|
||||
import * as JulianDate from './julianDate.js';
|
||||
|
||||
const SECONDS_PER_DAY = 86400.0;
|
||||
const DAYS_PER_JULIAN_CENTURY = 36525.0;
|
||||
const RADIANS_PER_ARCSECOND = CesiumMath.RADIANS_PER_DEGREE / 3600.0;
|
||||
|
||||
/** Cesium's `new Matrix3(...)`: row-major arguments, column-major storage. */
|
||||
function rowMajor(c0r0, c1r0, c2r0, c0r1, c1r1, c2r1, c0r2, c1r2, c2r2) {
|
||||
return [c0r0, c0r1, c0r2, c1r0, c1r1, c1r2, c2r0, c2r1, c2r2];
|
||||
}
|
||||
|
||||
function computeTdbMinusTtSpice(daysSinceJ2000InTerrestrialTime) {
|
||||
/* STK Comments ------------------------------------------------------
|
||||
* This function uses constants designed to be consistent with
|
||||
* the SPICE Toolkit from JPL version N0051 (unitim.c)
|
||||
* M0 = 6.239996
|
||||
* M0Dot = 1.99096871e-7 rad/s = 0.01720197 rad/d
|
||||
* EARTH_ECC = 1.671e-2
|
||||
* TDB_AMPL = 1.657e-3 secs
|
||||
*--------------------------------------------------------------------*/
|
||||
|
||||
//* Values taken as specified in STK Comments except: 0.01720197 rad/day = 1.99096871e-7 rad/sec
|
||||
//* Here we use the more precise value taken from the SPICE value 1.99096871e-7 rad/sec converted to rad/day
|
||||
//* All other constants are consistent with the SPICE implementation of the TDB conversion
|
||||
//* except where we treat the independent time parameter to be in TT instead of TDB.
|
||||
//* This is an approximation made to facilitate performance due to the higher prevalance of
|
||||
//* the TT2TDB conversion over TDB2TT in order to avoid having to iterate when converting to TDB for the JPL ephemeris.
|
||||
//* Days are used instead of seconds to provide a slight improvement in numerical precision.
|
||||
|
||||
//* For more information see:
|
||||
//* http://www.cv.nrao.edu/~rfisher/Ephemerides/times.html#TDB
|
||||
//* ftp://ssd.jpl.nasa.gov/pub/eph/planets/ioms/ExplSupplChap8.pdf
|
||||
|
||||
const g = 6.239996 + 0.0172019696544 * daysSinceJ2000InTerrestrialTime;
|
||||
return 1.657e-3 * Math.sin(g + 1.671e-2 * Math.sin(g));
|
||||
}
|
||||
|
||||
const TdtMinusTai = 32.184;
|
||||
const J2000d = 2451545;
|
||||
function taiToTdb(date, result) {
|
||||
//Converts TAI to TT
|
||||
result = JulianDate.addSeconds(date, TdtMinusTai, result);
|
||||
|
||||
//Converts TT to TDB
|
||||
const days = JulianDate.totalDays(result) - J2000d;
|
||||
result = JulianDate.addSeconds(result, computeTdbMinusTtSpice(days), result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const epoch = JulianDate.fromTai(2451545, 0); //Actually TDB (not TAI)
|
||||
const MetersPerKilometer = 1000.0;
|
||||
const RadiansPerDegree = CesiumMath.RADIANS_PER_DEGREE;
|
||||
const RadiansPerArcSecond = RADIANS_PER_ARCSECOND;
|
||||
const MetersPerAstronomicalUnit = 1.4959787e11; // IAU 1976 value
|
||||
|
||||
const perifocalToEquatorial = Matrix3.create();
|
||||
function elementsToCartesian(
|
||||
semimajorAxis,
|
||||
eccentricity,
|
||||
inclination,
|
||||
longitudeOfPerigee,
|
||||
longitudeOfNode,
|
||||
meanLongitude,
|
||||
result,
|
||||
) {
|
||||
if (inclination < 0.0) {
|
||||
inclination = -inclination;
|
||||
longitudeOfNode += Math.PI;
|
||||
}
|
||||
|
||||
const radiusOfPeriapsis = semimajorAxis * (1.0 - eccentricity);
|
||||
const argumentOfPeriapsis = longitudeOfPerigee - longitudeOfNode;
|
||||
const rightAscensionOfAscendingNode = longitudeOfNode;
|
||||
const trueAnomaly = meanAnomalyToTrueAnomaly(
|
||||
meanLongitude - longitudeOfPerigee,
|
||||
eccentricity,
|
||||
);
|
||||
const type = chooseOrbit(eccentricity, 0.0);
|
||||
|
||||
perifocalToCartesianMatrix(
|
||||
argumentOfPeriapsis,
|
||||
inclination,
|
||||
rightAscensionOfAscendingNode,
|
||||
perifocalToEquatorial,
|
||||
);
|
||||
const semilatus = radiusOfPeriapsis * (1.0 + eccentricity);
|
||||
const costheta = Math.cos(trueAnomaly);
|
||||
const sintheta = Math.sin(trueAnomaly);
|
||||
|
||||
const denom = 1.0 + eccentricity * costheta;
|
||||
|
||||
const radius = semilatus / denom;
|
||||
if (result == null) {
|
||||
result = Cartesian3.create(radius * costheta, radius * sintheta, 0.0);
|
||||
} else {
|
||||
result.x = radius * costheta;
|
||||
result.y = radius * sintheta;
|
||||
result.z = 0.0;
|
||||
}
|
||||
|
||||
return Matrix3.multiplyByVector(perifocalToEquatorial, result, result);
|
||||
}
|
||||
|
||||
function chooseOrbit(eccentricity, tolerance) {
|
||||
if (eccentricity <= tolerance) {
|
||||
return 'Circular';
|
||||
} else if (eccentricity < 1.0 - tolerance) {
|
||||
return 'Elliptical';
|
||||
} else if (eccentricity <= 1.0 + tolerance) {
|
||||
return 'Parabolic';
|
||||
}
|
||||
return 'Hyperbolic';
|
||||
}
|
||||
|
||||
// Calculates the true anomaly given the mean anomaly and the eccentricity.
|
||||
function meanAnomalyToTrueAnomaly(meanAnomaly, eccentricity) {
|
||||
const eccentricAnomaly = meanAnomalyToEccentricAnomaly(
|
||||
meanAnomaly,
|
||||
eccentricity,
|
||||
);
|
||||
return eccentricAnomalyToTrueAnomaly(eccentricAnomaly, eccentricity);
|
||||
}
|
||||
|
||||
const maxIterationCount = 50;
|
||||
const keplerEqConvergence = 1.0e-8;
|
||||
// Calculates the eccentric anomaly given the mean anomaly and the eccentricity.
|
||||
function meanAnomalyToEccentricAnomaly(meanAnomaly, eccentricity) {
|
||||
const revs = Math.floor(meanAnomaly / CesiumMath.TWO_PI);
|
||||
|
||||
// Find angle in current revolution
|
||||
meanAnomaly -= revs * CesiumMath.TWO_PI;
|
||||
|
||||
// calculate starting value for iteration sequence
|
||||
let iterationValue =
|
||||
meanAnomaly +
|
||||
(eccentricity * Math.sin(meanAnomaly)) /
|
||||
(1.0 - Math.sin(meanAnomaly + eccentricity) + Math.sin(meanAnomaly));
|
||||
|
||||
// Perform Newton-Raphson iteration on Kepler's equation
|
||||
let eccentricAnomaly = Number.MAX_VALUE;
|
||||
|
||||
let count;
|
||||
for (
|
||||
count = 0;
|
||||
count < maxIterationCount &&
|
||||
Math.abs(eccentricAnomaly - iterationValue) > keplerEqConvergence;
|
||||
++count
|
||||
) {
|
||||
eccentricAnomaly = iterationValue;
|
||||
const NRfunction =
|
||||
eccentricAnomaly -
|
||||
eccentricity * Math.sin(eccentricAnomaly) -
|
||||
meanAnomaly;
|
||||
const dNRfunction = 1 - eccentricity * Math.cos(eccentricAnomaly);
|
||||
iterationValue = eccentricAnomaly - NRfunction / dNRfunction;
|
||||
}
|
||||
|
||||
eccentricAnomaly = iterationValue + revs * CesiumMath.TWO_PI;
|
||||
return eccentricAnomaly;
|
||||
}
|
||||
|
||||
// Calculates the true anomaly given the eccentric anomaly and the eccentricity.
|
||||
function eccentricAnomalyToTrueAnomaly(eccentricAnomaly, eccentricity) {
|
||||
// Calculate the number of previous revolutions
|
||||
const revs = Math.floor(eccentricAnomaly / CesiumMath.TWO_PI);
|
||||
|
||||
// Find angle in current revolution
|
||||
eccentricAnomaly -= revs * CesiumMath.TWO_PI;
|
||||
|
||||
// Calculate true anomaly from eccentric anomaly
|
||||
const trueAnomalyX = Math.cos(eccentricAnomaly) - eccentricity;
|
||||
const trueAnomalyY =
|
||||
Math.sin(eccentricAnomaly) * Math.sqrt(1 - eccentricity * eccentricity);
|
||||
|
||||
let trueAnomaly = Math.atan2(trueAnomalyY, trueAnomalyX);
|
||||
|
||||
// Ensure the correct quadrant
|
||||
trueAnomaly = CesiumMath.zeroToTwoPi(trueAnomaly);
|
||||
if (eccentricAnomaly < 0) {
|
||||
trueAnomaly -= CesiumMath.TWO_PI;
|
||||
}
|
||||
|
||||
// Add on previous revolutions
|
||||
trueAnomaly += revs * CesiumMath.TWO_PI;
|
||||
|
||||
return trueAnomaly;
|
||||
}
|
||||
|
||||
// Calculates the transformation matrix to convert from the perifocal (PQW) coordinate
|
||||
// system to inertial cartesian coordinates.
|
||||
function perifocalToCartesianMatrix(
|
||||
argumentOfPeriapsis,
|
||||
inclination,
|
||||
rightAscension,
|
||||
result,
|
||||
) {
|
||||
const cosap = Math.cos(argumentOfPeriapsis);
|
||||
const sinap = Math.sin(argumentOfPeriapsis);
|
||||
|
||||
const cosi = Math.cos(inclination);
|
||||
const sini = Math.sin(inclination);
|
||||
|
||||
const cosraan = Math.cos(rightAscension);
|
||||
const sinraan = Math.sin(rightAscension);
|
||||
if (result == null) {
|
||||
result = rowMajor(
|
||||
cosraan * cosap - sinraan * sinap * cosi,
|
||||
-cosraan * sinap - sinraan * cosap * cosi,
|
||||
sinraan * sini,
|
||||
|
||||
sinraan * cosap + cosraan * sinap * cosi,
|
||||
-sinraan * sinap + cosraan * cosap * cosi,
|
||||
-cosraan * sini,
|
||||
|
||||
sinap * sini,
|
||||
cosap * sini,
|
||||
cosi,
|
||||
);
|
||||
} else {
|
||||
result[0] = cosraan * cosap - sinraan * sinap * cosi;
|
||||
result[1] = sinraan * cosap + cosraan * sinap * cosi;
|
||||
result[2] = sinap * sini;
|
||||
result[3] = -cosraan * sinap - sinraan * cosap * cosi;
|
||||
result[4] = -sinraan * sinap + cosraan * cosap * cosi;
|
||||
result[5] = cosap * sini;
|
||||
result[6] = sinraan * sini;
|
||||
result[7] = -cosraan * sini;
|
||||
result[8] = cosi;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// From section 5.8
|
||||
const semiMajorAxis0 = 1.0000010178 * MetersPerAstronomicalUnit;
|
||||
const meanLongitude0 = 100.46645683 * RadiansPerDegree;
|
||||
const meanLongitude1 = 1295977422.83429 * RadiansPerArcSecond;
|
||||
|
||||
// From table 6
|
||||
const p1u = 16002;
|
||||
const p2u = 21863;
|
||||
const p3u = 32004;
|
||||
const p4u = 10931;
|
||||
const p5u = 14529;
|
||||
const p6u = 16368;
|
||||
const p7u = 15318;
|
||||
const p8u = 32794;
|
||||
|
||||
const Ca1 = 64 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Ca2 = -152 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Ca3 = 62 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Ca4 = -8 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Ca5 = 32 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Ca6 = -41 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Ca7 = 19 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Ca8 = -11 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
|
||||
const Sa1 = -150 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Sa2 = -46 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Sa3 = 68 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Sa4 = 54 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Sa5 = 14 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Sa6 = 24 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Sa7 = -28 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
const Sa8 = 22 * 1e-7 * MetersPerAstronomicalUnit;
|
||||
|
||||
const q1u = 10;
|
||||
const q2u = 16002;
|
||||
const q3u = 21863;
|
||||
const q4u = 10931;
|
||||
const q5u = 1473;
|
||||
const q6u = 32004;
|
||||
const q7u = 4387;
|
||||
const q8u = 73;
|
||||
|
||||
const Cl1 = -325 * 1e-7;
|
||||
const Cl2 = -322 * 1e-7;
|
||||
const Cl3 = -79 * 1e-7;
|
||||
const Cl4 = 232 * 1e-7;
|
||||
const Cl5 = -52 * 1e-7;
|
||||
const Cl6 = 97 * 1e-7;
|
||||
const Cl7 = 55 * 1e-7;
|
||||
const Cl8 = -41 * 1e-7;
|
||||
|
||||
const Sl1 = -105 * 1e-7;
|
||||
const Sl2 = -137 * 1e-7;
|
||||
const Sl3 = 258 * 1e-7;
|
||||
const Sl4 = 35 * 1e-7;
|
||||
const Sl5 = -116 * 1e-7;
|
||||
const Sl6 = -88 * 1e-7;
|
||||
const Sl7 = -112 * 1e-7;
|
||||
const Sl8 = -80 * 1e-7;
|
||||
|
||||
const scratchDate = JulianDate.fromTai(0, 0.0);
|
||||
// Gets a point describing the motion of the Earth-Moon barycenter according to the equations described in section 6.
|
||||
function computeSimonEarthMoonBarycenter(date, result) {
|
||||
// t is thousands of years from J2000 TDB
|
||||
taiToTdb(date, scratchDate);
|
||||
const x =
|
||||
scratchDate.dayNumber -
|
||||
epoch.dayNumber +
|
||||
(scratchDate.secondsOfDay - epoch.secondsOfDay) / SECONDS_PER_DAY;
|
||||
const t = x / (DAYS_PER_JULIAN_CENTURY * 10.0);
|
||||
|
||||
const u = 0.3595362 * t;
|
||||
const semimajorAxis =
|
||||
semiMajorAxis0 +
|
||||
Ca1 * Math.cos(p1u * u) +
|
||||
Sa1 * Math.sin(p1u * u) +
|
||||
Ca2 * Math.cos(p2u * u) +
|
||||
Sa2 * Math.sin(p2u * u) +
|
||||
Ca3 * Math.cos(p3u * u) +
|
||||
Sa3 * Math.sin(p3u * u) +
|
||||
Ca4 * Math.cos(p4u * u) +
|
||||
Sa4 * Math.sin(p4u * u) +
|
||||
Ca5 * Math.cos(p5u * u) +
|
||||
Sa5 * Math.sin(p5u * u) +
|
||||
Ca6 * Math.cos(p6u * u) +
|
||||
Sa6 * Math.sin(p6u * u) +
|
||||
Ca7 * Math.cos(p7u * u) +
|
||||
Sa7 * Math.sin(p7u * u) +
|
||||
Ca8 * Math.cos(p8u * u) +
|
||||
Sa8 * Math.sin(p8u * u);
|
||||
const meanLongitude =
|
||||
meanLongitude0 +
|
||||
meanLongitude1 * t +
|
||||
Cl1 * Math.cos(q1u * u) +
|
||||
Sl1 * Math.sin(q1u * u) +
|
||||
Cl2 * Math.cos(q2u * u) +
|
||||
Sl2 * Math.sin(q2u * u) +
|
||||
Cl3 * Math.cos(q3u * u) +
|
||||
Sl3 * Math.sin(q3u * u) +
|
||||
Cl4 * Math.cos(q4u * u) +
|
||||
Sl4 * Math.sin(q4u * u) +
|
||||
Cl5 * Math.cos(q5u * u) +
|
||||
Sl5 * Math.sin(q5u * u) +
|
||||
Cl6 * Math.cos(q6u * u) +
|
||||
Sl6 * Math.sin(q6u * u) +
|
||||
Cl7 * Math.cos(q7u * u) +
|
||||
Sl7 * Math.sin(q7u * u) +
|
||||
Cl8 * Math.cos(q8u * u) +
|
||||
Sl8 * Math.sin(q8u * u);
|
||||
|
||||
// All constants in this part are from section 5.8
|
||||
const eccentricity = 0.0167086342 - 0.0004203654 * t;
|
||||
const longitudeOfPerigee =
|
||||
102.93734808 * RadiansPerDegree + 11612.3529 * RadiansPerArcSecond * t;
|
||||
const inclination = 469.97289 * RadiansPerArcSecond * t;
|
||||
const longitudeOfNode =
|
||||
174.87317577 * RadiansPerDegree - 8679.27034 * RadiansPerArcSecond * t;
|
||||
|
||||
return elementsToCartesian(
|
||||
semimajorAxis,
|
||||
eccentricity,
|
||||
inclination,
|
||||
longitudeOfPerigee,
|
||||
longitudeOfNode,
|
||||
meanLongitude,
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
// Gets a point describing the position of the moon according to the equations described in section 4.
|
||||
function computeSimonMoon(date, result) {
|
||||
taiToTdb(date, scratchDate);
|
||||
const x =
|
||||
scratchDate.dayNumber -
|
||||
epoch.dayNumber +
|
||||
(scratchDate.secondsOfDay - epoch.secondsOfDay) / SECONDS_PER_DAY;
|
||||
const t = x / DAYS_PER_JULIAN_CENTURY;
|
||||
const t2 = t * t;
|
||||
const t3 = t2 * t;
|
||||
const t4 = t3 * t;
|
||||
|
||||
// Terms from section 3.4 (b.1)
|
||||
let semimajorAxis = 383397.7725 + 0.004 * t;
|
||||
let eccentricity = 0.055545526 - 0.000000016 * t;
|
||||
const inclinationConstant = 5.15668983 * RadiansPerDegree;
|
||||
let inclinationSecPart =
|
||||
-0.00008 * t + 0.02966 * t2 - 0.000042 * t3 - 0.00000013 * t4;
|
||||
const longitudeOfPerigeeConstant = 83.35324312 * RadiansPerDegree;
|
||||
let longitudeOfPerigeeSecPart =
|
||||
14643420.2669 * t - 38.2702 * t2 - 0.045047 * t3 + 0.00021301 * t4;
|
||||
const longitudeOfNodeConstant = 125.04455501 * RadiansPerDegree;
|
||||
let longitudeOfNodeSecPart =
|
||||
-6967919.3631 * t + 6.3602 * t2 + 0.007625 * t3 - 0.00003586 * t4;
|
||||
const meanLongitudeConstant = 218.31664563 * RadiansPerDegree;
|
||||
let meanLongitudeSecPart =
|
||||
1732559343.4847 * t - 6.391 * t2 + 0.006588 * t3 - 0.00003169 * t4;
|
||||
|
||||
// Delaunay arguments from section 3.5 b
|
||||
const D =
|
||||
297.85019547 * RadiansPerDegree +
|
||||
RadiansPerArcSecond *
|
||||
(1602961601.209 * t - 6.3706 * t2 + 0.006593 * t3 - 0.00003169 * t4);
|
||||
const F =
|
||||
93.27209062 * RadiansPerDegree +
|
||||
RadiansPerArcSecond *
|
||||
(1739527262.8478 * t - 12.7512 * t2 - 0.001037 * t3 + 0.00000417 * t4);
|
||||
const l =
|
||||
134.96340251 * RadiansPerDegree +
|
||||
RadiansPerArcSecond *
|
||||
(1717915923.2178 * t + 31.8792 * t2 + 0.051635 * t3 - 0.0002447 * t4);
|
||||
const lprime =
|
||||
357.52910918 * RadiansPerDegree +
|
||||
RadiansPerArcSecond *
|
||||
(129596581.0481 * t - 0.5532 * t2 + 0.000136 * t3 - 0.00001149 * t4);
|
||||
const psi =
|
||||
310.17137918 * RadiansPerDegree -
|
||||
RadiansPerArcSecond *
|
||||
(6967051.436 * t + 6.2068 * t2 + 0.007618 * t3 - 0.00003219 * t4);
|
||||
|
||||
// Add terms from Table 4
|
||||
const twoD = 2.0 * D;
|
||||
const fourD = 4.0 * D;
|
||||
const sixD = 6.0 * D;
|
||||
const twol = 2.0 * l;
|
||||
const threel = 3.0 * l;
|
||||
const fourl = 4.0 * l;
|
||||
const twoF = 2.0 * F;
|
||||
semimajorAxis +=
|
||||
3400.4 * Math.cos(twoD) -
|
||||
635.6 * Math.cos(twoD - l) -
|
||||
235.6 * Math.cos(l) +
|
||||
218.1 * Math.cos(twoD - lprime) +
|
||||
181.0 * Math.cos(twoD + l);
|
||||
eccentricity +=
|
||||
0.014216 * Math.cos(twoD - l) +
|
||||
0.008551 * Math.cos(twoD - twol) -
|
||||
0.001383 * Math.cos(l) +
|
||||
0.001356 * Math.cos(twoD + l) -
|
||||
0.001147 * Math.cos(fourD - threel) -
|
||||
0.000914 * Math.cos(fourD - twol) +
|
||||
0.000869 * Math.cos(twoD - lprime - l) -
|
||||
0.000627 * Math.cos(twoD) -
|
||||
0.000394 * Math.cos(fourD - fourl) +
|
||||
0.000282 * Math.cos(twoD - lprime - twol) -
|
||||
0.000279 * Math.cos(D - l) -
|
||||
0.000236 * Math.cos(twol) +
|
||||
0.000231 * Math.cos(fourD) +
|
||||
0.000229 * Math.cos(sixD - fourl) -
|
||||
0.000201 * Math.cos(twol - twoF);
|
||||
inclinationSecPart +=
|
||||
486.26 * Math.cos(twoD - twoF) -
|
||||
40.13 * Math.cos(twoD) +
|
||||
37.51 * Math.cos(twoF) +
|
||||
25.73 * Math.cos(twol - twoF) +
|
||||
19.97 * Math.cos(twoD - lprime - twoF);
|
||||
longitudeOfPerigeeSecPart +=
|
||||
-55609 * Math.sin(twoD - l) -
|
||||
34711 * Math.sin(twoD - twol) -
|
||||
9792 * Math.sin(l) +
|
||||
9385 * Math.sin(fourD - threel) +
|
||||
7505 * Math.sin(fourD - twol) +
|
||||
5318 * Math.sin(twoD + l) +
|
||||
3484 * Math.sin(fourD - fourl) -
|
||||
3417 * Math.sin(twoD - lprime - l) -
|
||||
2530 * Math.sin(sixD - fourl) -
|
||||
2376 * Math.sin(twoD) -
|
||||
2075 * Math.sin(twoD - threel) -
|
||||
1883 * Math.sin(twol) -
|
||||
1736 * Math.sin(sixD - 5.0 * l) +
|
||||
1626 * Math.sin(lprime) -
|
||||
1370 * Math.sin(sixD - threel);
|
||||
longitudeOfNodeSecPart +=
|
||||
-5392 * Math.sin(twoD - twoF) -
|
||||
540 * Math.sin(lprime) -
|
||||
441 * Math.sin(twoD) +
|
||||
423 * Math.sin(twoF) -
|
||||
288 * Math.sin(twol - twoF);
|
||||
meanLongitudeSecPart +=
|
||||
-3332.9 * Math.sin(twoD) +
|
||||
1197.4 * Math.sin(twoD - l) -
|
||||
662.5 * Math.sin(lprime) +
|
||||
396.3 * Math.sin(l) -
|
||||
218.0 * Math.sin(twoD - lprime);
|
||||
|
||||
// Add terms from Table 5
|
||||
const twoPsi = 2.0 * psi;
|
||||
const threePsi = 3.0 * psi;
|
||||
inclinationSecPart +=
|
||||
46.997 * Math.cos(psi) * t -
|
||||
0.614 * Math.cos(twoD - twoF + psi) * t +
|
||||
0.614 * Math.cos(twoD - twoF - psi) * t -
|
||||
0.0297 * Math.cos(twoPsi) * t2 -
|
||||
0.0335 * Math.cos(psi) * t2 +
|
||||
0.0012 * Math.cos(twoD - twoF + twoPsi) * t2 -
|
||||
0.00016 * Math.cos(psi) * t3 +
|
||||
0.00004 * Math.cos(threePsi) * t3 +
|
||||
0.00004 * Math.cos(twoPsi) * t3;
|
||||
const perigeeAndMean =
|
||||
2.116 * Math.sin(psi) * t -
|
||||
0.111 * Math.sin(twoD - twoF - psi) * t -
|
||||
0.0015 * Math.sin(psi) * t2;
|
||||
longitudeOfPerigeeSecPart += perigeeAndMean;
|
||||
meanLongitudeSecPart += perigeeAndMean;
|
||||
longitudeOfNodeSecPart +=
|
||||
-520.77 * Math.sin(psi) * t +
|
||||
13.66 * Math.sin(twoD - twoF + psi) * t +
|
||||
1.12 * Math.sin(twoD - psi) * t -
|
||||
1.06 * Math.sin(twoF - psi) * t +
|
||||
0.66 * Math.sin(twoPsi) * t2 +
|
||||
0.371 * Math.sin(psi) * t2 -
|
||||
0.035 * Math.sin(twoD - twoF + twoPsi) * t2 -
|
||||
0.015 * Math.sin(twoD - twoF + psi) * t2 +
|
||||
0.0014 * Math.sin(psi) * t3 -
|
||||
0.0011 * Math.sin(threePsi) * t3 -
|
||||
0.0009 * Math.sin(twoPsi) * t3;
|
||||
|
||||
// Add constants and convert units
|
||||
semimajorAxis *= MetersPerKilometer;
|
||||
const inclination =
|
||||
inclinationConstant + inclinationSecPart * RadiansPerArcSecond;
|
||||
const longitudeOfPerigee =
|
||||
longitudeOfPerigeeConstant +
|
||||
longitudeOfPerigeeSecPart * RadiansPerArcSecond;
|
||||
const meanLongitude =
|
||||
meanLongitudeConstant + meanLongitudeSecPart * RadiansPerArcSecond;
|
||||
const longitudeOfNode =
|
||||
longitudeOfNodeConstant + longitudeOfNodeSecPart * RadiansPerArcSecond;
|
||||
|
||||
return elementsToCartesian(
|
||||
semimajorAxis,
|
||||
eccentricity,
|
||||
inclination,
|
||||
longitudeOfPerigee,
|
||||
longitudeOfNode,
|
||||
meanLongitude,
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
// Gets a point describing the motion of the Earth. This point uses the Moon point and
|
||||
// the 1992 mu value (ratio between Moon and Earth masses) in Table 2 of the paper in order
|
||||
// to determine the position of the Earth relative to the Earth-Moon barycenter.
|
||||
const moonEarthMassRatio = 0.012300034; // From 1992 mu value in Table 2
|
||||
const factor = (moonEarthMassRatio / (moonEarthMassRatio + 1.0)) * -1;
|
||||
function computeSimonEarth(date, result) {
|
||||
result = computeSimonMoon(date, result);
|
||||
return Cartesian3.multiplyByScalar(result, factor, result);
|
||||
}
|
||||
|
||||
// Values for the <code>axesTransformation</code> needed for the rotation were found using the STK Components
|
||||
// GeographicTransformer on the position of the sun center of mass point and the earth J2000 frame.
|
||||
|
||||
const axesTransformation = rowMajor(
|
||||
1.0000000000000002,
|
||||
5.619723173785822e-16,
|
||||
4.690511510146299e-19,
|
||||
-5.154129427414611e-16,
|
||||
0.9174820620691819,
|
||||
-0.39777715593191376,
|
||||
-2.23970096136568e-16,
|
||||
0.39777715593191376,
|
||||
0.9174820620691819,
|
||||
);
|
||||
let translation = Cartesian3.create();
|
||||
|
||||
/**
|
||||
* Computes the position of the Sun in the Earth-centered inertial frame
|
||||
*
|
||||
* @param {JulianDate} [julianDate] The time at which to compute the Sun's position, if not provided the current system time is used.
|
||||
* @param {Cartesian3} [result] The object onto which to store the result.
|
||||
* @returns {Cartesian3} Calculated sun position
|
||||
*/
|
||||
export function computeSunPositionInEarthInertialFrame(julianDate, result) {
|
||||
if (result == null) result = Cartesian3.create();
|
||||
|
||||
//first forward transformation
|
||||
translation = computeSimonEarthMoonBarycenter(julianDate, translation);
|
||||
result = Cartesian3.negate(translation, result);
|
||||
|
||||
//second forward transformation
|
||||
computeSimonEarth(julianDate, translation);
|
||||
|
||||
Cartesian3.subtract(result, translation, result);
|
||||
Matrix3.multiplyByVector(axesTransformation, result, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the position of the Moon in the Earth-centered inertial frame
|
||||
*
|
||||
* @param {JulianDate} [julianDate] The time at which to compute the Moon's position, if not provided the current system time is used.
|
||||
* @param {Cartesian3} [result] The object onto which to store the result.
|
||||
* @returns {Cartesian3} Calculated moon position
|
||||
*/
|
||||
export function computeMoonPositionInEarthInertialFrame(julianDate, result) {
|
||||
result = computeSimonMoon(julianDate, result);
|
||||
Matrix3.multiplyByVector(axesTransformation, result, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Bit-for-bit comparison of the Sun and Moon positions, the TAI dates they
|
||||
// take and the TEME rotation with Cesium's, over dates from 1960 to 2100
|
||||
// (leap second edges included).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from './index.js';
|
||||
|
||||
const same = (a, b, label) =>
|
||||
assert.ok(Object.is(a, b), `${label}: ${a} !== ${b}`);
|
||||
const dates = [
|
||||
Date.UTC(1960, 0, 1),
|
||||
Date.UTC(1972, 0, 1),
|
||||
Date.UTC(1972, 0, 1) - 1,
|
||||
Date.UTC(2016, 11, 31, 23, 59, 59, 999),
|
||||
Date.UTC(2017, 0, 1),
|
||||
Date.UTC(2026, 10, 4, 12, 30, 15, 250),
|
||||
Date.UTC(2100, 5, 30, 11, 59, 59, 500),
|
||||
];
|
||||
for (let i = 0; i < 400; i++)
|
||||
dates.push(Date.UTC(1990, 0, 1) + Math.floor(i * 7919.123 * 3600 * 1000));
|
||||
|
||||
test('dates from JavaScript Dates match Cesium JulianDate.fromDate', () => {
|
||||
for (const ms of dates) {
|
||||
const ours = geo.julianDate.fromDate(new Date(ms));
|
||||
const theirs = Cesium.JulianDate.fromDate(new Date(ms));
|
||||
same(ours.dayNumber, theirs.dayNumber, `${ms} day`);
|
||||
same(ours.secondsOfDay, theirs.secondsOfDay, `${ms} seconds`);
|
||||
same(
|
||||
geo.julianDate.computeTaiMinusUtc(ours),
|
||||
Cesium.JulianDate.computeTaiMinusUtc(theirs),
|
||||
`${ms} TAI - UTC`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Sun and Moon inertial positions and the TEME rotation match Cesium bit for bit', () => {
|
||||
const Simon = Cesium.Simon1994PlanetaryPositions;
|
||||
for (const ms of dates) {
|
||||
const ours = geo.julianDate.fromDate(new Date(ms));
|
||||
const theirs = Cesium.JulianDate.fromDate(new Date(ms));
|
||||
for (const [name, mine, cesium] of [
|
||||
[
|
||||
'sun',
|
||||
geo.simon1994.computeSunPositionInEarthInertialFrame(ours),
|
||||
Simon.computeSunPositionInEarthInertialFrame(theirs),
|
||||
],
|
||||
[
|
||||
'moon',
|
||||
geo.simon1994.computeMoonPositionInEarthInertialFrame(ours),
|
||||
Simon.computeMoonPositionInEarthInertialFrame(theirs),
|
||||
],
|
||||
])
|
||||
for (const axis of ['x', 'y', 'z'])
|
||||
same(mine[axis], cesium[axis], `${ms} ${name}.${axis}`);
|
||||
const teme = geo.transforms.computeTemeToPseudoFixedMatrix(ours);
|
||||
const cesiumTeme = Cesium.Transforms.computeTemeToPseudoFixedMatrix(theirs);
|
||||
for (let k = 0; k < 9; k++)
|
||||
same(teme[k], cesiumTeme[k], `${ms} teme[${k}]`);
|
||||
}
|
||||
});
|
||||
+60
-1
@@ -9,7 +9,12 @@
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
import { DeveloperError } from './developerError.js';
|
||||
import { EPSILON14, equalsEpsilon as scalarEqualsEpsilon } from './math.js';
|
||||
import {
|
||||
EPSILON14,
|
||||
TWO_PI,
|
||||
equalsEpsilon as scalarEqualsEpsilon,
|
||||
} from './math.js';
|
||||
import * as JulianDate from './julianDate.js';
|
||||
import {
|
||||
create as createCartesian3,
|
||||
cross,
|
||||
@@ -238,3 +243,57 @@ export function headingPitchRollToFixedFrame(
|
||||
result = fixedFrameTransform(origin, ellipsoid, result);
|
||||
return multiply(result, hprMatrix, result);
|
||||
}
|
||||
|
||||
const gmstConstant0 = 6 * 3600 + 41 * 60 + 50.54841;
|
||||
const gmstConstant1 = 8640184.812866;
|
||||
const gmstConstant2 = 0.093104;
|
||||
const gmstConstant3 = -6.2e-6;
|
||||
const rateCoef = 1.1772758384668e-19;
|
||||
const wgs84WRPrecessing = 7.2921158553e-5;
|
||||
const twoPiOverSecondsInDay = TWO_PI / 86400.0;
|
||||
let dateInUtc = { dayNumber: 0, secondsOfDay: 0 };
|
||||
|
||||
/**
|
||||
* The rotation from the True Equator Mean Equinox (TEME) axes to the
|
||||
* pseudo-fixed axes at a geo/julianDate.js date: Cesium's
|
||||
* computeTemeToPseudoFixedMatrix, which takes UTC for UT1.
|
||||
*/
|
||||
export function computeTemeToPseudoFixedMatrix(date, result) {
|
||||
dateInUtc = JulianDate.addSeconds(
|
||||
date,
|
||||
-JulianDate.computeTaiMinusUtc(date),
|
||||
dateInUtc,
|
||||
);
|
||||
const utcDayNumber = dateInUtc.dayNumber;
|
||||
const utcSecondsIntoDay = dateInUtc.secondsOfDay;
|
||||
|
||||
let t;
|
||||
const diffDays = utcDayNumber - 2451545;
|
||||
if (utcSecondsIntoDay >= 43200.0) {
|
||||
t = (diffDays + 0.5) / 36525.0;
|
||||
} else {
|
||||
t = (diffDays - 0.5) / 36525.0;
|
||||
}
|
||||
|
||||
const gmst0 =
|
||||
gmstConstant0 +
|
||||
t * (gmstConstant1 + t * (gmstConstant2 + t * gmstConstant3));
|
||||
const angle = (gmst0 * twoPiOverSecondsInDay) % TWO_PI;
|
||||
const ratio = wgs84WRPrecessing + rateCoef * (utcDayNumber - 2451545.5);
|
||||
const secondsSinceMidnight = (utcSecondsIntoDay + 86400.0 * 0.5) % 86400.0;
|
||||
const gha = angle + ratio * secondsSinceMidnight;
|
||||
const cosGha = Math.cos(gha);
|
||||
const sinGha = Math.sin(gha);
|
||||
|
||||
if (result == null) result = new Array(9);
|
||||
result[0] = cosGha;
|
||||
result[1] = -sinGha;
|
||||
result[2] = 0.0;
|
||||
result[3] = sinGha;
|
||||
result[4] = cosGha;
|
||||
result[5] = 0.0;
|
||||
result[6] = 0.0;
|
||||
result[7] = 0.0;
|
||||
result[8] = 1.0;
|
||||
return result;
|
||||
}
|
||||
@@ -9,11 +9,11 @@ import { toCesium2, toCesium3, toGeo3 } from './convert.js';
|
||||
import { createCesiumFollow } from './follow.js';
|
||||
|
||||
const { math } = geo;
|
||||
const EASINGS = {
|
||||
const easings = () => ({
|
||||
cubicInOut: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||
sinusoidalInOut: Cesium.EasingFunction.SINUSOIDAL_IN_OUT,
|
||||
linear: Cesium.EasingFunction.LINEAR_NONE,
|
||||
};
|
||||
});
|
||||
|
||||
// Cesium reports heading and roll in [0, 2π]; the contract wants [0, 2π).
|
||||
function wrapAngle(angle) {
|
||||
@@ -43,7 +43,7 @@ const cesiumDestination = (destination) =>
|
||||
)
|
||||
: toCesium3(destination);
|
||||
|
||||
const cesiumEasing = (easing) => (easing ? EASINGS[easing] : undefined);
|
||||
const cesiumEasing = (easing) => (easing ? easings()[easing] : undefined);
|
||||
|
||||
// A HeadingPitchRange or a Cartesian offset, as Cesium's lookAt takes them.
|
||||
const cesiumOffset = (offset) =>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { bridgeEvent } from './events.js';
|
||||
|
||||
const scratchGregorian = new Cesium.GregorianDate();
|
||||
let scratchGregorian;
|
||||
|
||||
// JulianDate.toDate(time).getTime() without the Date: frame events must not
|
||||
// allocate. A raise without a time (not Cesium's own) reads the clock.
|
||||
@@ -27,6 +27,7 @@ function unixMilliseconds(scene, time) {
|
||||
* @returns {{ frames: object, destroy: () => void }}
|
||||
*/
|
||||
export function createCesiumFrames(viewer) {
|
||||
scratchGregorian ??= new Cesium.GregorianDate();
|
||||
const { scene } = viewer;
|
||||
const bridges = ['preUpdate', 'preRender', 'postRender'].map((name) => [
|
||||
name,
|
||||
|
||||
+13
-13
@@ -6,19 +6,6 @@ import { normalizeInputOptions } from '../contract.js';
|
||||
import { createPickDelivery } from '../pickDelivery.js';
|
||||
import { toGeo2 } from './convert.js';
|
||||
|
||||
const TYPES = {
|
||||
click: Cesium.ScreenSpaceEventType.LEFT_CLICK,
|
||||
doubleClick: Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK,
|
||||
move: Cesium.ScreenSpaceEventType.MOUSE_MOVE,
|
||||
down: Cesium.ScreenSpaceEventType.LEFT_DOWN,
|
||||
up: Cesium.ScreenSpaceEventType.LEFT_UP,
|
||||
};
|
||||
const KEYS = {
|
||||
ctrl: Cesium.KeyboardEventModifier.CTRL,
|
||||
shift: Cesium.KeyboardEventModifier.SHIFT,
|
||||
alt: Cesium.KeyboardEventModifier.ALT,
|
||||
};
|
||||
|
||||
// Cesium reuses its event objects; listeners get geo copies.
|
||||
const toEvent = (event) => {
|
||||
const result = {};
|
||||
@@ -44,6 +31,19 @@ export function createCesiumInput(
|
||||
scheduleFrame = (run) => requestAnimationFrame(run),
|
||||
} = {},
|
||||
) {
|
||||
const TYPES = {
|
||||
click: Cesium.ScreenSpaceEventType.LEFT_CLICK,
|
||||
doubleClick: Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK,
|
||||
move: Cesium.ScreenSpaceEventType.MOUSE_MOVE,
|
||||
down: Cesium.ScreenSpaceEventType.LEFT_DOWN,
|
||||
up: Cesium.ScreenSpaceEventType.LEFT_UP,
|
||||
};
|
||||
const KEYS = {
|
||||
ctrl: Cesium.KeyboardEventModifier.CTRL,
|
||||
shift: Cesium.KeyboardEventModifier.SHIFT,
|
||||
alt: Cesium.KeyboardEventModifier.ALT,
|
||||
};
|
||||
|
||||
const { scene } = viewer;
|
||||
let handler = null;
|
||||
let destroyed = false;
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { createMoverRegistry } from '../movers.js';
|
||||
|
||||
const scratchPosition = new Cesium.Cartesian3();
|
||||
const scratchColor = new Cesium.Color();
|
||||
const scratchQuaternion = new Cesium.Quaternion();
|
||||
const scratchRotation = new Cesium.Matrix3();
|
||||
let scratchPosition;
|
||||
let scratchColor;
|
||||
let scratchQuaternion;
|
||||
let scratchRotation;
|
||||
// Model.modelMatrix and Model.color copy what they are given.
|
||||
const scratchModelMatrix = new Cesium.Matrix4();
|
||||
let scratchModelMatrix;
|
||||
|
||||
/** The atlas cells as canvases, by key (a DOM canvas or image source). */
|
||||
async function cropAtlasCells(atlas, keys) {
|
||||
@@ -287,6 +287,11 @@ export function createCesiumMovers(
|
||||
viewer,
|
||||
{ requestRender = () => viewer.scene.requestRender(), factories = {} } = {},
|
||||
) {
|
||||
scratchPosition ??= new Cesium.Cartesian3();
|
||||
scratchColor ??= new Cesium.Color();
|
||||
scratchQuaternion ??= new Cesium.Quaternion();
|
||||
scratchRotation ??= new Cesium.Matrix3();
|
||||
scratchModelMatrix ??= new Cesium.Matrix4();
|
||||
const make = { ...DEFAULT_FACTORIES, ...factories };
|
||||
const { scene } = viewer;
|
||||
let root = null;
|
||||
|
||||
@@ -43,7 +43,9 @@ test('passive Contacts releases its installation anchor and subject refresh cann
|
||||
const subject = createSubject({ state, services, parts });
|
||||
subject.refreshSelectedSubject();
|
||||
assert.equal(anchors.length, 1);
|
||||
assert.ok(anchors[0] instanceof Cesium.Cartesian3);
|
||||
// A copy of the subject's position, not the live one.
|
||||
assert.notEqual(anchors[0], position);
|
||||
assert.deepEqual({ ...anchors[0] }, { ...position });
|
||||
controls.setParams({ passive: true });
|
||||
assert.equal(anchors.at(-1), null);
|
||||
const released = anchors.length;
|
||||
@@ -57,5 +59,5 @@ test('passive Contacts releases its installation anchor and subject refresh cann
|
||||
controls.setParams({ passive: false });
|
||||
assert.equal(activations, 1);
|
||||
subject.refreshSelectedSubject();
|
||||
assert.ok(anchors.at(-1) instanceof Cesium.Cartesian3);
|
||||
assert.deepEqual({ ...anchors.at(-1) }, { ...position });
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
AWARENESS_RADIUS_M,
|
||||
AWARENESS_RELATIONSHIP,
|
||||
} from '../../data/militaryAwarenessEngine.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import {
|
||||
@@ -265,7 +264,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
layerState.display?.trackedEntity?.gevDisplayPosition?.();
|
||||
if (trackedPosition) {
|
||||
return {
|
||||
position: Cesium.Cartesian3.clone(trackedPosition),
|
||||
position: geo.cartesian3.clone(trackedPosition),
|
||||
// Only the follow camera's own contact proves presence this way; a
|
||||
// different tracked entity says nothing about this subject.
|
||||
presence:
|
||||
@@ -280,7 +279,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
// awareness position instead of allocating/scanning up to 1,000 contacts.
|
||||
if (!allowCollectionMaterialization) {
|
||||
return {
|
||||
position: Cesium.Cartesian3.clone(subject.position),
|
||||
position: geo.cartesian3.clone(subject.position),
|
||||
presence: SUBJECT_PRESENCE.UNCHECKED,
|
||||
};
|
||||
}
|
||||
@@ -295,7 +294,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
if (subject.layerId === 'ais-live-vessels') {
|
||||
if (!allowCollectionMaterialization) {
|
||||
return {
|
||||
position: Cesium.Cartesian3.clone(subject.position),
|
||||
position: geo.cartesian3.clone(subject.position),
|
||||
presence: SUBJECT_PRESENCE.UNCHECKED,
|
||||
};
|
||||
}
|
||||
@@ -308,7 +307,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
// Mapped installations come from static geometry, not a live feed: there is
|
||||
// nothing to be culled from.
|
||||
return {
|
||||
position: Cesium.Cartesian3.clone(subject.position),
|
||||
position: geo.cartesian3.clone(subject.position),
|
||||
presence: SUBJECT_PRESENCE.LIVE,
|
||||
};
|
||||
}
|
||||
@@ -343,8 +342,8 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
: SUBJECT_PRESENCE.UNCHECKED;
|
||||
return {
|
||||
position: current?.position
|
||||
? Cesium.Cartesian3.clone(current.position)
|
||||
: Cesium.Cartesian3.clone(subject.position),
|
||||
? geo.cartesian3.clone(current.position)
|
||||
: geo.cartesian3.clone(subject.position),
|
||||
presence,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export function createCyclonesLayer({
|
||||
feed,
|
||||
hitTestOverlay = hitTestWorldOverlay,
|
||||
overlayHost,
|
||||
cesium = Cesium,
|
||||
cesium = null,
|
||||
createRendering = createCycloneRendering,
|
||||
createNativeRendering = null,
|
||||
matchMedia = globalThis.matchMedia?.bind(globalThis),
|
||||
@@ -99,24 +99,27 @@ export function createCyclonesLayer({
|
||||
selectedId = id;
|
||||
rendering?.setSelection(id);
|
||||
}
|
||||
// The SDK, on Cesium only: the production build's Cesium global loads with
|
||||
// the first Cesium globe, so a Navara page has none.
|
||||
const sdk = () => cesium ?? (engine?.kind === 'navara' ? null : Cesium);
|
||||
// Photorealistic 3D Tiles pick as tileset content without an entity id;
|
||||
// that is empty map, the same as no pick at all on the globe.
|
||||
const isSurfacePick = (picked) =>
|
||||
!picked ||
|
||||
(picked.id === undefined &&
|
||||
(picked.content !== undefined ||
|
||||
(typeof cesium.Cesium3DTileset === 'function' &&
|
||||
picked.primitive instanceof cesium.Cesium3DTileset)));
|
||||
(typeof sdk()?.Cesium3DTileset === 'function' &&
|
||||
picked.primitive instanceof sdk().Cesium3DTileset)));
|
||||
function installSelection() {
|
||||
const canvas = engine?.host.canvas ?? viewer?.scene?.canvas;
|
||||
const native = engine?.kind === 'navara';
|
||||
if (
|
||||
clickHandler ||
|
||||
!canvas ||
|
||||
(!native && typeof cesium.ScreenSpaceEventHandler !== 'function')
|
||||
(!native && typeof sdk()?.ScreenSpaceEventHandler !== 'function')
|
||||
)
|
||||
return;
|
||||
const owner = native ? {} : new cesium.ScreenSpaceEventHandler(canvas);
|
||||
const owner = native ? {} : new (sdk().ScreenSpaceEventHandler)(canvas);
|
||||
clickHandler = owner;
|
||||
let capturedHit = null;
|
||||
const overlayHit = (x, y) => {
|
||||
@@ -192,7 +195,7 @@ export function createCyclonesLayer({
|
||||
};
|
||||
if (native)
|
||||
owner.destroy = engine.input.on('click', onClick, { pick: true });
|
||||
else owner.setInputAction(onClick, cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
else owner.setInputAction(onClick, sdk().ScreenSpaceEventType.LEFT_CLICK);
|
||||
}
|
||||
function removeSelection() {
|
||||
const owner = clickHandler;
|
||||
@@ -217,7 +220,7 @@ export function createCyclonesLayer({
|
||||
rendering =
|
||||
engine?.kind === 'navara'
|
||||
? createNativeRendering(engine, { overlayHost })
|
||||
: createRendering({ viewer, cesium, overlayHost });
|
||||
: createRendering({ viewer, cesium: sdk(), overlayHost });
|
||||
if (!enabled) return;
|
||||
installSelection();
|
||||
if (snapshot && !snapshot.unavailable) {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { WHITE, rgbaFromCss } from '../shared/color.js';
|
||||
import { roadLabel } from '../../data/routeSteps.js';
|
||||
import {
|
||||
t,
|
||||
@@ -68,10 +70,12 @@ export function presentRoutingFailure(error) {
|
||||
|
||||
/** Route colour — the annotation palette's cyan, so voice routes and Directions match. */
|
||||
export const DIRECTIONS_ROUTE_COLOR = '#39d0ff';
|
||||
const MARKER_A_COLOR = Cesium.Color.fromCssColorString('#5dff9f');
|
||||
const MARKER_B_COLOR = Cesium.Color.fromCssColorString('#ff6b6b');
|
||||
const STEP_COLOR = Cesium.Color.WHITE.withAlpha(0.95);
|
||||
const STEP_OUTLINE = Cesium.Color.fromCssColorString(DIRECTIONS_ROUTE_COLOR);
|
||||
// Engine-neutral colours (src/layers/shared/color.js): a Navara page has no
|
||||
// Cesium SDK.
|
||||
const MARKER_A_COLOR = () => rgbaFromCss('#5dff9f');
|
||||
const MARKER_B_COLOR = () => rgbaFromCss('#ff6b6b');
|
||||
const STEP_COLOR = () => WHITE.withAlpha(0.95);
|
||||
const STEP_OUTLINE = () => rgbaFromCss(DIRECTIONS_ROUTE_COLOR);
|
||||
const STEP_PIXEL_SIZE = 8;
|
||||
const STEP_SELECTED_PIXEL_SIZE = 13;
|
||||
/** Route request timeout (ms) — the proxy itself gives OSRM 12 s. */
|
||||
@@ -978,11 +982,11 @@ export function createDirectionsLayer({
|
||||
? _engine.picker.pickGlobe(screenPosition)
|
||||
: services.scenePick.pickGroundPosition(_viewer, screenPosition);
|
||||
if (!cartesian) return null;
|
||||
const carto = Cesium.Cartographic.fromCartesian(cartesian);
|
||||
const carto = geo.cartographic.fromCartesian(cartesian);
|
||||
if (!carto) return null;
|
||||
return {
|
||||
lat: Cesium.Math.toDegrees(carto.latitude),
|
||||
lon: Cesium.Math.toDegrees(carto.longitude),
|
||||
lat: geo.math.toDegrees(carto.latitude),
|
||||
lon: geo.math.toDegrees(carto.longitude),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1026,10 +1030,10 @@ export function createDirectionsLayer({
|
||||
if (!_viewer && !_native) return;
|
||||
if (letter === 'a') {
|
||||
removeEntity(_markerA);
|
||||
_markerA = markerEntity('A', point, MARKER_A_COLOR);
|
||||
_markerA = markerEntity('A', point, MARKER_A_COLOR());
|
||||
} else {
|
||||
removeEntity(_markerB);
|
||||
_markerB = markerEntity('B', point, MARKER_B_COLOR);
|
||||
_markerB = markerEntity('B', point, MARKER_B_COLOR());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1136,9 +1140,9 @@ export function createDirectionsLayer({
|
||||
_stepPoints.add({
|
||||
id: `directions:step:${index}`,
|
||||
position: Cesium.Cartesian3.fromDegrees(step.lon, step.lat, 0),
|
||||
color: STEP_COLOR,
|
||||
color: STEP_COLOR(),
|
||||
pixelSize: STEP_PIXEL_SIZE,
|
||||
outlineColor: STEP_OUTLINE,
|
||||
outlineColor: STEP_OUTLINE(),
|
||||
outlineWidth: 2,
|
||||
show: false,
|
||||
// The shared ground floor is a coarse cell, and on the photoreal stack
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
horizonOccluder,
|
||||
} from '../../../data/render/cesiumProjection.js';
|
||||
|
||||
export const FIRMS_LEFT_CLICK = Cesium.ScreenSpaceEventType.LEFT_CLICK;
|
||||
// Cesium ScreenSpaceEventType.LEFT_CLICK; available before the SDK loads.
|
||||
export const FIRMS_LEFT_CLICK = 2;
|
||||
export const projectFirmsWindow = worldToWindowCoordinates;
|
||||
export const firmsHorizonOccluder = horizonOccluder;
|
||||
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { GNSS_LEVEL_COLORS } from '../records.js';
|
||||
|
||||
/**
|
||||
* GroundPrimitive allowPicking:false suppresses ids but still submits stencil
|
||||
* commands in Cesium's pick pass, erasing picks of overlapping ground geometry.
|
||||
* This fill participates only in rendering, so sibling polygons remain pickable.
|
||||
*/
|
||||
class GnssGroundFill extends Cesium.GroundPrimitive {
|
||||
update(frameState) {
|
||||
if (!frameState.passes.pick) super.update(frameState);
|
||||
}
|
||||
}
|
||||
|
||||
/** Degrees at the centre of the current view, or null before the camera settles. */
|
||||
export function defaultGnssViewAnchor(viewer) {
|
||||
const canvas = viewer?.scene?.canvas;
|
||||
@@ -44,6 +33,14 @@ export function createGnssCesiumRenderer({
|
||||
viewer,
|
||||
viewAnchor = defaultGnssViewAnchor,
|
||||
}) {
|
||||
// GroundPrimitive allowPicking:false still submits stencil commands in
|
||||
// the pick pass. Keep this fill from erasing overlapping geometry's picks.
|
||||
class GnssGroundFill extends Cesium.GroundPrimitive {
|
||||
update(frameState) {
|
||||
if (!frameState.passes.pick) super.update(frameState);
|
||||
}
|
||||
}
|
||||
|
||||
const primitives = new Cesium.PrimitiveCollection();
|
||||
primitives.id = 'gnss-integrity';
|
||||
primitives.show = false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { whenCesiumSdk } from '../../app/loadCesium.js';
|
||||
import { createState } from './state.js';
|
||||
import { createMarker } from './marker.js';
|
||||
import { createCameraFraming } from './cameraFraming.js';
|
||||
@@ -549,15 +550,19 @@ export function createStreetLevelLayer({
|
||||
mapStack = null;
|
||||
state.viewer = null;
|
||||
},
|
||||
enable(viewer) {
|
||||
state.enabled = true;
|
||||
if (viewer) state.viewer = viewer;
|
||||
parts.marker.setVisible(true);
|
||||
parts.selection.install(state.viewer);
|
||||
for (const entry of activeEntries()) activate(entry);
|
||||
watchCamera(state.viewer);
|
||||
remeasureGround();
|
||||
notify();
|
||||
enable(viewer, { signal } = {}) {
|
||||
// The marker, cones, coverage and ground picks use Cesium's math and
|
||||
// primitives on either globe: a Navara page loads the Cesium SDK first.
|
||||
return whenCesiumSdk(() => {
|
||||
state.enabled = true;
|
||||
if (viewer) state.viewer = viewer;
|
||||
parts.marker.setVisible(true);
|
||||
parts.selection.install(state.viewer);
|
||||
for (const entry of activeEntries()) activate(entry);
|
||||
watchCamera(state.viewer);
|
||||
remeasureGround();
|
||||
notify();
|
||||
}, signal);
|
||||
},
|
||||
|
||||
disable() {
|
||||
|
||||
@@ -227,11 +227,7 @@ export function viewFocus(
|
||||
* (NYC −22 m), and an occluder at 0 m would hide the cones around it.
|
||||
*/
|
||||
const HORIZON_CULL_DEPTH_M = 1000;
|
||||
const HORIZON_CULL_ELLIPSOID = new Cesium.Ellipsoid(
|
||||
Cesium.Ellipsoid.WGS84.radii.x - HORIZON_CULL_DEPTH_M,
|
||||
Cesium.Ellipsoid.WGS84.radii.y - HORIZON_CULL_DEPTH_M,
|
||||
Cesium.Ellipsoid.WGS84.radii.z - HORIZON_CULL_DEPTH_M,
|
||||
);
|
||||
let HORIZON_CULL_ELLIPSOID;
|
||||
|
||||
/**
|
||||
* Hide billboards and points behind the horizon: they skip the depth test, so
|
||||
@@ -240,12 +236,18 @@ const HORIZON_CULL_ELLIPSOID = new Cesium.Ellipsoid(
|
||||
* @param {{getViewer: () => object|null, items: () => Iterable<{position: object, show: boolean}>, onChange?: () => void}} options
|
||||
*/
|
||||
export function createHorizonCull({ getViewer, items, onChange }) {
|
||||
const occluder = new Cesium.EllipsoidalOccluder(HORIZON_CULL_ELLIPSOID);
|
||||
let occluder;
|
||||
let from = null;
|
||||
let stopListening = null;
|
||||
|
||||
/** Show what is in front of the horizon, hide the rest; the count seen. */
|
||||
function cull(list, position) {
|
||||
HORIZON_CULL_ELLIPSOID ??= new Cesium.Ellipsoid(
|
||||
Cesium.Ellipsoid.WGS84.radii.x - HORIZON_CULL_DEPTH_M,
|
||||
Cesium.Ellipsoid.WGS84.radii.y - HORIZON_CULL_DEPTH_M,
|
||||
Cesium.Ellipsoid.WGS84.radii.z - HORIZON_CULL_DEPTH_M,
|
||||
);
|
||||
occluder ??= new Cesium.EllipsoidalOccluder(HORIZON_CULL_ELLIPSOID);
|
||||
occluder.cameraPosition = position;
|
||||
let seen = 0;
|
||||
let changed = false;
|
||||
|
||||
@@ -86,11 +86,15 @@ export const CABLE_GLOBE_STACK_IDS = Object.freeze(
|
||||
export const MARKER_COLLECTION_EXPECTATIONS = Object.freeze([
|
||||
Object.freeze({
|
||||
key: '_billboardCollection',
|
||||
type: Cesium.BillboardCollection,
|
||||
get type() {
|
||||
return Cesium.BillboardCollection;
|
||||
},
|
||||
}),
|
||||
Object.freeze({
|
||||
key: '_pointCollection',
|
||||
type: Cesium.PointPrimitiveCollection,
|
||||
get type() {
|
||||
return Cesium.PointPrimitiveCollection;
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export function createReticleFootprint() {
|
||||
const pixels = Array.from({ length: 25 }, () => geo.cartesian2.create());
|
||||
const hits = Array.from({ length: 25 }, () => geo.cartesian3.create());
|
||||
const carto = geo.cartographic.create();
|
||||
const ray = new Cesium.Ray();
|
||||
let ray;
|
||||
return (viewer, altitude, engine) => {
|
||||
const camera = engine?.camera ?? viewer?.camera;
|
||||
if (!camera?.getPickRay && !camera?.pickRay) return null;
|
||||
@@ -91,7 +91,9 @@ export function createReticleFootprint() {
|
||||
p = pixels[i];
|
||||
p.x = left + (side * col) / 4;
|
||||
p.y = top + (side * row) / 4;
|
||||
const hit = camera.pickEllipsoid(p, Cesium.Ellipsoid.WGS84, hits[i]);
|
||||
const hit = engine
|
||||
? camera.pickEllipsoid(p)
|
||||
: camera.pickEllipsoid(p, Cesium.Ellipsoid.WGS84, hits[i]);
|
||||
let dx, dy;
|
||||
if (hit) {
|
||||
geo.cartographic.fromCartesian(hit, geo.ellipsoid.WGS84, carto);
|
||||
@@ -102,7 +104,7 @@ export function createReticleFootprint() {
|
||||
} else {
|
||||
const picked = engine
|
||||
? camera.pickRay(p)
|
||||
: camera.getPickRay(p, ray) || ray;
|
||||
: camera.getPickRay(p, (ray ??= new Cesium.Ray())) || ray;
|
||||
if (!picked) continue;
|
||||
const d = picked.direction;
|
||||
dx =
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
claimCameraSensitivity,
|
||||
releaseCameraSensitivity,
|
||||
} from '../../data/cameraSensitivity.js';
|
||||
import { whenCesiumSdk } from '../../app/loadCesium.js';
|
||||
|
||||
export function createLifecycle({
|
||||
state: layerState,
|
||||
@@ -245,36 +246,40 @@ export function createLifecycle({
|
||||
*
|
||||
* @param {Cesium.Viewer} viewer - The Cesium viewer instance.
|
||||
*/
|
||||
enable(viewer) {
|
||||
if (layerState._enabled) return;
|
||||
if (viewer) layerState._viewer = viewer;
|
||||
layerState._enabled = true;
|
||||
if (layerState._roadMode !== 'tomtom') source.prefetch?.();
|
||||
bindScene();
|
||||
// Enabling is explicit intent, not a camera gesture that needs settling.
|
||||
parts.viewport.onCameraChanged({ immediate: true });
|
||||
enable(viewer, { signal } = {}) {
|
||||
// The simulation's math is Cesium's on either globe: a Navara page
|
||||
// loads the Cesium SDK first.
|
||||
return whenCesiumSdk(() => {
|
||||
if (layerState._enabled) return;
|
||||
if (viewer) layerState._viewer = viewer;
|
||||
layerState._enabled = true;
|
||||
if (layerState._roadMode !== 'tomtom') source.prefetch?.();
|
||||
bindScene();
|
||||
// Enabling is explicit intent, not a camera gesture that needs settling.
|
||||
parts.viewport.onCameraChanged({ immediate: true });
|
||||
|
||||
// Boot-order guard (field-test round 1: layer sat empty until the user
|
||||
// moved): when the persisted layer state re-enables traffic during the
|
||||
// intro flyTo, the initial check bails at high altitude — and a camera
|
||||
// that then parks never re-fires camera.changed. Retry cheaply until the
|
||||
// first load commits, then self-clear. Also acts as a safety kick if a
|
||||
// failed first fetch left the viewport unloaded while parked.
|
||||
clearInterval(layerState._enableKickTimer);
|
||||
layerState._enableKickTimer = setInterval(() => {
|
||||
if (
|
||||
!layerState._enabled ||
|
||||
layerState._lastUpdate ||
|
||||
layerState._roadRetryStopped ||
|
||||
layerState._retryAttempts >= 3
|
||||
) {
|
||||
clearInterval(layerState._enableKickTimer);
|
||||
layerState._enableKickTimer = null;
|
||||
return;
|
||||
}
|
||||
if (!layerState._fetching && !layerState._retryTimer)
|
||||
parts.viewport.onCameraChanged();
|
||||
}, 1500);
|
||||
// Boot-order guard (field-test round 1: layer sat empty until the user
|
||||
// moved): when the persisted layer state re-enables traffic during the
|
||||
// intro flyTo, the initial check bails at high altitude — and a camera
|
||||
// that then parks never re-fires camera.changed. Retry cheaply until the
|
||||
// first load commits, then self-clear. Also acts as a safety kick if a
|
||||
// failed first fetch left the viewport unloaded while parked.
|
||||
clearInterval(layerState._enableKickTimer);
|
||||
layerState._enableKickTimer = setInterval(() => {
|
||||
if (
|
||||
!layerState._enabled ||
|
||||
layerState._lastUpdate ||
|
||||
layerState._roadRetryStopped ||
|
||||
layerState._retryAttempts >= 3
|
||||
) {
|
||||
clearInterval(layerState._enableKickTimer);
|
||||
layerState._enableKickTimer = null;
|
||||
return;
|
||||
}
|
||||
if (!layerState._fetching && !layerState._retryTimer)
|
||||
parts.viewport.onCameraChanged();
|
||||
}, 1500);
|
||||
}, signal);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { rgbaFromCss } from '../shared/color.js';
|
||||
|
||||
/**
|
||||
* @file Street Traffic selects TomTom, OpenMapTiles or Hybrid motor roads,
|
||||
@@ -112,9 +112,9 @@ export const SIZE_BY_TYPE = {
|
||||
*/
|
||||
|
||||
export const FLOW_BUCKET_COLORS = {
|
||||
free: Cesium.Color.fromCssColorString('#2ecc71').withAlpha(0.9),
|
||||
slow: Cesium.Color.fromCssColorString('#f0b23e').withAlpha(0.9),
|
||||
jam: Cesium.Color.fromCssColorString('#e05252').withAlpha(0.9),
|
||||
free: rgbaFromCss('#2ecc71').withAlpha(0.9),
|
||||
slow: rgbaFromCss('#f0b23e').withAlpha(0.9),
|
||||
jam: rgbaFromCss('#e05252').withAlpha(0.9),
|
||||
};
|
||||
|
||||
// ─── Jam-viz prototype (live mode only — see 2026-07-21 design doc) ────────
|
||||
@@ -140,12 +140,11 @@ export const HEAT_JAM_PULSE_ALPHA = 0.2;
|
||||
|
||||
/** @const {Cesium.Color} Jam corridor color (bucket red, alpha pulsed live). */
|
||||
|
||||
export const HEAT_JAM_COLOR = Cesium.Color.fromCssColorString('#e05252');
|
||||
export const HEAT_JAM_COLOR = rgbaFromCss('#e05252');
|
||||
|
||||
/** @const {Cesium.Color} Slow corridor color (bucket amber, faint + static). */
|
||||
|
||||
export const HEAT_SLOW_COLOR =
|
||||
Cesium.Color.fromCssColorString('#f0b23e').withAlpha(0.2);
|
||||
export const HEAT_SLOW_COLOR = rgbaFromCss('#f0b23e').withAlpha(0.2);
|
||||
|
||||
/** @const {number} Far-distance scale floor for jam dots (shipped: 0.3). */
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ export function roadIdentity(road) {
|
||||
/** Retain one road and its dot population across tile and surface publications. */
|
||||
export function createRetention({ state, parts }) {
|
||||
const records = new Map();
|
||||
const heightStep = new Cesium.Cartesian3();
|
||||
const screenFrom = new Cesium.Cartesian2();
|
||||
const screenTo = new Cesium.Cartesian2();
|
||||
const eye = new Cesium.Cartesian3();
|
||||
const heightStep = { x: 0, y: 0, z: 0 };
|
||||
const screenFrom = { x: 0, y: 0 };
|
||||
const screenTo = { x: 0, y: 0 };
|
||||
const eye = { x: 0, y: 0, z: 0 };
|
||||
function projectSafely(scene, point, screen) {
|
||||
if (scene.project) return Boolean(scene.project(point, screen));
|
||||
if (scene.camera.viewMatrix) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { normalizeRoadMode } from './roadModes.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { FLOW_BUCKET_COLORS } from './policy.js';
|
||||
|
||||
export function createState({ services }) {
|
||||
@@ -294,7 +293,7 @@ export function createState({ services }) {
|
||||
|
||||
/** Reusable scratch Cartesian3 to avoid per-frame allocation / GC pressure */
|
||||
|
||||
layerState._scratchLerp = new Cesium.Cartesian3();
|
||||
layerState._scratchLerp = { x: 0, y: 0, z: 0 };
|
||||
|
||||
// ─── Animation ─────────────────────────────────────────────
|
||||
|
||||
|
||||
+24
-16
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { createWeatherRendering } from './rendering.js';
|
||||
import { imageryHostStatus } from './imageryHost.js';
|
||||
import { weatherDisplay, insideExtent } from './display.js';
|
||||
@@ -57,7 +58,7 @@ export function createWeatherLayer({
|
||||
feed,
|
||||
clock,
|
||||
id = 'weather-radar',
|
||||
cesium = Cesium,
|
||||
cesium = null,
|
||||
createRendering = createWeatherRendering,
|
||||
createNativeRendering = null,
|
||||
documentRef = globalThis.document,
|
||||
@@ -311,11 +312,13 @@ export function createWeatherLayer({
|
||||
attachRenderer(nextViewer, { engine = null } = {}) {
|
||||
viewer = nextViewer;
|
||||
sceneEngine = engine;
|
||||
const options = { viewer, cesium, getHost, onChange: notify };
|
||||
const options = { viewer, getHost, onChange: notify };
|
||||
// The SDK is read here, on Cesium only: the production build's Cesium
|
||||
// global loads with the first Cesium globe.
|
||||
rendering =
|
||||
engine?.kind === 'navara'
|
||||
? createNativeRendering(engine, options)
|
||||
: createRendering(options);
|
||||
: createRendering({ ...options, cesium: cesium ?? Cesium });
|
||||
checkHost(false);
|
||||
eventTarget?.addEventListener?.(
|
||||
'gev:map-stack-changed',
|
||||
@@ -530,12 +533,20 @@ export function createWeatherLayer({
|
||||
: manifest.bounds);
|
||||
runNavigation(() =>
|
||||
(sceneEngine?.camera ?? viewer.camera).flyTo({
|
||||
destination: cesium.Rectangle.fromDegrees(
|
||||
b.west,
|
||||
b.south,
|
||||
b.east,
|
||||
b.north,
|
||||
),
|
||||
// The engine camera takes a plain rectangle in radians.
|
||||
destination: sceneEngine
|
||||
? {
|
||||
west: geo.math.toRadians(b.west),
|
||||
south: geo.math.toRadians(b.south),
|
||||
east: geo.math.toRadians(b.east),
|
||||
north: geo.math.toRadians(b.north),
|
||||
}
|
||||
: (cesium ?? Cesium).Rectangle.fromDegrees(
|
||||
b.west,
|
||||
b.south,
|
||||
b.east,
|
||||
b.north,
|
||||
),
|
||||
duration: motion?.matches ? 0 : 1.4,
|
||||
}),
|
||||
);
|
||||
@@ -634,20 +645,17 @@ export function createWeatherLayer({
|
||||
const point =
|
||||
canvas &&
|
||||
(sceneEngine?.camera ?? viewer.camera).pickEllipsoid?.(
|
||||
new cesium.Cartesian2(
|
||||
geo.cartesian2.create(
|
||||
canvas.clientWidth / 2,
|
||||
canvas.clientHeight / 2,
|
||||
),
|
||||
viewer?.scene?.globe?.ellipsoid,
|
||||
);
|
||||
const camera = point
|
||||
? cesium.Cartographic.fromCartesian(
|
||||
point,
|
||||
viewer?.scene?.globe?.ellipsoid,
|
||||
)
|
||||
? geo.cartographic.fromCartesian(point, viewer?.scene?.globe?.ellipsoid)
|
||||
: null;
|
||||
const lon = camera ? cesium.Math.toDegrees(camera.longitude) : 0;
|
||||
const lat = camera ? cesium.Math.toDegrees(camera.latitude) : 0;
|
||||
const lon = camera ? geo.math.toDegrees(camera.longitude) : 0;
|
||||
const lat = camera ? geo.math.toDegrees(camera.latitude) : 0;
|
||||
const outside =
|
||||
b &&
|
||||
camera &&
|
||||
|
||||
@@ -7,22 +7,9 @@ import { inspectWindAtCenter } from './inspection.js';
|
||||
import { windReadingResult } from './presentation.js';
|
||||
import { sampleWind, windColor, windSpeed } from './model.js';
|
||||
import { windStats } from './index.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
|
||||
const cesium = {
|
||||
Cartesian2: class {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
},
|
||||
Ellipsoid: { WGS84: {} },
|
||||
Cartographic: { fromCartesian: (p) => p },
|
||||
Math: { toDegrees: (x) => (x * 180) / Math.PI },
|
||||
};
|
||||
const at = (lonDeg, latDeg) => ({
|
||||
longitude: (lonDeg * Math.PI) / 180,
|
||||
latitude: (latDeg * Math.PI) / 180,
|
||||
});
|
||||
const at = (lonDeg, latDeg) => geo.cartesian3.fromDegrees(lonDeg, latDeg);
|
||||
const grid = { nx: 4, ny: 3, lo1: 0, la1: 90, dx: 90, dy: 90 };
|
||||
const field = (u, v) => ({
|
||||
grid,
|
||||
@@ -44,11 +31,10 @@ const viaViewer = (snapshot, point) =>
|
||||
camera: { pickEllipsoid: () => point },
|
||||
scene: { canvas: { clientWidth: 800, clientHeight: 600 } },
|
||||
},
|
||||
cesium,
|
||||
options,
|
||||
);
|
||||
const viaPosition = (snapshot, point) =>
|
||||
inspectWindAtCenter(snapshot, null, cesium, { ...options, position: point });
|
||||
inspectWindAtCenter(snapshot, null, { ...options, position: point });
|
||||
const windLine = (reading) =>
|
||||
windReadingResult(reading).lines.find(({ id }) => id === 'wind').text;
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export function windStats(manifest) {
|
||||
export function createWindLayer({
|
||||
feed,
|
||||
clock,
|
||||
cesium = Cesium,
|
||||
cesium = null,
|
||||
container,
|
||||
createRendering = createWindRendering,
|
||||
createNativeRendering,
|
||||
@@ -90,7 +90,7 @@ export function createWindLayer({
|
||||
y: canvas.clientHeight / 2,
|
||||
});
|
||||
}
|
||||
reading = inspectWindAtCenter(manifest, viewer, cesium, {
|
||||
reading = inspectWindAtCenter(manifest, viewer, {
|
||||
position,
|
||||
units,
|
||||
overlay,
|
||||
@@ -129,7 +129,9 @@ export function createWindLayer({
|
||||
engine?.kind === 'navara'
|
||||
? createNativeRendering(engine)
|
||||
: createRendering({
|
||||
cesium,
|
||||
// Read on Cesium only: the production build's Cesium global
|
||||
// loads with the first Cesium globe.
|
||||
cesium: cesium ?? Cesium,
|
||||
container: container ?? nextViewer.container,
|
||||
getViewer: () => viewer,
|
||||
getHost: () =>
|
||||
@@ -147,7 +149,7 @@ export function createWindLayer({
|
||||
inspectionMarker = createWindInspectionMarker({
|
||||
container: target,
|
||||
viewer,
|
||||
cesium,
|
||||
cesium: engine ? null : (cesium ?? Cesium),
|
||||
engine,
|
||||
});
|
||||
if (enabled && manifest && !manifest.unavailable) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
||||
import { createWindLayer, formatWindValidTime, windStats } from './index.js';
|
||||
import { useWindInspectionStore } from '../../ui/stores/windInspection.js';
|
||||
import { applicationPinia } from '../../ui/vueRuntime.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
|
||||
const snapshot = (model) => ({
|
||||
model,
|
||||
@@ -314,7 +315,7 @@ test('sample stays fixed across model, field and unit changes; dismissal and dis
|
||||
};
|
||||
const viewer = {
|
||||
container,
|
||||
camera: { positionWC: {}, pickEllipsoid: () => { samples++; return { longitude: 0, latitude: 0 }; } },
|
||||
camera: { positionWC: {}, pickEllipsoid: () => { samples++; return geo.cartesian3.fromDegrees(0, 0); } },
|
||||
scene: {
|
||||
mode: 3,
|
||||
canvas: { clientWidth: 800, clientHeight: 600, getBoundingClientRect: () => ({ left: 0, top: 0 }) },
|
||||
@@ -352,7 +353,7 @@ test('sample stays fixed across model, field and unit changes; dismissal and dis
|
||||
assert.deepEqual(layer.getRowControls().summary.settings.map(({ label }) => label), ['MODEL', 'FIELD', 'UNITS', 'MOTION']);
|
||||
assert.ok(marker());
|
||||
assert.equal(typeof listener, 'function');
|
||||
viewer.camera.pickEllipsoid = () => { samples++; return { longitude: 70, latitude: 20 }; };
|
||||
viewer.camera.pickEllipsoid = () => { samples++; return geo.cartesian3.fromDegrees(70, 20); };
|
||||
layer.setParams({ overlay: 'speed' });
|
||||
assert.equal(layer.getRowControls().summary.reading.coordinates, captured.coordinates);
|
||||
layer.setParams({ model: 'ifs' });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { sampleWind, windSpeed } from './model.js';
|
||||
import { sampleScalar } from './fields.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
|
||||
// The reading stays English: AI tools read it too. Text is marked for i18n
|
||||
// extraction and translated where the WEATHER card shows it (presentation.js).
|
||||
@@ -45,17 +46,17 @@ export function formatWindSpeed(speed, units = 'km/h') {
|
||||
export function inspectWindAtCenter(
|
||||
snapshot,
|
||||
viewer,
|
||||
cesium,
|
||||
{ units, overlay, model, validTime, status, position } = {},
|
||||
) {
|
||||
const scene = viewer?.scene;
|
||||
const canvas = scene?.canvas;
|
||||
const ellipsoid = scene?.globe?.ellipsoid || cesium.Ellipsoid?.WGS84;
|
||||
// The globe's own ellipsoid, else WGS84 (geo's default).
|
||||
const ellipsoid = scene?.globe?.ellipsoid;
|
||||
const point =
|
||||
position ||
|
||||
(canvas &&
|
||||
viewer.camera?.pickEllipsoid?.(
|
||||
new cesium.Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2),
|
||||
geo.cartesian2.create(canvas.clientWidth / 2, canvas.clientHeight / 2),
|
||||
ellipsoid,
|
||||
));
|
||||
const noReading = () => ({
|
||||
@@ -69,9 +70,9 @@ export function inspectWindAtCenter(
|
||||
),
|
||||
});
|
||||
if (!point || !snapshot?.u || !snapshot?.v) return noReading();
|
||||
const location = cesium.Cartographic.fromCartesian(point, ellipsoid);
|
||||
const lon = cesium.Math.toDegrees(location.longitude);
|
||||
const lat = cesium.Math.toDegrees(location.latitude);
|
||||
const location = geo.cartographic.fromCartesian(point, ellipsoid);
|
||||
const lon = geo.math.toDegrees(location.longitude);
|
||||
const lat = geo.math.toDegrees(location.latitude);
|
||||
const vector = sampleWind(
|
||||
{ ...snapshot.grid, u: snapshot.u, v: snapshot.v },
|
||||
lon,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import {
|
||||
windFrom,
|
||||
formatWindSpeed,
|
||||
@@ -27,21 +28,10 @@ test('center inspection uses a fixed forecast reading without double-converting
|
||||
},
|
||||
};
|
||||
const viewer = {
|
||||
camera: { pickEllipsoid: () => ({ longitude: 0, latitude: 0 }) },
|
||||
camera: { pickEllipsoid: () => geo.cartesian3.fromDegrees(0, 0) },
|
||||
scene: { canvas: { clientWidth: 800, clientHeight: 600 } },
|
||||
};
|
||||
const cesium = {
|
||||
Cartesian2: class {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
},
|
||||
Ellipsoid: { WGS84: {} },
|
||||
Cartographic: { fromCartesian: (p) => p },
|
||||
Math: { toDegrees: (x) => (x * 180) / Math.PI },
|
||||
};
|
||||
const result = inspectWindAtCenter(snapshot, viewer, cesium, {
|
||||
const result = inspectWindAtCenter(snapshot, viewer, {
|
||||
overlay: 'temperature',
|
||||
units: 'km/h',
|
||||
model: 'GFS',
|
||||
@@ -54,7 +44,7 @@ test('center inspection uses a fixed forecast reading without double-converting
|
||||
assert.equal(result.coordinates, '0.00°N · 0.00°E');
|
||||
viewer.camera.pickEllipsoid = () => null;
|
||||
assert.equal(
|
||||
inspectWindAtCenter(snapshot, viewer, cesium).coordinates,
|
||||
inspectWindAtCenter(snapshot, viewer).coordinates,
|
||||
'No surface reading',
|
||||
);
|
||||
});
|
||||
@@ -16,6 +16,10 @@ import { createSessionDiagnostics } from './app/sessionDiagnostics.js';
|
||||
import { useDisplayStore } from './ui/stores/display.js';
|
||||
import { applicationPinia } from './ui/vueRuntime.js';
|
||||
import { t } from './i18n/index.js';
|
||||
import { guardCesiumGlobal } from './app/loadCesium.js';
|
||||
|
||||
// Cesium.js loads with the first Cesium globe (src/app/loadCesium.js).
|
||||
guardCesiumGlobal();
|
||||
|
||||
// The Vite boundary exposes only undefined, null, or a validated URL.
|
||||
const photonSetting = import.meta.env.PHOTON_URL;
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import * as Cesium from 'cesium';
|
||||
// Ion world imagery asset IDs; the catalog is available before the SDK loads.
|
||||
export const MAP_STACKS = [
|
||||
{
|
||||
id: 'photoreal',
|
||||
@@ -12,7 +12,7 @@ export const MAP_STACKS = [
|
||||
label: 'Bing Aerial', // i18n-ignore: imagery product name
|
||||
shortLabel: 'Aerial',
|
||||
kind: 'ion',
|
||||
style: Cesium.IonWorldImageryStyle.AERIAL,
|
||||
style: 2,
|
||||
requiresIon: true,
|
||||
},
|
||||
{
|
||||
@@ -20,7 +20,7 @@ export const MAP_STACKS = [
|
||||
label: 'Bing Labels', // i18n-ignore: imagery product name
|
||||
shortLabel: 'Labels',
|
||||
kind: 'ion',
|
||||
style: Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS,
|
||||
style: 3,
|
||||
requiresIon: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
@@ -11,6 +10,8 @@ import {
|
||||
SRGBColorSpace,
|
||||
Texture,
|
||||
} from 'three';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { polygonShape, rectangleShape } from './navaraGeometry.js';
|
||||
import {
|
||||
attachSceneDepth,
|
||||
createSceneDepthTest,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
const KEY = 'gevScenePack',
|
||||
registered = new WeakSet();
|
||||
const cyan = [0, 1, 1, 1],
|
||||
xyz = (p) => Cesium.Cartesian3.fromDegrees(...p);
|
||||
xyz = ([lon, lat, height]) => geo.cartesian3.fromDegrees(lon, lat, height);
|
||||
export function createNavaraPackRenderer(engine, { view, MeshDesc }) {
|
||||
if (!registered.has(view)) {
|
||||
view.registerMesh(
|
||||
@@ -153,20 +154,9 @@ export function createNavaraPackRenderer(engine, { view, MeshDesc }) {
|
||||
line.setPositions(f.coordinates.map(xyz));
|
||||
} else {
|
||||
batch ??= meshGroup(handle);
|
||||
const polygon = new Cesium.PolygonGeometry({
|
||||
polygonHierarchy: new Cesium.PolygonHierarchy(
|
||||
f.coordinates[0].map(xyz),
|
||||
f.coordinates
|
||||
.slice(1)
|
||||
.map((r) => new Cesium.PolygonHierarchy(r.map(xyz))),
|
||||
),
|
||||
perPositionHeight: true,
|
||||
vertexFormat: Cesium.VertexFormat.POSITION_ONLY,
|
||||
arcType: Cesium.ArcType.GEODESIC,
|
||||
});
|
||||
addMesh(
|
||||
batch,
|
||||
Cesium.PolygonGeometry.createGeometry(polygon),
|
||||
polygonShape(f.coordinates),
|
||||
{ color: new Color(0x00ffff), opacity: 0.35 },
|
||||
id,
|
||||
null,
|
||||
@@ -189,7 +179,7 @@ export function createNavaraPackRenderer(engine, { view, MeshDesc }) {
|
||||
const hits = raycaster.intersectObjects(batch.group.children);
|
||||
const earth = engine.picker.pickGlobe(at);
|
||||
const depth = earth
|
||||
? Cesium.Cartesian3.distance(ray.origin, earth)
|
||||
? geo.cartesian3.distance(ray.origin, earth)
|
||||
: Infinity;
|
||||
return hits
|
||||
.filter((h) => h.distance <= depth + 1)
|
||||
@@ -210,12 +200,9 @@ export function createNavaraPackRenderer(engine, { view, MeshDesc }) {
|
||||
texture.colorSpace = SRGBColorSpace;
|
||||
texture.needsUpdate = true;
|
||||
batch.owned.push(texture);
|
||||
const shape = Cesium.RectangleGeometry.createGeometry(
|
||||
new Cesium.RectangleGeometry({
|
||||
rectangle: Cesium.Rectangle.fromDegrees(...pack.placement.bounds),
|
||||
height: pack.placement.height,
|
||||
vertexFormat: Cesium.VertexFormat.POSITION_AND_ST,
|
||||
}),
|
||||
const shape = rectangleShape(
|
||||
pack.placement.bounds,
|
||||
pack.placement.height,
|
||||
);
|
||||
addMesh(batch, shape, { color: 0xffffff, opacity: 1 }, null, texture);
|
||||
engine.render.request('scene-pack');
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Pack polygons and images as Navara mesh geometry, without the Cesium SDK (a
|
||||
// page that starts on Navara never loads it). A shape is what Cesium's
|
||||
// Geometry gives the renderer: Earth-fixed positions, triangle indices, an
|
||||
// optional texture coordinate per vertex and a centre to draw relative to.
|
||||
import { ShapeUtils, Vector2 } from 'three';
|
||||
import * as geo from '../../geo/index.js';
|
||||
|
||||
// Cesium's default granularity: no edge or cell spans more than one degree.
|
||||
const GRANULARITY_DEGREES = 1;
|
||||
|
||||
function shape(positions, indices, st = null) {
|
||||
const center = geo.cartesian3.create();
|
||||
for (const p of positions) geo.cartesian3.add(center, p, center);
|
||||
geo.cartesian3.divideByScalar(center, positions.length || 1, center);
|
||||
const values = new Float64Array(positions.length * 3);
|
||||
positions.forEach((p, i) => values.set([p.x, p.y, p.z], i * 3));
|
||||
return {
|
||||
attributes: {
|
||||
position: { values },
|
||||
...(st ? { st: { values: st } } : {}),
|
||||
},
|
||||
indices: new Uint32Array(indices),
|
||||
boundingSphere: { center },
|
||||
};
|
||||
}
|
||||
|
||||
/** A [lon, lat, height?] ring with long edges split along great circles. */
|
||||
function densify(ring) {
|
||||
const out = [];
|
||||
ring.forEach((a, i) => {
|
||||
const b = ring[(i + 1) % ring.length];
|
||||
const steps = Math.max(
|
||||
1,
|
||||
Math.ceil(
|
||||
Math.max(Math.abs(b[0] - a[0]), Math.abs(b[1] - a[1])) /
|
||||
GRANULARITY_DEGREES,
|
||||
),
|
||||
);
|
||||
for (let k = 0; k < steps; k++) {
|
||||
const t = k / steps;
|
||||
out.push([
|
||||
a[0] + (b[0] - a[0]) * t,
|
||||
a[1] + (b[1] - a[1]) * t,
|
||||
(a[2] ?? 0) + ((b[2] ?? 0) - (a[2] ?? 0)) * t,
|
||||
]);
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* A filled polygon: `rings` are GeoJSON-style [lon, lat, height?] rings, the
|
||||
* first the outline and the rest holes, each vertex at its own height (as
|
||||
* Cesium's perPositionHeight). Triangulated in the plane tangent to the
|
||||
* Earth at the outline's centre; null when nothing is left to fill.
|
||||
*/
|
||||
export function polygonShape(rings) {
|
||||
const loops = rings
|
||||
.map((ring) => {
|
||||
const open =
|
||||
ring.length > 1 &&
|
||||
ring[0][0] === ring.at(-1)[0] &&
|
||||
ring[0][1] === ring.at(-1)[1]
|
||||
? ring.slice(0, -1)
|
||||
: ring;
|
||||
return densify(open);
|
||||
})
|
||||
.filter((ring) => ring.length >= 3);
|
||||
if (!loops.length) return null;
|
||||
const [outline, ...holes] = loops;
|
||||
const points = loops.flat();
|
||||
const world = points.map(([lon, lat, h]) =>
|
||||
geo.cartesian3.fromDegrees(lon, lat, h),
|
||||
);
|
||||
const middle = geo.cartesian3.create();
|
||||
for (const p of world.slice(0, outline.length))
|
||||
geo.cartesian3.add(middle, p, middle);
|
||||
geo.cartesian3.divideByScalar(middle, outline.length, middle);
|
||||
const frame = geo.transforms.eastNorthUpToFixedFrame(middle);
|
||||
const east = geo.cartesian3.create(frame[0], frame[1], frame[2]);
|
||||
const north = geo.cartesian3.create(frame[4], frame[5], frame[6]);
|
||||
const offset = geo.cartesian3.create();
|
||||
const plane = world.map((p) => {
|
||||
geo.cartesian3.subtract(p, middle, offset);
|
||||
return new Vector2(
|
||||
geo.cartesian3.dot(offset, east),
|
||||
geo.cartesian3.dot(offset, north),
|
||||
);
|
||||
});
|
||||
let start = outline.length;
|
||||
const holePoints = holes.map((hole) => {
|
||||
const ring = plane.slice(start, start + hole.length);
|
||||
start += hole.length;
|
||||
return ring;
|
||||
});
|
||||
const triangles = ShapeUtils.triangulateShape(
|
||||
plane.slice(0, outline.length),
|
||||
holePoints,
|
||||
);
|
||||
if (!triangles.length) return null;
|
||||
return shape(world, triangles.flat());
|
||||
}
|
||||
|
||||
/**
|
||||
* An image's rectangle, [west, south, east, north] in degrees at `height`:
|
||||
* a grid of cells no larger than the granularity, texture coordinates from
|
||||
* (0, 0) at the south-west corner to (1, 1) at the north-east one.
|
||||
*/
|
||||
export function rectangleShape([west, south, east, north], height = 0) {
|
||||
const span = east >= west ? east - west : east + 360 - west;
|
||||
const columns = Math.max(1, Math.ceil(span / GRANULARITY_DEGREES));
|
||||
const rows = Math.max(
|
||||
1,
|
||||
Math.ceil(Math.abs(north - south) / GRANULARITY_DEGREES),
|
||||
);
|
||||
const positions = [];
|
||||
const st = new Float32Array((rows + 1) * (columns + 1) * 2);
|
||||
for (let row = 0; row <= rows; row++)
|
||||
for (let column = 0; column <= columns; column++) {
|
||||
const s = column / columns,
|
||||
t = row / rows;
|
||||
st.set([s, t], positions.length * 2);
|
||||
positions.push(
|
||||
geo.cartesian3.fromDegrees(
|
||||
west + span * s,
|
||||
south + (north - south) * t,
|
||||
height,
|
||||
),
|
||||
);
|
||||
}
|
||||
const indices = [];
|
||||
for (let row = 0; row < rows; row++)
|
||||
for (let column = 0; column < columns; column++) {
|
||||
const a = row * (columns + 1) + column,
|
||||
b = a + 1,
|
||||
c = a + columns + 1,
|
||||
d = c + 1;
|
||||
indices.push(a, b, d, a, d, c);
|
||||
}
|
||||
return shape(positions, indices, st);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { polygonShape, rectangleShape } from './navaraGeometry.js';
|
||||
|
||||
const vertex = (shape, index) => {
|
||||
const v = shape.attributes.position.values;
|
||||
return geo.cartographic.fromCartesian(
|
||||
geo.cartesian3.create(v[index * 3], v[index * 3 + 1], v[index * 3 + 2]),
|
||||
);
|
||||
};
|
||||
const area = (shape) => {
|
||||
// Sum of triangle areas in square metres.
|
||||
const v = shape.attributes.position.values,
|
||||
i = shape.indices;
|
||||
const p = (k) => geo.cartesian3.create(v[k * 3], v[k * 3 + 1], v[k * 3 + 2]);
|
||||
let sum = 0;
|
||||
for (let t = 0; t < i.length; t += 3) {
|
||||
const a = p(i[t]),
|
||||
ab = geo.cartesian3.subtract(p(i[t + 1]), a, geo.cartesian3.create()),
|
||||
ac = geo.cartesian3.subtract(p(i[t + 2]), a, geo.cartesian3.create());
|
||||
sum +=
|
||||
geo.cartesian3.magnitude(
|
||||
geo.cartesian3.cross(ab, ac, geo.cartesian3.create()),
|
||||
) / 2;
|
||||
}
|
||||
return sum;
|
||||
};
|
||||
|
||||
test('a pack polygon fills its outline less its holes, each vertex at its own height', () => {
|
||||
const square = [
|
||||
[-97.75, 30.25, 10],
|
||||
[-97.74, 30.25, 10],
|
||||
[-97.74, 30.26, 10],
|
||||
[-97.75, 30.26, 10],
|
||||
[-97.75, 30.25, 10],
|
||||
];
|
||||
const hole = [
|
||||
[-97.747, 30.253, 10],
|
||||
[-97.743, 30.253, 10],
|
||||
[-97.743, 30.257, 10],
|
||||
[-97.747, 30.257, 10],
|
||||
];
|
||||
const full = polygonShape([square]);
|
||||
const holed = polygonShape([square, hole]);
|
||||
assert.equal(full.indices.length, 6, 'the closing vertex is not repeated');
|
||||
assert.ok(
|
||||
Math.abs(area(holed) / area(full) - (1 - 0.16)) < 0.01,
|
||||
`the hole is left out: ${area(holed) / area(full)}`,
|
||||
);
|
||||
for (let k = 0; k < holed.attributes.position.values.length / 3; k++)
|
||||
assert.ok(Math.abs(vertex(holed, k).height - 10) < 1e-6);
|
||||
assert.equal(polygonShape([[square[0], square[1]]]), null);
|
||||
});
|
||||
|
||||
test('a long pack edge is split so no step spans more than a degree', () => {
|
||||
const shape = polygonShape([
|
||||
[
|
||||
[0, 0],
|
||||
[3, 0],
|
||||
[3, 1],
|
||||
[0, 1],
|
||||
],
|
||||
]);
|
||||
assert.equal(shape.attributes.position.values.length / 3, 8);
|
||||
});
|
||||
|
||||
test('a pack image rectangle maps its texture from the south-west corner', () => {
|
||||
const shape = rectangleShape([-97.8, 30.2, -97.7, 30.3], 25);
|
||||
const st = shape.attributes.st.values;
|
||||
assert.deepEqual([...st.slice(0, 2)], [0, 0]);
|
||||
assert.deepEqual([...st.slice(-2)], [1, 1]);
|
||||
const first = vertex(shape, 0),
|
||||
last = vertex(shape, st.length / 2 - 1);
|
||||
assert.ok(Math.abs(geo.math.toDegrees(first.longitude) + 97.8) < 1e-9);
|
||||
assert.ok(Math.abs(geo.math.toDegrees(last.latitude) - 30.3) < 1e-9);
|
||||
assert.ok(Math.abs(first.height - 25) < 1e-6);
|
||||
assert.equal(shape.indices.length, 6);
|
||||
const wide = rectangleShape([10, 0, 13.5, 1]);
|
||||
assert.equal(wide.indices.length, 4 * 1 * 6, 'cells of at most a degree');
|
||||
});
|
||||
@@ -11,7 +11,6 @@ import { defaultGeospatial } from '../search/defaults.js';
|
||||
import { msg } from '../i18n/index.js';
|
||||
import { useProviderSettingsStore } from '../ui/stores/providerSettings.js';
|
||||
import { applicationPinia } from '../ui/vueRuntime.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../geo/index.js';
|
||||
import { CITY_POIS } from '../cityPois.js';
|
||||
import { greatCircleKm } from '../geoDistance.js';
|
||||
@@ -2798,8 +2797,8 @@ async function frameOverhead(engine, dataManager, styleManager, args = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// Cesium's bounding sphere (pure math, no viewer), flown by the engine.
|
||||
const bounds = Cesium.BoundingSphere.fromPoints(
|
||||
// Cesium's bounding sphere (geo's port, no SDK), flown by the engine.
|
||||
const bounds = geo.boundingSphere.fromPoints(
|
||||
entries.map((entry) => entry.position),
|
||||
);
|
||||
const sphere = {
|
||||
|
||||
Reference in new issue
Block a user