From dbed6a1ceca9e6de6cf7e2d1c4be7e187cf7e833 Mon Sep 17 00:00:00 2001 From: Theodore Kruczek Date: Sun, 14 Jan 2024 13:52:51 -0500 Subject: [PATCH] test: :white_check_mark: add tests and documentation to FormatTle --- examples/satellite-js-migration.ts | 42 +++++--- src/coordinate/ClassicalElements.ts | 2 +- src/coordinate/FormatTle.ts | 132 +++++++++++++++++------ src/coordinate/index.ts | 2 +- src/{coordinate => enums}/OrbitRegime.ts | 0 src/types/types.ts | 18 ++++ test/coordinate/FormatTle.test.ts | 92 ++++++++++++++++ 7 files changed, 237 insertions(+), 51 deletions(-) rename src/{coordinate => enums}/OrbitRegime.ts (100%) create mode 100644 test/coordinate/FormatTle.test.ts diff --git a/examples/satellite-js-migration.ts b/examples/satellite-js-migration.ts index 41313fb..7ce5cce 100644 --- a/examples/satellite-js-migration.ts +++ b/examples/satellite-js-migration.ts @@ -20,7 +20,7 @@ const exampleDate = new Date(1705109326817); // Sample TLE const tle1 = '1 56006U 23042W 24012.45049317 .00000296 00000-0 36967-4 0 9992' as TleLine1; -const tle2 = '2 56006 43.0043 13.3620 0001137 267.5965 92.4747 15.02542972 44491' as TleLine2; +const tle2 = '2 56006 143.0043 13.3620 0001137 267.5965 92.4747 15.02542972 44491' as TleLine2; // Initialize a Satellite Object const satellite = new Satellite({ @@ -28,11 +28,14 @@ const satellite = new Satellite({ tle2, }); -const elements = satellite.getClassicalElements(new Date(1705109326817)); +const elements = satellite.getClassicalElements(new Date(1705109326817)).toEquinoctialElements(); console.warn(elements); -// You can still propagate a satellite using time since epoch (in minutes), but it's not recommended. +/* + * You can still propagate a satellite using time since epoch (in minutes), but + * it's not recommended. + */ const timeSinceTleEpochMinutes = 10; const positionAndVelocity = Sgp4.propagate(satellite.satrec, timeSinceTleEpochMinutes); @@ -42,20 +45,23 @@ const positionAndVelocity2 = satellite.eci(new Date(2024, 0, 1)); const positionAndVelocity3 = 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. + * 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; // positionAndVelocity might be false const velocityEci = positionAndVelocity.velocity; // typescript will error on this code /* * Unlike satellite.js using the eci method will ALWAYS return a result or throw - * an error if it can't propagate the satellite. No more checking for false and trying to handle - * a combined object and boolean result. + * an error if it can't propagate the satellite. No more checking for false and + * trying to handle a combined object and boolean result. */ const positionEci2 = satellite.eci().position; // This is correctly typed -// Set the Observer at 71°W, 41°N, 0.37 km altitude using DEGREES because who likes using Radians? +/* + * Set the Observer at 71°W, 41°N, 0.37 km altitude using DEGREES because who + * likes using Radians? + */ const observer = new GroundPosition({ lon: -71.0308 as Degrees, lat: 41.9613422 as Degrees, @@ -63,7 +69,8 @@ const observer = new GroundPosition({ }); /** - * You can still calculate GMST if you want to, but unlike satellite.js it's not required. + * You can still calculate GMST if you want to, but unlike satellite.js it's not + * required. */ const { gmst, j } = calcGmst(new Date()); @@ -80,10 +87,11 @@ let dopplerShiftedFrequency = uplinkFreq * dopplerFactor; dopplerShiftedFrequency = satellite.applyDoppler(uplinkFreq, observer, exampleDate); /** - * The coordinates are all stored in strongly typed key-value pairs. - * ECI and ECF are accessed by `x`, `y`, `z` properties. + * The coordinates are all stored in strongly typed key-value pairs. ECI and ECF + * are accessed by `x`, `y`, `z` properties. * - * satellite.js generates Property 'x' does not exist on type 'boolean | { x: number; y: number; z: number; }'. + * satellite.js generates Property 'x' does not exist on type 'boolean | { x: + * number; y: number; z: number; }'. */ const position = satellite.eci(exampleDate).position; const satelliteX = position.x; // This is typed as Kilometers @@ -118,13 +126,17 @@ const height = positionGd.alt; // Height is in Kilometers const longitudeRad = longitude * DEG2RAD; const latitudeRad = latitude * DEG2RAD; /** - * In TypeScript you need to label your units. - * This will help prevent you from passing the wrong units into functions. + * In TypeScript you need to label your units. This will help prevent you from + * passing the wrong units into functions. */ const longitudeRad2 = (longitude * DEG2RAD) as Radians; const latitudeRad2 = (latitude * DEG2RAD) as Radians; -// lla2eci(positionGd, gmst); // Throws an error: Argument of type 'LlaVec3' is not assignable to parameter of type 'LlaVec3'. +/* + * lla2eci(positionGd, gmst); // Throws an error: Argument of type + * 'LlaVec3' is not assignable to parameter of type + * 'LlaVec3'. + */ lla2eci(observer.llaRad(), gmst); // This is correctly typed console.log('Satellite.js Migration Example'); diff --git a/src/coordinate/ClassicalElements.ts b/src/coordinate/ClassicalElements.ts index cd696a9..249033e 100644 --- a/src/coordinate/ClassicalElements.ts +++ b/src/coordinate/ClassicalElements.ts @@ -4,7 +4,7 @@ import { EpochUTC } from '../time/EpochUTC'; import { earthGravityParam, RAD2DEG, sec2min, secondsPerDay, TAU } from '../utils/constants'; import { clamp, matchHalfPlane, newtonNu } from '../utils/functions'; import { EquinoctialElements } from './EquinoctialElements'; -import { OrbitRegime } from './OrbitRegime'; +import { OrbitRegime } from '../enums/OrbitRegime'; import { PositionVelocity, StateVector } from './StateVector'; import { ClassicalElementsParams } from '../interfaces/ClassicalElementsParams'; diff --git a/src/coordinate/FormatTle.ts b/src/coordinate/FormatTle.ts index d88e0bf..237405e 100644 --- a/src/coordinate/FormatTle.ts +++ b/src/coordinate/FormatTle.ts @@ -1,40 +1,20 @@ -import type { Satellite } from '../objects'; +import { StringifiedNumber, TleParams } from 'src/types/types'; import { Tle } from './Tle'; -export type StringifiedNumber = `${number}.${number}`; - -export type TleParams = { - sat?: Satellite; - inc: StringifiedNumber; - meanmo: StringifiedNumber; - rasc: StringifiedNumber; - argPe: StringifiedNumber; - meana: StringifiedNumber; - ecen: string; - epochyr: string; - epochday: string; - /** COSPAR International Designator */ - intl: string; - /** alpha 5 satellite number */ - scc: string; -}; - +/** + * A class containing static methods for formatting TLEs (Two-Line Elements). + */ export abstract class FormatTle { - static argumentOfPerigee(argPe: number | string): StringifiedNumber { - if (typeof argPe === 'number') { - argPe = argPe.toString(); - } - - const argPeNum = parseFloat(argPe).toFixed(4); - const argPe0 = argPeNum.padStart(8, '0'); - - if (argPe0.length !== 8) { - throw new Error('argPe length is not 8'); - } - - return argPe0 as StringifiedNumber; + private constructor() { + // Static class } + /** + * Creates a TLE (Two-Line Element) string based on the provided TleParams. + * @param tleParams - The parameters used to generate the TLE. + * + * @returns An object containing the TLE strings tle1 and tle2. + */ static createTle(tleParams: TleParams): { tle1: string; tle2: string } { const { inc, meanmo, rasc, argPe, meana, ecen, epochyr, epochday, intl } = tleParams; const scc = Tle.convert6DigitToA5(tleParams.scc); @@ -46,6 +26,7 @@ export abstract class FormatTle { const argPeStr = FormatTle.argumentOfPerigee(argPe); const meanaStr = FormatTle.meanAnomaly(meana); const ecenStr = FormatTle.eccentricity(ecen); + const intlStr = intl.padEnd(8, ' '); // M' and M'' are both set to 0 to put the object in a perfect stable orbit let TLE1Ending = tleParams.sat ? tleParams.sat.tle1.substring(32, 71) : ' +.00000000 +00000+0 +00000-0 0 9990'; @@ -56,12 +37,44 @@ export abstract class FormatTle { TLE1Ending = TLE1Ending[21] === ' ' ? FormatTle.setCharAt(TLE1Ending, 21, '+') : TLE1Ending; TLE1Ending = TLE1Ending[32] === ' ' ? FormatTle.setCharAt(TLE1Ending, 32, '0') : TLE1Ending; - const tle1 = `1 ${scc}U ${intl} ${epochYrStr}${epochdayStr}${TLE1Ending}`; + const tle1 = `1 ${scc}U ${intlStr} ${epochYrStr}${epochdayStr}${TLE1Ending}`; const tle2 = `2 ${scc} ${incStr} ${rascStr} ${ecenStr} ${argPeStr} ${meanaStr} ${meanmoStr} 00010`; return { tle1, tle2 }; } + /** + * Converts the argument of perigee to a stringified number. + * + * @param argPe - The argument of perigee to be converted. Can be either a + * number or a string. + * @returns The argument of perigee as a stringified number. + * + * @throws Error if the length of the argument of perigee is not 8. + */ + static argumentOfPerigee(argPe: number | string): StringifiedNumber { + if (typeof argPe === 'number') { + argPe = argPe.toString(); + } + + const argPeNum = parseFloat(argPe).toFixed(4); + const argPe0 = argPeNum.padStart(8, '0'); + + if (argPe0.length !== 8) { + throw new Error('argPe length is not 8'); + } + + return argPe0 as StringifiedNumber; + } + + /** + * Returns the eccentricity value of a given string. + * + * @param ecen - The string representing the eccentricity. + * @returns The eccentricity value. + * + * @throws Error if the length of the eccentricity string is not 7. + */ static eccentricity(ecen: string): string { let ecen0 = ecen.padEnd(9, '0'); @@ -77,6 +90,14 @@ export abstract class FormatTle { return ecen0; } + /** + * Converts the inclination value to a string representation. + * + * @param inc - The inclination value to be converted. + * @returns The string representation of the inclination value. + * + * @throws Error if the length of the converted value is not 8. + */ static inclination(inc: number | string): StringifiedNumber { if (typeof inc === 'number') { inc = inc.toString(); @@ -92,6 +113,17 @@ export abstract class FormatTle { return inc0 as StringifiedNumber; } + /** + * Converts the mean anomaly to a string representation with 8 digits, padded + * with leading zeros. + * @param meana - The mean anomaly to be converted. Can be either a number or + * a string. + * + * @returns The mean anomaly as a string with 8 digits, padded with leading + * zeros. + * + * @throws Error if the length of the mean anomaly is not 8. + */ static meanAnomaly(meana: number | string): StringifiedNumber { if (typeof meana === 'number') { meana = meana.toString(); @@ -107,6 +139,21 @@ export abstract class FormatTle { return meana0 as StringifiedNumber; } + /** + * Converts the mean motion value to a string representation with 8 decimal + * places. If the input is a number, it is converted to a string. If the input + * is already a string, it is parsed as a float and then converted to a string + * with 8 decimal places. The resulting string is padded with leading zeros to + * ensure a length of 11 characters. Throws an error if the resulting string + * does not have a length of 11 characters. + * + * @param meanmo - The mean motion value to be converted. + * @returns The string representation of the mean motion value with 8 decimal + * places and padded with leading zeros. + * + * @throws Error if the resulting string does not have a length of 11 + * characters. + */ static meanMotion(meanmo: number | string): StringifiedNumber { if (typeof meanmo === 'number') { meanmo = meanmo.toString(); @@ -122,6 +169,13 @@ export abstract class FormatTle { return meanmo0 as StringifiedNumber; } + /** + * Converts the right ascension value to a stringified number. + * + * @param rasc - The right ascension value to convert. + * @returns The stringified number representation of the right ascension. + * @throws Error if the length of the converted right ascension is not 8. + */ static rightAscension(rasc: number | string): StringifiedNumber { if (typeof rasc === 'number') { rasc = rasc.toString(); @@ -137,7 +191,17 @@ export abstract class FormatTle { return rasc0 as StringifiedNumber; } - static setCharAt(str: string, index: number, chr: string) { + /** + * Sets a character at a specific index in a string. If the index is out of + * range, the original string is returned. + * @param str - The input string. + * + * @param index - The index at which to set the character. + * + * @param chr - The character to set at the specified index. @returns The + * modified string with the character set at the specified index. + */ + static setCharAt(str: string, index: number, chr: string): string { if (index > str.length - 1) { return str; } diff --git a/src/coordinate/index.ts b/src/coordinate/index.ts index 94a13e0..22fbae7 100644 --- a/src/coordinate/index.ts +++ b/src/coordinate/index.ts @@ -4,7 +4,7 @@ export * from './FormatTle'; export * from './Geodetic'; export * from './ITRF'; export * from './J2000'; -export * from './OrbitRegime'; +export * from '../enums/OrbitRegime'; export * from './RelativeState'; export * from './RIC'; export * from './StateVector'; diff --git a/src/coordinate/OrbitRegime.ts b/src/enums/OrbitRegime.ts similarity index 100% rename from src/coordinate/OrbitRegime.ts rename to src/enums/OrbitRegime.ts diff --git a/src/types/types.ts b/src/types/types.ts index 286a1dd..adc4222 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,3 +1,4 @@ +import { Satellite } from 'src/objects'; import { PassType } from '../enums/PassType'; /** @@ -696,3 +697,20 @@ export type TleData = { * Represents a set of data containing both Line 1 and Line 2 TLE information. */ export type TleDataFull = Line1Data & Line2Data; +export type StringifiedNumber = `${number}.${number}`; + +export type TleParams = { + sat?: Satellite; + inc: StringifiedNumber; + meanmo: StringifiedNumber; + rasc: StringifiedNumber; + argPe: StringifiedNumber; + meana: StringifiedNumber; + ecen: string; + epochyr: string; + epochday: string; + /** COSPAR International Designator */ + intl: string; + /** alpha 5 satellite number */ + scc: string; +}; diff --git a/test/coordinate/FormatTle.test.ts b/test/coordinate/FormatTle.test.ts new file mode 100644 index 0000000..ad93256 --- /dev/null +++ b/test/coordinate/FormatTle.test.ts @@ -0,0 +1,92 @@ +import { FormatTle, TleParams } from '../../src/main'; + +describe('FormatTle', () => { + // Should be able to create a TLE string based on provided TleParams + it('should create a TLE string when given valid TleParams', () => { + const tleParams: TleParams = { + inc: '51.6400', + meanmo: '15.54225995', + rasc: '208.9163', + argPe: '69.9862', + meana: '25.2906', + ecen: '0.0006317', + epochyr: '17', + epochday: '206.18396726', + intl: '58001A', + scc: '00001', + }; + + const tle = FormatTle.createTle(tleParams); + + expect(tle.tle1).toBe('1 00001U 58001A 17206.18396726 +.00000000 +00000+0 +00000-0 0 09990'); + expect(tle.tle2).toBe('2 00001 051.6400 208.9163 0006317 069.9862 025.2906 15.54225995 00010'); + }); + + // Should be able to convert argument of perigee to a stringified number + it('should convert argument of perigee to a stringified number when given a number', () => { + const argPe = 69.9862; + + const result = FormatTle.argumentOfPerigee(argPe); + + expect(result).toBe('069.9862'); + }); + + // Should be able to return the eccentricity value of a given string + it('should return the eccentricity value of a given string', () => { + const ecen = '0.0006317'; + + const result = FormatTle.eccentricity(ecen); + + expect(result).toBe('0006317'); + }); + + // Should throw an error if the length of the eccentricity string is not 7 + it('should throw an error if the length of the eccentricity string is not 7', () => { + const ecen = '0.00063171'; + + expect(() => { + FormatTle.eccentricity(ecen); + }).toThrow('ecen length is not 7'); + }); + + /* + * Should be able to convert the mean anomaly to a string representation with + * 8 digits + */ + it('should convert the mean anomaly to a string representation with 8 digits', () => { + const meana = 25.2906; + const result = FormatTle.meanAnomaly(meana); + + expect(result).toBe('025.2906'); + }); + + /* + * Should be able to convert the mean motion value to a string representation + * with 8 decimal places + */ + it('should convert the mean motion value to a string representation with 8 decimal places', () => { + const meanmo = 15.54225995; + const result = FormatTle.meanMotion(meanmo); + + expect(result).toBe('15.54225995'); + }); + + // Should be able to convert the right ascension value to a stringified number + it('should convert the right ascension value to a stringified number', () => { + const rasc = 123.4567; + const result = FormatTle.rightAscension(rasc); + + expect(result).toBe('123.4567'); + }); + + // Should be able to set a character at a specific index in a string + it('should set a character at a specific index in a string when given valid parameters', () => { + const str = 'Hello, World!'; + const index = 7; + const chr = '!'; + + const result = FormatTle.setCharAt(str, index, chr); + + expect(result).toBe('Hello, !orld!'); + }); +});