refactor(vessels,satellites,awareness): take vector maths from src/geo

Moves the Cartesian3/Cartographic/Matrix3/Matrix4/Math calls of the vessel, satellite and Contacts (awareness) layers, plus flights controller/enrichment, to src/geo. The satellites-layer package boundary now owns the src/geo modules. Awareness keeps Cesium.Cartesian3.clone for the resolved subject position, which reaches the installations anchor (anchor.test pins a Cesium instance).
This commit is contained in:
German Khokhlov committed 2026-10-15 00:17:17 +04:00
1 parent 1a69368ae6
commit 2b83606765
15 files changed
+97 -86

No files matched your search

+15 -1
View File
@@ -1104,7 +1104,21 @@
"src/layers/satellites/state.js",
"src/layers/satellites/testing.js",
"src/layers/satellites/tracking.js",
"src/data/inputOwnership.js"
"src/data/inputOwnership.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
],
"external": ["cesium", "satellite.js"]
},
+3 -2
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { VESSEL_FOCUS_RADIUS_M } from './policy.js';
export function createFocus({ state: layerState, services, parts, source }) {
@@ -74,7 +75,7 @@ export function createFocus({ state: layerState, services, parts, source }) {
layerId,
id: String(known.id || known.mmsi),
label: known.label || known.name || known.callsign || String(id),
position: Cesium.Cartesian3.clone(known.position),
position: geo.cartesian3.clone(known.position),
});
return true;
}
@@ -120,7 +121,7 @@ export function createFocus({ state: layerState, services, parts, source }) {
let closestDistance = Infinity;
for (const candidate of candidates) {
if (!candidate?.position) continue;
const distance = Cesium.Cartesian3.distance(
const distance = geo.cartesian3.distance(
cameraPosition,
candidate.position,
);
+20 -20
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
createCyberSonarSampler,
isCyberContactSonarActive as isCyberSonarActive,
@@ -132,7 +133,7 @@ export function createRendering({
const cameraHeading = layerState.viewer.camera.heading || 0;
const headingDeg =
(Math.round(Cesium.Math.toDegrees(cameraHeading)) + 360) % 360;
(Math.round(geo.math.toDegrees(cameraHeading)) + 360) % 360;
layerState.compassRing.style.setProperty(
'--compass-rotation',
`${-headingDeg}deg`,
@@ -141,8 +142,8 @@ export function createRendering({
layerState.compassHeading.style.left = `${geometry.centerX}px`;
layerState.compassHeading.style.top = `${geometry.centerY - compassRadius + 39}px`;
for (const label of layerState.compassLabels) {
const bearing = Cesium.Math.toRadians(Number(label.dataset.bearing));
const angle = bearing - cameraHeading - Cesium.Math.PI_OVER_TWO;
const bearing = geo.math.toRadians(Number(label.dataset.bearing));
const angle = bearing - cameraHeading - geo.math.PI_OVER_TWO;
const labelRadius = compassRadius - 17;
label.style.left = `${geometry.centerX + Math.cos(angle) * labelRadius}px`;
label.style.top = `${geometry.centerY + Math.sin(angle) * labelRadius}px`;
@@ -166,19 +167,19 @@ export function createRendering({
marker.hidden = true;
continue;
}
const direction = Cesium.Cartesian3.subtract(
const direction = geo.cartesian3.subtract(
item.position,
layerState.subject.position,
layerState.DIRECTION_SCRATCH[index],
);
if (Cesium.Cartesian3.magnitudeSquared(direction) < 1) {
if (geo.cartesian3.magnitudeSquared(direction) < 1) {
marker.hidden = true;
continue;
}
Cesium.Cartesian3.normalize(direction, direction);
geo.cartesian3.normalize(direction, direction);
const projection = celestialScreenAngle(
Cesium.Cartesian3.dot(direction, layerState.viewer.camera.rightWC),
Cesium.Cartesian3.dot(direction, layerState.viewer.camera.upWC),
geo.cartesian3.dot(direction, layerState.viewer.camera.rightWC),
geo.cartesian3.dot(direction, layerState.viewer.camera.upWC),
marker._lastAngle,
);
if (projection.stable) marker._lastAngle = projection.angle;
@@ -201,12 +202,12 @@ export function createRendering({
marker._arrow.style.transform = `translate(-50%, -50%) rotate(${projection.angle}rad)`;
marker._label.style.transform = `translate(-50%, -50%) translate(${-Math.cos(projection.angle) * 56}px, ${-Math.sin(projection.angle) * 56}px)`;
const label = formatAwarenessLabel(item);
const subjectCartographic = Cesium.Cartographic.fromCartesian(
const subjectCartographic = geo.cartographic.fromCartesian(
layerState.subject.position,
undefined,
layerState.SUBJECT_CARTOGRAPHIC_SCRATCH,
);
const targetCartographic = Cesium.Cartographic.fromCartesian(
const targetCartographic = geo.cartographic.fromCartesian(
item.position,
undefined,
layerState.TARGET_CARTOGRAPHIC_SCRATCH[index],
@@ -214,10 +215,10 @@ export function createRendering({
const bearing =
subjectCartographic && targetCartographic
? bearingBetweenCoordinates(
Cesium.Math.toDegrees(subjectCartographic.latitude),
Cesium.Math.toDegrees(subjectCartographic.longitude),
Cesium.Math.toDegrees(targetCartographic.latitude),
Cesium.Math.toDegrees(targetCartographic.longitude),
geo.math.toDegrees(subjectCartographic.latitude),
geo.math.toDegrees(subjectCartographic.longitude),
geo.math.toDegrees(targetCartographic.latitude),
geo.math.toDegrees(targetCartographic.longitude),
)
: null;
const bearingText = Number.isFinite(bearing)
@@ -243,9 +244,9 @@ export function createRendering({
function renderVisual(subject) {
if (!layerState.viewer || !subject?.position) return;
const cartographic = Cesium.Cartographic.fromCartesian(subject.position);
const cartographic = geo.cartographic.fromCartesian(subject.position);
if (!cartographic) return;
const groundCenter = Cesium.Cartesian3.fromRadians(
const groundCenter = geo.cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
0,
@@ -256,12 +257,11 @@ export function createRendering({
// Move it only after a meaningful displacement so live tracking does not
// churn ten large geometries for sub-pixel motion every 750 ms.
if (
Cesium.Cartesian3.distance(layerState.visual.center, groundCenter) >=
250
geo.cartesian3.distance(layerState.visual.center, groundCenter) >= 250
) {
for (const entity of layerState.visual.entities)
entity.position.setValue(groundCenter);
Cesium.Cartesian3.clone(groundCenter, layerState.visual.center);
geo.cartesian3.clone(groundCenter, layerState.visual.center);
// Content actually changed — buy exactly one frame instead of holding.
governorRequestRender('awareness-visual');
}
@@ -289,7 +289,7 @@ export function createRendering({
layerState.visual = {
key,
entities,
center: Cesium.Cartesian3.clone(groundCenter),
center: geo.cartesian3.clone(groundCenter),
};
governorRequestRender('awareness-visual');
}
+6 -8
View File
@@ -1,18 +1,16 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
export function createState({ services }) {
const state = {};
state.DIRECTION_SCRATCH = Array.from(
{ length: 3 },
() => new Cesium.Cartesian3(),
state.DIRECTION_SCRATCH = Array.from({ length: 3 }, () =>
geo.cartesian3.create(),
);
state.SUBJECT_CARTOGRAPHIC_SCRATCH = new Cesium.Cartographic();
state.SUBJECT_CARTOGRAPHIC_SCRATCH = geo.cartographic.create();
state.TARGET_CARTOGRAPHIC_SCRATCH = Array.from(
{ length: 3 },
() => new Cesium.Cartographic(),
state.TARGET_CARTOGRAPHIC_SCRATCH = Array.from({ length: 3 }, () =>
geo.cartographic.create(),
);
Object.assign(state, {
+5 -4
View File
@@ -3,6 +3,7 @@ import {
AWARENESS_RELATIONSHIP,
} from '../../data/militaryAwarenessEngine.js';
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
AWARENESS_QUERY_LIMIT,
SOURCE_LABEL,
@@ -218,7 +219,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
subject,
parts.history.historySourceItem(subject),
),
position: Cesium.Cartesian3.clone(subject.position),
position: geo.cartesian3.clone(subject.position),
});
layerState.navigationIndex = layerState.navigationHistory.length - 1;
}
@@ -389,7 +390,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
const labelChanged = nextLabel !== layerState.subject.label;
layerState.subject = { ...layerState.subject, position, label: nextLabel };
const movementM = layerState.lastEvaluatedPosition
? Cesium.Cartesian3.distance(layerState.lastEvaluatedPosition, position)
? geo.cartesian3.distance(layerState.lastEvaluatedPosition, position)
: Infinity;
if (
!parts.model.awarenessRefreshRequired({
@@ -413,7 +414,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
}
layerState.results = evaluateSubject(layerState.subject, sources);
layerState.sourceRevision = nextSourceRevision;
layerState.lastEvaluatedPosition = Cesium.Cartesian3.clone(
layerState.lastEvaluatedPosition = geo.cartesian3.clone(
position,
layerState.lastEvaluatedPosition,
);
@@ -448,7 +449,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
id: record.properties?.mmsi || record.id,
label: record.label || record.id,
memberNames: record.properties?.memberNames || [],
position: Cesium.Cartesian3.fromDegrees(longitude, latitude, 0),
position: geo.cartesian3.fromDegrees(longitude, latitude, 0),
};
}
+3 -3
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
export function createController({
flightState,
@@ -17,8 +17,8 @@ export function createController({
const cartographic = viewer?.camera?.positionCartographic;
return cartographic
? {
latitude: Cesium.Math.toDegrees(cartographic.latitude),
longitude: Cesium.Math.toDegrees(cartographic.longitude),
latitude: geo.math.toDegrees(cartographic.latitude),
longitude: geo.math.toDegrees(cartographic.longitude),
}
: {};
}
+3 -5
View File
@@ -1,6 +1,7 @@
import { classifyAircraft } from '../../data/aircraftClass.js';
import { horizonOccluder } from '../../data/iconOrientation.js';
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
ENRICH_MAX_INFLIGHT,
ENRICH_DISPATCH_GAP_MS,
@@ -200,10 +201,7 @@ export function createEnrichment({
if (!/^[0-9a-f]{6}$/i.test(icao24)) continue; // adsbdb keys are 6-char hex only
if (flightState.records.data.get(icao24)?.onGround) continue; // ground traffic never spends ambient budget (click-to-enrich still works)
if (!bb.position || !occluder.isPointVisible(bb.position)) continue; // beyond the limb
Cesium.Cartesian3.clone(
bb.position,
flightState._scratchModelBS.center,
);
geo.cartesian3.clone(bb.position, flightState._scratchModelBS.center);
if (
cull.computeVisibility(flightState._scratchModelBS) ===
Cesium.Intersect.OUTSIDE
@@ -211,7 +209,7 @@ export function createEnrichment({
continue; // off-screen
cand.push([
icao24,
Cesium.Cartesian3.distanceSquared(camPos, bb.position),
geo.cartesian3.distanceSquared(camPos, bb.position),
]);
}
cand.sort((a, b) => a[1] - b[1]); // nearest first — what the user is looking at resolves first
+5 -5
View File
@@ -3,7 +3,7 @@ import {
satelliteClassLabel,
satelliteClassLegend,
} from '../../data/satelliteClass.js';
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { ISS_NORAD, POINT_STYLES } from './policy.js';
export function createControls({ state: layerState, services, parts, source }) {
@@ -195,7 +195,7 @@ export function createControls({ state: layerState, services, parts, source }) {
return {
noradId,
name: sat.name.trim(),
position: Cesium.Cartesian3.fromDegrees(
position: geo.cartesian3.fromDegrees(
pos.longitude,
pos.latitude,
pos.altitude,
@@ -227,15 +227,15 @@ export function createControls({ state: layerState, services, parts, source }) {
layerState._catalog.get(noradId)?.group === 'dense'
)
continue;
const carto = Cesium.Cartographic.fromCartesian(point.position);
const carto = geo.cartographic.fromCartesian(point.position);
if (!carto) continue;
const sat = layerState._catalog.get(noradId);
result.push({
id: noradId,
label: sat ? sat.name.trim() : String(noradId),
position: point.position,
latitude: Cesium.Math.toDegrees(carto.latitude),
longitude: Cesium.Math.toDegrees(carto.longitude),
latitude: geo.math.toDegrees(carto.latitude),
longitude: geo.math.toDegrees(carto.longitude),
altitudeM: carto.height,
});
}
+14 -14
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
gstime,
propagate,
@@ -24,16 +24,16 @@ export function createOrbits({ state: layerState, services, parts, source }) {
function orbitFrameModelMatrix(
gmstAtBake,
nowDate,
result = new Cesium.Matrix4(),
result = geo.matrix4.create(),
) {
const deltaGmst = gstime(nowDate) - gmstAtBake;
const rotation = Cesium.Matrix3.fromRotationZ(
const rotation = geo.matrix3.fromRotationZ(
-deltaGmst,
layerState._scratchRingRotation,
);
return Cesium.Matrix4.fromRotationTranslation(
return geo.matrix4.fromRotationTranslation(
rotation,
Cesium.Cartesian3.ZERO,
geo.cartesian3.ZERO,
result,
);
}
@@ -55,7 +55,7 @@ export function createOrbits({ state: layerState, services, parts, source }) {
if (!posVel.position || typeof posVel.position === 'boolean') return null;
const gmst = gstime(date);
const geo = eciToGeodetic(posVel.position, gmst);
const geodetic = eciToGeodetic(posVel.position, gmst);
const velocity =
posVel.velocity && typeof posVel.velocity !== 'boolean'
? posVel.velocity
@@ -65,9 +65,9 @@ export function createOrbits({ state: layerState, services, parts, source }) {
: null;
return {
longitude: degreesLong(geo.longitude),
latitude: degreesLat(geo.latitude),
altitude: geo.height * 1000, // km → meters
longitude: degreesLong(geodetic.longitude),
latitude: degreesLat(geodetic.latitude),
altitude: geodetic.height * 1000, // km → meters
speedMps: Number.isFinite(speedMps) ? speedMps : null,
};
} catch {
@@ -101,12 +101,12 @@ export function createOrbits({ state: layerState, services, parts, source }) {
try {
const posVel = propagate(satrec, t);
if (!posVel.position || typeof posVel.position === 'boolean') continue;
const geo = eciToGeodetic(posVel.position, fixedGmst);
const geodetic = eciToGeodetic(posVel.position, fixedGmst);
positions.push(
Cesium.Cartesian3.fromDegrees(
degreesLong(geo.longitude),
degreesLat(geo.latitude),
geo.height * 1000,
geo.cartesian3.fromDegrees(
degreesLong(geodetic.longitude),
degreesLat(geodetic.latitude),
geodetic.height * 1000,
),
);
} catch {
+4 -4
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { satelliteClassLabel } from '../../data/satelliteClass.js';
import { ISS_NORAD } from './policy.js';
@@ -62,11 +62,11 @@ export function createRecords({ state, parts }) {
}
const point = state._points.get(noradId);
if (!point?.position) return null;
const carto = Cesium.Cartographic.fromCartesian(point.position);
const carto = geo.cartographic.fromCartesian(point.position);
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),
altitudeM: carto.height,
speedMps: null,
};
+5 -5
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
export function createState({ services }) {
const { setOverlayEntries, setOverlaySourceVisible, clearOverlaySource } =
@@ -58,7 +58,7 @@ export function createState({ services }) {
state._activeFocusCount = 0;
state._scratchFocusScreen = new Cesium.Cartesian2();
state._scratchFocusScreen = geo.cartesian2.create();
state._enabled = false;
@@ -141,7 +141,7 @@ export function createState({ services }) {
state._trackedFrameGeo = null;
// { longitude, latitude, altitude } or null
state._trackedFrameCartesian = new Cesium.Cartesian3();
state._trackedFrameCartesian = geo.cartesian3.create();
/** Optional deterministic clock used only by the production-frame test seam. */
@@ -149,9 +149,9 @@ export function createState({ services }) {
// Scratch variables
state._scratchCartesian = new Cesium.Cartesian3();
state._scratchCartesian = geo.cartesian3.create();
state._scratchRingRotation = new Cesium.Matrix3();
state._scratchRingRotation = geo.matrix3.create();
/** Update the explicit model only when the rounded altitude line changes. */
/** Epoch of the last shared-context refresh for the tracked satellite. */
+5 -5
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { viewAreaRadiusKm } from './viewArea.js';
/**
@@ -18,16 +18,16 @@ export function vesselViewArea(viewer) {
if (canvas?.clientWidth && canvas?.clientHeight) {
try {
const hit = camera.pickEllipsoid(
new Cesium.Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2),
geo.cartesian2.create(canvas.clientWidth / 2, canvas.clientHeight / 2),
scene.globe?.ellipsoid,
);
if (hit) center = Cesium.Cartographic.fromCartesian(hit);
if (hit) center = geo.cartographic.fromCartesian(hit);
} catch {
/* the camera position stands in */
}
}
const lat = Cesium.Math.toDegrees(center.latitude);
const lon = Cesium.Math.toDegrees(center.longitude);
const lat = geo.math.toDegrees(center.latitude);
const lon = geo.math.toDegrees(center.longitude);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
return { lat, lon, radiusKm };
}
+2 -2
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
AIS_DEGRADED_STATUSES,
AIS_HEALTHY_STATUSES,
@@ -316,7 +316,7 @@ export function createQueries({
continue;
const position = visual.billboard?.position || visual.position;
if (!position) continue;
const distanceM = Cesium.Cartesian3.distance(centerCartesian, position);
const distanceM = geo.cartesian3.distance(centerCartesian, position);
if (!Number.isFinite(distanceM) || distanceM > range) continue;
entries.push({
mmsi: record.mmsi,
+2 -2
View File
@@ -1,6 +1,6 @@
import { createVesselFeed } from './ingestion.js';
import { VesselRecords } from './records.js';
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { DEFAULT_AIS_RUNTIME } from './policy.js';
export function createVesselState({ source, services }) {
@@ -18,7 +18,7 @@ export function createVesselState({ source, services }) {
vesselState._lastCamPoseSig = '';
vesselState._scratchFocusScreen = new Cesium.Cartesian2();
vesselState._scratchFocusScreen = geo.cartesian2.create();
vesselState.DEFAULT_VESSEL_OVERLAY_HOST = Object.freeze({
setEntries: setOverlayEntries,
+5 -6
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
TRAIL_HEIGHT_M,
VESSEL_LIFT_M,
@@ -44,7 +44,7 @@ export function createTracking({
currentGeoidN(record.lat, record.lon),
TRAIL_HEIGHT_M,
);
return Cesium.Cartesian3.fromDegrees(record.lon, record.lat, heightM);
return geo.cartesian3.fromDegrees(record.lon, record.lat, heightM);
}
/**
@@ -66,7 +66,7 @@ export function createTracking({
currentGeoidN(record.lat, record.lon),
VESSEL_LIFT_M,
);
visual.position = Cesium.Cartesian3.fromDegrees(
visual.position = geo.cartesian3.fromDegrees(
record.lon,
record.lat,
heightM,
@@ -146,7 +146,7 @@ export function createTracking({
currentGeoidN(lat, lon),
TRAIL_HEIGHT_M,
);
older.push(Cesium.Cartesian3.fromDegrees(lon, lat, heightM));
older.push(geo.cartesian3.fromDegrees(lon, lat, heightM));
}
if (!older.length) return;
@@ -170,8 +170,7 @@ export function createTracking({
const next = vesselTrailPosition(record);
if (!next) return;
const last = state.trailPositions[state.trailPositions.length - 1];
if (last && Cesium.Cartesian3.distance(last, next) <= TRAIL_MIN_MOVE_M)
return;
if (last && geo.cartesian3.distance(last, next) <= TRAIL_MIN_MOVE_M) return;
state.trailPositions.push(next);
if (state.trailPositions.length > TRAIL_MAX_POINTS)
state.trailPositions.shift();