diff --git a/src/coordinate/ClassicalElements.ts b/src/coordinate/ClassicalElements.ts index 2ae25a8..cd696a9 100644 --- a/src/coordinate/ClassicalElements.ts +++ b/src/coordinate/ClassicalElements.ts @@ -1,4 +1,4 @@ -import { Degrees, Kilometers, Radians, Seconds } from 'src/main'; +import { Degrees, Kilometers, Radians, Seconds } from '../main'; import { Vector3D } from '../operations/Vector3D'; import { EpochUTC } from '../time/EpochUTC'; import { earthGravityParam, RAD2DEG, sec2min, secondsPerDay, TAU } from '../utils/constants'; @@ -6,19 +6,11 @@ import { clamp, matchHalfPlane, newtonNu } from '../utils/functions'; import { EquinoctialElements } from './EquinoctialElements'; import { OrbitRegime } from './OrbitRegime'; import { PositionVelocity, StateVector } from './StateVector'; +import { ClassicalElementsParams } from '../interfaces/ClassicalElementsParams'; -interface ClassicalElementsParams { - epoch: EpochUTC; - semimajorAxis: Kilometers; - eccentricity: number; - inclination: Radians; - rightAscension: Radians; - argPerigee: Radians; - trueAnomaly: Radians; - mu?: number; -} /** - * The ClassicalElements class represents the classical orbital elements of an object. + * The ClassicalElements class represents the classical orbital elements of an + * object. * * @example * ```ts @@ -67,10 +59,10 @@ export class ClassicalElements { /** * 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. + * @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) { @@ -155,8 +147,8 @@ export class ClassicalElements { } /** - * Gets the perigee of the classical elements. - * The perigee is the point in an orbit that is closest to the focus (in this case, the Earth). + * Gets the perigee of the classical elements. The perigee is the point in an + * orbit that is closest to the focus (in this case, the Earth). * @returns The perigee distance in kilometers. */ get perigee(): number { @@ -181,7 +173,7 @@ export class ClassicalElements { * @returns The mean motion in radians. */ get meanMotion(): Radians { - return Math.sqrt(this.mu / (this.semimajorAxis * this.semimajorAxis * this.semimajorAxis)) as Radians; + return Math.sqrt(this.mu / this.semimajorAxis ** 3) as Radians; } /** @@ -246,15 +238,16 @@ export class ClassicalElements { * @returns {EquinoctialElements} The equinoctial elements. */ toEquinoctialElements(): EquinoctialElements { - const fr = Math.abs(this.inclination - Math.PI) < 1e-9 ? -1 : 1; - const af = this.eccentricity * Math.cos(this.argPerigee + fr * this.rightAscension); - const ag = this.eccentricity * Math.sin(this.argPerigee + fr * this.rightAscension); - const l = this.argPerigee + fr * this.rightAscension + newtonNu(this.eccentricity, this.trueAnomaly).m; - const n = this.meanMotion; - const chi = Math.tan(0.5 * this.inclination) ** fr * Math.sin(this.rightAscension); - const psi = Math.tan(0.5 * this.inclination) ** fr * Math.cos(this.rightAscension); + 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(this.epoch, af, ag, l, n, chi, psi, { mu: this.mu, fr }); + return new EquinoctialElements({ epoch: this.epoch, k, h, lambda, a, p, q, mu: this.mu, I }); } /** diff --git a/src/coordinate/EquinoctialElements.ts b/src/coordinate/EquinoctialElements.ts index b0d745a..90c338d 100644 --- a/src/coordinate/EquinoctialElements.ts +++ b/src/coordinate/EquinoctialElements.ts @@ -1,77 +1,171 @@ -import { Kilometers, Radians } from 'src/main'; +import { Kilometers, Radians, Seconds } from '../main'; import { EpochUTC } from '../time/EpochUTC'; import { earthGravityParam, secondsPerDay, TAU } from '../utils/constants'; import { newtonM } from '../utils/functions'; import { ClassicalElements } from './ClassicalElements'; import { PositionVelocity } from './StateVector'; -/** Equinoctial element set. */ +import { EquinoctialElementsParams } from '../interfaces/EquinoctialElementsParams'; + +/** + * Equinoctial elements are a set of orbital elements used to describe the + * orbits of celestial bodies, such as satellites around a planet. They provide + * an alternative to the traditional Keplerian elements and are especially + * useful for avoiding singularities and numerical issues in certain types of + * orbits. + * + * Unlike Keplerian elements, equinoctial elements don't suffer from + * singularities at zero eccentricity (circular orbits) or zero inclination + * (equatorial orbits). This makes them more reliable for numerical simulations + * and analytical studies, especially in these edge cases. + * + * Reference: https://faculty.nps.edu/dad/orbital/th0.pdf + */ export class EquinoctialElements { + epoch: EpochUTC; + /** + * The semi-major axis of the orbit in kilometers. + */ + a: Kilometers; + /** + * The h component of the eccentricity vector. + */ + h: number; + /** + * The k component of the eccentricity vector. + */ + k: number; + /** + * The p component of the ascending node vector. + */ + p: number; + /** + * The q component of the ascending node vector. + */ + q: number; + /** + * The mean longitude of the orbit in radians. + */ + lambda: Radians; + /** + * The gravitational parameter of the central body in km³/s². + */ mu: number; - fr: number; - /** Create a new [EquinoctialElements] object from orbital elements. */ - constructor( - public epoch: EpochUTC, - public af: number, - public ag: number, - public l: number, - public n: number, - public chi: number, - public psi: number, - { mu = earthGravityParam, fr = 1 }: { mu?: number; fr?: number } = {}, - ) { - this.mu = mu; - this.fr = fr; + /** + * The retrograde factor. 1 for prograde orbits, -1 for retrograde orbits. + */ + I: 1 | -1; + constructor({ epoch, h, k, lambda, a, p, q, mu, I }: EquinoctialElementsParams) { + this.epoch = epoch; + this.h = h; + this.k = k; + this.lambda = lambda; + this.a = a; + this.p = p; + this.q = q; + this.mu = mu ?? earthGravityParam; + this.I = I ?? 1; } + /** + * Returns a string representation of the EquinoctialElements object. + * @returns A string representation of the EquinoctialElements object. + */ toString(): string { return [ '[EquinoctialElements]', ` Epoch: ${this.epoch}`, - ` af: ${this.af}`, - ` ag: ${this.ag}`, - ` l: ${this.l} rad`, - ` n: ${this.n} rad/s`, - ` chi: ${this.chi}`, - ` psi: ${this.psi}`, + ` a: ${this.a} km`, + ` h: ${this.h}`, + ` k: ${this.k}`, + ` p: ${this.p}`, + ` q: ${this.q}`, + ` lambda: ${this.lambda} rad`, ].join('\n'); } - /** Return the orbit semimajor-axis _(km)_. */ - semimajorAxis(): number { - return Math.cbrt(this.mu / (this.n * this.n)); + /** + * Gets the semimajor axis. + * @returns The semimajor axis in kilometers. + */ + get semimajorAxis(): Kilometers { + return this.a; + } + + /** + * Gets the mean longitude. + * @returns The mean longitude in radians. + */ + get meanLongitude(): Radians { + return this.lambda; + } + + /** + * Calculates the mean motion of the celestial object. + * + * @returns The mean motion in units of radians per second. + */ + get meanMotion(): number { + return Math.sqrt(this.mu / this.a ** 3); + } + + /** + * Gets the retrograde factor. + * + * @returns The retrograde factor. + */ + get retrogradeFactor(): number { + return this.I; + } + + /** + * Checks if the orbit is prograde. + * @returns {boolean} True if the orbit is prograde, false otherwise. + */ + isPrograde(): boolean { + return this.I === 1; } - /** Compute the mean motion _(rad/s)_ of this orbit. */ - meanMotion(): number { - const a = this.semimajorAxis(); - return Math.sqrt(this.mu / (a * a * a)); + /** + * Checks if the orbit is retrograde. + * @returns {boolean} True if the orbit is retrograde, false otherwise. + */ + isRetrograde(): boolean { + return this.I === -1; } - /** Compute the period _(seconds)_ of this orbit. */ - period(): number { - return TAU * Math.sqrt(this.semimajorAxis() ** 3 / this.mu); + /** + * Gets the period of the orbit. + * @returns The period in seconds. + */ + get period(): Seconds { + return (TAU * Math.sqrt(this.semimajorAxis ** 3 / this.mu)) as Seconds; } - // / Compute the number of revolutions completed per day for this orbit. - revsPerDay(): number { - return secondsPerDay / this.period(); + /** + * Gets the number of revolutions per day. + * + * @returns The number of revolutions per day. + */ + get revsPerDay(): number { + return secondsPerDay / this.period; } - // / Convert this to [ClassicalElements]. + /** + * Converts the equinoctial elements to classical elements. + * @returns The classical elements. + */ toClassicalElements(): ClassicalElements { - const a = this.semimajorAxis(); - const e = Math.sqrt(this.af * this.af + this.ag * this.ag); - const i = - Math.PI * ((1.0 - this.fr) * 0.5) + - 2.0 * this.fr * Math.atan(Math.sqrt(this.chi * this.chi + this.psi * this.psi)); - const o = Math.atan2(this.chi, this.psi); - const w = Math.atan2(this.ag, this.af) - this.fr * Math.atan2(this.chi, this.psi); - const m = this.l - this.fr * o - w; + const a = this.semimajorAxis; + const e = Math.sqrt(this.k * this.k + this.h * this.h); + const i = Math.PI * ((1.0 - this.I) * 0.5) + 2.0 * this.I * Math.atan(Math.sqrt(this.p * this.p + this.q * this.q)); + const o = Math.atan2(this.p, this.q); + const w = Math.atan2(this.h, this.k) - this.I * Math.atan2(this.p, this.q); + const m = this.lambda - this.I * o - w; const v = newtonM(e, m).nu; return new ClassicalElements({ epoch: this.epoch, - semimajorAxis: a as Kilometers, + semimajorAxis: a, eccentricity: e, inclination: i as Radians, rightAscension: o as Radians, @@ -81,7 +175,10 @@ export class EquinoctialElements { }); } - /** Convert this to inertial position and velocity vectors. */ + /** + * Converts the equinoctial elements to position and velocity. + * @returns The position and velocity in classical elements. + */ toPositionVelocity(): PositionVelocity { return this.toClassicalElements().toPositionVelocity(); } diff --git a/src/coordinate/J2000.ts b/src/coordinate/J2000.ts index 4f61ab4..f7af5e6 100644 --- a/src/coordinate/J2000.ts +++ b/src/coordinate/J2000.ts @@ -30,7 +30,8 @@ export class J2000 extends StateVector { } /** - * Converts the coordinates from J2000 to the International Terrestrial Reference Frame (ITRF). + * Converts the coordinates from J2000 to the International Terrestrial + * Reference Frame (ITRF). * * This is an ECI to ECF transformation. */ diff --git a/src/coordinate/StateVector.ts b/src/coordinate/StateVector.ts index 950def9..2ad561f 100644 --- a/src/coordinate/StateVector.ts +++ b/src/coordinate/StateVector.ts @@ -12,7 +12,11 @@ export type PositionVelocity = { // / Base class for state vectors. export abstract class StateVector { - constructor(public epoch: EpochUTC, public position: Vector3D, public velocity: Vector3D) { + constructor( + public epoch: EpochUTC, + public position: Vector3D, + public velocity: Vector3D, + ) { // Nothing to do here. } diff --git a/src/coordinate/TEME.ts b/src/coordinate/TEME.ts index 351e1e7..ad7cbca 100644 --- a/src/coordinate/TEME.ts +++ b/src/coordinate/TEME.ts @@ -5,13 +5,16 @@ import { J2000 } from './J2000'; import { StateVector } from './StateVector'; /** - * True Equator Mean Equinox (TEME) is a coordinate system commonly used in satellite tracking and orbit prediction. - * It is a reference frame that defines the position and orientation of an object relative to the Earth's equator - * and equinox. + * True Equator Mean Equinox (TEME) is a coordinate system commonly used in + * satellite tracking and orbit prediction. It is a reference frame that defines + * the position and orientation of an object relative to the Earth's equator and + * equinox. * - * By using the True Equator Mean Equinox (TEME) coordinate system, we can accurately describe the position and motion - * of satellites relative to the Earth's equator and equinox. This is particularly useful for tracking and predicting - * satellite orbits in various applications, such as satellite communication, navigation, and remote sensing. + * By using the True Equator Mean Equinox (TEME) coordinate system, we can + * accurately describe the position and motion of satellites relative to the + * Earth's equator and equinox. This is particularly useful for tracking and + * predicting satellite orbits in various applications, such as satellite + * communication, navigation, and remote sensing. */ export class TEME extends StateVector { // / Create a new [TEME] object from a [ClassicalElements] object. diff --git a/src/coordinate/Tle.ts b/src/coordinate/Tle.ts index 654135d..c0738bb 100644 --- a/src/coordinate/Tle.ts +++ b/src/coordinate/Tle.ts @@ -1,28 +1,31 @@ /** * @author Theodore Kruczek. - * @description Orbital Object ToolKit Core (ootk-core) is a collection of tools for working - * with satellites and other orbital objects. + * @description Orbital Object ToolKit Core (ootk-core) is a collection of tools + * for working with satellites and other orbital objects. * - * @file The Tle module contains a collection of functions for working with TLEs. + * @file The Tle module contains a collection of functions for working with + * TLEs. * * @license MIT License * - * @Copyright (c) 2024 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: + * @Copyright (c) 2024 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 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. + * 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 { ClassicalElements, FormatTle, TEME } from '.'; @@ -329,10 +332,10 @@ export class Tle { } /** - * Tle line 1 checksum (modulo 10), for verifying the integrity of this line of the Tle. + * Tle line 1 checksum (modulo 10), for verifying the integrity of this line + * of the Tle. * - * Range: 0 to 9 - * Example: 3 + * Range: 0 to 9 Example: 3 * * @param {string} tleLine The first line of the Tle to parse. * @returns {number} The checksum value. @@ -349,8 +352,8 @@ export class Tle { * * Example: 'U' * - * Some websites like https://KeepTrack.space and Celestrak.org will embed information - * in this field about the source of the Tle. + * Some websites like https://KeepTrack.space and Celestrak.org will embed + * information in this field about the source of the Tle. * */ static classification(tleLine1: TleLine1): string { @@ -358,8 +361,9 @@ export class Tle { } /** - * Orbital eccentricity, decimal point assumed. All artificial Earth satellites have an - * eccentricity between 0 (perfect circle) and 1 (parabolic orbit). + * Orbital eccentricity, decimal point assumed. All artificial Earth + * satellites have an eccentricity between 0 (perfect circle) and 1 (parabolic + * orbit). * * Range: 0 to 1 * @@ -379,29 +383,30 @@ export class Tle { } /** - * Tle element set number, incremented for each new Tle generated. 999 seems to mean the Tle - * has maxed out. + * Tle element set number, incremented for each new Tle generated. 999 seems + * to mean the Tle has maxed out. * - * Range: Technically 1 to 9999, though in practice the maximum number seems to be 999. + * Range: Technically 1 to 9999, though in practice the maximum number seems + * to be 999. * * Example: 999 - * @param {string} tleLine1 The first line of the Tle to parse. - * @returns {number} The element number. + * @param {string} tleLine1 The first line of the Tle to parse. @returns + * {number} The element number. */ static elsetNum(tleLine1: TleLine1): number { return parseInt(tleLine1.substring(Tle.elsetNum_.start, Tle.elsetNum_.stop)); } /** - * Private value - used by United States Space Force to reference the orbit model used to - * generate the Tle. Will always be seen as zero externally (e.g. by "us", unless you are - * "them" - in which case, hello!). + * Private value - used by United States Space Force to reference the orbit + * model used to generate the Tle. Will always be seen as zero externally + * (e.g. by "us", unless you are "them" - in which case, hello!). * * Example: 0 * - * Starting in 2024, this may contain a 4 if the Tle was generated using the new SGP4-XP - * model. Until the source code is released, there is no way to support that format in - * JavaScript or TypeScript. + * Starting in 2024, this may contain a 4 if the Tle was generated using the + * new SGP4-XP model. Until the source code is released, there is no way to + * support that format in JavaScript or TypeScript. * * @param {string} tleLine1 The first line of the Tle to parse. * @returns {0} The ephemeris type. @@ -485,8 +490,8 @@ export class Tle { } /** - * Inclination relative to the Earth's equatorial plane in degrees. 0 to 90 degrees is a - * prograde orbit and 90 to 180 degrees is a retrograde orbit. + * Inclination relative to the Earth's equatorial plane in degrees. 0 to 90 + * degrees is a prograde orbit and 90 to 180 degrees is a retrograde orbit. * * Units: degrees * @@ -508,10 +513,10 @@ export class Tle { } /** - * International Designator (COSPAR ID) - * See https://en.wikipedia.org/wiki/International_Designator - * @param {string} tleLine1 The first line of the Tle to parse. - * @returns {string} The International Designator. + * International Designator (COSPAR ID) See + * https://en.wikipedia.org/wiki/International_Designator + * @param {string} tleLine1 The first line of the Tle to parse. @returns + * {string} The International Designator. */ static intlDes(tleLine1: TleLine1): string { return tleLine1.substring(Tle.intlDes_.start, Tle.intlDes_.stop).trim(); @@ -561,8 +566,8 @@ export class Tle { /** * This should always return a 1 or a 2. - * @param {string} tleLine The first line of the Tle to parse. - * @returns {number} The line number of the Tle. + * @param {string} tleLine The first line of the Tle to parse. @returns + * {number} The line number of the Tle. */ static lineNumber(tleLine: TleLine1 | TleLine2): 1 | 2 { const lineNum = parseInt(tleLine.substring(Tle.lineNumber_.start, Tle.lineNumber_.stop)); @@ -575,9 +580,8 @@ export class Tle { } /** - * Mean anomaly. Indicates where the satellite was located within its orbit at the time of the - * Tle epoch. - * See https://en.wikipedia.org/wiki/Mean_Anomaly + * Mean anomaly. Indicates where the satellite was located within its orbit at + * the time of the Tle epoch. See https://en.wikipedia.org/wiki/Mean_Anomaly * * Units: degrees * @@ -599,9 +603,9 @@ export class Tle { } /** - * First Time Derivative of the Mean Motion divided by two. Defines how mean motion changes - * over time, so Tle propagators can still be used to make reasonable guesses when - * times are distant from the original Tle epoch. + * First Time Derivative of the Mean Motion divided by two. Defines how mean + * motion changes over time, so Tle propagators can still be used to make + * reasonable guesses when times are distant from the original Tle epoch. * * Units: Orbits / day ^ 2 * @@ -621,9 +625,9 @@ export class Tle { } /** - * Second Time Derivative of Mean Motion divided by six (decimal point assumed). Measures rate - * of change in the Mean Motion Dot so software can make reasonable guesses when times are - * distant from the original Tle epoch. + * Second Time Derivative of Mean Motion divided by six (decimal point + * assumed). Measures rate of change in the Mean Motion Dot so software can + * make reasonable guesses when times are distant from the original Tle epoch. * * Usually zero, unless the satellite is manuevering or in a decaying orbit. * @@ -646,13 +650,12 @@ export class Tle { } /** - * Revolutions around the Earth per day (mean motion). - * See https://en.wikipedia.org/wiki/Mean_Motion + * Revolutions around the Earth per day (mean motion). See + * https://en.wikipedia.org/wiki/Mean_Motion * - * Range: 0 to 17 (theoretically) - * Example: 15.54225995 - * @param {string} tleLine2 The second line of the Tle to parse. - * @returns {number} The mean motion of the satellite. + * Range: 0 to 17 (theoretically) Example: 15.54225995 + * @param {string} tleLine2 The second line of the Tle to parse. @returns + * {number} The mean motion of the satellite. */ static meanMotion(tleLine2: TleLine2): number { const meanMo = parseFloat(tleLine2.substring(Tle.meanMo_.start, Tle.meanMo_.stop)); @@ -677,9 +680,9 @@ export class Tle { } /** - * Right ascension of the ascending node in degrees. Essentially, this is the angle of the - * satellite as it crosses northward (ascending) across the Earth's equator (equatorial - * plane). + * Right ascension of the ascending node in degrees. Essentially, this is the + * angle of the satellite as it crosses northward (ascending) across the + * Earth's equator (equatorial plane). * * Units: degrees * @@ -705,8 +708,8 @@ export class Tle { * * Range: 0 to 99999 or 26999. * - * NOTE: To support Alpha-5, the first digit can be a letter. - * This will NOT be converted to a number. Use getSatNum() for that. + * NOTE: To support Alpha-5, the first digit can be a letter. This will NOT be + * converted to a number. Use getSatNum() for that. * * Example: 25544 or B1234 (e.g. Sputnik's rocket body was number 00001) * @@ -718,8 +721,8 @@ export class Tle { } /** - * Total satellite revolutions when this Tle was generated. This number rolls over - * (e.g. 99999 -> 0). + * Total satellite revolutions when this Tle was generated. This number rolls + * over (e.g. 99999 -> 0). * * Range: 0 to 99999 * @@ -737,8 +740,8 @@ export class Tle { * * Range: 0 to 99999 or 26999. * - * NOTE: To support Alpha-5, the first digit can be a letter. - * This will be converted to a number in order to expand the range to 26999. + * NOTE: To support Alpha-5, the first digit can be a letter. This will be + * converted to a number in order to expand the range to 26999. * * Example: 25544 or B1234 (e.g. Sputnik's rocket body was number 00001) * @@ -754,8 +757,8 @@ export class Tle { /** * Parse the first line of the Tle. - * @param {TleLine1} tleLine1 The first line of the Tle to parse. - * @returns {Line1Data} Returns the data from the first line of the Tle. + * @param {TleLine1} tleLine1 The first line of the Tle to parse. @returns + * {Line1Data} Returns the data from the first line of the Tle. */ static parseLine1(tleLine1: TleLine1): Line1Data { const lineNumber1 = Tle.lineNumber(tleLine1); @@ -799,8 +802,8 @@ export class Tle { /** * Parse the second line of the Tle. - * @param {TleLine2} tleLine2 The second line of the Tle to parse. - * @returns {Line2Data} Returns the data from the second line of the Tle. + * @param {TleLine2} tleLine2 The second line of the Tle to parse. @returns + * {Line2Data} Returns the data from the second line of the Tle. */ static parseLine2(tleLine2: TleLine2): Line2Data { const lineNumber2 = Tle.lineNumber(tleLine2); @@ -836,8 +839,7 @@ export class Tle { * Parses the Tle into orbital data. * * If you want all of the data then use parseTleFull instead. - * @param {TleLine1} tleLine1 Tle line 1 - * @param {TleLine2} tleLine2 Tle line 2 + * @param {TleLine1} tleLine1 Tle line 1 @param {TleLine2} tleLine2 Tle line 2 * @returns {TleData} Returns most commonly used orbital data from Tle */ static parse(tleLine1: TleLine1, tleLine2: TleLine2): TleData { @@ -882,9 +884,9 @@ export class Tle { * Parses all of the data contained in the Tle. * * If you only want the most commonly used data then use parseTle instead. - * @param {TleLine1} tleLine1 The first line of the Tle to parse. - * @param {TleLine2} tleLine2 The second line of the Tle to parse. - * @returns {TleDataFull} Returns all of the data from the Tle. + * @param {TleLine1} tleLine1 The first line of the Tle to parse. @param + * {TleLine2} tleLine2 The second line of the Tle to parse. @returns + * {TleDataFull} Returns all of the data from the Tle. */ static parseAll(tleLine1: TleLine1, tleLine2: TleLine2): TleDataFull { const line1 = Tle.parseLine1(tleLine1); @@ -927,9 +929,10 @@ export class Tle { const rest = sccNum.slice(2, 6); /* - * Convert the first two digit numbers into a Letter. Skip I and O as they look too similar to 1 and 0 - * A=10, B=11, C=12, D=13, E=14, F=15, G=16, H=17, J=18, K=19, L=20, M=21, N=22, P=23, Q=24, R=25, S=26, - * T=27, U=28, V=29, W=30, X=31, Y=32, Z=33 + * Convert the first two digit numbers into a Letter. Skip I and O as they + * look too similar to 1 and 0 A=10, B=11, C=12, D=13, E=14, F=15, G=16, + * H=17, J=18, K=19, L=20, M=21, N=22, P=23, Q=24, R=25, S=26, T=27, U=28, + * V=29, W=30, X=31, Y=32, Z=33 */ let first = parseInt(`${sccNum[0]}${sccNum[1]}`); const iPlus = first >= 18 ? 1 : 0; diff --git a/src/interfaces/ClassicalElementsParams.ts b/src/interfaces/ClassicalElementsParams.ts new file mode 100644 index 0000000..2bc157f --- /dev/null +++ b/src/interfaces/ClassicalElementsParams.ts @@ -0,0 +1,13 @@ +import { Kilometers, Radians } from 'src/main'; +import { EpochUTC } from '../time/EpochUTC'; + +export interface ClassicalElementsParams { + epoch: EpochUTC; + semimajorAxis: Kilometers; + eccentricity: number; + inclination: Radians; + rightAscension: Radians; + argPerigee: Radians; + trueAnomaly: Radians; + mu?: number; +} diff --git a/src/interfaces/EquinoctialElementsParams.ts b/src/interfaces/EquinoctialElementsParams.ts new file mode 100644 index 0000000..a061919 --- /dev/null +++ b/src/interfaces/EquinoctialElementsParams.ts @@ -0,0 +1,17 @@ +import { Kilometers, Radians } from 'src/main'; +import { EpochUTC } from '../time/EpochUTC'; + +export interface EquinoctialElementsParams { + epoch: EpochUTC; + h: number; + k: number; + lambda: Radians; + a: Kilometers; + p: number; + q: number; + mu?: number; + /** + * Retrograde factor. 1 for prograde orbits, -1 for retrograde orbits. + */ + I?: 1 | -1; +} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 325ff92..65d3aeb 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -4,3 +4,5 @@ export type { OptionsParams } from './OptionsParams'; export type { SatelliteParams } from './SatelliteParams'; export type { SensorParams } from './SensorParams'; export type { StarObjectParams } from './StarObjectParams'; +export type { ClassicalElementsParams } from './ClassicalElementsParams'; +export type { EquinoctialElementsParams } from './EquinoctialElementsParams'; diff --git a/src/objects/Satellite.ts b/src/objects/Satellite.ts index 5bddd8e..8cfded6 100644 --- a/src/objects/Satellite.ts +++ b/src/objects/Satellite.ts @@ -1,13 +1,12 @@ /** * @author Theodore Kruczek. - * @description Orbital Object ToolKit Core (ootk-core) is a collection of tools for working - * with satellites and other orbital objects. + * @description Orbital Object ToolKit Core (ootk-core) is a collection of tools + * for working with satellites and other orbital objects. * - * @file The Satellite class provides functions for calculating satellites positions - * relative to earth based sensors and other orbital objects. + * @file The Satellite class provides functions for calculating satellites + * positions relative to earth based sensors and other orbital objects. * - * @license MIT License - * @Copyright (c) 2020-2024 Theodore Kruczek + * @license MIT License @Copyright (c) 2020-2024 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 @@ -16,8 +15,8 @@ * 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 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, @@ -28,7 +27,7 @@ * SOFTWARE. */ -import type { ClassicalElements } from 'src/coordinate'; +import type { ClassicalElements } from '../coordinate'; import { Geodetic } from '../coordinate/Geodetic'; import { ITRF } from '../coordinate/ITRF'; import { J2000 } from '../coordinate/J2000'; @@ -62,7 +61,8 @@ import { BaseObject } from './BaseObject'; import { GroundPosition } from './GroundPosition'; /** - * Represents a satellite object with orbital information and methods for calculating its position and other properties. + * Represents a satellite object with orbital information and methods for + * calculating its position and other properties. */ export class Satellite extends BaseObject { apogee: Kilometers; @@ -152,9 +152,10 @@ export class Satellite extends BaseObject { } /** - * Checks if the given SatelliteRecord object is valid by checking if its properties are all numbers. - * @param satrec - The SatelliteRecord object to check. - * @returns True if the SatelliteRecord object is valid, false otherwise. + * Checks if the given SatelliteRecord object is valid by checking if its + * properties are all numbers. + * @param satrec - The SatelliteRecord object to check. @returns True if the + * SatelliteRecord object is valid, false otherwise. */ static isValidSatrec(satrec: SatelliteRecord): boolean { if ( @@ -173,8 +174,9 @@ export class Satellite extends BaseObject { } /** - * Calculates the azimuth angle of the satellite relative to the given sensor at the specified date. - * If no date is provided, the current time of the satellite is used. + * Calculates the azimuth angle of the satellite relative to the given sensor + * at the specified date. If no date is provided, the current time of the + * satellite is used. * * @optimized */ @@ -183,8 +185,8 @@ export class Satellite extends BaseObject { } /** - * Calculates the RAE (Range, Azimuth, Elevation) values for a given sensor and date. - * If no date is provided, the current time is used. + * Calculates the RAE (Range, Azimuth, Elevation) values for a given sensor + * and date. If no date is provided, the current time is used. * * @expanded */ @@ -239,11 +241,11 @@ export class Satellite extends BaseObject { } /** - * Calculates the J2000 coordinates for a given date. - * If no date is provided, the current time is used. + * Calculates the J2000 coordinates for a given date. If no date is provided, + * the current time is used. * @param date - The date for which to calculate the J2000 coordinates. - * @returns The J2000 coordinates for the specified date. - * @throws Error if propagation fails. + * @returns The J2000 coordinates for the specified date. @throws Error if + * propagation fails. */ getJ2000(date: Date = new Date()): J2000 { const { m } = Satellite.calculateTimeVariables(date, this.satrec); @@ -268,7 +270,8 @@ export class Satellite extends BaseObject { } /** - * Returns the elevation angle of the satellite as seen by the given sensor at the specified time. + * Returns the elevation angle of the satellite as seen by the given sensor at + * the specified time. * * @optimized */ @@ -308,7 +311,8 @@ export class Satellite extends BaseObject { } /** - * Calculates the RAE (Range, Azimuth, Elevation) vector for a given sensor and time. + * Calculates the RAE (Range, Azimuth, Elevation) vector for a given sensor + * and time. * * @optimized */ @@ -321,7 +325,8 @@ export class Satellite extends BaseObject { } /** - * Returns the range of the satellite from the given sensor at the specified time. + * Returns the range of the satellite from the given sensor at the specified + * time. * * @optimized */ @@ -330,10 +335,11 @@ export class Satellite extends BaseObject { } /** - * Applies the Doppler effect to the given frequency based on the observer's position and the date. - * @param freq - The frequency to apply the Doppler effect to. - * @param observer - The observer's position on the ground. - * @param date - The date at which to calculate the Doppler effect. Optional, defaults to the current date. + * Applies the Doppler effect to the given frequency based on the observer's + * position and the date. + * @param freq - The frequency to apply the Doppler effect to. @param observer + * - The observer's position on the ground. @param date - The date at which to + * calculate the Doppler effect. Optional, defaults to the current date. * @returns The frequency after applying the Doppler effect. */ applyDoppler(freq: number, observer: GroundPosition, date?: Date): number { @@ -346,8 +352,9 @@ export class Satellite extends BaseObject { * Calculates the Doppler factor for the satellite. * * @param observer The observer's ground position. - * @param date The optional date for which to calculate the Doppler factor. If not provided, the current date is used. - * @returns The calculated Doppler factor. + * @param date The optional date for which to calculate the Doppler factor. If + * not provided, the current date is used. @returns The calculated Doppler + * factor. */ dopplerFactor(observer: GroundPosition, date?: Date): number { const position = this.eci(date); @@ -357,9 +364,9 @@ export class Satellite extends BaseObject { /** * Calculates the time variables for a given date relative to the TLE epoch. - * @param {Date} date Date to calculate - * @param {SatelliteRecord} satrec Satellite orbital information - * @returns {{m: number, gmst: GreenwichMeanSiderealTime, j: number}} Time variables + * @param {Date} date Date to calculate @param {SatelliteRecord} satrec + * Satellite orbital information @returns {{m: number, gmst: + * GreenwichMeanSiderealTime, j: number}} Time variables */ private static calculateTimeVariables( date: Date, diff --git a/test/coordinate/EquinoctialElements.test.ts b/test/coordinate/EquinoctialElements.test.ts new file mode 100644 index 0000000..7e52a5b --- /dev/null +++ b/test/coordinate/EquinoctialElements.test.ts @@ -0,0 +1,108 @@ +import { EquinoctialElements, EpochUTC, Kilometers, Radians } from '../../src/main'; + +describe('ClassicalElements', () => { + const epoch = EpochUTC.fromDateTime(new Date('2024-01-14T14:39:39.914Z')); + let elements: EquinoctialElements; + let elements2: EquinoctialElements; + + beforeEach(() => { + elements = new EquinoctialElements({ + epoch, + a: 6935.754028093152 as Kilometers, + h: 0.000647924735354646, + k: -0.00023984363760690404, + p: 0.06877966401254976, + q: 0.38775089544898517, + lambda: 7.675800884962325 as Radians, + mu: 398600.4415, + I: 1, + }); + elements2 = new EquinoctialElements({ + epoch, + a: 6937.389795521736 as Kilometers, + h: 0.0008140324591775426, + k: 0.00025609012211642304, + p: 0.09761971480018718, + q: 0.3198440470005213, + lambda: 7.224398200203262 as Radians, + mu: 398600.4415, + I: -1, + }); + }); + + // toString + it('should return a string representation of the EquinoctialElements object', () => { + expect(elements.toString()).toEqual( + [ + '[EquinoctialElements]', + ` Epoch: ${epoch}`, + ` a: ${elements.a} km`, + ` h: ${elements.h}`, + ` k: ${elements.k}`, + ` p: ${elements.p}`, + ` q: ${elements.q}`, + ` lambda: ${elements.lambda} rad`, + ].join('\n'), + ); + }); + + // semiMajorAxis + it('should return the semi-major axis', () => { + expect(elements.semimajorAxis).toMatchSnapshot(); + expect(elements2.semimajorAxis).toMatchSnapshot(); + }); + + // meanLongitude + it('should return the mean longitude', () => { + expect(elements.meanLongitude).toMatchSnapshot(); + expect(elements2.meanLongitude).toMatchSnapshot(); + }); + + // meanMotion + it('should return the mean motion', () => { + expect(elements.meanMotion).toMatchSnapshot(); + expect(elements2.meanMotion).toMatchSnapshot(); + }); + + // retrogradeFactor + it('should return the retrograde factor', () => { + expect(elements.retrogradeFactor).toMatchSnapshot(); + expect(elements2.retrogradeFactor).toMatchSnapshot(); + }); + + // isPrograde + it('should return true if the orbit is prograde', () => { + expect(elements.isPrograde()).toMatchSnapshot(); + expect(elements2.isPrograde()).toMatchSnapshot(); + }); + + // isRetrograde + it('should return true if the orbit is retrograde', () => { + expect(elements.isRetrograde()).toMatchSnapshot(); + expect(elements2.isRetrograde()).toMatchSnapshot(); + }); + + // period + it('should return the period', () => { + expect(elements.period).toMatchSnapshot(); + expect(elements2.period).toMatchSnapshot(); + }); + + // revsPerDay + it('should return the revs per day', () => { + expect(elements.revsPerDay).toMatchSnapshot(); + expect(elements2.revsPerDay).toMatchSnapshot(); + }); + + // toClassicalElements + it('should return the ClassicalElements object', () => { + expect(elements.toClassicalElements()).toMatchSnapshot(); + expect(elements2.toClassicalElements()).toMatchSnapshot(); + }); + + // toPositionVelocity + it('should return the PositionVelocity object', () => { + expect(elements.toPositionVelocity()).toMatchSnapshot(); + expect(elements2.toPositionVelocity()).toMatchSnapshot(); + }); +}); diff --git a/test/coordinate/__snapshots__/ClassicalElements.test.ts.snap b/test/coordinate/__snapshots__/ClassicalElements.test.ts.snap index 66aee38..fe0a181 100644 --- a/test/coordinate/__snapshots__/ClassicalElements.test.ts.snap +++ b/test/coordinate/__snapshots__/ClassicalElements.test.ts.snap @@ -35,17 +35,17 @@ ClassicalElements { exports[`ClassicalElements should convert ClassicalElements to EquinoctialElements 1`] = ` EquinoctialElements { - "af": -0.0009430897960304828, - "ag": 0.0006107793657340771, - "chi": 0.01112918196689249, + "I": 1, + "a": 6943.547853722985, "epoch": EpochUTC { "posix": 1705243179.914, }, - "fr": 1, - "l": 3.15876545174161, + "h": 0.0006107793657340771, + "k": -0.0009430897960304828, + "lambda": 3.15876545174161, "mu": 398600.4415, - "n": 0.0010911808567933578, - "psi": 0.3939942790541112, + "p": 0.01112918196689249, + "q": 0.3939942790541112, } `; @@ -89,3 +89,18 @@ ClassicalElements { "trueAnomaly": 5.749771010517163, } `; + +exports[`ClassicalElements should propagate ClassicalElements to a new epoch 1`] = ` +ClassicalElements { + "argPerigee": 2.5386411901807353, + "eccentricity": 0.0011235968124658146, + "epoch": EpochUTC { + "posix": 1705329579.914, + }, + "inclination": 0.7509087232045765, + "mu": 398600.4415, + "rightAscension": 0.028239555738616327, + "semimajorAxis": 6943.547853722985, + "trueAnomaly": 0.6234422142843716, +} +`; diff --git a/test/coordinate/__snapshots__/EquinoctialElements.test.ts.snap b/test/coordinate/__snapshots__/EquinoctialElements.test.ts.snap new file mode 100644 index 0000000..75c1d43 --- /dev/null +++ b/test/coordinate/__snapshots__/EquinoctialElements.test.ts.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClassicalElements should return the ClassicalElements object 1`] = ` +ClassicalElements { + "argPerigee": 1.7497725502749217, + "eccentricity": 0.0006908917666211547, + "epoch": EpochUTC { + "posix": 1705243179.914, + }, + "inclination": 0.7503068041317155, + "mu": 398600.4415, + "rightAscension": 0.17555503341584255, + "semimajorAxis": 6935.754028093152, + "trueAnomaly": -0.5334142966624242, +} +`; + +exports[`ClassicalElements should return the ClassicalElements object 2`] = ` +ClassicalElements { + "argPerigee": 1.5622345094945798, + "eccentricity": 0.0008533645148705458, + "epoch": EpochUTC { + "posix": 1705243179.914, + }, + "inclination": 2.4961547762752434, + "mu": 398600.4415, + "rightAscension": 0.296230054374017, + "semimajorAxis": 6937.389795521736, + "trueAnomaly": -0.32533674963545683, +} +`; + +exports[`ClassicalElements should return the PositionVelocity object 1`] = ` +Object { + "position": Vector3D { + "x": 1538.2233358428962, + "y": 5102.261204021967, + "z": 4432.634965003576, + }, + "velocity": Vector3D { + "x": -7.3415189093793, + "y": 0.6516718453998642, + "z": 1.7933882499862026, + }, +} +`; + +exports[`ClassicalElements should return the PositionVelocity object 2`] = ` +Object { + "position": Vector3D { + "x": 3699.9682301219336, + "y": -4340.514588573352, + "z": 3939.5098844969084, + }, + "velocity": Vector3D { + "x": -6.276352257339577, + "y": -3.9904909019658032, + "z": 1.4943961405848338, + }, +} +`; + +exports[`ClassicalElements should return the mean longitude 1`] = `7.675800884962325`; + +exports[`ClassicalElements should return the mean longitude 2`] = `7.224398200203262`; + +exports[`ClassicalElements should return the mean motion 1`] = `0.0010930206413363934`; + +exports[`ClassicalElements should return the mean motion 2`] = `0.0010926340790517568`; + +exports[`ClassicalElements should return the period 1`] = `5748.459882237341`; + +exports[`ClassicalElements should return the period 2`] = `5750.493626038512`; + +exports[`ClassicalElements should return the retrograde factor 1`] = `1`; + +exports[`ClassicalElements should return the retrograde factor 2`] = `-1`; + +exports[`ClassicalElements should return the revs per day 1`] = `15.030112720621878`; + +exports[`ClassicalElements should return the revs per day 2`] = `15.02479710763901`; + +exports[`ClassicalElements should return the semi-major axis 1`] = `6935.754028093152`; + +exports[`ClassicalElements should return the semi-major axis 2`] = `6937.389795521736`; + +exports[`ClassicalElements should return true if the orbit is prograde 1`] = `true`; + +exports[`ClassicalElements should return true if the orbit is prograde 2`] = `false`; + +exports[`ClassicalElements should return true if the orbit is retrograde 1`] = `false`; + +exports[`ClassicalElements should return true if the orbit is retrograde 2`] = `true`;