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:
German Khokhlov committed 2026-11-04 17:15:21 +04:00
1 parent f87da8523e
commit b8305de14b
58 files changed
+2130 -325

No files matched your search

+179
View File
@@ -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.');
}
+42
View File
@@ -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, []);
});
+118 -3
View File
@@ -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"]
}
+4 -4
View File
@@ -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',
+7 -2
View File
@@ -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.
+10 -15
View File
@@ -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;
+2 -2
View File
@@ -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();
+134
View File
@@ -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,
});
},
});
}
+151
View File
@@ -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);
});
+3 -1
View File
@@ -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
View File
@@ -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. */
+6 -2
View File
@@ -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(() =>
+28
View File
@@ -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;
};
}
+7 -26
View File
@@ -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 -8
View File
@@ -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 = {
+20 -7
View File
@@ -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
+19 -12
View File
@@ -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;
+4
View File
@@ -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;
+9 -9
View File
@@ -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);
+3 -12
View File
@@ -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();
+6 -4
View File
@@ -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;
+5 -4
View File
@@ -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;
}
+1 -1
View File
@@ -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;
+2
View File
@@ -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';
+193
View File
@@ -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;
}
+605
View File
@@ -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;
}
+61
View File
@@ -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
View File
@@ -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;
}
+3 -3
View File
@@ -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) =>
+2 -1
View File
@@ -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
View File
@@ -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;
+10 -5
View File
@@ -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;
+4 -2
View File
@@ -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 });
});
+6 -7
View File
@@ -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,
};
}
+10 -7
View File
@@ -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) {
+15 -11
View File
@@ -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
+2 -1
View File
@@ -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;
+8 -11
View File
@@ -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;
+14 -9
View File
@@ -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() {
+8 -6
View File
@@ -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;
+6 -2
View File
@@ -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;
},
}),
]);
+5 -3
View File
@@ -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 =
+34 -29
View File
@@ -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);
},
/**
+6 -7
View File
@@ -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). */
+4 -4
View File
@@ -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 -2
View File
@@ -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
View File
@@ -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 &&
+3 -17
View File
@@ -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;
+6 -4
View File
@@ -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 -2
View File
@@ -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' });
+7 -6
View File
@@ -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,
+4 -14
View File
@@ -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',
);
});
+4
View File
@@ -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
View File
@@ -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,
},
{
+8 -21
View File
@@ -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');
+141
View File
@@ -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');
});
+2 -3
View File
@@ -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 = {