From b5b506af622d673fbf03fa4edbb11b405f27099f Mon Sep 17 00:00:00 2001 From: Theodore Kruczek Date: Thu, 11 Jan 2024 08:46:45 -0500 Subject: [PATCH] refactor: :recycle: combine Tle classes and improve documentation --- .eslintignore | 4 +- examples/commonjs/satellite-js-migration.js | 71 ++ examples/commonjs/satellite.js | 12 - examples/mjs/satellite-js-migration.mjs | 71 ++ examples/mjs/satellite.mjs | 12 - examples/typescript/satellite-js-migration.ts | 6 +- examples/typescript/tle.ts | 87 ++ src/coordinate/FormatTle.ts | 57 +- src/coordinate/TLE.ts | 835 +++++++++++++++++- src/{tle => coordinate}/tle-format-data.ts | 0 src/enums/Sgp4OpsMode.ts | 4 + src/objects/Satellite.ts | 11 +- src/observation/RAE.ts | 9 + src/ootk-core.ts | 2 +- src/sgp4/sgp4.ts | 6 +- src/tle/tle.ts | 719 --------------- src/types/types.ts | 25 + 17 files changed, 1081 insertions(+), 850 deletions(-) create mode 100644 examples/commonjs/satellite-js-migration.js delete mode 100644 examples/commonjs/satellite.js create mode 100644 examples/mjs/satellite-js-migration.mjs delete mode 100644 examples/mjs/satellite.mjs create mode 100644 examples/typescript/tle.ts rename src/{tle => coordinate}/tle-format-data.ts (100%) create mode 100644 src/enums/Sgp4OpsMode.ts delete mode 100644 src/tle/tle.ts diff --git a/.eslintignore b/.eslintignore index 887d03c..b8c2665 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,3 @@ -/dist /lib -/src/sgp4/asc \ No newline at end of file +/examples/commonjs/satellite-js-migration.js +/examples/mjs/satellite-js-migration.mjs \ No newline at end of file diff --git a/examples/commonjs/satellite-js-migration.js b/examples/commonjs/satellite-js-migration.js new file mode 100644 index 0000000..fcf22e1 --- /dev/null +++ b/examples/commonjs/satellite-js-migration.js @@ -0,0 +1,71 @@ +const { Satellite, Sgp4, GroundPosition, calcGmst } = require('../../lib/ootk-core'); + +// Sample TLE +const tle1 = '1 25544U 98067A 19156.50900463 .00003075 00000-0 59442-4 0 9992'; +const tle2 = '2 25544 51.6433 59.2583 0008217 16.4489 347.6017 15.51174618173442'; + +// Initialize a Satellite Object +const satellite = new Satellite({ + tle1, + tle2, +}); + +// You can still propagate a satellite using time since epoch (in minutes), but it's not recommended. +const timeSinceTleEpochMinutes = 10; +let positionAndVelocity = Sgp4.propagate(satellite.satrec, timeSinceTleEpochMinutes); + +// Use a Date object instead +positionAndVelocity = satellite.eci(new Date(2024, 0, 1)); +// Or use the current time +positionAndVelocity = satellite.eci(); + +// The position_velocity result is a key-value pair of ECI coordinates. +// These are the base results from which all other coordinates are derived. +const positionEci = positionAndVelocity.position; +const velocityEci = positionAndVelocity.velocity; + +// Set the Observer at 122.03 West by 36.96 North, in DEGREES (because who likes working in radians?) +const observer = new GroundPosition({ + lon: -122.0308, + lat: 36.9613422, + alt: 0.37, +}); + +// You can still calculate GMST if you want to, but unlike satellite.js it's not required. +const { gmst, j } = calcGmst(new Date()); + +// You can get ECF, Geodetic, Look Angles, and Doppler Factor. +const positionEcf = satellite.ecf(); +const observerEcf = observer.ecf(); +const positionGd = satellite.lla(); +const lookAngles = satellite.rae(observer); +// This never worked in satellite.js, but it does now! +const dopplerFactor = satellite.dopplerFactor(observer); + +// The coordinates are all stored in strongly typed key-value pairs. +// ECI and ECF are accessed by `x`, `y`, `z` properties. +const position = satellite.eci().position; +const satelliteX = position.x; +const satelliteY = position.y; +const satelliteZ = position.z; + +// Look Angles may be accessed by `azimuth`, `elevation`, `range` properties. +const azimuth = lookAngles.azimuth; // Radians +const azimuthDegrees = lookAngles.azimuthDegrees; // Degrees +const elevation = lookAngles.elevation; // Radians +const elevationDegrees = lookAngles.elevationDegrees; // Degrees +const rangeSat = lookAngles.range; // Kilometers +const rangeRate = lookAngles.rangeRate; // Kilometers/Second + +// Geodetic coords are accessed via `longitude`, `latitude`, `height`. +const longitude = positionGd.lon; // longitude is in degrees +const latitude = positionGd.lat; // latitude is in degrees +const height = positionGd.alt; // height is in kilometers + +// Convert the degrees to radians if you want. +const longitudeRad = longitude * DEG2RAD; +const latitudeRad = latitude * DEG2RAD; + +// There is no need to use the units seen in TypeScript examples. +// const longitudeRad = (longitude * DEG2RAD) as Radians; +// const latitudeRad = (latitude * DEG2RAD) as Radians; diff --git a/examples/commonjs/satellite.js b/examples/commonjs/satellite.js deleted file mode 100644 index be400e9..0000000 --- a/examples/commonjs/satellite.js +++ /dev/null @@ -1,12 +0,0 @@ -const { Satellite } = require('../../lib/ootk-core'); - -const tle1 = '1 25544U 98067A 19156.50900463 .00003075 00000-0 59442-4 0 9992'; -const tle2 = '2 25544 51.6433 59.2583 0008217 16.4489 347.6017 15.51174618173442'; - -const satellite = new Satellite({ - tle1, - tle2, -}); - -// eslint-disable-next-line no-console -console.log(satellite.eci()); diff --git a/examples/mjs/satellite-js-migration.mjs b/examples/mjs/satellite-js-migration.mjs new file mode 100644 index 0000000..16c26ed --- /dev/null +++ b/examples/mjs/satellite-js-migration.mjs @@ -0,0 +1,71 @@ +import { calcGmst, GroundPosition, Satellite, Sgp4 } from '../../lib/ootk-core'; + +// Sample TLE +const tle1 = '1 25544U 98067A 19156.50900463 .00003075 00000-0 59442-4 0 9992'; +const tle2 = '2 25544 51.6433 59.2583 0008217 16.4489 347.6017 15.51174618173442'; + +// Initialize a Satellite Object +const satellite = new Satellite({ + tle1, + tle2, +}); + +// You can still propagate a satellite using time since epoch (in minutes), but it's not recommended. +const timeSinceTleEpochMinutes = 10; +let positionAndVelocity = Sgp4.propagate(satellite.satrec, timeSinceTleEpochMinutes); + +// Use a Date object instead +positionAndVelocity = satellite.eci(new Date(2024, 0, 1)); +// Or use the current time +positionAndVelocity = satellite.eci(); + +// The position_velocity result is a key-value pair of ECI coordinates. +// These are the base results from which all other coordinates are derived. +const positionEci = positionAndVelocity.position; +const velocityEci = positionAndVelocity.velocity; + +// Set the Observer at 122.03 West by 36.96 North, in DEGREES (because who likes working in radians?) +const observer = new GroundPosition({ + lon: -122.0308, + lat: 36.9613422, + alt: 0.37, +}); + +// You can still calculate GMST if you want to, but unlike satellite.js it's not required. +const { gmst, j } = calcGmst(new Date()); + +// You can get ECF, Geodetic, Look Angles, and Doppler Factor. +const positionEcf = satellite.ecf(); +const observerEcf = observer.ecf(); +const positionGd = satellite.lla(); +const lookAngles = satellite.rae(observer); +// This never worked in satellite.js, but it does now! +const dopplerFactor = satellite.dopplerFactor(observer); + +// The coordinates are all stored in strongly typed key-value pairs. +// ECI and ECF are accessed by `x`, `y`, `z` properties. +const position = satellite.eci().position; +const satelliteX = position.x; +const satelliteY = position.y; +const satelliteZ = position.z; + +// Look Angles may be accessed by `azimuth`, `elevation`, `range` properties. +const azimuth = lookAngles.azimuth; // Radians +const azimuthDegrees = lookAngles.azimuthDegrees; // Degrees +const elevation = lookAngles.elevation; // Radians +const elevationDegrees = lookAngles.elevationDegrees; // Degrees +const rangeSat = lookAngles.range; // Kilometers +const rangeRate = lookAngles.rangeRate; // Kilometers/Second + +// Geodetic coords are accessed via `longitude`, `latitude`, `height`. +const longitude = positionGd.lon; // longitude is in degrees +const latitude = positionGd.lat; // latitude is in degrees +const height = positionGd.alt; // height is in kilometers + +// Convert the degrees to radians if you want. +const longitudeRad = longitude * DEG2RAD; +const latitudeRad = latitude * DEG2RAD; + +// There is no need to use the units seen in TypeScript examples. +// const longitudeRad = (longitude * DEG2RAD) as Radians; +// const latitudeRad = (latitude * DEG2RAD) as Radians; diff --git a/examples/mjs/satellite.mjs b/examples/mjs/satellite.mjs deleted file mode 100644 index 3e9c959..0000000 --- a/examples/mjs/satellite.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import { Satellite } from '../../lib/ootk-core.mjs'; - -const tle1 = '1 25544U 98067A 19156.50900463 .00003075 00000-0 59442-4 0 9992'; -const tle2 = '2 25544 51.6433 59.2583 0008217 16.4489 347.6017 15.51174618173442'; - -const satellite = new Satellite({ - tle1, - tle2, -}); - -// eslint-disable-next-line no-console -console.log(satellite.eci()); diff --git a/examples/typescript/satellite-js-migration.ts b/examples/typescript/satellite-js-migration.ts index c558615..1ab33f3 100644 --- a/examples/typescript/satellite-js-migration.ts +++ b/examples/typescript/satellite-js-migration.ts @@ -79,8 +79,10 @@ const satelliteY = position.y; // to prevent you from accidentally const satelliteZ = position.z; // mixing Meters with Kilometers. // Look Angles may be accessed by `azimuth`, `elevation`, `range` properties. -const azimuth = lookAngles.azimuth; // Typed as Degrees -const elevation = lookAngles.elevation; // Typed as Degrees +const azimuth = lookAngles.azimuth; // Typed as Radians +const azimuthDegress = lookAngles.azimuthDegrees; // Typed as Degrees +const elevation = lookAngles.elevation; // Typed as Radains +const elevationDegrees = lookAngles.elevationDegrees; // Typed as Degrees const rangeSat = lookAngles.range; // Typed as Kilometers // Geodetic coords are accessed via `longitude`, `latitude`, `height`. diff --git a/examples/typescript/tle.ts b/examples/typescript/tle.ts new file mode 100644 index 0000000..2fff466 --- /dev/null +++ b/examples/typescript/tle.ts @@ -0,0 +1,87 @@ +/* eslint-disable multiline-comment-style */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { Tle, TleLine1, TleLine2 } from '../../lib/ootk-core'; + +// Sample TLE +const tle1 = '1 25544U 98067A 19156.50900463 .00003075 00000-0 59442-4 0 9992' as TleLine1; +const tle2 = '2 25544 51.6433 59.2583 0008217 16.4489 347.6017 15.51174618173442' as TleLine2; + +// Getting the inclination is as easy as passing the second line to the inclination function +const inc = Tle.inclination(tle2); + +/** + * TypeScript asserts that you can't call this on a TleLine2, so you don't need + * to memorize which functions are for which line. + * + * const bstar = Tle.getBstar(tle2); + */ + +const bstarGood = Tle.bstar(tle1); + +// You can get all parameters from a TLE line with the parseLine1 or parseLine2 functions +const { + satNum, + classification, + intlDes, + epochYear, + epochDay, + meanMoDev1, + meanMoDev2, + bstar, + ephemerisType, + elsetNum, + checksum1, +} = Tle.parseLine1(tle1); + +// You can get the most common parameters from a TLE with the parse function +const tle = Tle.parse(tle1, tle2); +// { +// satNum: 25544, +// intlDes: '98067A', +// epochYear: 19, +// epochDay: 156.50900463, +// meanMoDev1: 0.00003075, +// meanMoDev2: 0, +// bstar: 0.000059442, +// inclination: 51.6433, +// raan: 59.2583, +// eccentricity: 0.0008217, +// argOfPerigee: 16.4489, +// meanAnomaly: 347.6017, +// meanMotion: 15.51174618, +// period: 92.83287537651032 +// } + +// Or get everything with the parseAll function +const tleAll = Tle.parseAll(tle1, tle2); +// { +// lineNumber1: 1, +// satNum: 25544, +// satNumRaw: '25544', +// classification: 'U', +// intlDes: '98067A', +// intlDesYear: 98, +// intlDesLaunchNum: 67, +// intlDesLaunchPiece: 'A', +// epochYear: 19, +// epochYearFull: 2019, +// epochDay: 156.50900463, +// meanMoDev1: 0.00003075, +// meanMoDev2: 0, +// bstar: 0.000059442, +// ephemerisType: 0, +// elsetNum: 999, +// checksum1: 2, +// lineNumber2: 2, +// inclination: 51.6433, +// raan: 59.2583, +// eccentricity: 0.0008217, +// argOfPerigee: 16.4489, +// meanAnomaly: 347.6017, +// meanMotion: 15.51174618, +// revNum: 17344, +// checksum2: 2, +// period: 92.83287537651032 +// } diff --git a/src/coordinate/FormatTle.ts b/src/coordinate/FormatTle.ts index 033fc4e..4a2837d 100644 --- a/src/coordinate/FormatTle.ts +++ b/src/coordinate/FormatTle.ts @@ -1,4 +1,5 @@ -import { Satellite } from 'src/objects'; +import { Satellite } from '../objects'; +import { Tle } from './Tle'; export type StringifiedNumber = `${number}.${number}`; @@ -36,7 +37,7 @@ export abstract class FormatTle { static createTle(tleParams: TleParams): { tle1: string; tle2: string } { const { inc, meanmo, rasc, argPe, meana, ecen, epochyr, epochday, intl } = tleParams; - const scc = FormatTle.convert6DigitToA5(tleParams.scc); + const scc = Tle.convert6DigitToA5(tleParams.scc); const epochYrStr = epochyr.padStart(2, '0'); const epochdayStr = parseFloat(epochday).toFixed(8).padStart(12, '0'); const incStr = FormatTle.inclination(inc); @@ -143,56 +144,4 @@ export abstract class FormatTle { return `${str.substring(0, index)}${chr}${str.substring(index + 1)}`; } - - /** - * Converts a 6 digit SCC number to a 5 digit SCC alpha 5 number - */ - static convert6DigitToA5(sccNum: string): string { - // Only applies to 6 digit numbers - if (sccNum.length < 6) { - return sccNum; - } - - 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}`; - } - - static convertA5to6Digit(sccNum: string): string { - if (RegExp(/[A-Z]/iu, 'u').test(sccNum[0])) { - // Extract the trailing 4 digits - const rest = sccNum.slice(1, 5); - - /* - * Convert the first letter to a two digit number. 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 = sccNum[0].toUpperCase().charCodeAt(0) - 55; - const iPlus = first >= 18 ? 1 : 0; - const tPlus = first >= 24 ? 1 : 0; - - first = first - iPlus - tPlus; - - return `${first}${rest}`; - } - - return sccNum; - } } diff --git a/src/coordinate/TLE.ts b/src/coordinate/TLE.ts index ed270d6..f399bcd 100644 --- a/src/coordinate/TLE.ts +++ b/src/coordinate/TLE.ts @@ -1,25 +1,63 @@ -import { Earth } from '../body/Earth'; -import { Vector3D } from '../operations/Vector3D'; -import { Sgp4, Sgp4GravConstants } from '../sgp4/sgp4'; +/** + * @author Theodore Kruczek. + * @description Orbital Object ToolKit (OOTK) 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. + * + * @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: + * + * 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 { Earth } from '../body'; +import { ClassicalElements, FormatTle, TEME } from '../coordinate'; +import { Sgp4OpsMode } from '../enums/Sgp4OpsMode'; +import { Sgp4, Vector3D } from '../ootk-core'; +import { Sgp4GravConstants } from '../sgp4/sgp4'; import { EpochUTC } from '../time/EpochUTC'; -import { EciVec3, SatelliteRecord, StateVectorSgp4 } from '../types/types'; +import { + EciVec3, + Line1Data, + Line2Data, + SatelliteRecord, + StateVectorSgp4, + TleData, + TleDataFull, + TleLine1, + TleLine2, +} from '../types/types'; import { DEG2RAD, RAD2DEG, secondsPerDay, TAU } from '../utils/constants'; -import { newtonNu } from '../utils/functions'; -import { ClassicalElements } from './ClassicalElements'; -import { FormatTle } from './FormatTle'; -import { TEME } from './TEME'; - -export enum Sgp4OpsMode { - afspc = 'a', - improved = 'i', -} +import { newtonNu, toPrecision } from '../utils/functions'; +import { TleFormatData } from './tle-format-data'; -export class TLE { +/** + * 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 _satrec: SatelliteRecord; + private satrec_: SatelliteRecord; + /** + * Mapping of alphabets to their corresponding numeric values. + */ private static alpha5_ = { A: '10', B: '11', @@ -46,18 +84,62 @@ export class TLE { Y: '32', Z: '33', }; + /** 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 international designator field. */ + private static readonly intlDes_ = new TleFormatData(10, 17); + /** 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 raan_ = 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, + opsMode: Sgp4OpsMode = Sgp4OpsMode.AFSPC, gravConst: Sgp4GravConstants = Sgp4GravConstants.wgs72, ) { this.line1 = line1; this.line2 = line2; - this.epoch = TLE.parseEpoch_(line1.substring(18, 32)); - this.satnum = TLE.parseSatnum_(line1.substring(2, 7)); - this._satrec = Sgp4.createSatrec(line1, line2, gravConst, opsMode); + 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 { @@ -65,19 +147,19 @@ export class TLE { } get semimajorAxis(): number { - return TLE.tleSma_(this.line2); + return Tle.tleSma_(this.line2); } get eccentricity(): number { - return TLE.tleEcc_(this.line2); + return Tle.tleEcc_(this.line2); } get inclination(): number { - return TLE.tleInc_(this.line2); + return Tle.tleInc_(this.line2); } get inclinationDegrees(): number { - return TLE.tleInc_(this.line2) * RAD2DEG; + return Tle.tleInc_(this.line2) * RAD2DEG; } get apogee(): number { @@ -105,27 +187,17 @@ export class TLE { return EpochUTC.fromDateTimeString(`${year}-01-01T00:00:00.000Z`).roll(days * secondsPerDay); } - private static parseSatnum_(satnumStr: string): number { - const values = satnumStr.toUpperCase().split(''); - - if (values[0] in TLE.alpha5_) { - values[0] = TLE.alpha5_[values[0]]; - } - - return parseInt(values.join('')); - } - 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); + const stateVector = Sgp4.propagate(this.satrec_, epoch.difference(this.epoch) / 60.0); if (!stateVector) { throw new Error('Propagation failed'); } - TLE.sv2rv_(stateVector, r, v); + Tle.sv2rv_(stateVector, r, v); return new TEME(epoch, new Vector3D(r[0], r[1], r[2]), new Vector3D(v[0], v[1], v[2])); } @@ -146,9 +218,9 @@ export class TLE { const r = new Float64Array(3); const v = new Float64Array(3); - const stateVector = Sgp4.propagate(this._satrec, 0.0); + const stateVector = Sgp4.propagate(this.satrec_, 0.0); - TLE.sv2rv_(stateVector, r, v); + Tle.sv2rv_(stateVector, r, v); return new TEME(this.epoch, new Vector3D(r[0], r[1], r[2]), new Vector3D(v[0], v[1], v[2])); } @@ -171,7 +243,7 @@ export class TLE { return parseFloat(line2.substring(8, 16)) * DEG2RAD; } - static fromClassicalElements(elements: ClassicalElements): TLE { + static fromClassicalElements(elements: ClassicalElements): Tle { const { epochYr, epochDay } = elements.epoch.toEpochYearAndDay(); const intl = '58001A '; const scc = '00001'; @@ -189,6 +261,691 @@ export class TLE { intl, }); - return new TLE(tles.tle1, tles.tle2); + return new Tle(tles.tle1, tles.tle2); + } + + /** + * Argument of perigee. See https://en.wikipedia.org/wiki/Argument_of_perigee + * + * Units: degrees + * + * Range: 0 to 359.9999 + * + * Example: 69.9862 + * + * @param {string} tleLine2 The second line of the Tle to parse. + * @returns {number} The argument of perigee in degrees. + */ + static argOfPerigee(tleLine2: TleLine2): number { + 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); + } + + /** + * BSTAR drag term (decimal point assumed). Estimates the effects of + * atmospheric drag on the satellite's motion. + * + * Units: EarthRadii ^ -1 + * + * Example: 0.000036771 ('36771-4' in the original Tle [= 0.36771 * 10 ^ -4]) + * + * @param {string} tleLine1 The first line of the Tle to parse. + * @returns {number} 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. + * + * Range: 0 to 9 + * Example: 3 + * + * @param {string} tleLine The first line of the Tle to parse. + * @returns {number} The checksum value. + */ + static checksum(tleLine: TleLine1 | TleLine2): number { + return parseInt(tleLine.substring(Tle.checksum_.start, Tle.checksum_.stop)); + } + + /** + * Returns the satellite classification. + * * 'U' = unclassified + * * 'C' = confidential + * * 'S' = secret + * + * Example: 'U' + * + * 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 { + 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). + * + * Range: 0 to 1 + * + * Example: 0.0006317 (`0006317` in the original Tle) + * + * @param {string} tleLine2 The second line of the Tle to parse. + * @returns {number} The eccentricity of the satellite. + */ + 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. 999 seems to mean the Tle + * has maxed out. + * + * 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. + */ + 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!). + * + * 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. + * + * @param {string} tleLine1 The first line of the Tle to parse. + * @returns {0} 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). + * + * Range: 1 to 365.99999999 + * + * Example: 206.18396726 + * + * @param {string} tleLine1 The first line of the Tle to parse. + * @returns {number} The day of the year the Tle was generated. + */ + static epochDay(tleLine1: string): number { + const epochDay = parseFloat(tleLine1.substring(Tle.epochDay_.start, Tle.epochDay_.stop)); + + if (epochDay < 1 || epochDay > 365.99999999) { + throw new Error('Invalid epoch day'); + } + + return toPrecision(epochDay, 8); + } + + /** + * Year when the Tle was generated (Tle epoch), last two digits. + * + * Range: 00 to 99 + * + * Example: 17 + * + * @param {string} tleLine1 The first line of the Tle to parse. + * @returns {number} The year the Tle was generated. + */ + 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. + * + * Range: 1957 to 2056 + * + * Example: 2008 + * + * @param {string} tleLine1 The first line of the Tle to parse. + * @returns {number} The year the Tle was generated. + */ + 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. + * + * Units: degrees + * + * Range: 0 to 180 + * + * Example: 51.6400 + * + * @param {string} tleLine2 The second line of the Tle to parse. + * @returns {number} The inclination of the satellite. + */ + static inclination(tleLine2: TleLine2): number { + 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); + } + + /** + * 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(); + } + + /** + * International Designator (COSPAR ID): Launch number of the year. + * + * Range: 1 to 999 + * + * Example: 67 + * + * @param {string} tleLine1 The first line of the Tle to parse. + * @returns {number} The launch number of the International Designator. + */ + static intlDesLaunchNum(tleLine1: string): number { + return parseInt(tleLine1.substring(Tle.intlDesLaunchNum_.start, Tle.intlDesLaunchNum_.stop)); + } + + /** + * International Designator (COSPAR ID): Piece of the launch. + * + * Range: A to ZZZ + * + * Example: 'A' + * + * @param {string} tleLine1 The first line of the Tle to parse. + * @returns {string} The launch piece of the International Designator. + */ + 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. + * + * Range: 00 to 99 + * + * Example: 98 + * + * @param {string} tleLine1 The first line of the Tle to parse. + * @returns {number} The year of the International Designator. + */ + static intlDesYear(tleLine1: TleLine1): number { + return parseInt(tleLine1.substring(Tle.intlDesYear_.start, Tle.intlDesYear_.stop)); + } + + /** + * 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. + */ + 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 + * + * Units: degrees + * + * Range: 0 to 359.9999 + * + * Example: 25.2906 + * + * @param {string} tleLine2 The second line of the Tle to parse. + * @returns {number} The mean anomaly of the satellite. + */ + static meanAnomaly(tleLine2: TleLine2): number { + 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); + } + + /** + * 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 + * + * Example: 0.00001961 + * + * @param {string} tleLine1 The first line of the Tle to parse. + * @returns {number} 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. + * + * Units: Orbits / day ^ 3. + * + * Example: 0 ('00000-0' in the original Tle [= 0.00000 * 10 ^ 0]) + * + * @param {string} tleLine1 The first line of the Tle to parse. + * @returns {number} 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 + * + * 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)); + + if (!(meanMo > 0 && meanMo <= 17)) { + 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. + * + * @param tleLine2 The Tle line 2. + * @returns The period of the satellite orbit in minutes. + */ + static period(tleLine2: TleLine2): number { + const meanMo = Tle.meanMotion(tleLine2); + + return 1440 / meanMo; + } + + /** + * 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 + * + * Range: 0 to 359.9999 + * + * Example: 208.9163 + * + * @param {string} tleLine2 The second line of the Tle to parse. + * @returns {number} The right ascension of the satellite. + */ + static rightAscension(tleLine2: TleLine2): number { + const raan = parseFloat(tleLine2.substring(Tle.raan_.start, Tle.raan_.stop)); + + if (!(raan >= 0 && raan <= 360)) { + throw new Error(`Invalid RAAN: ${raan}`); + } + + return toPrecision(raan, 4); + } + + /** + * See https://en.wikipedia.org/wiki/Satellite_Catalog_Number + * + * 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. + * + * Example: 25544 or B1234 (e.g. Sputnik's rocket body was number 00001) + * + * @param {string} tleLine The first line of the Tle to parse. + * @returns {string} 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). + * + * Range: 0 to 99999 + * + * Example: 6766 + * + * @param {string} tleLine2 The second line of the Tle to parse. + * @returns {number} The revolutions around the Earth per day (mean motion). + */ + static revNum(tleLine2: TleLine2): number { + return parseInt(tleLine2.substring(Tle.revNum_.start, Tle.revNum_.stop)); + } + + /** + * See https://en.wikipedia.org/wiki/Satellite_Catalog_Number + * + * 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. + * + * Example: 25544 or B1234 (e.g. Sputnik's rocket body was number 00001) + * + * @param {string} tleLine The first line of the Tle to parse. + * @returns {number} NORAD catalog number. + */ + 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} 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); + 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} 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); + const satNum = Tle.satNum(tleLine2); + const satNumRaw = Tle.rawSatNum(tleLine2); + const inclination = Tle.inclination(tleLine2); + const raan = 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, + raan, + 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} 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 { + 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, + raan: line2.raan, + 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} 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); + 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 + */ + static convert6DigitToA5(sccNum: string): string { + // Only applies to 6 digit numbers + if (sccNum.length < 6) { + return sccNum; + } + + // 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}`; + } + + static convertA5to6Digit(sccNum: string): string { + const values = sccNum.toUpperCase().split(''); + + if (values[0] in Tle.alpha5_) { + values[0] = Tle.alpha5_[values[0]]; + } + + return values.join(''); } } diff --git a/src/tle/tle-format-data.ts b/src/coordinate/tle-format-data.ts similarity index 100% rename from src/tle/tle-format-data.ts rename to src/coordinate/tle-format-data.ts diff --git a/src/enums/Sgp4OpsMode.ts b/src/enums/Sgp4OpsMode.ts new file mode 100644 index 0000000..7f288e4 --- /dev/null +++ b/src/enums/Sgp4OpsMode.ts @@ -0,0 +1,4 @@ +export enum Sgp4OpsMode { + AFSPC = 'a', + IMPROVED = 'i', +} diff --git a/src/objects/Satellite.ts b/src/objects/Satellite.ts index 97bc6c5..6d29bc5 100644 --- a/src/objects/Satellite.ts +++ b/src/objects/Satellite.ts @@ -30,16 +30,15 @@ import { OptionsParams } from 'src/interfaces/OptionsParams'; import { SatelliteParams } from 'src/interfaces/SatelliteParams'; -import { FormatTle } from '../coordinate/FormatTle'; import { Geodetic } from '../coordinate/Geodetic'; import { ITRF } from '../coordinate/ITRF'; import { J2000 } from '../coordinate/J2000'; import { RIC } from '../coordinate/RIC'; +import { Tle } from '../coordinate/Tle'; import { RAE } from '../observation/RAE'; import { Vector3D } from '../operations/Vector3D'; import { Sgp4 } from '../sgp4/sgp4'; import { EpochUTC } from '../time/EpochUTC'; -import { Tle } from '../tle/tle'; import { ecf2rae, eci2ecf, eci2lla, jday } from '../transforms'; import { Degrees, @@ -105,14 +104,14 @@ export class Satellite extends BaseObject { constructor(info: SatelliteParams, options?: OptionsParams) { super(info); - const tleData = Tle.parseTle(info.tle1, info.tle2); + const tleData = Tle.parse(info.tle1, info.tle2); this.tle1 = info.tle1; this.tle2 = info.tle2; this.sccNum = info.sccNum || tleData.satNum.toString(); - this.sccNum5 = FormatTle.convert6DigitToA5(this.sccNum); - this.sccNum6 = FormatTle.convertA5to6Digit(this.sccNum5); + this.sccNum5 = Tle.convert6DigitToA5(this.sccNum); + this.sccNum6 = Tle.convertA5to6Digit(this.sccNum5); this.epochYear = tleData.epochYear; this.epochDay = tleData.epochDay; this.meanMoDev1 = tleData.meanMoDev1; @@ -124,7 +123,7 @@ export class Satellite extends BaseObject { this.argOfPerigee = tleData.argOfPerigee; this.meanAnomaly = tleData.meanAnomaly; this.meanMotion = tleData.meanMotion; - this.period = 1440 / this.meanMotion; + this.period = tleData.period; // NOTE: Calculate apogee and perigee diff --git a/src/observation/RAE.ts b/src/observation/RAE.ts index be10b01..ccebf08 100644 --- a/src/observation/RAE.ts +++ b/src/observation/RAE.ts @@ -16,8 +16,17 @@ export class RAE { public range: number, public azimuth: Radians, public elevation: Radians, + /** + * The range rate of the satellite relative to the observer in kilometers per second. + */ public rangeRate?: number, + /** + * The azimuth rate of the satellite relative to the observer in radians per second. + */ public azimuthRate?: number, + /** + * The elevation rate of the satellite relative to the observer in radians per second. + */ public elevationRate?: number, ) { // Do nothing diff --git a/src/ootk-core.ts b/src/ootk-core.ts index 4e79e49..a029e74 100644 --- a/src/ootk-core.ts +++ b/src/ootk-core.ts @@ -33,6 +33,7 @@ export * from './body'; export * from './coordinate'; +export { Tle } from './coordinate/Tle'; export * from './data/DataHandler'; export * from './enums'; export * from './interfaces'; @@ -41,7 +42,6 @@ export * from './observation'; export * from './operations/operations'; export { Sgp4 } from './sgp4/sgp4'; export * from './time/time'; -export { Tle } from './tle/tle'; export * from './transforms'; export * from './types/types'; export * from './utils/constants'; diff --git a/src/sgp4/sgp4.ts b/src/sgp4/sgp4.ts index 2cfa309..2f448bb 100644 --- a/src/sgp4/sgp4.ts +++ b/src/sgp4/sgp4.ts @@ -42,7 +42,7 @@ /* eslint-disable max-lines-per-function */ /* eslint-disable max-lines */ -import { Sgp4OpsMode } from '../coordinate/TLE'; +import { Sgp4OpsMode } from '../enums/Sgp4OpsMode'; import { GreenwichMeanSiderealTime, SatelliteRecord, StateVectorSgp4, Vec3Flat } from '../types/types'; import { DEG2RAD, PI, TAU, temp4, x2o3 } from '../utils/constants'; @@ -233,7 +233,7 @@ class Sgp4 { tleLine1: string, tleLine2: string, whichconst = Sgp4GravConstants.wgs72, - opsmode = Sgp4OpsMode.improved, + opsmode = Sgp4OpsMode.IMPROVED, ): SatelliteRecord { let year = 0; @@ -3519,7 +3519,7 @@ class Sgp4 { /* eslint-disable no-param-reassign */ const { whichconst = Sgp4GravConstants.wgs72, - opsmode = Sgp4OpsMode.improved, + opsmode = Sgp4OpsMode.IMPROVED, satn = satrec.satnum, epoch, xbstar, diff --git a/src/tle/tle.ts b/src/tle/tle.ts deleted file mode 100644 index 3dcf539..0000000 --- a/src/tle/tle.ts +++ /dev/null @@ -1,719 +0,0 @@ -/** - * @author Theodore Kruczek. - * @description Orbital Object ToolKit (OOTK) 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. - * - * @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: - * - * 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 { Line1Data, Line2Data, TleLine1, TleLine2 } from '../types/types'; -import { toPrecision } from '../utils/functions'; -import { TleFormatData } from './tle-format-data'; - -/** - * Two-line element set data for a satellite. - */ -type TleData = { - satNum: number; - intlDes: string; - epochYear: number; - epochDay: number; - meanMoDev1: number; - meanMoDev2: number; - bstar: number; - inclination: number; - raan: number; - eccentricity: number; - argOfPerigee: number; - meanAnomaly: number; - meanMotion: number; -}; - -/** - * Represents a set of data containing both Line 1 and Line 2 TLE information. - */ -type TleDataFull = Line1Data & Line2Data; - -/** - * TLE is a static class with a collection of methods for working with TLEs. - */ -export class Tle { - /** 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 international designator field. */ - private static readonly intlDes_ = new TleFormatData(10, 17); - /** 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 raan_ = 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); - - /** - * Argument of perigee. See https://en.wikipedia.org/wiki/Argument_of_perigee - * - * Units: degrees - * - * Range: 0 to 359.9999 - * - * Example: 69.9862 - * - * @param {string} tleLine2 The second line of the TLE to parse. - * @returns {number} The argument of perigee in degrees. - */ - static getArgOfPerigee(tleLine2: TleLine2): number { - 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); - } - - /** - * BSTAR drag term (decimal point assumed). Estimates the effects of - * atmospheric drag on the satellite's motion. - * - * Units: EarthRadii ^ -1 - * - * Example: 0.000036771 ('36771-4' in the original TLE [= 0.36771 * 10 ^ -4]) - * - * @param {string} tleLine1 The first line of the TLE to parse. - * @returns {number} The drag coefficient. - */ - static getBstar(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. - * - * Range: 0 to 9 - * Example: 3 - * - * @param {string} tleLine The first line of the TLE to parse. - * @returns {number} The checksum value. - */ - static getChecksum(tleLine: TleLine1 | TleLine2): number { - return parseInt(tleLine.substring(Tle.checksum_.start, Tle.checksum_.stop)); - } - - /** - * Returns the satellite classification. - * * 'U' = unclassified - * * 'C' = confidential - * * 'S' = secret - * - * Example: 'U' - */ - static getClassification(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). - * - * Range: 0 to 1 - * - * Example: 0.0006317 (`0006317` in the original TLE) - * - * @param {string} tleLine2 The second line of the TLE to parse. - * @returns {number} The eccentricity of the satellite. - */ - static getEccentricity(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. 999 seems to mean the TLE - * has maxed out. - * - * 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. - */ - static getElsetNum(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!). - * - * Example: 0 - * - * @param {string} tleLine1 The first line of the TLE to parse. - * @returns {number} The ephemeris type. - */ - static getEphemerisType(tleLine1: TleLine1): number { - return parseInt(tleLine1.substring(Tle.ephemerisType_.start, Tle.ephemerisType_.stop)); - } - - /** - * Fractional day of the year when the TLE was generated (TLE epoch). - * - * Range: 1 to 365.99999999 - * - * Example: 206.18396726 - * - * @param {string} tleLine1 The first line of the TLE to parse. - * @returns {number} The day of the year the TLE was generated. - */ - static getEpochDay(tleLine1: string): number { - const epochDay = parseFloat(tleLine1.substring(Tle.epochDay_.start, Tle.epochDay_.stop)); - - if (epochDay < 1 || epochDay > 365.99999999) { - throw new Error('Invalid epoch day'); - } - - return toPrecision(epochDay, 8); - } - - /** - * Year when the TLE was generated (TLE epoch), last two digits. - * - * Range: 00 to 99 - * - * Example: 17 - * - * @param {string} tleLine1 The first line of the TLE to parse. - * @returns {number} The year the TLE was generated. - */ - static getEpochYear(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. - * - * Range: 1957 to 2056 - * - * Example: 2008 - * - * @param {string} tleLine1 The first line of the TLE to parse. - * @returns {number} The year the TLE was generated. - */ - static getEpochYearFull(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. - * - * Units: degrees - * - * Range: 0 to 180 - * - * Example: 51.6400 - * - * @param {string} tleLine2 The second line of the TLE to parse. - * @returns {number} The inclination of the satellite. - */ - static getInclination(tleLine2: TleLine2): number { - 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); - } - - /** - * 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 getIntlDes(tleLine1: TleLine1): string { - return tleLine1.substring(Tle.intlDes_.start, Tle.intlDes_.stop).trim(); - } - - /** - * International Designator (COSPAR ID): Launch number of the year. - * - * Range: 1 to 999 - * - * Example: 67 - * - * @param {string} tleLine1 The first line of the TLE to parse. - * @returns {number} The launch number of the International Designator. - */ - static getIntlDesLaunchNum(tleLine1: string): number { - return parseInt(tleLine1.substring(Tle.intlDesLaunchNum_.start, Tle.intlDesLaunchNum_.stop)); - } - - /** - * International Designator (COSPAR ID): Piece of the launch. - * - * Range: A to ZZZ - * - * Example: 'A' - * - * @param {string} tleLine1 The first line of the TLE to parse. - * @returns {string} The launch piece of the International Designator. - */ - static getIntlDesLaunchPiece(tleLine1: TleLine1): string { - return tleLine1.substring(Tle.intlDesLaunchPiece_.start, Tle.intlDesLaunchPiece_.stop).trim(); - } - - /** - * International Designator (COSPAR ID): Last 2 digits of launch year. - * - * Range: 00 to 99 - * - * Example: 98 - * - * @param {string} tleLine1 The first line of the TLE to parse. - * @returns {number} The year of the International Designator. - */ - static getIntlDesYear(tleLine1: TleLine1): number { - return parseInt(tleLine1.substring(Tle.intlDesYear_.start, Tle.intlDesYear_.stop)); - } - - /** - * 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. - */ - static getLineNumber(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 - * - * Units: degrees - * - * Range: 0 to 359.9999 - * - * Example: 25.2906 - * - * @param {string} tleLine2 The second line of the TLE to parse. - * @returns {number} The mean anomaly of the satellite. - */ - static getMeanAnomaly(tleLine2: TleLine2): number { - 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); - } - - /** - * 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 - * - * Example: 0.00001961 - * - * @param {string} tleLine1 The first line of the TLE to parse. - * @returns {number} The first derivative of the mean motion. - */ - static getMeanMoDev1(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. - * - * Units: Orbits / day ^ 3. - * - * Example: 0 ('00000-0' in the original TLE [= 0.00000 * 10 ^ 0]) - * - * @param {string} tleLine1 The first line of the TLE to parse. - * @returns {number} The second derivative of the mean motion. - */ - static getMeanMoDev2(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 - * - * 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 getMeanMotion(tleLine2: TleLine2): number { - const meanMo = parseFloat(tleLine2.substring(Tle.meanMo_.start, Tle.meanMo_.stop)); - - if (!(meanMo >= 0 && meanMo <= 17)) { - throw new Error(`Invalid mean motion: ${meanMo}`); - } - - return toPrecision(meanMo, 8); - } - - /** - * 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 - * - * Range: 0 to 359.9999 - * - * Example: 208.9163 - * - * @param {string} tleLine2 The second line of the TLE to parse. - * @returns {number} The right ascension of the satellite. - */ - static getRaan(tleLine2: TleLine2): number { - const raan = parseFloat(tleLine2.substring(Tle.raan_.start, Tle.raan_.stop)); - - if (!(raan >= 0 && raan <= 360)) { - throw new Error(`Invalid RAAN: ${raan}`); - } - - return toPrecision(raan, 4); - } - - /** - * See https://en.wikipedia.org/wiki/Satellite_Catalog_Number - * - * 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. - * - * Example: 25544 or B1234 (e.g. Sputnik's rocket body was number 00001) - * - * @param {string} tleLine The first line of the TLE to parse. - * @returns {string} NORAD catalog number. - */ - static getRawSatNum(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). - * - * Range: 0 to 99999 - * - * Example: 6766 - * - * @param {string} tleLine2 The second line of the TLE to parse. - * @returns {number} The revolutions around the Earth per day (mean motion). - */ - static getRevNum(tleLine2: TleLine2): number { - return parseInt(tleLine2.substring(Tle.revNum_.start, Tle.revNum_.stop)); - } - - /** - * See https://en.wikipedia.org/wiki/Satellite_Catalog_Number - * - * 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. - * - * Example: 25544 or B1234 (e.g. Sputnik's rocket body was number 00001) - * - * @param {string} tleLine The first line of the TLE to parse. - * @returns {number} NORAD catalog number. - */ - static getSatNum(tleLine: TleLine1 | TleLine2): number { - const satNumStr = tleLine.substring(Tle.satNum_.start, Tle.satNum_.stop); - const leadingChar = satNumStr.split('')[0].toLowerCase(); // Using uppercase will break the -96 math. - - if (isNaN(parseInt(leadingChar))) { - return parseInt(leadingChar.charCodeAt(0) - 96 + 9 + satNumStr.slice(1, 5)); - } - - return parseInt(satNumStr); - } - - /** - * 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. - */ - static parseLine1(tleLine1: TleLine1): Line1Data { - const lineNumber1 = Tle.getLineNumber(tleLine1); - const satNum = Tle.getSatNum(tleLine1); - const satNumRaw = Tle.getRawSatNum(tleLine1); - const classification = Tle.getClassification(tleLine1); - const intlDes = Tle.getIntlDes(tleLine1); - const intlDesYear = Tle.getIntlDesYear(tleLine1); - const intlDesLaunchNum = Tle.getIntlDesLaunchNum(tleLine1); - const intlDesLaunchPiece = Tle.getIntlDesLaunchPiece(tleLine1); - const epochYear = Tle.getEpochYear(tleLine1); - const epochYearFull = Tle.getEpochYearFull(tleLine1); - const epochDay = Tle.getEpochDay(tleLine1); - const meanMoDev1 = Tle.getMeanMoDev1(tleLine1); - const meanMoDev2 = Tle.getMeanMoDev2(tleLine1); - const bstar = Tle.getBstar(tleLine1); - const ephemerisType = Tle.getEphemerisType(tleLine1); - const elsetNum = Tle.getElsetNum(tleLine1); - const checksum1 = Tle.getChecksum(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} 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.getLineNumber(tleLine2); - const satNum = Tle.getSatNum(tleLine2); - const satNumRaw = Tle.getRawSatNum(tleLine2); - const inclination = Tle.getInclination(tleLine2); - const raan = Tle.getRaan(tleLine2); - const eccentricity = Tle.getEccentricity(tleLine2); - const argOfPerigee = Tle.getArgOfPerigee(tleLine2); - const meanAnomaly = Tle.getMeanAnomaly(tleLine2); - const meanMotion = Tle.getMeanMotion(tleLine2); - const revNum = Tle.getRevNum(tleLine2); - const checksum2 = Tle.getChecksum(tleLine2); - - return { - lineNumber2, - satNum, - satNumRaw, - inclination, - raan, - eccentricity, - argOfPerigee, - meanAnomaly, - meanMotion, - revNum, - checksum2, - }; - } - - /** - * 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 - * @returns {TleData} Returns most commonly used orbital data from TLE - */ - static parseTle(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, - raan: line2.raan, - eccentricity: line2.eccentricity, - argOfPerigee: line2.argOfPerigee, - meanAnomaly: line2.meanAnomaly, - meanMotion: line2.meanMotion, - }; - } - - /** - * 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. - */ - static parseTleFull(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 }; - } -} diff --git a/src/types/types.ts b/src/types/types.ts index d28fe84..b769aca 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -521,6 +521,7 @@ export type Line1Data = { * - meanMotion: The mean motion of the satellite. * - revNum: The revolution number at epoch. * - checksum2: The checksum of the second line of the TLE. + * - period: The period of the satellite's orbit, derived from the mean motion. * * @see https://en.wikipedia.org/wiki/Two-line_element_set */ @@ -536,6 +537,7 @@ export type Line2Data = { meanMotion: number; revNum: number; checksum2: number; + period: number; }; /** @@ -680,3 +682,26 @@ export type Lookangle = { rng: Kilometers; maxElPass?: Degrees; }; +/** + * Two-line element set data for a satellite. + */ +export type TleData = { + satNum: number; + intlDes: string; + epochYear: number; + epochDay: number; + meanMoDev1: number; + meanMoDev2: number; + bstar: number; + inclination: number; + raan: number; + eccentricity: number; + argOfPerigee: number; + meanAnomaly: number; + meanMotion: number; + period: number; +}; +/** + * Represents a set of data containing both Line 1 and Line 2 TLE information. + */ +export type TleDataFull = Line1Data & Line2Data;