From 49501a3f157645fadbadbb2a458a9d5a948056ad Mon Sep 17 00:00:00 2001 From: Theodore Kruczek Date: Sun, 14 Jan 2024 16:21:00 -0500 Subject: [PATCH] test: :white_check_mark: add testing for ITRF class --- src/coordinate/EquinoctialElements.ts | 2 +- src/coordinate/Geodetic.ts | 2 +- src/coordinate/ITRF.ts | 76 +++-- test/coordinate/ITRF.test.ts | 201 +++++++++++ .../__snapshots__/ITRF.test.ts.snap | 321 ++++++++++++++++++ 5 files changed, 573 insertions(+), 29 deletions(-) create mode 100644 test/coordinate/ITRF.test.ts create mode 100644 test/coordinate/__snapshots__/ITRF.test.ts.snap diff --git a/src/coordinate/EquinoctialElements.ts b/src/coordinate/EquinoctialElements.ts index 90c338d..be201c0 100644 --- a/src/coordinate/EquinoctialElements.ts +++ b/src/coordinate/EquinoctialElements.ts @@ -18,7 +18,7 @@ import { EquinoctialElementsParams } from '../interfaces/EquinoctialElementsPara * (equatorial orbits). This makes them more reliable for numerical simulations * and analytical studies, especially in these edge cases. * - * Reference: https://faculty.nps.edu/dad/orbital/th0.pdf + * @see https://faculty.nps.edu/dad/orbital/th0.pdf */ export class EquinoctialElements { epoch: EpochUTC; diff --git a/src/coordinate/Geodetic.ts b/src/coordinate/Geodetic.ts index a2d91a1..60733b2 100644 --- a/src/coordinate/Geodetic.ts +++ b/src/coordinate/Geodetic.ts @@ -34,7 +34,7 @@ export class Geodetic { } if (altitude < -Earth.radiusMean) { - throw new RangeError('Altitude cannot be less than -6378.137 km.'); + throw new RangeError(`Altitude must be greater than ${-Earth.radiusMean} km. Got ${altitude} km.`); } this.lat = latitude; diff --git a/src/coordinate/ITRF.ts b/src/coordinate/ITRF.ts index 668b1e0..3468997 100644 --- a/src/coordinate/ITRF.ts +++ b/src/coordinate/ITRF.ts @@ -15,23 +15,40 @@ import { StateVector } from './StateVector'; * 1980 to 2014. * @see https://en.wikipedia.org/wiki/International_Terrestrial_Reference_Frame * - * This is a geocentric coordinate system, also referenced as ECEF (Earth + * This is a geocentric coordinate system, also referenced as ECF/ECEF (Earth * Centered Earth Fixed). It is a Cartesian coordinate system with the origin at * the center of the Earth. The x-axis intersects the sphere of the Earth at 0° * latitude (the equator) and 0° longitude (the Prime Meridian). The z-axis goes - * through the North Pole. The y-axis goes through 90° East longitude. @see + * through the North Pole. The y-axis goes through 90° East longitude. + * + * @see * https://en.wikipedia.org/wiki/Earth-centered,_Earth-fixed_coordinate_system */ export class ITRF extends StateVector { + /** + * Gets the name of the ITRF coordinate system. + * @returns The name of the coordinate system. + */ get name(): string { return 'ITRF'; } + /** + * Gets a value indicating whether the coordinate system is inertial. + * @returns A boolean value indicating whether the coordinate system is + * inertial. + */ get inertial(): boolean { return false; } - getHeight(): number { + /** + * Gets the height of the ITRF coordinate above the surface of the Earth in + * kilometers. + * + * @returns The height in kilometers. + */ + get height(): Kilometers { const a = Earth.radiusEquator; const e2 = Earth.eccentricitySquared; const r = this.position.magnitude(); @@ -39,11 +56,23 @@ export class ITRF extends StateVector { const cl2 = 1 - sl * sl; const coeff = Math.sqrt((1 - e2) / (1 - e2 * cl2)); - return r - a * coeff; + return (r - a * coeff) as Kilometers; } /** - * Converts the current coordinate to the J2000 coordinate system. + * Gets the altitude in kilometers. + * @returns The altitude in kilometers. + */ + get alt(): Kilometers { + return this.height; + } + + /** + * Converts the current coordinate to the J2000 coordinate system. This is an + * Earth-Centered Inertial (ECI) coordinate system with the origin at the + * center of the Earth. + * + * @see https://en.wikipedia.org/wiki/Epoch_(astronomy)#Julian_years_and_J2000 * @returns The coordinate in the J2000 coordinate system. */ toJ2000(): J2000 { @@ -61,15 +90,9 @@ export class ITRF extends StateVector { } /** - * Converts the coordinate from ITRF (ECEF) to J2000 (ECI) reference frame. - * @returns The coordinate in J2000 (ECI) reference frame. - */ - toEci(): J2000 { - return this.toJ2000(); - } - - /** - * Converts the current ITRF coordinate to Geodetic coordinate. + * Converts the current ITRF coordinate to Geodetic coordinate. This is a + * coordinate system for latitude, longitude, and altitude. + * * @returns {Geodetic} The converted Geodetic coordinate. */ toGeodetic(): Geodetic { @@ -82,23 +105,22 @@ export class ITRF extends StateVector { const r = Math.sqrt(x * x + y * y); const phi = Math.atan(z / r); let lat = phi; + let alt: Kilometers; let c = 0.0; - for (let i = 0; i < 12; i++) { - const slat = Math.sin(lat); + if (x === 0 && y === 0) { + lat = phi; + alt = z > 0 ? ((z - Earth.radiusPolar) as Kilometers) : ((z + Earth.radiusPolar) as Kilometers); + } else { + for (let i = 0; i < 20; i++) { + const slat = Math.sin(lat); - c = 1 / Math.sqrt(1 - esq * slat * slat); - lat = Math.atan((z + sma * c * esq * slat) / r); + c = 1 / Math.sqrt(1 - esq * slat * slat); + lat = Math.atan((z + sma * c * esq * slat) / r); + } + alt = (r / Math.cos(lat) - sma * c) as Kilometers; } - const alt = r / Math.cos(lat) - sma * c; - return new Geodetic(lat as Radians, lon as Radians, alt as Kilometers); - } - - /** - * Converts the current ECI coordinate to latitude, longitude, and altitude. - */ - toLla(): Geodetic { - return this.toGeodetic(); + return new Geodetic(lat as Radians, lon as Radians, alt); } } diff --git a/test/coordinate/ITRF.test.ts b/test/coordinate/ITRF.test.ts new file mode 100644 index 0000000..c739206 --- /dev/null +++ b/test/coordinate/ITRF.test.ts @@ -0,0 +1,201 @@ +import { exampleDate } from '../lib/mockData'; +import { Earth, EpochUTC, ITRF, Kilometers, Vector3D } from './../../src/main'; +// Generated by CodiumAI + +describe('ITRF', () => { + let epochUtc: EpochUTC; + let itrf: ITRF; + + beforeEach(() => { + epochUtc = EpochUTC.fromDateTime(exampleDate); + itrf = new ITRF(epochUtc, new Vector3D(1000, 2000, 3000), new Vector3D(10, 20, 30)); + }); + + // can get the name of the ITRF coordinate system + it('should return the name of the coordinate system', () => { + expect(itrf.name).toMatchSnapshot(); + }); + + // can determine if the coordinate system is inertial + it('should return whether the coordinate system is inertial', () => { + expect(itrf.inertial).toMatchSnapshot(); + }); + + /* + * can get the height of the ITRF coordinate above the surface of the Earth in + * kilometers + */ + it('should return the height above the surface of the Earth in kilometers', () => { + expect(itrf.height).toMatchSnapshot(); + }); + + // can get the altitude in kilometers + it('should return the altitude in kilometers', () => { + expect(itrf.alt).toMatchSnapshot(); + }); + + // can convert the current coordinate to the J2000 coordinate system + it('should convert the current coordinate to the J2000 coordinate system', () => { + expect(itrf.toJ2000()).toMatchSnapshot(); + }); + + // can convert the current ITRF coordinate to Geodetic coordinate + it('should convert the current ITRF coordinate to Geodetic coordinate', () => { + expect(itrf.toGeodetic()).toMatchSnapshot(); + }); + + // can handle position at the center of the Earth + it('should handle position at the center of the Earth', () => { + itrf = new ITRF(epochUtc, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0)); + expect(itrf.toJ2000()).toMatchSnapshot(); + }); + + // can handle position at the North Pole + it('should handle position at the North Pole', () => { + const itrf = new ITRF( + epochUtc, + new Vector3D(0 as Kilometers, 0 as Kilometers, Earth.radiusPolar), + new Vector3D(0, 0, 0), + ); + + expect(itrf.toJ2000()).toMatchSnapshot(); + // Test the toGeodetic method + expect(itrf.toGeodetic()).toMatchSnapshot(); + }); + + // can handle position at the South Pole + it('should handle position at the South Pole', () => { + const itrf = new ITRF(epochUtc, new Vector3D(0, 0, -Earth.radiusPolar), new Vector3D(0, 0, 0)); + + itrf.position = new Vector3D(0, 0, -Earth.radiusPolar); + expect(itrf.toJ2000()).toMatchSnapshot(); + // Test the toGeodetic method + expect(itrf.toGeodetic()).toMatchSnapshot(); + }); + + // can handle position at the International Date Line + it('should handle position at the International Date Line', () => { + const itrf = new ITRF( + epochUtc, + new Vector3D(Earth.radiusEquator, 0 as Kilometers, 0 as Kilometers), + new Vector3D(0, 0, 0), + ); + + // Test height + expect(itrf.height).toMatchSnapshot(); + + // Test alt + expect(itrf.alt).toMatchSnapshot(); + + // Test toJ2000 + expect(itrf.toJ2000()).toMatchSnapshot(); + + // Test toGeodetic + expect(itrf.toGeodetic()).toMatchSnapshot(); + }); + + // can handle position at the Prime Meridian + it('should handle position at the Prime Meridian', () => { + const itrf = new ITRF( + epochUtc, + new Vector3D(0 as Kilometers, Earth.radiusEquator, 0 as Kilometers), + new Vector3D(0, 0, 0), + ); + + expect(itrf.name).toMatchSnapshot(); + expect(itrf.inertial).toMatchSnapshot(); + expect(itrf.height).toMatchSnapshot(); + expect(itrf.alt).toMatchSnapshot(); + expect(itrf.toJ2000()).toMatchSnapshot(); + expect(itrf.toGeodetic()).toMatchSnapshot(); + }); + + // can handle position at high altitude + it('should handle position at high altitude', () => { + const itrf = new ITRF( + epochUtc, + new Vector3D(0 as Kilometers, (Earth.radiusEquator + 1000) as Kilometers, 0 as Kilometers), + new Vector3D(0, 0, 0), + ); + + expect(itrf.height).toMatchSnapshot(); + expect(itrf.alt).toMatchSnapshot(); + expect(itrf.toJ2000()).toMatchSnapshot(); + expect(itrf.toGeodetic()).toMatchSnapshot(); + }); + + // can handle position at low altitude + it('should handle position at low altitude', () => { + const itrf = new ITRF( + epochUtc, + new Vector3D(0 as Kilometers, (Earth.radiusEquator - 10) as Kilometers, 0 as Kilometers), + new Vector3D(0, 0, 0), + ); + + expect(itrf.height).toMatchSnapshot(); + expect(itrf.alt).toMatchSnapshot(); + expect(itrf.toJ2000()).toMatchSnapshot(); + expect(itrf.toGeodetic()).toMatchSnapshot(); + }); + + // can handle position at the antimeridian + it('should handle position at the antimeridian', () => { + const itrf = new ITRF( + epochUtc, + new Vector3D(0 as Kilometers, 0 as Kilometers, Earth.radiusEquator), + new Vector3D(0, 0, 0), + ); + + // Test the height property + expect(itrf.height).toMatchSnapshot(); + + // Test the alt property + expect(itrf.alt).toMatchSnapshot(); + + // Test the toJ2000 method + expect(itrf.toJ2000()).toMatchSnapshot(); + + // Test the toGeodetic method + expect(() => itrf.toGeodetic()).toMatchSnapshot(); + }); + + // can handle position at the equator + it('should handle position at the equator with negative y-coordinate', () => { + const itrf = new ITRF(epochUtc, new Vector3D(0, -Earth.radiusEquator, 0), new Vector3D(0, 0, 0)); + + expect(itrf.name).toMatchSnapshot(); + expect(itrf.inertial).toMatchSnapshot(); + expect(itrf.height).toMatchSnapshot(); + expect(itrf.alt).toMatchSnapshot(); + expect(itrf.toJ2000()).toMatchSnapshot(); + expect(itrf.toGeodetic()).toMatchSnapshot(); + }); + + // can handle position at different epochs + it('should handle position at different epochs', () => { + const position = new Vector3D(1000, 2000, 3000); + const velocity = new Vector3D(10, 20, 30); + const epoch1 = EpochUTC.fromDateTime(exampleDate); + const epoch2 = EpochUTC.fromDateTime(new Date(exampleDate.getTime() + 1000)); + const itrf1 = new ITRF(epoch1, position, velocity); + const itrf2 = new ITRF(epoch2, position, velocity); + + // Test height property + expect(itrf1.height).toMatchSnapshot(); + expect(itrf2.height).toMatchSnapshot(); + + // Test alt property + expect(itrf1.alt).toMatchSnapshot(); + expect(itrf2.alt).toMatchSnapshot(); + + // Test toJ2000 method + const j2000 = itrf1.toJ2000(); + + expect(j2000).toMatchSnapshot(); + + // Test toGeodetic method + const geodetic = itrf1.toGeodetic(); + + expect(geodetic).toMatchSnapshot(); + }); +}); diff --git a/test/coordinate/__snapshots__/ITRF.test.ts.snap b/test/coordinate/__snapshots__/ITRF.test.ts.snap new file mode 100644 index 0000000..ab4dd79 --- /dev/null +++ b/test/coordinate/__snapshots__/ITRF.test.ts.snap @@ -0,0 +1,321 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ITRF should convert the current ITRF coordinate to Geodetic coordinate 1`] = ` +Geodetic { + "alt": -2622.6838742181076, + "lat": 0.935735641017013, + "lon": 1.1071487177940904, +} +`; + +exports[`ITRF should convert the current coordinate to the J2000 coordinate system 1`] = ` +J2000 { + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "position": Vector3D { + "x": -2127.189919581918, + "y": -667.2073608873604, + "z": 3004.978765916125, + }, + "velocity": Vector3D { + "x": -21.223238500045827, + "y": -6.827700074091167, + "z": 30.049679644550032, + }, +} +`; + +exports[`ITRF should handle position at different epochs 1`] = `-2622.7069029008003`; + +exports[`ITRF should handle position at different epochs 2`] = `-2622.7069029008003`; + +exports[`ITRF should handle position at different epochs 3`] = `-2622.7069029008003`; + +exports[`ITRF should handle position at different epochs 4`] = `-2622.7069029008003`; + +exports[`ITRF should handle position at different epochs 5`] = ` +J2000 { + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "position": Vector3D { + "x": -2127.189919581918, + "y": -667.2073608873604, + "z": 3004.978765916125, + }, + "velocity": Vector3D { + "x": -21.223238500045827, + "y": -6.827700074091167, + "z": 30.049679644550032, + }, +} +`; + +exports[`ITRF should handle position at different epochs 6`] = ` +Geodetic { + "alt": -2622.6838742181076, + "lat": 0.935735641017013, + "lon": 1.1071487177940904, +} +`; + +exports[`ITRF should handle position at high altitude 1`] = `1000`; + +exports[`ITRF should handle position at high altitude 2`] = `1000`; + +exports[`ITRF should handle position at high altitude 3`] = ` +J2000 { + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "position": Vector3D { + "x": -5313.7806839984505, + "y": -5118.639766380342, + "z": 12.531790472796377, + }, + "velocity": Vector3D { + "x": 0.37325612599463637, + "y": -0.38748808327640394, + "z": -0.0008553895575912965, + }, +} +`; + +exports[`ITRF should handle position at high altitude 4`] = ` +Geodetic { + "alt": 1000, + "lat": 0, + "lon": 1.5707963267948966, +} +`; + +exports[`ITRF should handle position at low altitude 1`] = `-10`; + +exports[`ITRF should handle position at low altitude 2`] = `-10`; + +exports[`ITRF should handle position at low altitude 3`] = ` +J2000 { + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "position": Vector3D { + "x": -4586.372260974545, + "y": -4417.944366643129, + "z": 10.816301918115789, + }, + "velocity": Vector3D { + "x": 0.32216074473221884, + "y": -0.3344444760297924, + "z": -0.0007382944785579285, + }, +} +`; + +exports[`ITRF should handle position at low altitude 4`] = ` +Geodetic { + "alt": -10, + "lat": 0, + "lon": 1.5707963267948966, +} +`; + +exports[`ITRF should handle position at the International Date Line 1`] = `0`; + +exports[`ITRF should handle position at the International Date Line 2`] = `0`; + +exports[`ITRF should handle position at the International Date Line 3`] = ` +J2000 { + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "position": Vector3D { + "x": -4424.870330550322, + "y": 4593.587094016537, + "z": 10.140457479062466, + }, + "velocity": Vector3D { + "x": -0.33496872909712727, + "y": -0.32266748652491783, + "z": 0.0007899757588086132, + }, +} +`; + +exports[`ITRF should handle position at the International Date Line 4`] = ` +Geodetic { + "alt": 0, + "lat": 0, + "lon": 0, +} +`; + +exports[`ITRF should handle position at the North Pole 1`] = ` +J2000 { + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "position": Vector3D { + "x": 14.787499061093714, + "y": 0.21172360852619188, + "z": 6356.734413210253, + }, + "velocity": Vector3D { + "x": 0, + "y": 0, + "z": 0, + }, +} +`; + +exports[`ITRF should handle position at the North Pole 2`] = ` +Geodetic { + "alt": 0, + "lat": 1.5707963267948966, + "lon": 0, +} +`; + +exports[`ITRF should handle position at the Prime Meridian 1`] = `"ITRF"`; + +exports[`ITRF should handle position at the Prime Meridian 2`] = `false`; + +exports[`ITRF should handle position at the Prime Meridian 3`] = `0`; + +exports[`ITRF should handle position at the Prime Meridian 4`] = `0`; + +exports[`ITRF should handle position at the Prime Meridian 5`] = ` +J2000 { + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "position": Vector3D { + "x": -4593.574324568842, + "y": -4424.881944858347, + "z": 10.833286953310731, + }, + "velocity": Vector3D { + "x": 0.32266663959620306, + "y": -0.3349696602599569, + "z": -0.0007394538357761064, + }, +} +`; + +exports[`ITRF should handle position at the Prime Meridian 6`] = ` +Geodetic { + "alt": 0, + "lat": 0, + "lon": 1.5707963267948966, +} +`; + +exports[`ITRF should handle position at the South Pole 1`] = ` +J2000 { + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "position": Vector3D { + "x": -14.787499061093714, + "y": -0.21172360852619188, + "z": -6356.734413210253, + }, + "velocity": Vector3D { + "x": 0, + "y": 0, + "z": 0, + }, +} +`; + +exports[`ITRF should handle position at the South Pole 2`] = ` +Geodetic { + "alt": 0, + "lat": -1.5707963267948966, + "lon": 0, +} +`; + +exports[`ITRF should handle position at the antimeridian 1`] = `21.384683407853117`; + +exports[`ITRF should handle position at the antimeridian 2`] = `21.384683407853117`; + +exports[`ITRF should handle position at the antimeridian 3`] = ` +J2000 { + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "position": Vector3D { + "x": 14.837245536162833, + "y": 0.21243586576184958, + "z": 6378.119038744385, + }, + "velocity": Vector3D { + "x": 0, + "y": 0, + "z": 0, + }, +} +`; + +exports[`ITRF should handle position at the antimeridian 4`] = `[Function]`; + +exports[`ITRF should handle position at the center of the Earth 1`] = ` +J2000 { + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "position": Vector3D { + "x": 0, + "y": 0, + "z": 0, + }, + "velocity": Vector3D { + "x": 0, + "y": 0, + "z": 0, + }, +} +`; + +exports[`ITRF should handle position at the equator with negative y-coordinate 1`] = `"ITRF"`; + +exports[`ITRF should handle position at the equator with negative y-coordinate 2`] = `false`; + +exports[`ITRF should handle position at the equator with negative y-coordinate 3`] = `0`; + +exports[`ITRF should handle position at the equator with negative y-coordinate 4`] = `0`; + +exports[`ITRF should handle position at the equator with negative y-coordinate 5`] = ` +J2000 { + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "position": Vector3D { + "x": 4593.574324568842, + "y": 4424.881944858347, + "z": -10.833286953310731, + }, + "velocity": Vector3D { + "x": -0.32266663959620306, + "y": 0.3349696602599569, + "z": 0.0007394538357761064, + }, +} +`; + +exports[`ITRF should handle position at the equator with negative y-coordinate 6`] = ` +Geodetic { + "alt": 0, + "lat": 0, + "lon": -1.5707963267948966, +} +`; + +exports[`ITRF should return the altitude in kilometers 1`] = `-2622.7069029008003`; + +exports[`ITRF should return the height above the surface of the Earth in kilometers 1`] = `-2622.7069029008003`; + +exports[`ITRF should return the name of the coordinate system 1`] = `"ITRF"`; + +exports[`ITRF should return whether the coordinate system is inertial 1`] = `false`;