diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index fc7308ab5..34c075eae 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -668,7 +668,21 @@ "src/ui/localSdrControls.js", "src/layers/localAdsb/feedNames.js", "src/ui/localSdrPresentation.js", - "src/data/cameraArrival.js" + "src/data/cameraArrival.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"] }, @@ -828,7 +842,21 @@ "src/layers/flights/snapshotRenderer.js", "src/layers/flights/modelSpec.js", "src/data/inputOwnership.js", - "src/data/modelEligibility.js" + "src/data/modelEligibility.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"] }, @@ -867,7 +895,21 @@ "src/layers/military/recordPolicy.js", "src/layers/military/snapshotRenderer.js", "src/data/inputOwnership.js", - "src/data/modelEligibility.js" + "src/data/modelEligibility.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"] }, @@ -894,7 +936,21 @@ "src/data/trackingClickGesture.js", "src/layers/aircraft/classification.js", "src/layers/aircraft/index.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"] }, @@ -920,7 +976,21 @@ "src/layers/vessels/records.js", "src/layers/vessels/recordPolicy.js", "src/layers/vessels/snapshotRenderer.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"] }, @@ -966,7 +1036,21 @@ "src/layers/firms/source.js", "src/layers/firms/state.js", "src/layers/firms/viewport.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"] }, @@ -1081,7 +1165,21 @@ "src/layers/awareness/state.js", "src/layers/awareness/subject.js", "src/layers/awareness/testing.js", - "src/ui/panelRailGeometry.js" + "src/ui/panelRailGeometry.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"] }, @@ -1186,7 +1284,21 @@ "src/overlays/worldOverlayTokens.js", "src/data/cctvFootprint.js", "src/data/inputOwnership.js", - "src/layers/cctv/sourcePolicy.js" + "src/layers/cctv/sourcePolicy.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": ["hls.js", "cesium"] }, @@ -1287,7 +1399,21 @@ "src/layers/radio/tuningNoise.js", "src/layers/radio/volume.js", "src/ui/radioTunerModel.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"] }, @@ -2209,7 +2335,21 @@ "src/tools/functions.js", "src/voice/actionSchemas.js", "src/view/index.js", - "src/app/embed.js" + "src/app/embed.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": [ "hls.js", @@ -2736,7 +2876,21 @@ "src/data/local_data/osm_military_names/names.json", "src/layers/installations/namedMarkers.js", "src/data/infrastructureOverlayEntry.js", - "src/sources/infrastructureData.js" + "src/sources/infrastructureData.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": [ "hls.js", @@ -2801,7 +2955,21 @@ "src/sources/overpassFeatures.js", "src/sources/featureSource.js", "src/sources/capability.js", - "src/data/cameraArrival.js" + "src/data/cameraArrival.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", @@ -3238,7 +3406,21 @@ "src/sources/overpassFeaturesRecords.js", "src/sources/overpassFeatures.js", "src/sources/featureSource.js", - "src/sources/capability.js" + "src/sources/capability.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", "egm96-universal", "pbf"] }, @@ -3275,7 +3457,21 @@ "scripts/qa-transit-controls.mjs", "scripts/qa-transit-browser.mjs", "src/layers/transit/source.js", - "src/sources/transitHistory.js" + "src/sources/transitHistory.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", "pbf"] }, diff --git a/src/camera.js b/src/camera.js index 3941c9d48..261f7c543 100644 --- a/src/camera.js +++ b/src/camera.js @@ -1,4 +1,5 @@ import * as Cesium from 'cesium'; +import * as geo from './geo/index.js'; /** * Camera presets for notable locations. @@ -6,26 +7,26 @@ import * as Cesium from 'cesium'; */ export const CAMERA_PRESETS = { austin: { - destination: Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 800), + destination: geo.cartesian3.fromDegrees(-97.7431, 30.2672, 800), orientation: { - heading: Cesium.Math.toRadians(0), - pitch: Cesium.Math.toRadians(-35), + heading: geo.math.toRadians(0), + pitch: geo.math.toRadians(-35), roll: 0.0, }, }, sf: { - destination: Cesium.Cartesian3.fromDegrees(-122.4194, 37.7749, 1000), + destination: geo.cartesian3.fromDegrees(-122.4194, 37.7749, 1000), orientation: { - heading: Cesium.Math.toRadians(30), - pitch: Cesium.Math.toRadians(-30), + heading: geo.math.toRadians(30), + pitch: geo.math.toRadians(-30), roll: 0.0, }, }, nyc: { - destination: Cesium.Cartesian3.fromDegrees(-73.9857, 40.7484, 1200), + destination: geo.cartesian3.fromDegrees(-73.9857, 40.7484, 1200), orientation: { - heading: Cesium.Math.toRadians(-20), - pitch: Cesium.Math.toRadians(-30), + heading: geo.math.toRadians(-20), + pitch: geo.math.toRadians(-30), roll: 0.0, }, }, @@ -53,10 +54,10 @@ export function flyToPreset(viewer, presetName, duration = 3.0) { export function flyToAustin(viewer) { // Start from a high altitude, then fly down viewer.camera.setView({ - destination: Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 25000), + destination: geo.cartesian3.fromDegrees(-97.7431, 30.2672, 25000), orientation: { - heading: Cesium.Math.toRadians(0), - pitch: Cesium.Math.toRadians(-90), + heading: geo.math.toRadians(0), + pitch: geo.math.toRadians(-90), roll: 0.0, }, }); @@ -65,10 +66,10 @@ export function flyToAustin(viewer) { const timer = setTimeout(() => { if (viewer.isDestroyed()) return; viewer.camera.flyTo({ - destination: Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 600), + destination: geo.cartesian3.fromDegrees(-97.7431, 30.2672, 600), orientation: { - heading: Cesium.Math.toRadians(15), - pitch: Cesium.Math.toRadians(-30), + heading: geo.math.toRadians(15), + pitch: geo.math.toRadians(-30), roll: 0.0, }, duration: 4.0, diff --git a/src/cameraGroundGuard.js b/src/cameraGroundGuard.js index 5a5fa3775..d69c92190 100644 --- a/src/cameraGroundGuard.js +++ b/src/cameraGroundGuard.js @@ -15,6 +15,7 @@ * held, so the correction reads as the shot settling. */ import * as Cesium from 'cesium'; +import * as geo from './geo/index.js'; import { ownCameraArrival } from './data/cameraArrival.js'; /** Minimum eye height above the rendered surface for a usable view. */ @@ -25,7 +26,7 @@ export const GUARD_INTERVAL_MS = 600; /** Ignore sub-metre noise rather than nudging the camera forever. */ const GUARD_EPSILON_M = 2; -const probeScratch = new Cesium.Cartographic(); +const probeScratch = geo.cartographic.create(); /** * Highest rendered surface across a few cells, with how many cells answered. @@ -41,7 +42,7 @@ function sampleSurfaceM(scene, cells) { for (const cell of cells) { try { const height = scene.sampleHeight( - Cesium.Cartographic.fromDegrees(cell.lon, cell.lat, 0, probeScratch), + geo.cartographic.fromDegrees(cell.lon, cell.lat, 0, probeScratch), ); if (!Number.isFinite(height)) continue; sampled += 1; @@ -152,8 +153,8 @@ export function guardCameraAboveGround(viewer, at, options = {}) { const carto = camera.positionCartographic; // The eye must clear both the framed target and the ground beneath it. const below = { - lat: Cesium.Math.toDegrees(carto.latitude), - lon: Cesium.Math.toDegrees(carto.longitude), + lat: geo.math.toDegrees(carto.latitude), + lon: geo.math.toDegrees(carto.longitude), }; const { heightM, sampled } = sampleSurfaceM(viewer.scene, [ { lat: at.lat, lon: at.lon }, @@ -168,7 +169,7 @@ export function guardCameraAboveGround(viewer, at, options = {}) { if (deficit > 0) { stop(); camera.flyTo({ - destination: Cesium.Cartesian3.fromRadians( + destination: geo.cartesian3.fromRadians( carto.longitude, carto.latitude, carto.height + deficit, diff --git a/src/cameraVerbs.js b/src/cameraVerbs.js index 278a86206..78ad8fac1 100644 --- a/src/cameraVerbs.js +++ b/src/cameraVerbs.js @@ -16,6 +16,7 @@ */ import * as Cesium from 'cesium'; +import * as geo from './geo/index.js'; import { holdContinuousRender, releaseContinuousRender, @@ -36,8 +37,8 @@ const ONCE = { panViewFraction: 0.25, durationS: 0.9, }; -const PITCH_MIN = Cesium.Math.toRadians(-89); -const PITCH_MAX = Cesium.Math.toRadians(-5); +const PITCH_MIN = geo.math.toRadians(-89); +const PITCH_MAX = geo.math.toRadians(-5); /* ── Route dolly: cinematic tuning ────────────────────────────────────────── * Every knob an owner may want to retune lives in this block. The shaping is @@ -296,30 +297,30 @@ export const ROUTE_CINEMA = Object.freeze({ /* ── Route dolly geometry ────────────────────────────────────────────────── */ -const _arcA = new Cesium.Cartesian3(); -const _arcB = new Cesium.Cartesian3(); -const _chordVertical = new Cesium.Cartesian3(); -const _turnCross = new Cesium.Cartesian3(); -const _framePos = new Cesium.Cartesian3(); -const _frameAheadPos = new Cesium.Cartesian3(); -const _frameUp = new Cesium.Cartesian3(); -const _frameIn = new Cesium.Cartesian3(); -const _frameOut = new Cesium.Cartesian3(); -const _frameGaze = new Cesium.Cartesian3(); -const _frameRight = new Cesium.Cartesian3(); -const _frameLevelUp = new Cesium.Cartesian3(); -const _frameDir = new Cesium.Cartesian3(); -const _frameEye = new Cesium.Cartesian3(); -const _frameBankedUp = new Cesium.Cartesian3(); -const _frameCarto = new Cesium.Cartographic(); -const _frameAheadCarto = new Cesium.Cartographic(); -const _bankQuat = new Cesium.Quaternion(); -const _bankMatrix = new Cesium.Matrix3(); -const _headingQuat = new Cesium.Quaternion(); -const _headingMatrix = new Cesium.Matrix3(); -const _warmPos = new Cesium.Cartesian3(); -const _warmCarto = new Cesium.Cartographic(); -const _probeCarto = new Cesium.Cartographic(); +const _arcA = geo.cartesian3.create(); +const _arcB = geo.cartesian3.create(); +const _chordVertical = geo.cartesian3.create(); +const _turnCross = geo.cartesian3.create(); +const _framePos = geo.cartesian3.create(); +const _frameAheadPos = geo.cartesian3.create(); +const _frameUp = geo.cartesian3.create(); +const _frameIn = geo.cartesian3.create(); +const _frameOut = geo.cartesian3.create(); +const _frameGaze = geo.cartesian3.create(); +const _frameRight = geo.cartesian3.create(); +const _frameLevelUp = geo.cartesian3.create(); +const _frameDir = geo.cartesian3.create(); +const _frameEye = geo.cartesian3.create(); +const _frameBankedUp = geo.cartesian3.create(); +const _frameCarto = geo.cartographic.create(); +const _frameAheadCarto = geo.cartographic.create(); +const _bankQuat = geo.quaternion.create(); +const _bankMatrix = geo.matrix3.create(); +const _headingQuat = geo.quaternion.create(); +const _headingMatrix = geo.matrix3.create(); +const _warmPos = geo.cartesian3.create(); +const _warmCarto = geo.cartographic.create(); +const _probeCarto = geo.cartographic.create(); /** Point at arc length `s` along the polyline (binary search — queries jump * backwards for the inbound chord, so a forward cursor would not do). */ @@ -336,23 +337,23 @@ function arcPoint(state, s, result) { const segLen = cumM[lo + 1] - cumM[lo]; const t = segLen > 1e-6 ? Math.min(1, Math.max(0, (clamped - cumM[lo]) / segLen)) : 0; - return Cesium.Cartesian3.lerp(pts[lo], pts[lo + 1], t, result); + return geo.cartesian3.lerp(pts[lo], pts[lo + 1], t, result); } /** Unit horizontal direction from arc `sA` to arc `sB`, or null when degenerate. */ function horizontalChord(state, sA, sB, up, result) { const a = arcPoint(state, sA, _arcA); const b = arcPoint(state, sB, _arcB); - Cesium.Cartesian3.subtract(b, a, result); - const vertical = Cesium.Cartesian3.multiplyByScalar( + geo.cartesian3.subtract(b, a, result); + const vertical = geo.cartesian3.multiplyByScalar( up, - Cesium.Cartesian3.dot(result, up), + geo.cartesian3.dot(result, up), _chordVertical, ); - Cesium.Cartesian3.subtract(result, vertical, result); - const len = Cesium.Cartesian3.magnitude(result); + geo.cartesian3.subtract(result, vertical, result); + const len = geo.cartesian3.magnitude(result); if (!(len > 1e-3)) return null; - return Cesium.Cartesian3.divideByScalar(result, len, result); + return geo.cartesian3.divideByScalar(result, len, result); } /** @@ -368,9 +369,9 @@ function horizontalChord(state, sA, sB, up, result) { * @returns {number} Radians, + = right. */ export function signedTurnRad(a, b, up) { - const cross = Cesium.Cartesian3.cross(a, b, _turnCross); - const sinLeft = Cesium.Cartesian3.dot(cross, up); - const cosTurn = Cesium.Cartesian3.dot(a, b); + const cross = geo.cartesian3.cross(a, b, _turnCross); + const sinLeft = geo.cartesian3.dot(cross, up); + const cosTurn = geo.cartesian3.dot(a, b); if (cosTurn <= -1 + ANTIPODAL_EPS && Math.abs(sinLeft) < ANTIPODAL_EPS) return Math.PI; return -Math.atan2(sinLeft, cosTurn); @@ -403,13 +404,13 @@ export function routeCorridorCells( s += ROUTE_WARM_STEP_M ) { const point = arcPoint(state, s, _warmPos); - const carto = Cesium.Cartographic.fromCartesian( + const carto = geo.cartographic.fromCartesian( point, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, _warmCarto, ); - const lat = Number(Cesium.Math.toDegrees(carto.latitude).toFixed(3)); - const lon = Number(Cesium.Math.toDegrees(carto.longitude).toFixed(3)); + const lat = Number(geo.math.toDegrees(carto.latitude).toFixed(3)); + const lon = Number(geo.math.toDegrees(carto.longitude).toFixed(3)); const key = `${lat},${lon}`; if (seen.has(key)) continue; seen.add(key); @@ -555,7 +556,7 @@ export function probeMeshFloorM(scene, cells) { let sampled = 0; for (const cell of cells) { try { - const carto = Cesium.Cartographic.fromDegrees( + const carto = geo.cartographic.fromDegrees( cell.lon, cell.lat, 0, @@ -604,9 +605,9 @@ export function createRouteFlight({ // the mean: routes under 10 m (slow) / 20 m (normal) / 45 m (fast) fly slower // than asked. Those are shorter than the camera is tall — nothing to see. const durationS = Math.max(0.5, totalM / cruiseMps); - const pathHeightM = Cesium.Cartographic.fromCartesian( + const pathHeightM = geo.cartographic.fromCartesian( pts[0], - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, _warmCarto, ).height; const state = { @@ -702,12 +703,12 @@ export function advanceRouteFlight(state, dt) { state.groundSpeedMps = cruiseMps * profile.speed; const pos = arcPoint(state, traveled, _framePos); - const carto = Cesium.Cartographic.fromCartesian( + const carto = geo.cartographic.fromCartesian( pos, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, _frameCarto, ); - const up = Cesium.Ellipsoid.WGS84.geodeticSurfaceNormalCartographic( + const up = geo.ellipsoid.WGS84.geodeticSurfaceNormalCartographic( carto, _frameUp, ); @@ -731,7 +732,7 @@ export function advanceRouteFlight(state, dt) { ); const turnRateDegS = inbound && outbound - ? Cesium.Math.toDegrees(signedTurnRad(inbound, outbound, up)) / + ? geo.math.toDegrees(signedTurnRad(inbound, outbound, up)) / ROUTE_BANK_WINDOW_S : 0; const bankTarget = routeBankTargetDeg(turnRateDegS, state.reducedMotion); @@ -763,8 +764,7 @@ export function advanceRouteFlight(state, dt) { horizontalChord(state, traveled, traveled + lookaheadM, up, _frameGaze) || outbound || inbound; - if (gaze && !state.headingDir) - state.headingDir = Cesium.Cartesian3.clone(gaze); + if (gaze && !state.headingDir) state.headingDir = geo.cartesian3.clone(gaze); else if (gaze) { // Rotate the heading TOWARD the gaze about the local up, rather than // lerping the two vectors. A Cartesian lerp cannot cross an antipodal pair @@ -775,66 +775,62 @@ export function advanceRouteFlight(state, dt) { const turnToGaze = signedTurnRad(state.headingDir, gaze, up); // signedTurnRad is + for a RIGHT turn; a positive rotation about up is a // LEFT turn, so the step is negated. - const rotation = Cesium.Matrix3.fromQuaternion( - Cesium.Quaternion.fromAxisAngle(up, -turnToGaze * k, _headingQuat), + const rotation = geo.matrix3.fromQuaternion( + geo.quaternion.fromAxisAngle(up, -turnToGaze * k, _headingQuat), _headingMatrix, ); - const turned = Cesium.Matrix3.multiplyByVector( + const turned = geo.matrix3.multiplyByVector( rotation, state.headingDir, _frameDir, ); - const vertical = Cesium.Cartesian3.multiplyByScalar( + const vertical = geo.cartesian3.multiplyByScalar( up, - Cesium.Cartesian3.dot(turned, up), + geo.cartesian3.dot(turned, up), _chordVertical, ); - Cesium.Cartesian3.subtract(turned, vertical, turned); - if (Cesium.Cartesian3.magnitude(turned) > 1e-6) { - Cesium.Cartesian3.normalize(turned, state.headingDir); + geo.cartesian3.subtract(turned, vertical, turned); + if (geo.cartesian3.magnitude(turned) > 1e-6) { + geo.cartesian3.normalize(turned, state.headingDir); } } // A path with no horizontal extent (identical lat/lon, differing heights) // has no azimuth to fly: point east so the frame stays orthonormal. if (!state.headingDir) { - const east = Cesium.Cartesian3.fromElements(-pos.y, pos.x, 0, _frameGaze); + const east = geo.cartesian3.fromElements(-pos.y, pos.x, 0, _frameGaze); state.headingDir = - Cesium.Cartesian3.magnitude(east) > 1e-6 - ? Cesium.Cartesian3.normalize(east, new Cesium.Cartesian3()) - : Cesium.Cartesian3.fromElements(1, 0, 0, new Cesium.Cartesian3()); + geo.cartesian3.magnitude(east) > 1e-6 + ? geo.cartesian3.normalize(east, geo.cartesian3.create()) + : geo.cartesian3.fromElements(1, 0, 0, geo.cartesian3.create()); } const heading = state.headingDir; // Locked pitch: heading gives the azimuth, the pitch never wanders. - const pitch = Cesium.Math.toRadians(ROUTE_PITCH_DEG); - const direction = Cesium.Cartesian3.normalize( - Cesium.Cartesian3.add( - Cesium.Cartesian3.multiplyByScalar(heading, Math.cos(pitch), _frameDir), - Cesium.Cartesian3.multiplyByScalar(up, Math.sin(pitch), _frameLevelUp), + const pitch = geo.math.toRadians(ROUTE_PITCH_DEG); + const direction = geo.cartesian3.normalize( + geo.cartesian3.add( + geo.cartesian3.multiplyByScalar(heading, Math.cos(pitch), _frameDir), + geo.cartesian3.multiplyByScalar(up, Math.sin(pitch), _frameLevelUp), _frameDir, ), _frameDir, ); // Orthonormal camera frame, then roll it about its own forward axis: a // positive rotation drops the right wing, which is what a right turn wants. - const right = Cesium.Cartesian3.normalize( - Cesium.Cartesian3.cross(direction, up, _frameRight), + const right = geo.cartesian3.normalize( + geo.cartesian3.cross(direction, up, _frameRight), _frameRight, ); - const levelUp = Cesium.Cartesian3.normalize( - Cesium.Cartesian3.cross(right, direction, _frameLevelUp), + const levelUp = geo.cartesian3.normalize( + geo.cartesian3.cross(right, direction, _frameLevelUp), _frameLevelUp, ); - const bankRad = Cesium.Math.toRadians(appliedBankDeg); + const bankRad = geo.math.toRadians(appliedBankDeg); let bankedUp = levelUp; if (Math.abs(bankRad) > 1e-6) { - const quat = Cesium.Quaternion.fromAxisAngle(direction, bankRad, _bankQuat); - const rotation = Cesium.Matrix3.fromQuaternion(quat, _bankMatrix); - bankedUp = Cesium.Matrix3.multiplyByVector( - rotation, - levelUp, - _frameBankedUp, - ); + const quat = geo.quaternion.fromAxisAngle(direction, bankRad, _bankQuat); + const rotation = geo.matrix3.fromQuaternion(quat, _bankMatrix); + bankedUp = geo.matrix3.multiplyByVector(rotation, levelUp, _frameBankedUp); } // Keep the corridor ahead of the camera warm. The batch dedupes to coarse @@ -850,19 +846,19 @@ export function advanceRouteFlight(state, dt) { let sampledFloor = Number.NaN; if (typeof state.floorFn === 'function') { const aheadPos = arcPoint(state, traveled + lookaheadM / 2, _frameAheadPos); - const aheadCarto = Cesium.Cartographic.fromCartesian( + const aheadCarto = geo.cartographic.fromCartesian( aheadPos, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, _frameAheadCarto, ); try { const here = state.floorFn( - Cesium.Math.toDegrees(carto.latitude), - Cesium.Math.toDegrees(carto.longitude), + geo.math.toDegrees(carto.latitude), + geo.math.toDegrees(carto.longitude), ); const ahead = state.floorFn( - Cesium.Math.toDegrees(aheadCarto.latitude), - Cesium.Math.toDegrees(aheadCarto.longitude), + geo.math.toDegrees(aheadCarto.latitude), + geo.math.toDegrees(aheadCarto.longitude), ); const warm = [here, ahead].filter((value) => Number.isFinite(value)); if (warm.length) sampledFloor = Math.max(...warm); @@ -926,11 +922,11 @@ export function advanceRouteFlight(state, dt) { ); } state.appliedHeightM = heightM; - const eye = Cesium.Cartesian3.fromRadians( + const eye = geo.cartesian3.fromRadians( carto.longitude, carto.latitude, heightM, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, _frameEye, ); @@ -1019,7 +1015,7 @@ function levelCameraRoll() { try { const cam = _viewer?.camera; if (!cam || typeof cam.setView !== 'function') return false; - const roll = Cesium.Math.negativePiToPi(cam.roll || 0); + const roll = geo.math.negativePiToPi(cam.roll || 0); if (Math.abs(roll) < 1e-5) return false; cam.setView({ orientation: { heading: cam.heading, pitch: cam.pitch, roll: 0 }, @@ -1123,9 +1119,9 @@ function onTick() { const target = _getTarget?.(_viewer); if (!target) return; const cam2 = _viewer.camera; - const range = Cesium.Cartesian3.distance(cam2.positionWC, target); - const carto2 = Cesium.Cartographic.fromCartesian(cam2.positionWC); - const tCarto2 = Cesium.Cartographic.fromCartesian(target); + const range = geo.cartesian3.distance(cam2.positionWC, target); + const carto2 = geo.cartographic.fromCartesian(cam2.positionWC); + const tCarto2 = geo.cartographic.fromCartesian(target); const dh2 = carto2.height - tCarto2.height; m.target = target; m.hpr = new Cesium.HeadingPitchRange( @@ -1140,12 +1136,11 @@ function onTick() { } if (m.kind === 'orbit') { const rate = - Cesium.Math.toRadians(ORBIT_DEG_S[m.speed]) * + geo.math.toRadians(ORBIT_DEG_S[m.speed]) * (m.direction === 'left' ? -1 : 1); if (m.mode === 'once') { const total = - Cesium.Math.toRadians(ONCE.orbitDeg) * - (m.direction === 'left' ? -1 : 1); + geo.math.toRadians(ONCE.orbitDeg) * (m.direction === 'left' ? -1 : 1); m.hpr.heading = m.hprStartHeading + total * scale; } else { m.hpr.heading += rate * dt; @@ -1166,11 +1161,11 @@ function onTick() { const degS = TILT_DEG_S[m.speed]; const stepRad = m.mode === 'once' - ? Cesium.Math.toRadians( + ? geo.math.toRadians( m.kind === 'tilt' ? ONCE.tiltDeg : ONCE.rotateDeg, ) * (scale - (m.lastScale || 0)) - : Cesium.Math.toRadians(degS) * dt; + : geo.math.toRadians(degS) * dt; m.lastScale = scale; if (m.kind === 'tilt') { const up = m.direction === 'up'; @@ -1298,7 +1293,7 @@ export function moveCamera(args = {}, runNavigation = null) { // Honest limits: at the clamp a tilt is a no-op — say so instead of // reporting success and letting the model gaslight itself (field test). const pitch = _viewer.camera.pitch; - if (direction === 'up' && pitch >= PITCH_MAX - Cesium.Math.toRadians(0.5)) { + if (direction === 'up' && pitch >= PITCH_MAX - geo.math.toRadians(0.5)) { return { ok: false, action: 'move_camera', @@ -1306,10 +1301,7 @@ export function moveCamera(args = {}, runNavigation = null) { 'Already at the upper tilt limit (near the horizon) — tilt down, or zoom/pan instead.', }; } - if ( - direction === 'down' && - pitch <= PITCH_MIN + Cesium.Math.toRadians(0.5) - ) { + if (direction === 'down' && pitch <= PITCH_MIN + geo.math.toRadians(0.5)) { return { ok: false, action: 'move_camera', @@ -1389,10 +1381,10 @@ export function moveCamera(args = {}, runNavigation = null) { }; } const cam = _viewer.camera; - const range = Cesium.Cartesian3.distance(cam.positionWC, target); + const range = geo.cartesian3.distance(cam.positionWC, target); // Pitch from geometry so lookAt reproduces the CURRENT framing exactly. - const carto = Cesium.Cartographic.fromCartesian(cam.positionWC); - const tCarto = Cesium.Cartographic.fromCartesian(target); + const carto = geo.cartographic.fromCartesian(cam.positionWC); + const tCarto = geo.cartographic.fromCartesian(target); const dh = carto.height - tCarto.height; const pitch = -Math.asin( Math.min(1, Math.max(-1, dh / Math.max(1, range))), @@ -1505,11 +1497,11 @@ export function flyRoute( }; } const pts = route.path.map((p) => - Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height || 0), + geo.cartesian3.fromDegrees(p.lon, p.lat, p.height || 0), ); const cumM = [0]; for (let i = 1; i < pts.length; i += 1) { - cumM.push(cumM[i - 1] + Cesium.Cartesian3.distance(pts[i - 1], pts[i])); + cumM.push(cumM[i - 1] + geo.cartesian3.distance(pts[i - 1], pts[i])); } if (!Number.isFinite(cumM.at(-1)) || cumM.at(-1) <= 0) { return { diff --git a/src/cockpitCloudEffects.js b/src/cockpitCloudEffects.js index 053217810..536a44e90 100644 --- a/src/cockpitCloudEffects.js +++ b/src/cockpitCloudEffects.js @@ -1,5 +1,5 @@ import { applicationServices } from './services/application.js'; -import * as Cesium from 'cesium'; +import * as geo from './geo/index.js'; import { deriveWeatherEffectProfile, weatherAltitudeFactors, @@ -133,10 +133,10 @@ function greatCircleM(a, b) { ) ) return Infinity; - const latitudeA = Cesium.Math.toRadians(a.latitude); - const latitudeB = Cesium.Math.toRadians(b.latitude); - const latitudeDelta = Cesium.Math.toRadians(b.latitude - a.latitude); - const longitudeDelta = Cesium.Math.toRadians(b.longitude - a.longitude); + const latitudeA = geo.math.toRadians(a.latitude); + const latitudeB = geo.math.toRadians(b.latitude); + const latitudeDelta = geo.math.toRadians(b.latitude - a.latitude); + const longitudeDelta = geo.math.toRadians(b.longitude - a.longitude); const haversine = Math.sin(latitudeDelta / 2) ** 2 + Math.cos(latitudeA) * @@ -342,8 +342,8 @@ export class CockpitCloudEffectsController { cameraPoint() { const cartographic = this.viewer?.camera?.positionCartographic; if (!cartographic) return null; - const latitude = Cesium.Math.toDegrees(cartographic.latitude); - const longitude = Cesium.Math.toDegrees(cartographic.longitude); + const latitude = geo.math.toDegrees(cartographic.latitude); + const longitude = geo.math.toDegrees(cartographic.longitude); if (![latitude, longitude].every(Number.isFinite)) return null; return { latitude, @@ -541,7 +541,7 @@ export class CockpitCloudEffectsController { render(timeSec) { const gl = this.gl; if (!gl || !this.program || !this.locations) return; - const windRadians = Cesium.Math.toRadians(this.windDirectionDeg); + const windRadians = geo.math.toRadians(this.windDirectionDeg); const windScale = 0.008 + this.windStrength * 0.022; gl.useProgram(this.program); gl.uniform2f( diff --git a/src/data/iconOrientation.js b/src/data/iconOrientation.js index 857f1aeec..e633cf45b 100644 --- a/src/data/iconOrientation.js +++ b/src/data/iconOrientation.js @@ -22,6 +22,7 @@ * unverified math swap, decides whether exact projection becomes preferred. */ import * as Cesium from 'cesium'; +import * as geo from '../geo/index.js'; /** Meters used to give the course vector a stable projection magnitude. */ const FORWARD_PROBE_M = 2000; @@ -31,11 +32,11 @@ const FORWARD_PROBE_M = 2000; */ const MIN_SCREEN_COMPONENT_M = 0.5; /** Ignore sub-degree projection noise while retaining deliberate camera-orbit rotation. */ -const ROTATION_DEADBAND_RAD = Cesium.Math.toRadians(0.5); +const ROTATION_DEADBAND_RAD = geo.math.toRadians(0.5); -const _scratchEnu = new Cesium.Matrix4(); -const _scratchForward = new Cesium.Cartesian3(); -const _scratchWorldForward = new Cesium.Cartesian3(); +const _scratchEnu = geo.matrix4.create(); +const _scratchForward = geo.cartesian3.create(); +const _scratchWorldForward = geo.cartesian3.create(); /** * Computes the billboard rotation (radians, CCW-positive, for @@ -58,19 +59,19 @@ export function screenProjectedRotation( const camera = scene?.camera; if (!camera?.rightWC || !camera?.upWC || !position) return previous; - const courseRad = Cesium.Math.toRadians(courseDeg || 0); - Cesium.Cartesian3.fromElements( + const courseRad = geo.math.toRadians(courseDeg || 0); + geo.cartesian3.fromElements( Math.sin(courseRad) * FORWARD_PROBE_M, Math.cos(courseRad) * FORWARD_PROBE_M, 0, _scratchForward, ); - const enu = Cesium.Transforms.eastNorthUpToFixedFrame( + const enu = geo.transforms.eastNorthUpToFixedFrame( position, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, _scratchEnu, ); - Cesium.Matrix4.multiplyByPointAsVector( + geo.matrix4.multiplyByPointAsVector( enu, _scratchForward, _scratchWorldForward, @@ -79,8 +80,8 @@ export function screenProjectedRotation( // Screen x follows camera-right. Window y grows downward, the opposite of // camera-up. Projecting the vector itself avoids clipping/behind-camera // failure modes from the old forward-point worldToWindowCoordinates probe. - const dx = Cesium.Cartesian3.dot(_scratchWorldForward, camera.rightWC); - const dy = -Cesium.Cartesian3.dot(_scratchWorldForward, camera.upWC); + const dx = geo.cartesian3.dot(_scratchWorldForward, camera.rightWC); + const dy = -geo.cartesian3.dot(_scratchWorldForward, camera.upWC); if (dx * dx + dy * dy < MIN_SCREEN_COMPONENT_M * MIN_SCREEN_COMPONENT_M) return previous; @@ -129,7 +130,7 @@ const _occluder = new Cesium.EllipsoidalOccluder( */ export const HORIZON_FEATHER_RAD = 0.019; -const _wgs84OneOverRadii = Cesium.Ellipsoid.WGS84.oneOverRadii; +const _wgs84OneOverRadii = geo.ellipsoid.WGS84.oneOverRadii; /** * How much SKY sits behind a world point from this camera: 0 (planet behind) @@ -271,9 +272,9 @@ export function cameraPoseSignature(camera) { * frustum the vehicle is nowhere near. */ const NEAR_PROBE_M = 30; -const _scratchProbe = new Cesium.Cartesian3(); -const _scratchWindowAt = new Cesium.Cartesian2(); -const _scratchWindowAhead = new Cesium.Cartesian2(); +const _scratchProbe = geo.cartesian3.create(); +const _scratchWindowAt = geo.cartesian2.create(); +const _scratchWindowAhead = geo.cartesian2.create(); /** * Billboard rotation for a SURFACE contact, projected exactly — perspective @@ -311,24 +312,24 @@ export function perspectiveProjectedRotation( if (!scene?.camera || !position || !Number.isFinite(courseDeg)) { return previous; } - const courseRad = Cesium.Math.toRadians(courseDeg); - Cesium.Cartesian3.fromElements( + const courseRad = geo.math.toRadians(courseDeg); + geo.cartesian3.fromElements( Math.sin(courseRad) * NEAR_PROBE_M, Math.cos(courseRad) * NEAR_PROBE_M, 0, _scratchForward, ); - const enu = Cesium.Transforms.eastNorthUpToFixedFrame( + const enu = geo.transforms.eastNorthUpToFixedFrame( position, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, _scratchEnu, ); - Cesium.Matrix4.multiplyByPointAsVector( + geo.matrix4.multiplyByPointAsVector( enu, _scratchForward, _scratchWorldForward, ); - Cesium.Cartesian3.add(position, _scratchWorldForward, _scratchProbe); + geo.cartesian3.add(position, _scratchWorldForward, _scratchProbe); const at = Cesium.SceneTransforms.worldToWindowCoordinates( scene, diff --git a/src/data/modelVisualAnchor.js b/src/data/modelVisualAnchor.js index 733cd5049..613332618 100644 --- a/src/data/modelVisualAnchor.js +++ b/src/data/modelVisualAnchor.js @@ -1,4 +1,5 @@ import * as Cesium from 'cesium'; +import * as geo from '../geo/index.js'; /** * Model-space visual centres measured from the shipped GLBs' scene-space AABBs. * @@ -76,13 +77,13 @@ export const MODEL_TRAIL_ANCHOR_NATIVE = Object.freeze({ * 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 = Cesium.Matrix4.multiplyTransformation( +const AXIS_CORRECTION = geo.matrix4.multiplyTransformation( Cesium.Axis.Y_UP_TO_Z_UP, Cesium.Axis.Z_UP_TO_X_UP, - new Cesium.Matrix4(), + geo.matrix4.create(), ); -const _anchorScratch = new Cesium.Cartesian3(); -const _chainScratch = new Cesium.Matrix4(); +const _anchorScratch = geo.cartesian3.create(); +const _chainScratch = geo.matrix4.create(); /** * World position of a model-local anchor, through the SAME transform chain @@ -121,17 +122,17 @@ export function modelAnchorWorld(model, nativeAnchor, result) { // correction. Read from the instance rather than assumed, so an asset with a // baked root transform is carried too. const components = model?.sceneGraph?.components?.transform; - Cesium.Matrix4.multiplyTransformation( + geo.matrix4.multiplyTransformation( model.modelMatrix, - components || Cesium.Matrix4.IDENTITY, + components || geo.matrix4.IDENTITY, _chainScratch, ); - Cesium.Matrix4.multiplyTransformation( + geo.matrix4.multiplyTransformation( _chainScratch, AXIS_CORRECTION, _chainScratch, ); - return Cesium.Matrix4.multiplyByPoint(_chainScratch, _anchorScratch, result); + return geo.matrix4.multiplyByPoint(_chainScratch, _anchorScratch, result); } /** diff --git a/src/data/motionModel.js b/src/data/motionModel.js index eb6020850..5b985240d 100644 --- a/src/data/motionModel.js +++ b/src/data/motionModel.js @@ -7,10 +7,11 @@ * skylight (https://github.com/cpaczek/skylight, MIT) shared/src/aim.ts. */ import * as Cesium from 'cesium'; +import * as geo from '../geo/index.js'; const DEG = Math.PI / 180; -const _mmCarto = new Cesium.Cartographic(); -const _mmCandidateCarto = new Cesium.Cartographic(); +const _mmCarto = geo.cartographic.create(); +const _mmCandidateCarto = geo.cartographic.create(); export function norm360(deg) { return ((deg % 360) + 360) % 360; @@ -28,22 +29,22 @@ export function norm360(deg) { */ export function liftRepeatedGroundFix(newest, candidatePosition, onGround) { if (!onGround || !newest?.position || !candidatePosition) return false; - const stored = Cesium.Cartographic.fromCartesian( + const stored = geo.cartographic.fromCartesian( newest.position, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, _mmCarto, ); - const candidate = Cesium.Cartographic.fromCartesian( + const candidate = geo.cartographic.fromCartesian( candidatePosition, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, _mmCandidateCarto, ); if (!stored || !candidate || candidate.height <= stored.height) return false; - Cesium.Cartesian3.fromRadians( + geo.cartesian3.fromRadians( stored.longitude, stored.latitude, candidate.height, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, newest.position, ); return true; @@ -66,9 +67,9 @@ export function courseBetweenCartesians(from, to, minChordM = 25) { const dy = to.y - from.y; const dz = to.z - from.z; if (dx * dx + dy * dy + dz * dz < minChordM * minChordM) return null; - const carto = Cesium.Cartographic.fromCartesian( + const carto = geo.cartographic.fromCartesian( from, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, _mmCarto, ); if (!carto) return null; @@ -436,8 +437,8 @@ export function arcOffsetEnu(speedMps, trackDeg, turnRateDps, dtSec, result) { } const _forwardFixArc = { east: 0, north: 0, endCourseDeg: 0 }; -const _forwardFixEnu = new Cesium.Matrix4(); -const _forwardFixOffset = new Cesium.Cartesian3(); +const _forwardFixEnu = geo.matrix4.create(); +const _forwardFixOffset = geo.cartesian3.create(); /** * Create a forward-only synthetic fix when a repeated position epoch carries @@ -474,21 +475,21 @@ export function synthesizeForwardKinematicsFix(newest, next = {}) { dtSec, _forwardFixArc, ); - Cesium.Cartesian3.fromElements( + geo.cartesian3.fromElements( _forwardFixArc.east, _forwardFixArc.north, 0, _forwardFixOffset, ); - const enu = Cesium.Transforms.eastNorthUpToFixedFrame( + const enu = geo.transforms.eastNorthUpToFixedFrame( newest.position, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, _forwardFixEnu, ); - const position = Cesium.Matrix4.multiplyByPoint( + const position = geo.matrix4.multiplyByPoint( enu, _forwardFixOffset, - new Cesium.Cartesian3(), + geo.cartesian3.create(), ); return { time: Cesium.JulianDate.fromDate(new Date(next.epochMs)), diff --git a/src/data/trackedCamera.js b/src/data/trackedCamera.js index b5991835d..3a1d9f1e2 100644 --- a/src/data/trackedCamera.js +++ b/src/data/trackedCamera.js @@ -1,4 +1,4 @@ -import * as Cesium from 'cesium'; +import * as geo from '../geo/index.js'; const MAX_FRAME_ATTEMPTS = 120; const MIN_TRACKED_RANGE_M = 150; @@ -59,7 +59,7 @@ export function trackedModelScaleForPixelCap({ export function trackedDisplayPositionForCamera(entity, time, result) { const displayedPosition = entity?.gevDisplayPosition?.(); return displayedPosition - ? Cesium.Cartesian3.clone(displayedPosition, result) + ? geo.cartesian3.clone(displayedPosition, result) : entity?.position?.getValue(time, result); } @@ -106,15 +106,15 @@ export function clampTrackedCameraPosition( minimumRangeM = MIN_TRACKED_RANGE_M, ) { const crossedOrigin = - Cesium.Cartesian3.dot(camera.position, previousPosition) <= 0; - const forwardDistance = -Cesium.Cartesian3.dot( + geo.cartesian3.dot(camera.position, previousPosition) <= 0; + const forwardDistance = -geo.cartesian3.dot( camera.position, camera.direction, ); - const rangeSquared = Cesium.Cartesian3.magnitudeSquared(camera.position); + const rangeSquared = geo.cartesian3.magnitudeSquared(camera.position); if (crossedOrigin) { - Cesium.Cartesian3.normalize(previousPosition, camera.position); - Cesium.Cartesian3.multiplyByScalar( + geo.cartesian3.normalize(previousPosition, camera.position); + geo.cartesian3.multiplyByScalar( camera.position, minimumRangeM, camera.position, @@ -125,7 +125,7 @@ export function clampTrackedCameraPosition( forwardDistance < minimumRangeM || rangeSquared < minimumRangeM * minimumRangeM ) { - Cesium.Cartesian3.multiplyByScalar( + geo.cartesian3.multiplyByScalar( camera.direction, -minimumRangeM, camera.position, @@ -155,11 +155,11 @@ export function clampTrackedCameraPosition( export function applyTrackedCameraFrame(viewer, entity, viewFrom) { if (!viewer || !entity || !viewFrom) return; let attempts = 0; - const trackedTransform = new Cesium.Matrix4(); - const trackedPosition = new Cesium.Cartesian3(); - const resolvedViewFrom = new Cesium.Cartesian3(); - const cameraOffset = new Cesium.Cartesian3(); - const previousCameraPosition = new Cesium.Cartesian3(); + const trackedTransform = geo.matrix4.create(); + const trackedPosition = geo.cartesian3.create(); + const resolvedViewFrom = geo.cartesian3.create(); + const cameraOffset = geo.cartesian3.create(); + const previousCameraPosition = geo.cartesian3.create(); let framed = false; const restoreZoomInertia = acquireStableTrackedZoom( viewer.scene.screenSpaceCameraController, @@ -201,24 +201,24 @@ export function applyTrackedCameraFrame(viewer, entity, viewFrom) { if (attempts >= MAX_FRAME_ATTEMPTS) stop(); return; } - const transform = Cesium.Transforms.eastNorthUpToFixedFrame( + const transform = geo.transforms.eastNorthUpToFixedFrame( position, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, trackedTransform, ); const offset = typeof viewFrom.getValue === 'function' ? viewFrom.getValue(viewer.clock.currentTime, resolvedViewFrom) - : Cesium.Cartesian3.clone(viewFrom, resolvedViewFrom); + : geo.cartesian3.clone(viewFrom, resolvedViewFrom); if ( !offset || - Cesium.Cartesian3.magnitudeSquared(offset) < Cesium.Math.EPSILON12 + geo.cartesian3.magnitudeSquared(offset) < geo.math.EPSILON12 ) { stop(); return; } viewer.camera.lookAtTransform(transform, offset); - Cesium.Cartesian3.clone(viewer.camera.position, previousCameraPosition); + geo.cartesian3.clone(viewer.camera.position, previousCameraPosition); framed = true; return; } @@ -227,10 +227,10 @@ export function applyTrackedCameraFrame(viewer, entity, viewFrom) { // continuous camera writer. Only a programmatic zoom that bypasses the // controller's minimumZoomDistance needs correction. if (clampTrackedCameraPosition(viewer.camera, previousCameraPosition)) { - Cesium.Cartesian3.clone(viewer.camera.position, cameraOffset); + geo.cartesian3.clone(viewer.camera.position, cameraOffset); viewer.camera.lookAtTransform(viewer.camera.transform, cameraOffset); } - Cesium.Cartesian3.clone(viewer.camera.position, previousCameraPosition); + geo.cartesian3.clone(viewer.camera.position, previousCameraPosition); }); return stop; } diff --git a/src/hud.js b/src/hud.js index eb8c0ab34..b8190556b 100644 --- a/src/hud.js +++ b/src/hud.js @@ -14,7 +14,7 @@ import { applicationServices } from './services/application.js'; * Color theming is driven by the active shader mode via CSS custom properties. */ -import * as Cesium from 'cesium'; +import * as geo from './geo/index.js'; import { forward as toMGRS } from 'mgrs'; import { CITY_POIS } from './locations.js'; import { composeLocalityTag } from './hudLocality.js'; @@ -341,8 +341,8 @@ export class IntelHUD { const cartographic = camera.positionCartographic; if (!cartographic) return; - const lonDeg = Cesium.Math.toDegrees(cartographic.longitude); - const latDeg = Cesium.Math.toDegrees(cartographic.latitude); + const lonDeg = geo.math.toDegrees(cartographic.longitude); + const latDeg = geo.math.toDegrees(cartographic.latitude); const altM = cartographic.height; const latDMS = this._toDMS(latDeg, 'lat'); const lonDMS = this._toDMS(lonDeg, 'lon'); @@ -407,7 +407,7 @@ export class IntelHUD { // Off-nadir angle (ONA): camera pitch of -90 deg is nadir (straight down), // so ONA = 90 + pitch gives 0 at nadir and increases toward the horizon. - const pitchDeg = Cesium.Math.toDegrees(camera.pitch); + const pitchDeg = geo.math.toDegrees(camera.pitch); const ona = Math.max(0, 90 + pitchDeg); const onaEl = document.getElementById('hud-ona'); if (onaEl) onaEl.textContent = `ONA: ${ona.toFixed(1)}°`; @@ -512,19 +512,19 @@ export class IntelHUD { const declination = 23.45 * Math.sin( - Cesium.Math.toRadians( + geo.math.toRadians( (360 / 365) * (now.getUTCDate() + 30 * now.getUTCMonth() - 81), ), ); - const latRad = Cesium.Math.toRadians(lat); - const decRad = Cesium.Math.toRadians(declination); - const haRad = Cesium.Math.toRadians(hourAngle); + const latRad = geo.math.toRadians(lat); + const decRad = geo.math.toRadians(declination); + const haRad = geo.math.toRadians(hourAngle); // Standard formula: sin(el) = sin(lat)*sin(dec) + cos(lat)*cos(dec)*cos(ha) const sinEl = Math.sin(latRad) * Math.sin(decRad) + Math.cos(latRad) * Math.cos(decRad) * Math.cos(haRad); // Clamp to [-1,1] to guard against floating-point drift before asin - return Cesium.Math.toDegrees(Math.asin(Math.max(-1, Math.min(1, sinEl)))); + return geo.math.toDegrees(Math.asin(Math.max(-1, Math.min(1, sinEl)))); } /** @@ -571,10 +571,10 @@ export class IntelHUD { _viewWindowKm(latDeg) { const rect = this.viewer.camera.computeViewRectangle(); if (!rect) return null; - const north = Cesium.Math.toDegrees(rect.north); - const south = Cesium.Math.toDegrees(rect.south); - let east = Cesium.Math.toDegrees(rect.east); - let west = Cesium.Math.toDegrees(rect.west); + const north = geo.math.toDegrees(rect.north); + const south = geo.math.toDegrees(rect.south); + let east = geo.math.toDegrees(rect.east); + let west = geo.math.toDegrees(rect.west); let lonSpan = Math.abs(east - west); // Handle antimeridian wrap: if span exceeds 180 deg, take the shorter arc if (lonSpan > 180) lonSpan = 360 - lonSpan; @@ -583,7 +583,7 @@ export class IntelHUD { // longitude distance is scaled by cos(lat) to account for meridian convergence. const widthKm = Math.max( 0, - lonSpan * 111 * Math.cos(Cesium.Math.toRadians(latDeg)), + lonSpan * 111 * Math.cos(geo.math.toRadians(latDeg)), ); const heightKm = Math.max(0, latSpan * 111); return { widthKm, heightKm }; @@ -599,7 +599,7 @@ export class IntelHUD { * @returns {number} Distance in kilometers. */ _haversineKm(lat1, lon1, lat2, lon2) { - const toRad = (deg) => Cesium.Math.toRadians(deg); + const toRad = (deg) => geo.math.toRadians(deg); const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = diff --git a/src/layers/flights/snapshotRenderer.js b/src/layers/flights/snapshotRenderer.js index f6e74b125..e0c4b202d 100644 --- a/src/layers/flights/snapshotRenderer.js +++ b/src/layers/flights/snapshotRenderer.js @@ -147,7 +147,10 @@ export function createFlightSnapshotRenderer({ history.push(synthetic); if (history.length > POSITION_HISTORY_LIMIT) history.shift(); meta.turnRateDps = turnRateFromFixHistory(history); - if (isTracked) tracking._appendTrailFix(synthetic.position.clone()); + if (isTracked) + tracking._appendTrailFix( + Cesium.Cartesian3.clone(synthetic.position), + ); } } } diff --git a/src/layers/military/snapshotRenderer.js b/src/layers/military/snapshotRenderer.js index 58eb57972..8d1d9392b 100644 --- a/src/layers/military/snapshotRenderer.js +++ b/src/layers/military/snapshotRenderer.js @@ -118,7 +118,10 @@ export function createMilitarySnapshotRenderer({ history.push(synthetic); if (history.length > POSITION_HISTORY_LIMIT) history.shift(); meta.turnRateDps = turnRateFromFixHistory(history); - if (isTracked) tracking._appendTrailFix(synthetic.position.clone()); + if (isTracked) + tracking._appendTrailFix( + Cesium.Cartesian3.clone(synthetic.position), + ); } } } diff --git a/src/locations.js b/src/locations.js index 02bd853fb..389632f9e 100644 --- a/src/locations.js +++ b/src/locations.js @@ -2,6 +2,7 @@ import { isUnavailableCapability } from './sources/capability.js'; import { createOverpassFeatureSource } from './sources/overpassFeatures.js'; import { applicationServices } from './services/application.js'; import * as Cesium from 'cesium'; +import * as geo from './geo/index.js'; import { viewportBias, placesNearViewRecovery, @@ -543,18 +544,18 @@ export const GLOBE_VIEW = Object.freeze({ */ export function flyToGlobeView(viewer, options = {}) { const carto = viewer.camera.positionCartographic; - const longitude = Cesium.Math.toDegrees(carto.longitude); - const latitude = Cesium.Math.toDegrees(carto.latitude); + const longitude = geo.math.toDegrees(carto.longitude); + const latitude = geo.math.toDegrees(carto.latitude); viewer.camera.cancelFlight(); viewer.camera.flyTo({ - destination: Cesium.Cartesian3.fromDegrees( + destination: geo.cartesian3.fromDegrees( longitude, latitude, GLOBE_VIEW.heightM, ), orientation: { heading: 0, - pitch: Cesium.Math.toRadians(GLOBE_VIEW.pitchDeg), + pitch: geo.math.toRadians(GLOBE_VIEW.pitchDeg), roll: 0, }, duration: finitePositive(options.duration) || GLOBE_VIEW.durationS, @@ -614,7 +615,7 @@ export function flyToLandmark(viewer, lat, lon, options = {}) { } = options; // Sample terrain height (sync — uses loaded tiles; 0 if globe/terrain not ready) - const targetCartographic = Cesium.Cartographic.fromDegrees(lon, lat); + const targetCartographic = geo.cartographic.fromDegrees(lon, lat); const sampledHeight = viewer.scene.globe?.getHeight(targetCartographic); // Use sampled height if available, otherwise the cached ground-floor @@ -641,7 +642,7 @@ export function flyToLandmark(viewer, lat, lon, options = {}) { const targetHeight = bounds ? terrainHeight + bounds.height / 2 : terrainHeight + buildingHeight; - const targetPosition = Cesium.Cartesian3.fromDegrees(lon, lat, targetHeight); + const targetPosition = geo.cartesian3.fromDegrees(lon, lat, targetHeight); const boundingRadius = bounds ? buildingBoundingRadius(bounds) : 0; const framingRange = bounds ? Math.max( @@ -651,8 +652,8 @@ export function flyToLandmark(viewer, lat, lon, options = {}) { : range; const hpr = new Cesium.HeadingPitchRange( - Cesium.Math.toRadians(heading), - Cesium.Math.toRadians(pitch), + geo.math.toRadians(heading), + geo.math.toRadians(pitch), framingRange, ); @@ -1095,7 +1096,7 @@ export function viewportMetrics(viewport) { const latSpanKm = Math.abs(latSpanDeg) * KM_PER_DEGREE; const lonSpanKm = - lonSpanDeg * KM_PER_DEGREE * Math.cos(Cesium.Math.toRadians(centerLat)); + lonSpanDeg * KM_PER_DEGREE * Math.cos(geo.math.toRadians(centerLat)); return { latSpanDeg, lonSpanDeg, @@ -1157,12 +1158,12 @@ const PLACE_FALLBACK_HALF_SPAN_KM = 20; /** Great-circle distance in km (small enough here that the spherical model is fine). */ function greatCircleKm(lat1, lng1, lat2, lng2) { - const dLat = Cesium.Math.toRadians(lat2 - lat1); - const dLng = Cesium.Math.toRadians(lng2 - lng1); + const dLat = geo.math.toRadians(lat2 - lat1); + const dLng = geo.math.toRadians(lng2 - lng1); const a = Math.sin(dLat / 2) ** 2 + - Math.cos(Cesium.Math.toRadians(lat1)) * - Math.cos(Cesium.Math.toRadians(lat2)) * + Math.cos(geo.math.toRadians(lat1)) * + Math.cos(geo.math.toRadians(lat2)) * Math.sin(dLng / 2) ** 2; return 6371 * 2 * Math.asin(Math.min(1, Math.sqrt(a))); } @@ -1210,7 +1211,7 @@ export function placeFramingViewport( const latHalfDeg = PLACE_FALLBACK_HALF_SPAN_KM / KM_PER_DEGREE; // Guard the cosine so a near-polar anchor cannot blow the longitude half-extent up. - const cosLat = Math.max(0.05, Math.cos(Cesium.Math.toRadians(anchorLat))); + const cosLat = Math.max(0.05, Math.cos(geo.math.toRadians(anchorLat))); const lngHalfDeg = PLACE_FALLBACK_HALF_SPAN_KM / (KM_PER_DEGREE * cosLat); const wrapLng = (lng) => ((((lng + 180) % 360) + 360) % 360) - 180; return { @@ -1349,7 +1350,7 @@ function flyToViewportBounds(viewer, viewport, options = {}) { // Same short-way-round rule for the reported centre: averaging raw longitudes // puts a dateline-crossing box's centre on the opposite side of the planet. return { - targetPosition: Cesium.Cartesian3.fromDegrees( + targetPosition: geo.cartesian3.fromDegrees( metrics.centerLng, metrics.centerLat, 0, @@ -1379,7 +1380,7 @@ function buildingBoundingRadius(bounds) { function rangeForBoundingSphere(viewer, radius) { const frustum = viewer.camera.frustum; - const verticalFov = Number(frustum?.fov) || Cesium.Math.toRadians(60); + const verticalFov = Number(frustum?.fov) || geo.math.toRadians(60); const aspectRatio = Math.max(0.5, Number(frustum?.aspectRatio) || 1); const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * aspectRatio); const limitingFov = Math.min(verticalFov, horizontalFov); @@ -1537,7 +1538,7 @@ function pointInPolygon(lon, lat, coordinates) { function approximateDistanceM(latA, lonA, latB, lonB) { const latitudeScale = 111320; const longitudeScale = - latitudeScale * Math.cos(Cesium.Math.toRadians((latA + latB) / 2)); + latitudeScale * Math.cos(geo.math.toRadians((latA + latB) / 2)); return Math.hypot( (latB - latA) * latitudeScale, (lonB - lonA) * longitudeScale, diff --git a/src/orbit.js b/src/orbit.js index 7a8220566..02317a463 100644 --- a/src/orbit.js +++ b/src/orbit.js @@ -1,4 +1,5 @@ import * as Cesium from 'cesium'; +import * as geo from './geo/index.js'; import { holdContinuousRender, releaseContinuousRender, @@ -16,7 +17,7 @@ export class OrbitController { this.target = null; this.radius = 500; this.pitch = -30; - this.speed = Cesium.Math.toRadians(6); // ~6°/sec → full rotation in ~60s + this.speed = geo.math.toRadians(6); // ~6°/sec → full rotation in ~60s this.angle = 0; this._removeListener = null; } @@ -35,7 +36,7 @@ export class OrbitController { this.target = targetCartesian; this.radius = options.radius || this.radius; this.pitch = options.pitch || this.pitch; - this.speed = Cesium.Math.toRadians(options.speed || 6); + this.speed = geo.math.toRadians(options.speed || 6); this.active = true; // Orbit mutates the camera from preRender — without a hold the loop // starves after its first idle frame. (perf wave 2) @@ -56,7 +57,7 @@ export class OrbitController { const hpr = new Cesium.HeadingPitchRange( this.angle, - Cesium.Math.toRadians(this.pitch), + geo.math.toRadians(this.pitch), this.radius, ); this.viewer.camera.lookAt(this.target, hpr); diff --git a/src/services/groundSnap.js b/src/services/groundSnap.js index 159cd66fd..2100b19ce 100644 --- a/src/services/groundSnap.js +++ b/src/services/groundSnap.js @@ -46,7 +46,7 @@ * are per-layer, keyed by that layer's icao space). */ -import * as Cesium from 'cesium'; +import * as geo from '../geo/index.js'; /** Taxi threshold: a cached snap answers directly for moves up to this far from * the sampled spot (m). Past it the value is demoted to a held last-known and a @@ -178,13 +178,13 @@ export function createGroundSnap({ groundFloor }) { const entries = new Map(); let windowStartMs = 0; let windowCount = 0; - const scratchCarto = new Cesium.Cartographic(); - const scratchSurfacePos = new Cesium.Cartesian3(); + const scratchCarto = geo.cartographic.create(); + const scratchSurfacePos = geo.cartesian3.create(); /** Separate from `scratchCarto`: the held-evidence check runs on paths that * return BEFORE the sample writes `scratchCarto`, and on the sampling path it * must not disturb the cartographic that `reportValidatedMeshFloorCell` reads * back after the sample lands. */ - const scratchHeldCarto = new Cesium.Cartographic(); + const scratchHeldCarto = geo.cartographic.create(); /** * The last measurement for a contact whose fresh snap has been demoted — it @@ -205,7 +205,7 @@ export function createGroundSnap({ groundFloor }) { if (!entry || !entry.held || entry.h == null || !entry.samplePos) return null; if ( - Cesium.Cartesian3.distanceSquared(surfacePos, entry.samplePos) > + geo.cartesian3.distanceSquared(surfacePos, entry.samplePos) > HELD_SNAP_MAX_DRIFT_M * HELD_SNAP_MAX_DRIFT_M ) { return dropHold(entry); @@ -253,15 +253,15 @@ export function createGroundSnap({ groundFloor }) { * @returns {number|null} */ function freshMeasuredFloorAt(surfacePos) { - const carto = Cesium.Cartographic.fromCartesian( + const carto = geo.cartographic.fromCartesian( surfacePos, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, scratchHeldCarto, ); if (!carto) return null; return cachedMeshFloor( - Cesium.Math.toDegrees(carto.latitude), - Cesium.Math.toDegrees(carto.longitude), + geo.math.toDegrees(carto.latitude), + geo.math.toDegrees(carto.longitude), ); } @@ -284,7 +284,7 @@ export function createGroundSnap({ groundFloor }) { * @returns {number|null} */ function heightFor(viewer, icao, pos, getExclusions) { - const surfacePos = Cesium.Ellipsoid.WGS84.scaleToGeodeticSurface( + const surfacePos = geo.ellipsoid.WGS84.scaleToGeodeticSurface( pos, scratchSurfacePos, ); @@ -292,7 +292,7 @@ export function createGroundSnap({ groundFloor }) { const cached = entries.get(icao); if (cached && cached.h != null && cached.samplePos && !cached.held) { if ( - Cesium.Cartesian3.distanceSquared(surfacePos, cached.samplePos) <= + geo.cartesian3.distanceSquared(surfacePos, cached.samplePos) <= MOVE_INVALIDATE_M * MOVE_INVALIDATE_M ) { return cached.h; @@ -339,9 +339,9 @@ export function createGroundSnap({ groundFloor }) { windowCount += 1; let sampled; try { - const carto = Cesium.Cartographic.fromCartesian( + const carto = geo.cartographic.fromCartesian( pos, - Cesium.Ellipsoid.WGS84, + geo.ellipsoid.WGS84, scratchCarto, ); // sampleHeight throws when unsupported (no depth textures) — that's a miss. @@ -357,8 +357,8 @@ export function createGroundSnap({ groundFloor }) { if (!Number.isFinite(sampled) || sampled < -150) return miss(); if (meshFloorPreferred()) { reportValidatedMeshFloorCell( - Cesium.Math.toDegrees(scratchCarto.latitude), - Cesium.Math.toDegrees(scratchCarto.longitude), + geo.math.toDegrees(scratchCarto.latitude), + geo.math.toDegrees(scratchCarto.longitude), sampled, ); } @@ -366,7 +366,7 @@ export function createGroundSnap({ groundFloor }) { // contact is now, and it outranks a memory of where it was. entries.set(icao, { h: sampled, - samplePos: Cesium.Cartesian3.clone(surfacePos), + samplePos: geo.cartesian3.clone(surfacePos), held: false, nextRetryMs: 0, misses: 0, diff --git a/src/sharelink.js b/src/sharelink.js index e2a9e41b2..5e62dd266 100644 --- a/src/sharelink.js +++ b/src/sharelink.js @@ -1,4 +1,5 @@ import * as Cesium from 'cesium'; +import * as geo from './geo/index.js'; import { BLOOM_INTENSITY_DEFAULT, BLOOM_SCALE_VERSION } from './bloom.js'; import { migrateDetectionState, @@ -263,15 +264,11 @@ export class ShareLinkManager { if (this._destroyed || !state) return { succeeded: false, reason: 'unavailable' }; const view = { - destination: Cesium.Cartesian3.fromDegrees( - state.lon, - state.lat, - state.alt, - ), + destination: geo.cartesian3.fromDegrees(state.lon, state.lat, state.alt), orientation: { - heading: Cesium.Math.toRadians(state.heading), - pitch: Cesium.Math.toRadians(state.pitch), - roll: Cesium.Math.toRadians(state.roll), + heading: geo.math.toRadians(state.heading), + pitch: geo.math.toRadians(state.pitch), + roll: geo.math.toRadians(state.roll), }, }; let cameraPromise = Promise.resolve({ @@ -548,21 +545,18 @@ export class ShareLinkManager { const params = new URLSearchParams(); params.set('v', '2'); - params.set('lat', Cesium.Math.toDegrees(carto.latitude).toFixed(4)); - params.set('lon', Cesium.Math.toDegrees(carto.longitude).toFixed(4)); + params.set('lat', geo.math.toDegrees(carto.latitude).toFixed(4)); + params.set('lon', geo.math.toDegrees(carto.longitude).toFixed(4)); params.set('alt', Math.round(carto.height).toString()); params.set( 'heading', - Math.round(Cesium.Math.toDegrees(camera.heading)).toString(), + Math.round(geo.math.toDegrees(camera.heading)).toString(), ); params.set( 'pitch', - Math.round(Cesium.Math.toDegrees(camera.pitch)).toString(), - ); - params.set( - 'roll', - Math.round(Cesium.Math.toDegrees(camera.roll)).toString(), + Math.round(geo.math.toDegrees(camera.pitch)).toString(), ); + params.set('roll', Math.round(geo.math.toDegrees(camera.roll)).toString()); params.set('style', STYLE_TO_URL[this._currentStyle] || 'normal'); params.set('bloom', this._bloomEnabled ? '1' : '0'); params.set('sharpen', this._sharpenEnabled ? '1' : '0'); diff --git a/src/worldFocus.js b/src/worldFocus.js index 5b5ca7a8e..ebdc93649 100644 --- a/src/worldFocus.js +++ b/src/worldFocus.js @@ -2,6 +2,7 @@ * One-click camera transfer for layer-owned world targets. */ import * as Cesium from 'cesium'; +import * as geo from './geo/index.js'; export const WORLD_FOCUS_REQUEST_EVENT = 'gev:world-request-focus'; export const WORLD_CLICK_FOCUS_DURATION_SEC = 1.9; @@ -26,10 +27,10 @@ export function isValidWorldFocusTarget(detail) { // Vessel and fire targets are surface-anchored Earth positions. Merely // finite coordinates near the ECEF origin cannot be flown to, and must be // rejected before the camera policy releases a current follow owner. - const magnitude = Cesium.Cartesian3.magnitude(position); + const magnitude = geo.cartesian3.magnitude(position); return ( Number.isFinite(magnitude) && - magnitude >= Cesium.Ellipsoid.WGS84.minimumRadius * 0.95 + magnitude >= geo.ellipsoid.WGS84.minimumRadius * 0.95 ); } @@ -85,7 +86,7 @@ export function flyToWorldTarget(viewer, target = {}) { { offset: new Cesium.HeadingPitchRange( heading, - Cesium.Math.toRadians(framing.pitchDeg), + geo.math.toRadians(framing.pitchDeg), framing.rangeM, ), duration,