ootk-core
Version:
Orbital Object Toolkit. A modern typed replacement for satellite.js including SGP4 propagation, TLE parsing, Sun and Moon calculations, and more.
1,025 lines (892 loc) • 33.8 kB
text/typescript
/* eslint-disable max-lines */
/**
* @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 { ClassicalElements, FormatTle, TEME } from './index.js';
import { Sgp4OpsMode } from '../enums/Sgp4OpsMode.js';
import { Satellite, Sgp4, Vector3D } from '../main.js';
import { Sgp4GravConstants } from '../sgp4/sgp4.js';
import { EpochUTC } from '../time/EpochUTC.js';
import {
Degrees,
EciVec3,
Kilometers,
KilometersPerSecond,
Line1Data,
Line2Data,
Minutes,
SatelliteRecord,
Seconds,
StateVectorSgp4,
TleData,
TleDataFull,
TleLine1,
TleLine2,
} from '../types/types.js';
import { DEG2RAD, earthGravityParam, RAD2DEG, secondsPerDay, TAU } from '../utils/constants.js';
import { getDayOfYear, newtonNu, toPrecision } from '../utils/functions.js';
import { TleFormatData } from './tle-format-data.js';
/**
* Tle is a static class with a collection of methods for working with TLEs.
*/
export class Tle {
line1: string;
line2: string;
epoch: EpochUTC;
satnum: number;
private readonly satrec_: SatelliteRecord;
/**
* Mapping of alphabets to their corresponding numeric values.
*/
private static readonly alpha5_ = {
A: '10',
B: '11',
C: '12',
D: '13',
E: '14',
F: '15',
G: '16',
H: '17',
// I is skipped on purpose
J: '18',
K: '19',
L: '20',
M: '21',
N: '22',
// O is skipped on purpose
P: '23',
Q: '24',
R: '25',
S: '26',
T: '27',
U: '28',
V: '29',
W: '30',
X: '31',
Y: '32',
Z: '33',
} as const;
/** The argument of perigee field. */
private static readonly argPerigee_ = new TleFormatData(35, 42);
/** The BSTAR drag term field. */
private static readonly bstar_ = new TleFormatData(54, 61);
/** The checksum field. */
private static readonly checksum_ = new TleFormatData(69, 69);
/** The classification field. */
private static readonly classification_ = new TleFormatData(8, 8);
/** The eccentricity field. */
private static readonly eccentricity_ = new TleFormatData(27, 33);
/** The element set number field. */
private static readonly elsetNum_ = new TleFormatData(65, 68);
/** The ephemeris type field. */
private static readonly ephemerisType_ = new TleFormatData(63, 63);
/** The epoch day field. */
private static readonly epochDay_ = new TleFormatData(21, 32);
/** The epoch year field. */
private static readonly epochYear_ = new TleFormatData(19, 20);
/** The inclination field. */
private static readonly inclination_ = new TleFormatData(9, 16);
/** The international designator launch number field. */
private static readonly intlDesLaunchNum_ = new TleFormatData(12, 14);
/** The international designator launch piece field. */
private static readonly intlDesLaunchPiece_ = new TleFormatData(15, 17);
/** The international designator year field. */
private static readonly intlDesYear_ = new TleFormatData(10, 11);
/** The line number field. */
private static readonly lineNumber_ = new TleFormatData(1, 1);
/** The mean anomaly field. */
private static readonly meanAnom_ = new TleFormatData(44, 51);
/** The first derivative of the mean motion field. */
private static readonly meanMoDev1_ = new TleFormatData(34, 43);
/** The second derivative of the mean motion field. */
private static readonly meanMoDev2_ = new TleFormatData(45, 52);
/** The mean motion field. */
private static readonly meanMo_ = new TleFormatData(53, 63);
/** The right ascension of the ascending node field. */
private static readonly rightAscension_ = new TleFormatData(18, 25);
/** The revolution number field. */
private static readonly revNum_ = new TleFormatData(64, 68);
/** The satellite number field. */
private static readonly satNum_ = new TleFormatData(3, 7);
constructor(
line1: string,
line2: string,
opsMode: Sgp4OpsMode = Sgp4OpsMode.AFSPC,
gravConst: Sgp4GravConstants = Sgp4GravConstants.wgs72,
) {
this.line1 = line1;
this.line2 = line2;
this.epoch = Tle.parseEpoch_(line1.substring(18, 32));
this.satnum = parseInt(Tle.convertA5to6Digit(line1.substring(2, 7)));
this.satrec_ = Sgp4.createSatrec(line1, line2, gravConst, opsMode);
}
toString(): string {
return `${this.line1}\n${this.line2}`;
}
/**
* Gets the semimajor axis of the TLE.
* @returns The semimajor axis value.
*/
get semimajorAxis(): number {
return Tle.tleSma_(this.line2);
}
/**
* Gets the eccentricity of the TLE.
* @returns The eccentricity value.
*/
get eccentricity(): number {
return Tle.tleEcc_(this.line2);
}
/**
* Gets the inclination of the TLE.
* @returns The inclination in degrees.
*/
get inclination(): number {
return Tle.tleInc_(this.line2);
}
/**
* Gets the inclination in degrees.
* @returns The inclination in degrees.
*/
get inclinationDegrees(): number {
return Tle.tleInc_(this.line2) * RAD2DEG;
}
/**
* Gets the apogee of the TLE (Two-Line Elements) object.
* Apogee is the point in an orbit that is farthest from the Earth.
* It is calculated as the product of the semimajor axis and (1 + eccentricity).
* @returns The apogee value.
*/
get apogee(): number {
return this.semimajorAxis * (1 + this.eccentricity);
}
/**
* Gets the perigee of the TLE (Two-Line Element Set).
* The perigee is the point in the orbit of a satellite or other celestial body where it is closest to the Earth.
* It is calculated as the product of the semimajor axis and the difference between 1 and the eccentricity.
* @returns The perigee value.
*/
get perigee(): number {
return this.semimajorAxis * (1 - this.eccentricity);
}
/**
* Gets the period of the TLE in minutes.
* @returns The period of the TLE in minutes.
*/
get period(): Minutes {
const periodSec = (TAU * Math.sqrt(this.semimajorAxis ** 3 / earthGravityParam)) as Seconds;
return (periodSec / 60) as Minutes;
}
/**
* Parses the epoch string and returns the corresponding EpochUTC object.
* @param epochStr - The epoch string to parse.
* @returns The parsed EpochUTC object.
*/
private static parseEpoch_(epochStr: string): EpochUTC {
let year = parseInt(epochStr.substring(0, 2));
if (year >= 57) {
year += 1900;
} else {
year += 2000;
}
const days = parseFloat(epochStr.substring(2, 14)) - 1;
return EpochUTC.fromDateTimeString(`${year}-01-01T00:00:00.000Z`).roll(days * secondsPerDay as Seconds);
}
static calcElsetAge(
sat: Satellite,
nowInput?: Date,
outputUnits: 'days' | 'hours' | 'minutes' | 'seconds' = 'days',
): number {
nowInput ??= new Date();
const currentYearFull = nowInput.getUTCFullYear();
const currentYearShort = currentYearFull % 100;
const epochYearShort = parseInt(sat.tle1.substring(18, 20), 10);
const epochDayOfYear = parseFloat(sat.tle1.substring(20, 32));
let epochYearFull: number;
if (epochYearShort <= currentYearShort) {
epochYearFull = 2000 + epochYearShort;
} else {
epochYearFull = 1900 + epochYearShort;
}
const epochJday = epochDayOfYear + (epochYearFull * 365);
const currentJday = getDayOfYear() + (currentYearFull * 365);
const currentTime = (nowInput.getUTCHours() * 3600 + nowInput.getUTCMinutes() * 60 +
nowInput.getUTCSeconds()) / 86400;
const daysOld = (currentJday + currentTime) - epochJday;
switch (outputUnits) {
case 'hours':
return daysOld * 24;
case 'minutes':
return daysOld * 1440;
case 'seconds':
return daysOld * 86400;
default:
return daysOld;
}
}
/**
* Propagates the TLE (Two-Line Element Set) to a specific epoch and returns the TEME (True Equator Mean Equinox)
* coordinates.
* @param epoch The epoch to propagate the TLE to.
* @returns The TEME coordinates at the specified epoch.
* @throws Error if propagation fails.
*/
propagate(epoch: EpochUTC): TEME {
const r = new Float64Array(3);
const v = new Float64Array(3);
const stateVector = Sgp4.propagate(this.satrec_, epoch.difference(this.epoch) / 60.0);
if (!stateVector) {
throw new Error('Propagation failed');
}
Tle.sv2rv_(stateVector, r, v);
return new TEME(
epoch,
new Vector3D(<Kilometers>r[0], <Kilometers>r[1], <Kilometers>r[2]),
new Vector3D(<KilometersPerSecond>v[0], <KilometersPerSecond>v[1], <KilometersPerSecond>v[2]),
);
}
/**
* Converts the state vector to position and velocity arrays.
* @param stateVector - The state vector containing position and velocity information.
* @param r - The array to store the position values.
* @param v - The array to store the velocity values.
*/
private static sv2rv_(stateVector: StateVectorSgp4, r: Float64Array, v: Float64Array) {
const pos = stateVector.position as EciVec3;
const vel = stateVector.velocity as EciVec3;
r[0] = pos.x;
r[1] = pos.y;
r[2] = pos.z;
v[0] = vel.x;
v[1] = vel.y;
v[2] = vel.z;
}
/**
* Returns the current state of the satellite in the TEME coordinate system.
* @returns The current state of the satellite.
*/
private currentState_(): TEME {
const r = new Float64Array(3);
const v = new Float64Array(3);
const stateVector = Sgp4.propagate(this.satrec_, 0.0);
Tle.sv2rv_(stateVector, r, v);
return new TEME(
this.epoch,
new Vector3D(<Kilometers>r[0], <Kilometers>r[1], <Kilometers>r[2]),
new Vector3D(<KilometersPerSecond>v[0], <KilometersPerSecond>v[1], <KilometersPerSecond>v[2]),
);
}
/**
* Gets the state of the TLE in the TEME coordinate system.
* @returns The state of the TLE in the TEME coordinate system.
*/
get state(): TEME {
return this.currentState_();
}
/**
* Calculates the Semi-Major Axis (SMA) from the second line of a TLE.
* @param line2 The second line of the TLE.
* @returns The Semi-Major Axis (SMA) in kilometers.
*/
private static tleSma_(line2: string): number {
const n = parseFloat(line2.substring(52, 63));
return earthGravityParam ** (1 / 3) / ((TAU * n) / secondsPerDay) ** (2 / 3);
}
/**
* Parses the eccentricity value from the second line of a TLE.
* @param line2 The second line of the TLE.
* @returns The eccentricity value.
*/
private static tleEcc_(line2: string): number {
return parseFloat(`0.${line2.substring(26, 33)}`);
}
/**
* Calculates the inclination angle from the second line of a TLE.
* @param line2 The second line of the TLE.
* @returns The inclination angle in radians.
*/
private static tleInc_(line2: string): number {
return parseFloat(line2.substring(8, 16)) * DEG2RAD;
}
/**
* Creates a TLE (Two-Line Element) object from classical orbital elements.
* @param elements - The classical orbital elements.
* @returns A TLE object.
*/
static fromClassicalElements(elements: ClassicalElements): Tle {
const { epochYr, epochDay } = elements.epoch.toEpochYearAndDay();
const intl = '58001A ';
const scc = '00001';
const tles = FormatTle.createTle({
inc: FormatTle.inclination(elements.inclinationDegrees),
meanmo: FormatTle.meanMotion(elements.revsPerDay),
ecen: FormatTle.eccentricity(elements.eccentricity.toFixed(7)),
argPe: FormatTle.argumentOfPerigee(elements.argPerigeeDegrees),
meana: FormatTle.meanAnomaly(newtonNu(elements.eccentricity, elements.trueAnomaly).m * RAD2DEG),
rasc: FormatTle.rightAscension(elements.rightAscensionDegrees),
epochday: epochDay,
epochyr: epochYr,
scc,
intl,
});
return new Tle(tles.tle1, tles.tle2);
}
/**
* Argument of perigee.
* @see https://en.wikipedia.org/wiki/Argument_of_perigee
* @example 69.9862
* @param tleLine2 The second line of the Tle to parse.
* @returns The argument of perigee in degrees (0 to 360).
*/
static argOfPerigee(tleLine2: TleLine2): Degrees {
const argPe = parseFloat(tleLine2.substring(Tle.argPerigee_.start, Tle.argPerigee_.stop));
if (!(argPe >= 0 && argPe <= 360)) {
throw new Error(`Invalid argument of perigee: ${argPe}`);
}
return toPrecision(argPe, 4) as Degrees;
}
/**
* BSTAR drag term (decimal point assumed). Estimates the effects of atmospheric drag on the satellite's motion.
* @see https://en.wikipedia.org/wiki/BSTAR
* @example 0.000036771
* @description ('36771-4' in the original Tle or 0.36771 * 10 ^ -4)
* @param tleLine1 The first line of the Tle to parse.
* @returns The drag coefficient.
*/
static bstar(tleLine1: TleLine1): number {
const BSTAR_PART_2 = Tle.bstar_.start + 1;
const BSTAR_PART_3 = Tle.bstar_.start + 6;
const BSTAR_PART_4 = Tle.bstar_.stop - 1;
const bstarSymbol = tleLine1.substring(Tle.bstar_.start, BSTAR_PART_2);
// Decimal place is assumed
let bstar1 = parseFloat(`0.${tleLine1.substring(BSTAR_PART_2, BSTAR_PART_3)}`);
const exponentSymbol = tleLine1.substring(BSTAR_PART_3, BSTAR_PART_4);
let exponent = parseInt(tleLine1.substring(BSTAR_PART_4, Tle.bstar_.stop));
if (exponentSymbol === '-') {
exponent *= -1;
} else if (exponentSymbol !== '+') {
throw new Error(`Invalid BSTAR symbol: ${bstarSymbol}`);
}
bstar1 *= 10 ** exponent;
if (bstarSymbol === '-') {
bstar1 *= -1;
} else if (bstarSymbol === '+' || bstarSymbol === ' ') {
// Do nothing
} else {
throw new Error(`Invalid BSTAR symbol: ${bstarSymbol}`);
}
return toPrecision(bstar1, 14);
}
/**
* Tle line 1 checksum (modulo 10), for verifying the integrity of this line of the Tle.
* @example 3
* @param tleLine The first line of the Tle to parse.
* @returns The checksum value (0 to 9)
*/
static checksum(tleLine: TleLine1 | TleLine2): number {
return parseInt(tleLine.substring(Tle.checksum_.start, Tle.checksum_.stop));
}
/**
* Returns the satellite classification.
* Some websites like https://KeepTrack.space and Celestrak.org will embed
* information in this field about the source of the Tle.
* @example 'U'
* unclassified
* @example 'C'
* confidential
* @example 'S'
* secret
* @param tleLine1 The first line of the Tle to parse.
* @returns The satellite classification.
*/
static classification(tleLine1: TleLine1): string {
return tleLine1.substring(Tle.classification_.start, Tle.classification_.stop);
}
/**
* Orbital eccentricity, decimal point assumed. All artificial Earth satellites have an eccentricity between 0
* (perfect circle) and 1 (parabolic orbit).
* @example 0.0006317
* (`0006317` in the original Tle)
* @param tleLine2 The second line of the Tle to parse.
* @returns The eccentricity of the satellite (0 to 1)
*/
static eccentricity(tleLine2: TleLine2): number {
const ecc = parseFloat(`0.${tleLine2.substring(Tle.eccentricity_.start, Tle.eccentricity_.stop)}`);
if (!(ecc >= 0 && ecc <= 1)) {
throw new Error(`Invalid eccentricity: ${ecc}`);
}
return toPrecision(ecc, 7);
}
/**
* Tle element set number, incremented for each new Tle generated.
* @see https://en.wikipedia.org/wiki/Two-line_element_set
* @example 999
* @param tleLine1 The first line of the Tle to parse.
* @returns The element number (1 to 999)
*/
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!).
*
* 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.
* @example 0
* @param tleLine1 The first line of the Tle to parse.
* @returns The ephemeris type.
*/
static ephemerisType(tleLine1: TleLine1): 0 {
const ephemerisType = parseInt(tleLine1.substring(Tle.ephemerisType_.start, Tle.ephemerisType_.stop));
if (ephemerisType !== 0 && ephemerisType !== 4) {
throw new Error('Invalid ephemeris type');
}
if (ephemerisType === 4) {
throw new Error('SGP4-XP is not supported');
}
return ephemerisType;
}
/**
* Fractional day of the year when the Tle was generated (Tle epoch).
* @example 206.18396726
* @param tleLine1 The first line of the Tle to parse.
* @returns The day of the year the Tle was generated. (1 to 365.99999999)
*/
static epochDay(tleLine1: string): number {
const epochDay = parseFloat(tleLine1.substring(Tle.epochDay_.start, Tle.epochDay_.stop));
if (epochDay < 1 || epochDay > 366.99999999) {
throw new Error('Invalid epoch day');
}
return toPrecision(epochDay, 8);
}
/**
* Year when the Tle was generated (Tle epoch), last two digits.
* @example 17
* @param tleLine1 The first line of the Tle to parse.
* @returns The year the Tle was generated. (0 to 99)
*/
static epochYear(tleLine1: TleLine1) {
const epochYear = parseInt(tleLine1.substring(Tle.epochYear_.start, Tle.epochYear_.stop));
if (epochYear < 0 || epochYear > 99) {
throw new Error('Invalid epoch year');
}
return epochYear;
}
/**
* Year when the Tle was generated (Tle epoch), four digits.
* @example 2008
* @param tleLine1 The first line of the Tle to parse.
* @returns The year the Tle was generated. (1957 to 2056)
*/
static epochYearFull(tleLine1: TleLine1) {
const epochYear = parseInt(tleLine1.substring(Tle.epochYear_.start, Tle.epochYear_.stop));
if (epochYear < 0 || epochYear > 99) {
throw new Error('Invalid epoch year');
}
if (epochYear < 57) {
return epochYear + 2000;
}
return epochYear + 1900;
}
/**
* 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.
* @example 51.6400
* @param tleLine2 The second line of the Tle to parse.
* @returns The inclination of the satellite. (0 to 180)
*/
static inclination(tleLine2: TleLine2): Degrees {
const inc = parseFloat(tleLine2.substring(Tle.inclination_.start, Tle.inclination_.stop));
if (inc < 0 || inc > 180) {
throw new Error(`Invalid inclination: ${inc}`);
}
return toPrecision(inc, 4) as Degrees;
}
/**
* International Designator (COSPAR ID)
* @see https://en.wikipedia.org/wiki/International_Designator
* @param tleLine1 The first line of the Tle to parse.
* @returns The International Designator.
*/
static intlDes(tleLine1: TleLine1): string {
const year2 = this.intlDesYear(tleLine1);
// Some TLEs don't have a year, so we can't generate an IntlDes
if (isNaN(year2)) {
return '';
}
const year4 = year2 < 57 ? year2 + 2000 : year2 + 1900;
const launchNum = this.intlDesLaunchNum(tleLine1);
const launchPiece = this.intlDesLaunchPiece(tleLine1);
return `${year4}-${launchNum.toString().padStart(3, '0')}${launchPiece}`;
}
/**
* International Designator (COSPAR ID): Launch number of the year.
* @example 67
* @param tleLine1 The first line of the Tle to parse.
* @returns The launch number of the International Designator. (1 to 999)
*/
static intlDesLaunchNum(tleLine1: string): number {
return parseInt(tleLine1.substring(Tle.intlDesLaunchNum_.start, Tle.intlDesLaunchNum_.stop));
}
/**
* International Designator (COSPAR ID): Piece of the launch.
* @example 'A'
* @param tleLine1 The first line of the Tle to parse.
* @returns The launch piece of the International Designator. (A to ZZZ)
*/
static intlDesLaunchPiece(tleLine1: TleLine1): string {
return tleLine1.substring(Tle.intlDesLaunchPiece_.start, Tle.intlDesLaunchPiece_.stop).trim();
}
/**
* International Designator (COSPAR ID): Last 2 digits of launch year.
* @example 98
* @param tleLine1 The first line of the Tle to parse.
* @returns The year of the International Designator. (0 to 99)
*/
static intlDesYear(tleLine1: TleLine1): number {
return parseInt(tleLine1.substring(Tle.intlDesYear_.start, Tle.intlDesYear_.stop));
}
/**
* This should always return a 1 or a 2.
* @example 1
* @param tleLine The first line of the Tle to parse.
* @returns The line number of the Tle. (1 or 2)
*/
static lineNumber(tleLine: TleLine1 | TleLine2): 1 | 2 {
const lineNum = parseInt(tleLine.substring(Tle.lineNumber_.start, Tle.lineNumber_.stop));
if (lineNum !== 1 && lineNum !== 2) {
throw new Error('Invalid line number');
}
return lineNum;
}
/**
* 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
* @example 25.2906
* @param tleLine2 The second line of the Tle to parse.
* @returns The mean anomaly of the satellite. (0 to 360)
*/
static meanAnomaly(tleLine2: TleLine2): Degrees {
const meanA = parseFloat(tleLine2.substring(Tle.meanAnom_.start, Tle.meanAnom_.stop));
if (!(meanA >= 0 && meanA <= 360)) {
throw new Error(`Invalid mean anomaly: ${meanA}`);
}
return toPrecision(meanA, 4) as Degrees;
}
/**
* 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. This
* is recorded in units of orbits per day per day.
* @example 0.00001961
* @param tleLine1 The first line of the Tle to parse.
* @returns The first derivative of the mean motion.
*/
static meanMoDev1(tleLine1: TleLine1): number {
const meanMoDev1 = parseFloat(tleLine1.substring(Tle.meanMoDev1_.start, Tle.meanMoDev1_.stop));
if (isNaN(meanMoDev1)) {
throw new Error('Invalid first derivative of mean motion.');
}
return toPrecision(meanMoDev1, 8);
}
/**
* 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. This is recorded in units of orbits per day per
* day per day.
* @example 0
* '00000-0' in the original Tle or 0.00000 * 10 ^ 0
* @param tleLine1 The first line of the Tle to parse.
* @returns The second derivative of the mean motion.
*/
static meanMoDev2(tleLine1: string): number {
const meanMoDev2 = parseFloat(tleLine1.substring(Tle.meanMoDev2_.start, Tle.meanMoDev2_.stop));
if (isNaN(meanMoDev2)) {
throw new Error('Invalid second derivative of mean motion.');
}
// NOTE: Should this limit to a specific number of decimals?
return meanMoDev2;
}
/**
* Revolutions around the Earth per day (mean motion).
* @see https://en.wikipedia.org/wiki/Mean_Motion
* @example 15.54225995
* @param tleLine2 The second line of the Tle to parse.
* @returns The mean motion of the satellite. (0 to 18)
*/
static meanMotion(tleLine2: TleLine2): number {
const meanMo = parseFloat(tleLine2.substring(Tle.meanMo_.start, Tle.meanMo_.stop));
if (!(meanMo > 0 && meanMo <= 18)) {
throw new Error(`Invalid mean motion: ${meanMo}`);
}
return toPrecision(meanMo, 8);
}
/**
* Calculates the period of a satellite orbit based on the given Tle line 2.
* @example 92.53035747
* @param tleLine2 The Tle line 2.
* @returns The period of the satellite orbit in minutes.
*/
static period(tleLine2: TleLine2): Minutes {
const meanMo = Tle.meanMotion(tleLine2);
return (1440 / meanMo) as Minutes;
}
/**
* 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).
* @example 208.9163
* @param tleLine2 The second line of the Tle to parse.
* @returns The right ascension of the satellite. (0 to 360)
*/
static rightAscension(tleLine2: TleLine2): Degrees {
const rightAscension = parseFloat(tleLine2.substring(Tle.rightAscension_.start, Tle.rightAscension_.stop));
if (!(rightAscension >= 0 && rightAscension <= 360)) {
throw new Error(`Invalid Right Ascension: ${rightAscension}`);
}
return toPrecision(rightAscension, 4) as Degrees;
}
/**
* NORAD catalog number. To support Alpha-5, the first digit can be a letter. This will NOT be converted to a number.
* Use satNum() for that.
* @see https://en.wikipedia.org/wiki/Satellite_Catalog_Number
* @example 25544
* @example B1234
* @param tleLine The first line of the Tle to parse.
* @returns NORAD catalog number.
*/
static rawSatNum(tleLine: TleLine1 | TleLine2): string {
return tleLine.substring(Tle.satNum_.start, Tle.satNum_.stop);
}
/**
* Total satellite revolutions when this Tle was generated. This number rolls over (e.g. 99999 -> 0).
* @example 6766
* @param tleLine2 The second line of the Tle to parse.
* @returns The revolutions around the Earth per day (mean motion). (0 to 99999)
*/
static revNum(tleLine2: TleLine2): number {
return parseInt(tleLine2.substring(Tle.revNum_.start, Tle.revNum_.stop));
}
/**
* NORAD catalog number converted to a number.
* @see https://en.wikipedia.org/wiki/Satellite_Catalog_Number
* @example 25544
* @example 111234
* @param tleLine The first line of the Tle to parse.
* @returns NORAD catalog number. (0 to 339999)
*/
static satNum(tleLine: TleLine1 | TleLine2): number {
const satNumStr = tleLine.substring(Tle.satNum_.start, Tle.satNum_.stop);
const sixDigitSatNum = Tle.convertA5to6Digit(satNumStr);
return parseInt(sixDigitSatNum);
}
/**
* Parse the first line of the Tle.
* @param tleLine1 The first line of the Tle to parse.
* @returns Returns the data from the first line of the Tle.
*/
static parseLine1(tleLine1: TleLine1): Line1Data {
const lineNumber1 = Tle.lineNumber(tleLine1);
const satNum = Tle.satNum(tleLine1);
const satNumRaw = Tle.rawSatNum(tleLine1);
const classification = Tle.classification(tleLine1);
const intlDes = Tle.intlDes(tleLine1);
const intlDesYear = Tle.intlDesYear(tleLine1);
const intlDesLaunchNum = Tle.intlDesLaunchNum(tleLine1);
const intlDesLaunchPiece = Tle.intlDesLaunchPiece(tleLine1);
const epochYear = Tle.epochYear(tleLine1);
const epochYearFull = Tle.epochYearFull(tleLine1);
const epochDay = Tle.epochDay(tleLine1);
const meanMoDev1 = Tle.meanMoDev1(tleLine1);
const meanMoDev2 = Tle.meanMoDev2(tleLine1);
const bstar = Tle.bstar(tleLine1);
const ephemerisType = Tle.ephemerisType(tleLine1);
const elsetNum = Tle.elsetNum(tleLine1);
const checksum1 = Tle.checksum(tleLine1);
return {
lineNumber1,
satNum,
satNumRaw,
classification,
intlDes,
intlDesYear,
intlDesLaunchNum,
intlDesLaunchPiece,
epochYear,
epochYearFull,
epochDay,
meanMoDev1,
meanMoDev2,
bstar,
ephemerisType,
elsetNum,
checksum1,
};
}
/**
* Parse the second line of the Tle.
* @param tleLine2 The second line of the Tle to parse.
* @returns Returns the data from the second line of the Tle.
*/
static parseLine2(tleLine2: TleLine2): Line2Data {
const lineNumber2 = Tle.lineNumber(tleLine2);
const satNum = Tle.satNum(tleLine2);
const satNumRaw = Tle.rawSatNum(tleLine2);
const inclination = Tle.inclination(tleLine2);
const rightAscension = Tle.rightAscension(tleLine2);
const eccentricity = Tle.eccentricity(tleLine2);
const argOfPerigee = Tle.argOfPerigee(tleLine2);
const meanAnomaly = Tle.meanAnomaly(tleLine2);
const meanMotion = Tle.meanMotion(tleLine2);
const revNum = Tle.revNum(tleLine2);
const checksum2 = Tle.checksum(tleLine2);
const period = Tle.period(tleLine2);
return {
lineNumber2,
satNum,
satNumRaw,
inclination,
rightAscension,
eccentricity,
argOfPerigee,
meanAnomaly,
meanMotion,
revNum,
checksum2,
period,
};
}
/**
* Parses the Tle into orbital data.
*
* If you want all of the data then use parseTleFull instead.
* @param tleLine1 Tle line 1
* @param tleLine2 Tle line 2
* @returns Returns most commonly used orbital data from Tle
*/
static parse(tleLine1: TleLine1, tleLine2: TleLine2): TleData {
const line1 = Tle.parseLine1(tleLine1);
const line2 = Tle.parseLine2(tleLine2);
if (line1.satNum !== line2.satNum) {
throw new Error('Satellite numbers do not match');
}
if (line1.satNumRaw !== line2.satNumRaw) {
throw new Error('Raw satellite numbers do not match');
}
if (line1.lineNumber1 !== 1) {
throw new Error('First line number must be 1');
}
if (line2.lineNumber2 !== 2) {
throw new Error('Second line number must be 2');
}
return {
satNum: line1.satNum,
intlDes: line1.intlDes,
epochYear: line1.epochYear,
epochDay: line1.epochDay,
meanMoDev1: line1.meanMoDev1,
meanMoDev2: line1.meanMoDev2,
bstar: line1.bstar,
inclination: line2.inclination,
rightAscension: line2.rightAscension,
eccentricity: line2.eccentricity,
argOfPerigee: line2.argOfPerigee,
meanAnomaly: line2.meanAnomaly,
meanMotion: line2.meanMotion,
period: line2.period,
};
}
/**
* Parses all of the data contained in the Tle.
*
* If you only want the most commonly used data then use parseTle instead.
* @param tleLine1 The first line of the Tle to parse.
* @param tleLine2 The second line of the Tle to parse.
* @returns Returns all of the data from the Tle.
*/
static parseAll(tleLine1: TleLine1, tleLine2: TleLine2): TleDataFull {
const line1 = Tle.parseLine1(tleLine1);
const line2 = Tle.parseLine2(tleLine2);
if (line1.satNum !== line2.satNum) {
throw new Error('Satellite numbers do not match');
}
if (line1.satNumRaw !== line2.satNumRaw) {
throw new Error('Raw satellite numbers do not match');
}
if (line1.lineNumber1 !== 1) {
throw new Error('First line number must be 1');
}
if (line2.lineNumber2 !== 2) {
throw new Error('Second line number must be 2');
}
return { ...line1, ...line2 };
}
/**
* Converts a 6 digit SCC number to a 5 digit SCC alpha 5 number
* @param sccNum The 6 digit SCC number
* @returns The 5 digit SCC alpha 5 number
*/
static convert6DigitToA5(sccNum: string): string {
// Only applies to 6 digit numbers
if (sccNum.length < 6) {
return sccNum;
}
if (typeof sccNum[0] !== 'string') {
throw new Error('Invalid SCC number');
}
// Already an alpha 5 number
if (RegExp(/[A-Z]/iu, 'u').test(sccNum[0])) {
return sccNum;
}
// Extract the trailing 4 digits
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
*/
let first = parseInt(`${sccNum[0]}${sccNum[1]}`);
const iPlus = first >= 18 ? 1 : 0;
const tPlus = first >= 24 ? 1 : 0;
first = first + iPlus + tPlus;
return `${String.fromCharCode(first + 55)}${rest}`;
}
/**
* Converts a 5-digit SCC number to a 6-digit SCC number.
* @param sccNum - The 5-digit SCC number to convert.
* @returns The converted 6-digit SCC number.
*/
static convertA5to6Digit(sccNum: string): string {
if (sccNum.length < 5) {
return sccNum;
}
const values = sccNum.toUpperCase().split('');
if (!values[0]) {
throw new Error('Invalid SCC number');
}
if (values[0] in Tle.alpha5_) {
const firstLetter = values[0] as keyof typeof Tle.alpha5_;
values[0] = Tle.alpha5_[firstLetter];
}
return values.join('');
}
}