UNPKG

ootk

Version:

Orbital Object Toolkit including Multiple Propagators, Initial Orbit Determination, and Maneuver Calculations.

596 lines (510 loc) 19 kB
import { DEG2RAD, Degrees, Earth, EcefVec3, EcfVec3, EciVec3, EnuVec3, GreenwichMeanSiderealTime, Kilometers, LlaVec3, MILLISECONDS_TO_DAYS, PI, RAD2DEG, Radians, RaeVec3, Sensor, SezVec3, Sgp4, TAU, RfVec3, RuvVec3, RfSensor, } from '../main.js'; /** * Converts ECF to ECI coordinates. * * [X] [C -S 0][X] * [Y] = [S C 0][Y] * [Z]eci [0 0 1][Z]ecf * @param ecf takes xyz coordinates * @param gmst takes a number in gmst time * @returns array containing eci coordinates */ export function ecf2eci<T extends number>(ecf: EcfVec3<T>, gmst: number): EciVec3<T> { const X = (ecf.x * Math.cos(gmst) - ecf.y * Math.sin(gmst)) as T; const Y = (ecf.x * Math.sin(gmst) + ecf.y * Math.cos(gmst)) as T; const Z = ecf.z; return { x: X, y: Y, z: Z }; } /** * Converts ECEF coordinates to ENU coordinates. * @param ecf - The ECEF coordinates. * @param lla - The LLA coordinates. * @returns The ENU coordinates. */ export function ecf2enu<T extends number>(ecf: EcefVec3<T>, lla: LlaVec3): EnuVec3<T> { const { lat, lon } = lla; const { x, y, z } = ecf; const e = (-Math.sin(lon) * x + Math.cos(lon) * y) as T; const n = (-Math.sin(lat) * Math.cos(lon) * x - Math.sin(lat) * Math.sin(lon) * y + Math.cos(lat) * z) as T; const u = (Math.cos(lat) * Math.cos(lon) * x + Math.cos(lat) * Math.sin(lon) * y + Math.sin(lat) * z) as T; return { x: e, y: n, z: u }; } /** * Converts ECI to ECF coordinates. * * [X] [C -S 0][X] * [Y] = [S C 0][Y] * [Z]eci [0 0 1][Z]ecf * * Inverse: * [X] [C S 0][X] * [Y] = [-S C 0][Y] * [Z]ecf [0 0 1][Z]eci * @param eci takes xyz coordinates * @param gmst takes a number in gmst time * @returns array containing ecf coordinates */ export function eci2ecf<T extends number>(eci: EciVec3<T>, gmst: number): EcfVec3<T> { const x = <T>(eci.x * Math.cos(gmst) + eci.y * Math.sin(gmst)); const y = <T>(eci.x * -Math.sin(gmst) + eci.y * Math.cos(gmst)); const z = eci.z; return { x, y, z, }; } /** * EciToGeodetic converts eci coordinates to lla coordinates * @variation cached - results are cached * @param eci takes xyz coordinates * @param gmst takes a number in gmst time * @returns array containing lla coordinates */ export function eci2lla(eci: EciVec3, gmst: number): LlaVec3<Degrees, Kilometers> { // http://www.celestrak.com/columns/v02n03/ const a = 6378.137; const b = 6356.7523142; const R = Math.sqrt(eci.x * eci.x + eci.y * eci.y); const f = (a - b) / a; const e2 = 2 * f - f * f; let lon = Math.atan2(eci.y, eci.x) - gmst; while (lon < -PI) { lon += TAU; } while (lon > PI) { lon -= TAU; } const kmax = 20; let k = 0; let lat = Math.atan2(eci.z, Math.sqrt(eci.x * eci.x + eci.y * eci.y)); let C = 0; while (k < kmax) { C = 1 / Math.sqrt(1 - e2 * (Math.sin(lat) * Math.sin(lat))); lat = Math.atan2(eci.z + a * C * e2 * Math.sin(lat), R); k += 1; } const alt = R / Math.cos(lat) - a * C; lon = (lon * RAD2DEG) as Degrees; lat = (lat * RAD2DEG) as Degrees; return { lon: <Degrees>lon, lat: <Degrees>lat, alt: <Kilometers>alt }; } /** * Converts geodetic coordinates (longitude, latitude, altitude) to Earth-Centered Earth-Fixed (ECF) coordinates. * @param lla The geodetic coordinates in radians and meters. * @returns The ECF coordinates in meters. */ export function llaRad2ecf<AltitudeUnits extends number>(lla: LlaVec3<Radians, AltitudeUnits>): EcfVec3<AltitudeUnits> { const { lon, lat, alt } = lla; const a = 6378.137; const b = 6356.7523142; const f = (a - b) / a; const e2 = 2 * f - f * f; const normal = a / Math.sqrt(1 - e2 * Math.sin(lat) ** 2); const x = (normal + alt) * Math.cos(lat) * Math.cos(lon); const y = (normal + alt) * Math.cos(lat) * Math.sin(lon); const z = (normal * (1 - e2) + alt) * Math.sin(lat); return { x: <AltitudeUnits>x, y: <AltitudeUnits>y, z: <AltitudeUnits>z, }; } /** * Converts geodetic coordinates (longitude, latitude, altitude) to Earth-Centered Earth-Fixed (ECF) coordinates. * @param lla The geodetic coordinates in degrees and meters. * @returns The ECF coordinates in meters. */ export function lla2ecf<AltitudeUnits extends number>(lla: LlaVec3<Degrees, AltitudeUnits>): EcfVec3<AltitudeUnits> { const { lon, lat, alt } = lla; const lonRad = lon * DEG2RAD; const latRad = lat * DEG2RAD; return llaRad2ecf({ lon: lonRad as Radians, lat: latRad as Radians, alt, }); } /** * Converts geodetic coordinates (latitude, longitude, altitude) to Earth-centered inertial (ECI) coordinates. * @variation cached - results are cached * @param lla The geodetic coordinates in radians and meters. * @param gmst The Greenwich Mean Sidereal Time in seconds. * @returns The ECI coordinates in meters. */ export function lla2eci(lla: LlaVec3<Radians, Kilometers>, gmst: GreenwichMeanSiderealTime): EciVec3<Kilometers> { const { lat, lon, alt } = lla; const cosLat = Math.cos(lat); const sinLat = Math.sin(lat); const cosLon = Math.cos(lon + gmst); const sinLon = Math.sin(lon + gmst); const x = (Earth.radiusMean + alt) * cosLat * cosLon; const y = (Earth.radiusMean + alt) * cosLat * sinLon; const z = (Earth.radiusMean + alt) * sinLat; return { x, y, z } as EciVec3<Kilometers>; } /** * Calculates Geodetic Lat Lon Alt to ECEF coordinates. * @deprecated This needs to be validated. * @param lla The geodetic coordinates in degrees and meters. * @returns The ECEF coordinates in meters. */ export function lla2ecef<D extends number>(lla: LlaVec3<Degrees, D>): EcefVec3<D> { const { lat, lon, alt } = lla; const a = 6378.137; // semi-major axis length in meters according to the WGS84 const b = 6356.752314245; // semi-minor axis length in meters according to the WGS84 const e = Math.sqrt(1 - b ** 2 / a ** 2); // eccentricity const N = a / Math.sqrt(1 - e ** 2 * Math.sin(lat) ** 2); // radius of curvature in the prime vertical const x = ((N + alt) * Math.cos(lat) * Math.cos(lon)) as D; const y = ((N + alt) * Math.cos(lat) * Math.sin(lon)) as D; const z = ((N * (1 - e ** 2) + alt) * Math.sin(lat)) as D; return { x, y, z }; } /** * Converts LLA to SEZ coordinates. * @see http://www.celestrak.com/columns/v02n02/ * @param lla The LLA coordinates. * @param ecf The ECF coordinates. * @returns The SEZ coordinates. */ export function lla2sez<D extends number>(lla: LlaVec3<Radians, D>, ecf: EcfVec3<D>): SezVec3<D> { const lon = lla.lon; const lat = lla.lat; const observerEcf = llaRad2ecf({ lat, lon, alt: <Kilometers>0, }); const rx = ecf.x - observerEcf.x; const ry = ecf.y - observerEcf.y; const rz = ecf.z - observerEcf.z; // Top is short for topocentric const south = Math.sin(lat) * Math.cos(lon) * rx + Math.sin(lat) * Math.sin(lon) * ry - Math.cos(lat) * rz; const east = -Math.sin(lon) * rx + Math.cos(lon) * ry; const zenith = Math.cos(lat) * Math.cos(lon) * rx + Math.cos(lat) * Math.sin(lon) * ry + Math.sin(lat) * rz; return { s: <D>south, e: <D>east, z: <D>zenith }; } /** * Converts a vector in Right Ascension, Elevation, and Range (RAE) coordinate system * to a vector in South, East, and Zenith (SEZ) coordinate system. * @param rae The vector in RAE coordinate system. * @returns The vector in SEZ coordinate system. */ export function rae2sez<D extends number>(rae: RaeVec3<D, Radians>): SezVec3<D> { const south = -rae.rng * Math.cos(rae.el) * Math.cos(rae.az); const east = rae.rng * Math.cos(rae.el) * Math.sin(rae.az); const zenith = rae.rng * Math.sin(rae.el); return { s: <D>south, e: <D>east, z: <D>zenith, }; } /** * Converts a vector in Right Ascension, Elevation, and Range (RAE) coordinate system * to Earth-Centered Fixed (ECF) coordinate system. * @template D - The dimension of the RAE vector. * @template A - The dimension of the LLA vector. * @param rae - The vector in RAE coordinate system. * @param lla - The vector in LLA coordinate system. * @returns The vector in ECF coordinate system. */ export function rae2ecf<D extends number>(rae: RaeVec3<D, Degrees>, lla: LlaVec3<Degrees, D>): EcfVec3<D> { const llaRad = { lat: (lla.lat * DEG2RAD) as Radians, lon: (lla.lon * DEG2RAD) as Radians, alt: lla.alt, }; const raeRad = { az: (rae.az * DEG2RAD) as Radians, el: (rae.el * DEG2RAD) as Radians, rng: rae.rng, }; const obsEcf = llaRad2ecf(llaRad); const sez = rae2sez(raeRad); // Some needed calculations const slat = Math.sin(llaRad.lat); const slon = Math.sin(llaRad.lon); const clat = Math.cos(llaRad.lat); const clon = Math.cos(llaRad.lon); const x = slat * clon * sez.s + -slon * sez.e + clat * clon * sez.z + obsEcf.x; const y = slat * slon * sez.s + clon * sez.e + clat * slon * sez.z + obsEcf.y; const z = -clat * sez.s + slat * sez.z + obsEcf.z; return { x, y, z } as EcfVec3<D>; } /** * Converts a vector from RAE (Range, Azimuth, Elevation) coordinates to ECI (Earth-Centered Inertial) coordinates. * @variation cached - results are cached * @param rae The vector in RAE coordinates. * @param lla The vector in LLA (Latitude, Longitude, Altitude) coordinates. * @param gmst The Greenwich Mean Sidereal Time. * @returns The vector in ECI coordinates. */ export function rae2eci<D extends number>( rae: RaeVec3<D, Degrees>, lla: LlaVec3<Degrees, D>, gmst: number, ): EciVec3<D> { const ecf = rae2ecf(rae, lla); const eci = ecf2eci(ecf, gmst); return eci; } /** * Converts a vector in RAE (Range, Azimuth, Elevation) coordinates to ENU (East, North, Up) coordinates. * @param rae - The vector in RAE coordinates. * @returns The vector in ENU coordinates. */ export function rae2enu(rae: RaeVec3): EnuVec3<Kilometers> { const e = (rae.rng * Math.cos(rae.el) * Math.sin(rae.az)) as Kilometers; const n = (rae.rng * Math.cos(rae.el) * Math.cos(rae.az)) as Kilometers; const u = (rae.rng * Math.sin(rae.el)) as Kilometers; return { x: e, y: n, z: u }; } /** * Converts South, East, and Zenith (SEZ) coordinates to Right Ascension, Elevation, and Range (RAE) coordinates. * @param sez The SEZ coordinates. * @returns Rng, Az, El array */ export function sez2rae<D extends number>(sez: SezVec3<D>): RaeVec3<D, Radians> { const rng = <D>Math.sqrt(sez.s * sez.s + sez.e * sez.e + sez.z * sez.z); const el = <Radians>Math.asin(sez.z / rng); const az = <Radians>(Math.atan2(-sez.e, sez.s) + PI); return { rng, az, el }; } /** * Converts Earth-Centered Fixed (ECF) coordinates to Right Ascension (RA), * Elevation (E), and Azimuth (A) coordinates. * @param lla The Latitude, Longitude, and Altitude (LLA) coordinates. * @param ecf The Earth-Centered Fixed (ECF) coordinates. * @returns The Right Ascension (RA), Elevation (E), and Azimuth (A) coordinates. */ export function ecfRad2rae<D extends number>(lla: LlaVec3<Radians, D>, ecf: EcfVec3<D>): RaeVec3<D, Degrees> { const sezCoords = lla2sez(lla, ecf); const rae = sez2rae(sezCoords); return { rng: rae.rng, az: (rae.az * RAD2DEG) as Degrees, el: (rae.el * RAD2DEG) as Degrees }; } /** * Converts Earth-Centered Fixed (ECF) coordinates to Right Ascension (RA), * Elevation (E), and Azimuth (A) coordinates. * @variation cached - results are cached * @param lla The Latitude, Longitude, and Altitude (LLA) coordinates. * @param ecf The Earth-Centered Fixed (ECF) coordinates. * @returns The Right Ascension (RA), Elevation (E), and Azimuth (A) coordinates. */ export function ecf2rae<D extends number>(lla: LlaVec3<Degrees, D>, ecf: EcfVec3<D>): RaeVec3<D, Degrees> { const { lat, lon } = lla; const latRad = (lat * DEG2RAD) as Radians; const lonRad = (lon * DEG2RAD) as Radians; const rae = ecfRad2rae({ lat: latRad, lon: lonRad, alt: lla.alt }, ecf); return rae; } export const jday = (year?: number, mon?: number, day?: number, hr?: number, minute?: number, sec?: number) => { if (typeof year === 'undefined') { const now = new Date(); const jDayStart = new Date(now.getUTCFullYear(), 0, 0); const jDayDiff = now.getDate() - jDayStart.getDate(); return Math.floor(jDayDiff / MILLISECONDS_TO_DAYS); } if ( typeof mon === 'undefined' || typeof day === 'undefined' || typeof hr === 'undefined' || typeof minute === 'undefined' || typeof sec === 'undefined' ) { throw new Error('Invalid date'); } return ( 367.0 * year - Math.floor(7 * (year + Math.floor((mon + 9) / 12.0)) * 0.25) + Math.floor((275 * mon) / 9.0) + day + 1721013.5 + ((sec / 60.0 + minute) / 60.0 + hr) / 24.0 ); }; /** * Calculates the Greenwich Mean Sidereal Time (GMST) for a given date. * @param date - The date for which to calculate the GMST. * @returns An object containing the GMST value and the Julian date. */ export function calcGmst(date: Date): { gmst: GreenwichMeanSiderealTime; j: number } { const j = jday( date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), ) + date.getUTCMilliseconds() * MILLISECONDS_TO_DAYS; const gmst = Sgp4.gstime(j); return { gmst, j }; } /** * Converts ECI coordinates to RAE (Right Ascension, Azimuth, Elevation) coordinates. * @variation cached - results are cached * @param now - Current date and time. * @param eci - ECI coordinates of the satellite. * @param sensor - Sensor object containing observer's geodetic coordinates. * @returns Object containing azimuth, elevation and range in degrees and kilometers respectively. */ export function eci2rae(now: Date, eci: EciVec3<Kilometers>, sensor: Sensor): RaeVec3<Kilometers, Degrees> { now = new Date(now); const { gmst } = calcGmst(now); const positionEcf = eci2ecf(eci, gmst); const lla = { lat: (sensor.lat * DEG2RAD) as Radians, lon: (sensor.lon * DEG2RAD) as Radians, alt: sensor.alt, }; const rae = ecfRad2rae(lla, positionEcf); return rae; } /** * Calculates the inertial azimuth of a satellite given its latitude and inclination. * @param lat - The latitude of the satellite in degrees. * @param inc - The inclination of the satellite in degrees. * @returns The inertial azimuth of the satellite in degrees. */ export function calcInertAz(lat: Degrees, inc: Degrees): Degrees { const phi = lat * DEG2RAD; const i = inc * DEG2RAD; const az = Math.asin(Math.cos(i) / Math.cos(phi)); return <Degrees>(az * RAD2DEG); } /** * Calculates the inclination angle of a satellite from its launch azimuth and latitude. * @param lat - The latitude of the observer in degrees. * @param az - The launch azimuth angle of the satellite in degrees clockwise from north. * @returns The inclination angle of the satellite in degrees. */ export function calcIncFromAz(lat: number, az: number): number { const phi = lat * DEG2RAD; const beta = az * DEG2RAD; const inc = Math.acos(Math.sin(beta) * Math.cos(phi)); return <Degrees>(inc * RAD2DEG); } /** * Converts Azimuth and Elevation to U and V. * Azimuth is the angle off of boresight in the horizontal plane. * Elevation is the angle off of boresight in the vertical plane. * Cone half angle is the angle of the cone of the radar max field of view. * @param az - Azimuth in radians * @param el - Elevation in radians * @param coneHalfAngle - Cone half angle in radians * @returns U and V in radians */ export function azel2uv(az: Radians, el: Radians, coneHalfAngle: Radians): { u: number; v: number } { if (az > coneHalfAngle && az < coneHalfAngle) { throw new RangeError(`Azimuth is out of bounds: ${az}`); } if (el > coneHalfAngle && el < coneHalfAngle) { throw new RangeError(`Elevation is out of bounds: ${el}`); } const alpha = (az / (coneHalfAngle * RAD2DEG)) * 90; const beta = (el / (coneHalfAngle * RAD2DEG)) * 90; const u = Math.sin(alpha) as Radians; let v = -Math.sin(beta) as Radians; v = Object.is(v, -0) ? (0 as Radians) : v; return { u, v }; } /** * Determine azimuth and elevation off of boresight based on sensor orientation and RAE. * @param rae Range, Azimuth, Elevation * @param sensor Radar sensor object * @param face Face number of the sensor * @param maxSensorAz Maximum sensor azimuth * @returns Azimuth and Elevation off of boresight */ export function rae2raeOffBoresight( rae: RaeVec3, sensor: RfSensor, face: number, maxSensorAz: Degrees, ): { az: Radians; el: Radians } { let az = (rae.az * DEG2RAD) as Radians; let el = (rae.el * DEG2RAD) as Radians; // Correct azimuth for sensor orientation. az = az > maxSensorAz * DEG2RAD ? ((az - TAU) as Radians) : az; az = (az - sensor.boresightAz[face]) as Radians; el = (el - sensor.boresightEl[face]) as Radians; return { az, el }; } /** * Converts Range Az El to Range U V. * @param rae Range, Azimuth, Elevation * @param sensor Radar sensor object * @param face Face number of the sensor * @param maxSensorAz Maximum sensor azimuth * @returns Range, U, V */ export function rae2ruv(rae: RaeVec3, sensor: RfSensor, face: number, maxSensorAz: Degrees): RuvVec3 { const { az, el } = rae2raeOffBoresight(rae, sensor, face, maxSensorAz); const { u, v } = azel2uv(az, el, sensor.beamwidthRad); return { rng: rae.rng, u, v }; } /** * Converts U and V to Azimuth and Elevation off of boresight. * @param u The U coordinate. * @param v The V coordinate. * @param coneHalfAngle The cone half angle of the radar. * @returns Azimuth and Elevation off of boresight. */ export function uv2azel(u: number, v: number, coneHalfAngle: Radians): { az: Radians; el: Radians } { if (u > 1 || u < -1) { throw new RangeError(`u is out of bounds: ${u}`); } if (v > 1 || v < -1) { throw new RangeError(`v is out of bounds: ${v}`); } const alpha = Math.asin(u) as Radians; const beta = Math.asin(v) as Radians; const az = ((alpha / 90) * (coneHalfAngle * RAD2DEG)) as Radians; const el = ((beta / 90) * (coneHalfAngle * RAD2DEG)) as Radians; return { az, el }; } /** * Converts coordinates from East-North-Up (ENU) to Right-Front-Up (RF) coordinate system. * @param enu - The ENU coordinates to be converted. * @param enu.x - The east coordinate. * @param enu.y - The north coordinate. * @param enu.z - The up coordinate. * @param az - The azimuth angle in radians. * @param el - The elevation angle in radians. * @returns The converted RF coordinates. */ export function enu2rf<D extends number, A extends number = Radians>({ x, y, z }: EnuVec3<D>, az: A, el: A): RfVec3<D> { const xrf = Math.cos(el) * Math.cos(az) * x - Math.sin(az) * y + Math.sin(el) * Math.cos(az) * z; const yrf = Math.cos(el) * Math.sin(az) * x + Math.cos(az) * y + Math.sin(el) * Math.sin(az) * z; const zrf = -Math.sin(el) * x + Math.cos(el) * z; return { x: xrf as D, y: yrf as D, z: zrf as D, }; }