ootk-core
Version:
Orbital Object Toolkit. A modern typed replacement for satellite.js including SGP4 propagation, TLE parsing, Sun and Moon calculations, and more.
327 lines (293 loc) • 11.3 kB
text/typescript
/**
* @author Theodore Kruczek.
* @license MIT
* @copyright (c) 2022-2025 Theodore Kruczek Permission is
* hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the
* Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { Minutes, PositionVelocity, Degrees, Kilometers, Radians, Seconds, KilometersPerSecond, Earth }
from '../main.js';
import { Vector3D } from '../operations/Vector3D.js';
import { EpochUTC } from '../time/EpochUTC.js';
import { earthGravityParam, MINUTES_PER_DAY, RAD2DEG, sec2min, TAU } from '../utils/constants.js';
import { clamp, matchHalfPlane, newtonNu } from '../utils/functions.js';
import { EquinoctialElements } from './EquinoctialElements.js';
import { OrbitRegime } from '../enums/OrbitRegime.js';
import { StateVector } from './StateVector.js';
import { ClassicalElementsParams } from '../interfaces/ClassicalElementsParams.js';
/**
* The ClassicalElements class represents the classical orbital elements of an object.
* @example
* ```ts
* const epoch = EpochUTC.fromDateTime(new Date('2024-01-14T14:39:39.914Z'));
* const elements = new ClassicalElements({
* epoch,
* semimajorAxis: 6943.547853722985 as Kilometers,
* eccentricity: 0.0011235968124658146,
* inclination: 0.7509087232045765 as Radians,
* rightAscension: 0.028239555738616327 as Radians,
* argPerigee: 2.5386411901807353 as Radians,
* trueAnomaly: 0.5931399364974058 as Radians,
* });
* ```
*/
export class ClassicalElements {
epoch: EpochUTC;
semimajorAxis: Kilometers;
eccentricity: number;
inclination: Radians;
rightAscension: Radians;
argPerigee: Radians;
trueAnomaly: Radians;
/** Gravitational parameter in km³/s². */
mu: number;
constructor({
epoch,
semimajorAxis,
eccentricity,
inclination,
rightAscension,
argPerigee,
trueAnomaly,
mu = earthGravityParam,
}: ClassicalElementsParams) {
this.epoch = epoch;
this.semimajorAxis = semimajorAxis;
this.eccentricity = eccentricity;
this.inclination = inclination;
this.rightAscension = rightAscension;
this.argPerigee = argPerigee;
this.trueAnomaly = trueAnomaly;
this.mu = mu;
}
/**
* Creates a new instance of ClassicalElements from a StateVector.
* @param state The StateVector to convert.
* @param mu The gravitational parameter of the central body. Default value is Earth's gravitational parameter.
* @returns A new instance of ClassicalElements.
* @throws Error if the StateVector is not in an inertial frame.
*/
static fromStateVector(state: StateVector, mu = earthGravityParam): ClassicalElements {
if (!state.inertial) {
throw new Error('State vector must be in inertial frame (like J2000).');
}
const pos = state.position;
const vel = state.velocity;
const a = state.semimajorAxis;
const eVecA = pos.scale(vel.magnitude() ** 2 - mu / pos.magnitude() as KilometersPerSecond);
const eVecB = vel.scale(pos.dot(vel));
const eVec = eVecA.subtract(eVecB).scale(1 / mu);
const e = eVec.magnitude();
const h = pos.cross(vel);
const i = Math.acos(clamp(h.z / h.magnitude(), -1.0, 1.0)) as Radians;
const n = Vector3D.zAxis.cross(h);
let o = Math.acos(clamp(n.x / n.magnitude(), -1.0, 1.0)) as Radians;
if (n.y < 0) {
o = TAU - o as Radians;
}
let w = n.angle(eVec);
if (eVec.z < 0) {
w = TAU - w as Radians;
}
let v = eVec.angle(pos);
if (pos.dot(vel) < 0) {
v = TAU - v as Radians;
}
return new ClassicalElements({
epoch: state.epoch,
semimajorAxis: a,
eccentricity: e,
inclination: i,
rightAscension: o,
argPerigee: w,
trueAnomaly: v,
mu,
});
}
/**
* Gets the inclination in degrees.
* @returns The inclination in degrees.
*/
get inclinationDegrees(): Degrees {
return (this.inclination * RAD2DEG) as Degrees;
}
/**
* Gets the right ascension in degrees.
* @returns The right ascension in degrees.
*/
get rightAscensionDegrees(): Degrees {
return (this.rightAscension * RAD2DEG) as Degrees;
}
/**
* Gets the argument of perigee in degrees.
* @returns The argument of perigee in degrees.
*/
get argPerigeeDegrees(): Degrees {
return (this.argPerigee * RAD2DEG) as Degrees;
}
/**
* Gets the true anomaly in degrees.
* @returns The true anomaly in degrees.
*/
get trueAnomalyDegrees(): Degrees {
return (this.trueAnomaly * RAD2DEG) as Degrees;
}
/**
* Gets the apogee of the classical elements. It is measured from the surface of the earth.
* @returns The apogee in kilometers.
*/
get apogee(): Kilometers {
return (this.semimajorAxis * (1.0 + this.eccentricity) - Earth.radiusMean) as Kilometers;
}
/**
* Gets the perigee of the classical elements. The perigee is the point in an
* orbit that is closest to the surface of the earth.
* @returns The perigee distance in kilometers.
*/
get perigee(): number {
return (this.semimajorAxis * (1.0 - this.eccentricity) - Earth.radiusMean) as Kilometers;
}
toString(): string {
return [
'[ClassicalElements]',
` Epoch: ${this.epoch}`,
` Semimajor Axis (a): ${this.semimajorAxis.toFixed(4)} km`,
` Eccentricity (e): ${this.eccentricity.toFixed(7)}`,
` Inclination (i): ${this.inclinationDegrees.toFixed(4)}°`,
` Right Ascension (Ω): ${this.rightAscensionDegrees.toFixed(4)}°`,
` Argument of Perigee (ω): ${this.argPerigeeDegrees.toFixed(4)}°`,
` True Anomaly (ν): ${this.trueAnomalyDegrees.toFixed(4)}°`,
].join('\n');
}
/**
* Calculates the mean motion of the celestial object.
* @returns The mean motion in radians.
*/
get meanMotion(): Radians {
return Math.sqrt(this.mu / this.semimajorAxis ** 3) as Radians;
}
/**
* Calculates the period of the orbit.
* @returns The period in seconds.
*/
get period(): Minutes {
const periodSec = (TAU * Math.sqrt(this.semimajorAxis ** 3 / this.mu)) as Seconds;
return (periodSec / 60) as Minutes;
}
/**
* Compute the number of revolutions completed per day for this orbit.
* @returns The number of revolutions per day.
*/
get revsPerDay(): number {
return MINUTES_PER_DAY / this.period;
}
/**
* Returns the orbit regime based on the classical elements.
* @returns The orbit regime.
*/
getOrbitRegime(): OrbitRegime {
const n = this.revsPerDay;
const p = this.period * sec2min;
if (n >= 0.99 && n <= 1.01 && this.eccentricity < 0.01) {
return OrbitRegime.GEO;
}
if (p >= 600 && p <= 800 && this.eccentricity <= 0.25) {
return OrbitRegime.MEO;
}
if (n >= 11.25 && this.eccentricity <= 0.25) {
return OrbitRegime.LEO;
}
if (this.eccentricity > 0.25) {
return OrbitRegime.HEO;
}
return OrbitRegime.OTHER;
}
/**
* Converts the classical orbital elements to position and velocity vectors.
* @returns An object containing the position and velocity vectors.
*/
toPositionVelocity(): PositionVelocity {
const rVec = new Vector3D(Math.cos(this.trueAnomaly), Math.sin(this.trueAnomaly), 0.0);
const rPQW = rVec.scale(
(this.semimajorAxis * (1.0 - this.eccentricity ** 2)) / (1.0 + this.eccentricity * Math.cos(this.trueAnomaly)),
);
const vVec = new Vector3D(-Math.sin(this.trueAnomaly), this.eccentricity + Math.cos(this.trueAnomaly), 0.0);
const vPQW = vVec.scale(Math.sqrt(this.mu / (this.semimajorAxis * (1 - this.eccentricity ** 2))));
const position = rPQW
.rotZ(-this.argPerigee as Radians)
.rotX(-this.inclination)
.rotZ(-this.rightAscension as Radians) as Vector3D<Kilometers>;
const velocity = vPQW
.rotZ(-this.argPerigee as Radians)
.rotX(-this.inclination)
.rotZ(-this.rightAscension as Radians) as Vector3D<KilometersPerSecond>;
return { position, velocity };
}
/**
* Converts the classical elements to equinoctial elements.
* @returns The equinoctial elements.
*/
toEquinoctialElements(): EquinoctialElements {
const I = this.inclination > Math.PI / 2 ? -1 : 1;
const h = this.eccentricity * Math.sin(this.argPerigee + I * this.rightAscension);
const k = this.eccentricity * Math.cos(this.argPerigee + I * this.rightAscension);
const meanAnomaly = newtonNu(this.eccentricity, this.trueAnomaly).m;
const lambda = (meanAnomaly + this.argPerigee + I * this.rightAscension) as Radians;
const a = this.semimajorAxis;
const p = Math.tan(0.5 * this.inclination) ** I * Math.sin(this.rightAscension);
const q = Math.tan(0.5 * this.inclination) ** I * Math.cos(this.rightAscension);
return new EquinoctialElements({ epoch: this.epoch, k, h, lambda, a, p, q, mu: this.mu, I });
}
/**
* Propagates the classical elements to a given epoch.
* @param propEpoch - The epoch to propagate the classical elements to.
* @returns The classical elements at the propagated epoch.
*/
propagate(propEpoch: EpochUTC): ClassicalElements {
const t = this.epoch;
const n = this.meanMotion;
const delta = propEpoch.difference(t);
const cosV = Math.cos(this.trueAnomaly);
let eaInit = Math.acos(clamp((this.eccentricity + cosV) / (1 + this.eccentricity * cosV), -1, 1));
eaInit = matchHalfPlane(eaInit, this.trueAnomaly);
let maInit = eaInit - this.eccentricity * Math.sin(eaInit);
maInit = matchHalfPlane(maInit, eaInit);
const maFinal = (maInit + n * delta) % TAU;
let eaFinal = maFinal;
for (let iter = 0; iter < 32; iter++) {
const eaTemp = maFinal + this.eccentricity * Math.sin(eaFinal);
if (Math.abs(eaTemp - eaFinal) < 1e-12) {
break;
}
eaFinal = eaTemp;
}
const cosEaFinal = Math.cos(eaFinal);
let vFinal = clamp(Math.acos((cosEaFinal - this.eccentricity) / (1 - this.eccentricity * cosEaFinal)), -1, 1);
vFinal = matchHalfPlane(vFinal, eaFinal);
return new ClassicalElements({
epoch: propEpoch,
semimajorAxis: this.semimajorAxis,
eccentricity: this.eccentricity,
inclination: this.inclination,
rightAscension: this.rightAscension,
argPerigee: this.argPerigee,
trueAnomaly: vFinal as Radians,
mu: this.mu,
});
}
}