UNPKG

ootk-core

Version:

Orbital Object Toolkit. A modern typed replacement for satellite.js including SGP4 propagation, TLE parsing, Sun and Moon calculations, and more.

1,551 lines (1,400 loc) 118 kB
/** * @author Theodore Kruczek. * @license MIT * @copyright (c) 2022-2025 Theodore Kruczek Permission is * hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the * Software without restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * @copyright (c) 2012–2016 Brandon Rhodes * This was ported from the python-sgp4 library by Brandon Rhodes. */ // NOTE: This file is meant to maintain as much of the original format as possible. /* eslint-disable complexity */ /* eslint-disable max-statements */ /* eslint-disable max-lines-per-function */ /* eslint-disable max-lines */ /* eslint-disable @typescript-eslint/no-loss-of-precision */ import { OmmParsedDataFormat } from '../interfaces/OmmFormat.js'; import { Sgp4OpsMode } from '../enums/Sgp4OpsMode.js'; import { GreenwichMeanSiderealTime, SatelliteRecord, StateVectorSgp4, Vec3Flat } from '../types/types.js'; import { DEG2RAD, PI, TAU, temp4, x2o3 } from '../utils/constants.js'; import { Sgp4ErrorCode } from './sgp4-error.js'; import { Tle } from '../main.js'; export enum Sgp4GravConstants { wgs72old = 'wgs72old', wgs72 = 'wgs72', wgs84 = 'wgs84', } export enum Sgp4Methods { NEAR_EARTH = 'n', DEEP_SPACE = 'd', } interface DsInitParams { xke: number; cosim: number; argpo: number; s1: number; s2: number; s3: number; s4: number; s5: number; sinim: number; ss1: number; ss2: number; ss3: number; ss4: number; ss5: number; sz1: number; sz3: number; sz11: number; sz13: number; sz21: number; sz23: number; sz31: number; sz33: number; t: number; tc: number; gsto: number; mo: number; mdot: number; no: number; nodeo: number; nodedot: number; xPIdot: number; z1: number; z3: number; z11: number; z13: number; z21: number; z23: number; z31: number; z33: number; ecco: number; eccsq: number; emsq: number; em: number; argpm: number; inclm: number; mm: number; nm: number; nodem: number; irez: number; atime: number; d2201: number; d2211: number; d3210: number; d3222: number; d4410: number; d4422: number; d5220: number; d5232: number; d5421: number; d5433: number; dedt: number; didt: number; dmdt: number; dnodt: number; domdt: number; del1: number; del2: number; del3: number; xfact: number; xlamo: number; xli: number; xni: number; } /** Ootk -- Some variables imported from outside the class at the top */ const fasx2 = 0.13130908; const fasx4 = 2.8843198; const fasx6 = 0.37448087; const g22 = 5.7686396; const g32 = 0.95240898; const g44 = 1.8014998; const g52 = 1.050833; const g54 = 4.4108898; const rptim = 4.37526908801129966e-3; // Equates to 7.29211514668855e-5 rad/sec const stepp = 720.0; const stepn = -720.0; const step2 = 259200.0; /* * ---------------------------------------------------------------- * * sgp4unit.cpp * * this file contains the sgp4 procedures for analytical propagation * of a satellite. the code was originally released in the 1980 and 1986 * spacetrack papers. a detailed discussion of the theory and history * may be found in the 2006 aiaa paper by vallado, crawford, hujsak, * and kelso. * * companion code for * fundamentals of astrodynamics and applications * 2013 * by david vallado * * (w) 719-573-2600, email dvallado@agi.com, davallado@gmail.com * * current : * 12 mar 20 david vallado * chg satnum to string for alpha 5 or 9-digit * changes : * 7 dec 15 david vallado * fix jd, jdfrac * 3 nov 14 david vallado * update to msvs2013 c++ * 30 aug 10 david vallado * delete unused variables in initl * replace pow integer 2, 3 with multiplies for speed * 3 nov 08 david vallado * put returns in for error codes * 29 sep 08 david vallado * fix atime for faster operation in dspace * add operationmode for afspc (a) or improved (i) * performance mode * 16 jun 08 david vallado * update small eccentricity check * 16 nov 07 david vallado * misc fixes for better compliance * 20 apr 07 david vallado * misc fixes for constants * 11 aug 06 david vallado * chg lyddane choice back to strn3, constants, misc doc * 15 dec 05 david vallado * misc fixes * 26 jul 05 david vallado * fixes for paper * note that each fix is preceded by a * comment with "sgp4fix" and an explanation of * what was changed * 10 aug 04 david vallado * 2nd printing baseline working * 14 may 01 david vallado * 2nd edition baseline * 80 norad * original baseline * ---------------------------------------------------------------- */ export class Sgp4 { /* * ----------------------------------------------------------------------------- * * procedure angle_SGP4 * * this procedure calculates the angle between two vectors. the output is * set to 999999.1 to indicate an undefined value. be sure to check for * this at the output phase. * * author : david vallado 719-573-2600 1 mar 2001 * * inputs description range / units * vec1 - vector number 1 * vec2 - vector number 2 * * outputs : * theta - angle between the two vectors -pi to pi * * locals : * temp - temporary real variable * * coupling : * dot dot product of two vectors * --------------------------------------------------------------------------- */ private static angle_(vec1: Vec3Flat, vec2: Vec3Flat): number { const small = 0.00000001; const unknown = 999999.1; /** Ootk -- original 'undefined' is protected in JS */ const magv1 = Sgp4.mag_(vec1); const magv2 = Sgp4.mag_(vec2); const magnitudeProduct = magv1 * magv2; if (magnitudeProduct > small * small) { let temp = Sgp4.dot_(vec1, vec2) / (magnitudeProduct); // Clamp to [-1, 1] to avoid NaN from floating point errors if (temp > 1.0) { temp = 1.0; } if (temp < -1.0) { temp = -1.0; } return Math.acos(temp); } return unknown; } /* * ----------------------------------------------------------------------------- * * function asinh_SGP4 * * this function evaluates the inverse hyperbolic sine function. * * author : david vallado 719-573-2600 1 mar 2001 * * inputs description range / units * xval - angle value any real * * outputs : * arcsinh - result any real * * locals : * none. * * coupling : * none. * --------------------------------------------------------------------------- */ private static asinh_(xval: number): number { return Math.log(xval + Math.sqrt(xval * xval + 1.0)); } /* * ----------------------------------------------------------------------------- * * function twoline2rv * * this function converts the two line element set character string data to * variables and initializes the sgp4 variables. several intermediate varaibles * and quantities are determined. note that the result is a structure so multiple * satellites can be processed simultaneously without having to reinitialize. the * verification mode is an important option that permits quick checks of any * changes to the underlying technical theory. this option works using a * modified tle file in which the start, stop, and delta time values are * included at the end of the second line of data. this only works with the * verification mode. the catalog mode simply propagates from -1440 to 1440 min * from epoch and is useful when performing entire catalog runs. * * author : david vallado 719-573-2600 1 mar 2001 * * inputs : * longstr1 - first line of the tle * longstr2 - second line of the tle * typerun - type of run verification 'v', catalog 'c', * manual 'm' * typeinput - type of manual input mfe 'm', epoch 'e', dayofyr 'd' * opsmode - mode of operation afspc or improved 'a', 'i' * whichconst - which set of constants to use 72, 84 * * outputs : * satrec - structure containing all the sgp4 satellite information * * coupling : * getgravconst- * days2mdhms - conversion of days to month, day, hour, minute, second * jday - convert day month year hour minute second into julian date * sgp4init - initialize the sgp4 variables * * references : * norad spacetrack report #3 * vallado, crawford, hujsak, kelso 2006 * --------------------------------------------------------------------------- */ static createSatrec( tleLine1: string, tleLine2: string, whichconst = Sgp4GravConstants.wgs72, opsmode = Sgp4OpsMode.IMPROVED, ): SatelliteRecord { let year = 0; const satrec = { error: Sgp4ErrorCode.NO_ERROR, } as SatelliteRecord; /* * Sgp4fix no longer needed * getgravconst( whichconst, tumin, mu, radiusearthkm, xke, j2, j3, j4, j3oj2 ); */ const xpdotp = 1440.0 / (2.0 * PI); // 229.1831180523293; satrec.satnum = tleLine1.substring(2, 7); satrec.epochyr = parseInt(tleLine1.substring(18, 20)); satrec.epochdays = parseFloat(tleLine1.substring(20, 32)); satrec.ndot = parseFloat(tleLine1.substring(33, 43)); satrec.nddot = parseFloat( `${tleLine1.substring(44, 45)}.${tleLine1.substring(45, 50)}E${tleLine1.substring(50, 52)}`, ); satrec.bstar = parseFloat( `${tleLine1.substring(53, 54)}.${tleLine1.substring(54, 59)}E${tleLine1.substring(59, 61)}`, ); satrec.inclo = parseFloat(tleLine2.substring(8, 16)); satrec.nodeo = parseFloat(tleLine2.substring(17, 25)); satrec.ecco = parseFloat(`.${tleLine2.substring(26, 33)}`); satrec.argpo = parseFloat(tleLine2.substring(34, 42)); satrec.mo = parseFloat(tleLine2.substring(43, 51)); satrec.no = parseFloat(tleLine2.substring(52, 63)); // ---- find no, ndot, nddot ---- satrec.no /= xpdotp; // Rad/min /** Ootk -- nexp and ibexp are calculated above using template literals */ /* * Satrec.nddot = satrec.nddot * Math.pow(10.0, nexp); * satrec.bstar = satrec.bstar * Math.pow(10.0, ibexp); */ /* * ---- convert to sgp4 units ---- * satrec.a = (satrec.no * tumin) ** (-2.0 / 3.0); */ /** Ootk -- Not sure why the following two lines are added. 1st and 2nd derivatives aren't even used anymore */ /* * Satrec.ndot /= xpdotp * 1440.0; // ? * minperday * satrec.nddot /= xpdotp * 1440.0 * 1440; */ // ---- find standard orbital elements ---- satrec.inclo *= DEG2RAD; satrec.nodeo *= DEG2RAD; satrec.argpo *= DEG2RAD; satrec.mo *= DEG2RAD; /* * Sgp4fix not needed here * satrec.alta = satrec.a * (1.0 + satrec.ecco) - 1.0; * satrec.altp = satrec.a * (1.0 - satrec.ecco) - 1.0; */ /* * ---------------------------------------------------------------- * find sgp4epoch time of element set * remember that sgp4 uses units of days from 0 jan 1950 (sgp4epoch) * and minutes from the epoch (time) * ---------------------------------------------------------------- */ /* * ---------------- temp fix for years from 1957-2056 ------------------- * --------- correct fix will occur when year is 4-digit in tle --------- */ if (satrec.epochyr < 57) { year = satrec.epochyr + 2000; } else { year = satrec.epochyr + 1900; } const { mon, day, hr, min, sec } = Sgp4.days2mdhms(year, satrec.epochdays); const jdayRes = Sgp4.jday(year, mon, day, hr, min, sec); satrec.jdsatepoch = jdayRes.jd + jdayRes.jdFrac; // ---------------- initialize the orbit at sgp4epoch ------------------- Sgp4.sgp4init_(satrec as unknown as SatelliteRecord, { whichconst, opsmode, satn: satrec.satnum, epoch: satrec.jdsatepoch - 2433281.5, xbstar: satrec.bstar, xecco: satrec.ecco, xargpo: satrec.argpo, xinclo: satrec.inclo, xndot: satrec.ndot, xnddot: satrec.nddot, xmo: satrec.mo, xno: satrec.no, xnodeo: satrec.nodeo, }); return satrec as unknown as SatelliteRecord; } static createSatrecFromOmm( omm: OmmParsedDataFormat, whichconst = Sgp4GravConstants.wgs72, opsmode = Sgp4OpsMode.IMPROVED, ): SatelliteRecord { let year = 0; const satrec = { error: Sgp4ErrorCode.NO_ERROR, } as SatelliteRecord; const xpdotp = 1440.0 / (2.0 * PI); // 229.1831180523293; satrec.error = Sgp4ErrorCode.NO_ERROR; satrec.satnum = omm.NORAD_CAT_ID; satrec.epochyr = parseInt(omm.epoch.year.toString().slice(-2)); satrec.epochdays = omm.epoch.doy; satrec.ndot = parseFloat(omm.MEAN_MOTION_DOT); satrec.nddot = parseFloat(omm.MEAN_MOTION_DDOT); satrec.bstar = parseFloat(omm.BSTAR); satrec.inclo = parseFloat(omm.INCLINATION); satrec.nodeo = parseFloat(omm.RA_OF_ASC_NODE); satrec.ecco = parseFloat(omm.ECCENTRICITY); satrec.argpo = parseFloat(omm.ARG_OF_PERICENTER); satrec.mo = parseFloat(omm.MEAN_ANOMALY); satrec.no = parseFloat(omm.MEAN_MOTION); // ---- find no, ndot, nddot ---- satrec.no /= xpdotp; // Rad/min /** Ootk -- nexp and ibexp are calculated above using template literals */ /* * Satrec.nddot = satrec.nddot * Math.pow(10.0, nexp); * satrec.bstar = satrec.bstar * Math.pow(10.0, ibexp); */ /* * ---- convert to sgp4 units ---- * satrec.a = (satrec.no * tumin) ** (-2.0 / 3.0); */ /** Ootk -- Not sure why the following two lines are added. 1st and 2nd derivatives aren't even used anymore */ /* * Satrec.ndot /= xpdotp * 1440.0; // ? * minperday * satrec.nddot /= xpdotp * 1440.0 * 1440; */ // ---- find standard orbital elements ---- satrec.inclo *= DEG2RAD; satrec.nodeo *= DEG2RAD; satrec.argpo *= DEG2RAD; satrec.mo *= DEG2RAD; /* * Sgp4fix not needed here * satrec.alta = satrec.a * (1.0 + satrec.ecco) - 1.0; * satrec.altp = satrec.a * (1.0 - satrec.ecco) - 1.0; */ /* * ---------------------------------------------------------------- * find sgp4epoch time of element set * remember that sgp4 uses units of days from 0 jan 1950 (sgp4epoch) * and minutes from the epoch (time) * ---------------------------------------------------------------- */ /* * ---------------- temp fix for years from 1957-2056 ------------------- * --------- correct fix will occur when year is 4-digit in tle --------- */ if (satrec.epochyr < 57) { year = satrec.epochyr + 2000; } else { year = satrec.epochyr + 1900; } const { mon, day, hr, min, sec } = Sgp4.days2mdhms(year, satrec.epochdays); const jdayRes = Sgp4.jday(year, mon, day, hr, min, sec); satrec.jdsatepoch = jdayRes.jd + jdayRes.jdFrac; // ---------------- initialize the orbit at sgp4epoch ------------------- Sgp4.sgp4init_(satrec as unknown as SatelliteRecord, { whichconst, opsmode, satn: satrec.satnum, epoch: satrec.jdsatepoch - 2433281.5, xbstar: satrec.bstar, xecco: satrec.ecco, xargpo: satrec.argpo, xinclo: satrec.inclo, xndot: satrec.ndot, xnddot: satrec.nddot, xmo: satrec.mo, xno: satrec.no, xnodeo: satrec.nodeo, }); return satrec as unknown as SatelliteRecord; } /* * ----------------------------------------------------------------------------- * * procedure cross_SGP4 * * this procedure crosses two vectors. * * author : david vallado 719-573-2600 1 mar 2001 * * inputs description range / units * vec1 - vector number 1 * vec2 - vector number 2 * * outputs : * outvec - vector result of a x b * * locals : * none. * * coupling : * mag magnitude of a vector * ---------------------------------------------------------------------------- */ private static cross_(vec1: Vec3Flat, vec2: Vec3Flat): Vec3Flat { return [ vec1[1] * vec2[2] - vec1[2] * vec2[1], vec1[2] * vec2[0] - vec1[0] * vec2[2], vec1[0] * vec2[1] - vec1[1] * vec2[0], ]; } /* * ----------------------------------------------------------------------------- * * procedure days2mdhms * * this procedure converts the day of the year, days, to the equivalent month * day, hour, minute and second. * * algorithm : set up array for the number of days per month * find leap year - use 1900 because 2000 is a leap year * loop through a temp value while the value is < the days * perform int conversions to the correct day and month * convert remainder into h m s using type conversions * * author : david vallado 719-573-2600 1 mar 2001 * * inputs description range / units * year - year 1900 .. 2100 * days - julian day of the year 0.0 .. 366.0 * * outputs : * mon - month 1 .. 12 * day - day 1 .. 28,29,30,31 * hr - hour 0 .. 23 * min - minute 0 .. 59 * sec - second 0.0 .. 59.999 * * locals : * dayofyr - day of year * temp - temporary extended values * inttemp - temporary int value * i - index * lmonth[13] - int array containing the number of days per month * * coupling : * none. * --------------------------------------------------------------------------- */ static days2mdhms( year: number, days: number, ): { mon: number; day: number; hr: number; min: number; sec: number; } { const lmonth = [31, year % 4 === 0 ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const dayofyr = Math.floor(days); // ----------------- find month and day of month ---------------- /** Ootk -- Incorporated in the above declaration */ /* * If ((year % 4) == 0) * lmonth[2] = 29; */ let i = 1; let inttemp = 0; while (dayofyr > inttemp + (lmonth[i - 1] as number) && i < 12) { inttemp += (lmonth[i - 1] as number); i += 1; } const mon = i; const day = dayofyr - inttemp; // ----------------- find hours minutes and seconds ------------- let temp = (days - dayofyr) * 24.0; const hr = Math.floor(temp); temp = (temp - hr) * 60.0; const min = Math.floor(temp); const sec = (temp - min) * 60.0; return { mon, day, hr, min, sec, }; } /* * ----------------------------------------------------------------------------- * * function dot_SGP4 * * this function finds the dot product of two vectors. * * author : david vallado 719-573-2600 1 mar 2001 * * inputs description range / units * vec1 - vector number 1 * vec2 - vector number 2 * * outputs : * dot - result * * locals : * none. * * coupling : * none. * --------------------------------------------------------------------------- */ private static dot_(v1: Vec3Flat, v2: Vec3Flat): number { return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]; } /* * ----------------------------------------------------------------------------- * * function gstime * * this function finds the greenwich sidereal time. * * author : david vallado 719-573-2600 1 mar 2001 * * inputs description range / units * jdut1 - julian date in ut1 days from 4713 bc * * outputs : * gstime - greenwich sidereal time 0 to 2PI rad * * locals : * temp - temporary variable for doubles rad * tut1 - julian centuries from the * jan 1, 2000 12 h epoch (ut1) * * coupling : * none * * references : * vallado 2004, 191, eq 3-45 * --------------------------------------------------------------------------- */ static gstime(jdut1: number): GreenwichMeanSiderealTime { const tut1 = (jdut1 - 2451545.0) / 36525.0; let temp = -6.2e-6 * tut1 * tut1 * tut1 + 0.093104 * tut1 * tut1 + (876600.0 * 3600 + 8640184.812866) * tut1 + 67310.54841; // Sec temp = ((temp * DEG2RAD) / 240.0) % TAU; // 360/86400 = 1/240, to deg, to rad // ------------------------ check quadrants --------------------- if (temp < 0.0) { temp += TAU; } return temp as GreenwichMeanSiderealTime; } /* * ----------------------------------------------------------------------------- * * procedure invjday * * this procedure finds the year, month, day, hour, minute and second * given the julian date. tu can be ut1, tdt, tdb, etc. * * algorithm : set up starting values * find leap year - use 1900 because 2000 is a leap year * find the elapsed days through the year in a loop * call routine to find each individual value * * author : david vallado 719-573-2600 1 mar 2001 * * inputs description range / units * jd - julian date days from 4713 bc * jdfrac - julian date fraction into day days from 4713 bc * * outputs : * year - year 1900 .. 2100 * mon - month 1 .. 12 * day - day 1 .. 28,29,30,31 * hr - hour 0 .. 23 * min - minute 0 .. 59 * sec - second 0.0 .. 59.999 * * locals : * days - day of year plus fractional * portion of a day days * tu - julian centuries from 0 h * jan 0, 1900 * temp - temporary double values * leapyrs - number of leap years from 1900 * * coupling : * days2mdhms - finds month, day, hour, minute and second given days and year * * references : * vallado 2013, 203, alg 22, ex 3-13 * --------------------------------------------------------------------------- */ static invjday( jd: number, jdfrac: number, ): { year: number; mon: number; day: number; hr: number; min: number; sec: number } { let leapyrs; let days; // Check jdfrac for multiple days if (Math.abs(jdfrac) >= 1.0) { jd += Math.floor(jdfrac); jdfrac -= Math.floor(jdfrac); } // Check for fraction of a day included in the jd const dt = jd - Math.floor(jd) - 0.5; if (Math.abs(dt) > 0.00000001) { jd -= dt; jdfrac += dt; } /* --------------- find year and days of the year --------------- */ const temp = jd - 2415019.5; const tu = temp / 365.25; let year = 1900 + Math.floor(tu); leapyrs = Math.floor((year - 1901) * 0.25); days = Math.floor(temp - ((year - 1900) * 365.0 + leapyrs)); /* ------------ check for case of beginning of a year ----------- */ if (days + jdfrac < 1.0) { year -= 1; leapyrs = Math.floor((year - 1901) * 0.25); days = Math.floor(temp - ((year - 1900) * 365.0 + leapyrs)); } /* ----------------- find remaining data ------------------------- */ const { mon, day, hr, min, sec } = Sgp4.days2mdhms(year, days + jdfrac); return { year, mon, day, hr, min, sec, }; } /* * ----------------------------------------------------------------------------- * * procedure jday * * this procedure finds the julian date given the year, month, day, and time. * the julian date is defined by each elapsed day since noon, jan 1, 4713 bc. * * algorithm : calculate the answer in one step for efficiency * * author : david vallado 719-573-2600 1 mar 2001 * * inputs description range / units * year - year 1900 .. 2100 * mon - month 1 .. 12 * day - day 1 .. 28,29,30,31 * hr - universal time hour 0 .. 23 * min - universal time min 0 .. 59 * sec - universal time sec 0.0 .. 59.999 * * outputs : * jd - julian date days from 4713 bc * jdfrac - julian date fraction into day days from 4713 bc * * locals : * none. * * coupling : * none. * * references : * vallado 2013, 183, alg 14, ex 3-4 * * --------------------------------------------------------------------------- */ static jday(year: number | Date, mon = 0, day = 0, hr = 0, min = 0, sec = 0, ms = 0): { jd: number; jdFrac: number } { if (year instanceof Date) { mon = year.getUTCMonth() + 1; day = year.getUTCDate(); hr = year.getUTCHours(); min = year.getUTCMinutes(); sec = year.getUTCSeconds(); ms = year.getUTCMilliseconds(); year = year.getUTCFullYear(); } let jd = 367.0 * year - Math.floor(7 * (year + Math.floor((mon + 9) / 12.0)) * 0.25) + Math.floor((275 * mon) / 9.0) + day + 1721013.5; // Use - 678987.0 to go to mjd directly let jdFrac = (ms / 1000 + sec + min * 60.0 + hr * 3600.0) / 86400.0; // Check that the day and fractional day are correct if (Math.abs(jdFrac) > 1.0) { const dtt = Math.floor(jdFrac); jd += dtt; jdFrac -= dtt; } // - 0.5*sgn(100.0*year + mon - 190002.5) + 0.5; return { jd, jdFrac }; } /* * ----------------------------------------------------------------------------- * * function mag * * this procedure finds the magnitude of a vector. * * author : david vallado 719-573-2600 1 mar 2001 * * inputs description range / units * vec - vector * * outputs : * mag - answer * * locals : * none. * * coupling : * none. * --------------------------------------------------------------------------- */ private static mag_(v: Vec3Flat): number { return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); } /* * ----------------------------------------------------------------------------- * * function newtonnu_SGP4 * * this function solves keplers equation when the true anomaly is known. * the mean and eccentric, parabolic, or hyperbolic anomaly is also found. * the parabolic limit at 168ø is arbitrary. the hyperbolic anomaly is also * limited. the hyperbolic sine is used because it's not double valued. * * author : david vallado 719-573-2600 27 may 2002 * * revisions * vallado - fix small 24 sep 2002 * * inputs description range / units * ecc - eccentricity 0.0 to * nu - true anomaly -2pi to 2pi rad * * outputs : * e0 - eccentric anomaly 0.0 to 2pi rad 153.02 ø * m - mean anomaly 0.0 to 2pi rad 151.7425 ø * * locals : * e1 - eccentric anomaly, next value rad * sine - sine of e * cose - cosine of e * ktr - index * * coupling : * asinh - arc hyperbolic sine * * references : * vallado 2013, 77, alg 5 * --------------------------------------------------------------------------- */ private static newtonnu_( ecc: number, nu: number, ): { e0: number; m: number; } { // --------------------- implementation --------------------- let e0 = 999999.9; let m = 999999.9; const small = 0.00000001; if (Math.abs(ecc) < small) { // --------------------------- circular ------------------------ m = nu; e0 = nu; } else if (ecc < 1.0 - small) { // ---------------------- elliptical ----------------------- const sine = (Math.sqrt(1.0 - ecc * ecc) * Math.sin(nu)) / (1.0 + ecc * Math.cos(nu)); const cose = (ecc + Math.cos(nu)) / (1.0 + ecc * Math.cos(nu)); e0 = Math.atan2(sine, cose); m = e0 - ecc * Math.sin(e0); } else if (ecc > 1.0 + small) { // -------------------- hyperbolic -------------------- if (ecc > 1.0 && Math.abs(nu) + 0.00001 < PI - Math.acos(1.0 / ecc)) { const sine = (Math.sqrt(ecc * ecc - 1.0) * Math.sin(nu)) / (1.0 + ecc * Math.cos(nu)); e0 = Sgp4.asinh_(sine); m = ecc * Sgp4.sinh_(e0) - e0; } } else if (Math.abs(nu) < (168.0 * PI) / 180.0) { // ----------------- parabolic --------------------- e0 = Math.tan(nu * 0.5); m = e0 + (e0 * e0 * e0) / 3.0; } if (ecc < 1.0) { m %= 2.0 * PI; if (m < 0.0) { m += 2.0 * PI; } e0 %= 2.0 * PI; } return { e0, m, }; } /* *---------------------------------------------------------------------------- * * procedure sgp4 * * this procedure is the sgp4 prediction model from space command. this is an * updated and combined version of sgp4 and sdp4, which were originally * published separately in spacetrack report //3. this version follows the * methodology from the aiaa paper (2006) describing the history and * development of the code. * * author : david vallado 719-573-2600 28 jun 2005 * * inputs : * satrec - initialised structure from sgp4init() call. * tsince - time since epoch (minutes) * * outputs : * r - position vector km * v - velocity km/sec * return code - non-zero on error. * 1 - mean elements, ecc >= 1.0 or ecc < -0.001 or a < 0.95 er * 2 - mean motion less than 0.0 * 3 - pert elements, ecc < 0.0 or ecc > 1.0 * 4 - semi-latus rectum < 0.0 * 5 - epoch elements are sub-orbital * 6 - satellite has decayed * * locals : * am - * axnl, aynl - * betal - * cosim , sinim , cosomm , sinomm , cnod , snod , cos2u , * sin2u , coseo1 , sineo1 , cosi , sini , cosip , sinip , * cosisq , cossu , sinsu , cosu , sinu * delm - * delomg - * dndt - * eccm - * emsq - * ecose - * el2 - * eo1 - * eccp - * esine - * argpm - * argpp - * omgadf - * pl - * r - * rtemsq - * rdotl - * rl - * rvdot - * rvdotl - * su - * t2 , t3 , t4 , tc * tem5, temp , temp1 , temp2 , tempa , tempe , templ * u , ux , uy , uz , vx , vy , vz * inclm - inclination * mm - mean anomaly * nm - mean motion * nodem - right asc of ascending node * xinc - * xincp - * xl - * xlm - * mp - * xmdf - * xmx - * xmy - * nodedf - * xnode - * nodep - * np - * * coupling : * getgravconst- * dpper * dspace * * references : * hoots, roehrich, norad spacetrack report //3 1980 * hoots, norad spacetrack report //6 1986 * hoots, schumacher and glover 2004 * vallado, crawford, hujsak, kelso 2006 *---------------------------------------------------------------------------- */ static propagate(satrec: SatelliteRecord, tsince: number): StateVectorSgp4 { /* ------------------ set mathematical constants --------------- */ /* * Sgp4fix divisor for divide by zero check on inclination * the old check used 1.0 + cos(PI-1.0e-9), but then compared it to * 1.5 e-12, so the threshold was changed to 1.5e-12 for consistency */ /* * Sgp4fix identify constants and allow alternate values * getgravconst( whichconst, tumin, mu, radiusearthkm, xke, j2, j3, j4, j3oj2 ); */ const { xke, j2, j3oj2, vkmpersec } = satrec; // --------------------- clear sgp4 error flag ----------------- satrec.t = tsince; satrec.error = Sgp4ErrorCode.NO_ERROR; // ------- update for secular gravity and atmospheric drag ----- const xmdf = satrec.mo + satrec.mdot * satrec.t; const argpdf = satrec.argpo + satrec.argpdot * satrec.t; const nodedf = satrec.nodeo + satrec.nodedot * satrec.t; let argpm = argpdf; let mm = xmdf; const t2 = satrec.t * satrec.t; let nodem = nodedf + satrec.nodecf * t2; let tempa = 1.0 - satrec.cc1 * satrec.t; let tempe = satrec.bstar * satrec.cc4 * satrec.t; let templ = satrec.t2cof * t2; if (!satrec.isimp) { const delomg = satrec.omgcof * satrec.t; // Sgp4fix use mutliply for speed instead of pow const delmtemp = 1.0 + satrec.eta * Math.cos(xmdf); const delm = satrec.xmcof * (delmtemp * delmtemp * delmtemp - satrec.delmo); const temp = delomg + delm; mm = xmdf + temp; argpm = argpdf - temp; const t3 = t2 * satrec.t; const t4 = t3 * satrec.t; tempa = tempa - satrec.d2 * t2 - satrec.d3 * t3 - satrec.d4 * t4; tempe += satrec.bstar * satrec.cc5 * (Math.sin(mm) - satrec.sinmao); templ = templ + satrec.t3cof * t3 + t4 * (satrec.t4cof + satrec.t * satrec.t5cof); } let nm = satrec.no; let em = satrec.ecco; let inclm = satrec.inclo; if (satrec.method === Sgp4Methods.DEEP_SPACE) { [em, argpm, inclm, mm, nodem, nm] = Sgp4.dspace_( em, argpm, inclm, mm, nodem, nm, satrec, ); } if (nm <= 0.0) { satrec.error = Sgp4ErrorCode.MEAN_MOTION_NEGATIVE; return { position: false, velocity: false }; } const am = (xke / nm) ** x2o3 * tempa * tempa; nm = xke / am ** 1.5; em -= tempe; /* * Fix tolerance for error recognition * sgp4fix am is fixed from the previous nm check */ /* istanbul ignore next | This is no longer possible*/ if (em >= 1.0 || em < -0.001) { satrec.error = Sgp4ErrorCode.MEAN_MOTION_NEGATIVE; return { position: false, velocity: false }; } // Sgp4fix fix tolerance to avoid a divide by zero if (em < 1.0e-6) { em = 1.0e-6; } mm += satrec.no * templ; let xlm = mm + argpm + nodem; nodem %= TAU; argpm %= TAU; xlm %= TAU; mm = (xlm - argpm - nodem) % TAU; /* * Sgp4fix recover singly averaged mean elements * satrec.am = am; * satrec.em = em; * satrec.im = inclm; * satrec.Om = nodem; * satrec.om = argpm; * satrec.mm = mm; * satrec.nm = nm; */ // ----------------- compute extra mean quantities ------------- const sinim = Math.sin(inclm); const cosim = Math.cos(inclm); // -------------------- add lunar-solar periodics -------------- let ep = em; let xincp = inclm; let argpp = argpm; let nodep = nodem; let mp = mm; let sinip = sinim; let cosip = cosim; if (satrec.method === Sgp4Methods.DEEP_SPACE) { const dpperParameters = { inclo: satrec.inclo, init: false, ep, inclp: xincp, nodep, argpp, mp, opsmode: satrec.operationmode, satrec, }; ({ ep, nodep, argpp, mp, inclp: xincp } = Sgp4.dpper_(dpperParameters)); if (xincp < 0.0) { xincp = -xincp; nodep += PI; argpp -= PI; } if (ep < 0.0 || ep > 1.0) { satrec.error = Sgp4ErrorCode.PERT_ELEMENTS_INVALID; return { position: false, velocity: false }; } } // -------------------- long period periodics ------------------ if (satrec.method === Sgp4Methods.DEEP_SPACE) { sinip = Math.sin(xincp); cosip = Math.cos(xincp); satrec.aycof = -0.5 * j3oj2 * sinip; // Sgp4fix for divide by zero for xincp = 180 deg if (Math.abs(cosip + 1.0) > 1.5e-12) { satrec.xlcof = (-0.25 * j3oj2 * sinip * (3.0 + 5.0 * cosip)) / (1.0 + cosip); } else { satrec.xlcof = (-0.25 * j3oj2 * sinip * (3.0 + 5.0 * cosip)) / temp4; } } const axnl = ep * Math.cos(argpp); let temp = 1.0 / (am * (1.0 - ep * ep)); const aynl = ep * Math.sin(argpp) + temp * satrec.aycof; const xl = mp + argpp + nodep + temp * satrec.xlcof * axnl; // --------------------- solve kepler's equation --------------- const u = (xl - nodep) % TAU; let eo1 = u; let tem5 = 9999.9; let ktr = 1; /* * Sgp4fix for kepler iteration * the following iteration needs better limits on corrections */ let coseo1 = 0; let sineo1 = 0; while (Math.abs(tem5) >= 1.0e-12 && ktr <= 10) { sineo1 = Math.sin(eo1); coseo1 = Math.cos(eo1); tem5 = 1.0 - coseo1 * axnl - sineo1 * aynl; tem5 = (u - aynl * coseo1 + axnl * sineo1 - eo1) / tem5; if (Math.abs(tem5) >= 0.95) { if (tem5 > 0.0) { tem5 = 0.95; } else { tem5 = -0.95; } } eo1 += tem5; ktr += 1; } // ------------- short period preliminary quantities ----------- const ecose = axnl * coseo1 + aynl * sineo1; const esine = axnl * sineo1 - aynl * coseo1; const el2 = axnl * axnl + aynl * aynl; const pl = am * (1.0 - el2); if (pl < 0.0) { satrec.error = Sgp4ErrorCode.SEMI_LATUS_RECTUM_NEGATIVE; return { position: false, velocity: false }; } const rl = am * (1.0 - ecose); const rdotl = (Math.sqrt(am) * esine) / rl; const rvdotl = Math.sqrt(pl) / rl; const betal = Math.sqrt(1.0 - el2); temp = esine / (1.0 + betal); const sinu = (am / rl) * (sineo1 - aynl - axnl * temp); const cosu = (am / rl) * (coseo1 - axnl + aynl * temp); let su = Math.atan2(sinu, cosu); const sin2u = (cosu + cosu) * sinu; const cos2u = 1.0 - 2.0 * sinu * sinu; temp = 1.0 / pl; const temp1 = 0.5 * j2 * temp; const temp2 = temp1 * temp; // -------------- update for short period periodics ------------ if (satrec.method === Sgp4Methods.DEEP_SPACE) { const cosisq = cosip * cosip; satrec.con41 = 3.0 * cosisq - 1.0; satrec.x1mth2 = 1.0 - cosisq; satrec.x7thm1 = 7.0 * cosisq - 1.0; } const mrt = rl * (1.0 - 1.5 * temp2 * betal * satrec.con41) + 0.5 * temp1 * satrec.x1mth2 * cos2u; /** Moved this up to reduce unnecessary computation if you are going to return false anyway */ // Sgp4fix for decaying satellites if (mrt < 1.0) { satrec.error = Sgp4ErrorCode.SATELLITE_DECAYED; return { position: false, velocity: false, }; } su -= 0.25 * temp2 * satrec.x7thm1 * sin2u; const xnode = nodep + 1.5 * temp2 * cosip * sin2u; const xinc = xincp + 1.5 * temp2 * cosip * sinip * cos2u; const mvt = rdotl - (nm * temp1 * satrec.x1mth2 * sin2u) / xke; const rvdot = rvdotl + (nm * temp1 * (satrec.x1mth2 * cos2u + 1.5 * satrec.con41)) / xke; // --------------------- orientation vectors ------------------- const sinsu = Math.sin(su); const cossu = Math.cos(su); const snod = Math.sin(xnode); const cnod = Math.cos(xnode); const sini = Math.sin(xinc); const cosi = Math.cos(xinc); const xmx = -snod * cosi; const xmy = cnod * cosi; const ux = xmx * sinsu + cnod * cossu; const uy = xmy * sinsu + snod * cossu; const uz = sini * sinsu; const vx = xmx * cossu - cnod * sinsu; const vy = xmy * cossu - snod * sinsu; const vz = sini * cossu; // --------- position and velocity (in km and km/sec) ---------- const r = { x: mrt * ux * satrec.radiusearthkm, y: mrt * uy * satrec.radiusearthkm, z: mrt * uz * satrec.radiusearthkm, }; const v = { x: (mvt * ux + rvdot * vx) * vkmpersec, y: (mvt * uy + rvdot * vy) * vkmpersec, z: (mvt * uz + rvdot * vz) * vkmpersec, }; return { position: r, velocity: v, }; } /* * ----------------------------------------------------------------------------- * * function rv2coe_SGP4 * * this function finds the classical orbital elements given the geocentric * equatorial position and velocity vectors. * * author : david vallado 719-573-2600 21 jun 2002 * * revisions * vallado - fix special cases 5 sep 2002 * vallado - delete extra check in inclination code 16 oct 2002 * vallado - add constant file use 29 jun 2003 * vallado - add mu 2 apr 2007 * * inputs description range / units * r - ijk position vector km * v - ijk velocity vector km / s * mu - gravitational parameter km3 / s2 * * outputs : * p - semilatus rectum km * a - semimajor axis km * ecc - eccentricity * incl - inclination 0.0 to pi rad * omega - right ascension of ascending node 0.0 to 2pi rad * argp - argument of perigee 0.0 to 2pi rad * nu - true anomaly 0.0 to 2pi rad * m - mean anomaly 0.0 to 2pi rad * arglat - argument of latitude (ci) 0.0 to 2pi rad * truelon - true longitude (ce) 0.0 to 2pi rad * lonper - longitude of periapsis (ee) 0.0 to 2pi rad * * locals : * hbar - angular momentum h vector km2 / s * ebar - eccentricity e vector * nbar - line of nodes n vector * c1 - v**2 - u/r * rdotv - r dot v * hk - hk unit vector * sme - specfic mechanical energy km2 / s2 * i - index * e - eccentric, parabolic, * hyperbolic anomaly rad * temp - temporary variable * typeorbit - type of orbit ee, ei, ce, ci * * coupling : * mag - magnitude of a vector * cross - cross product of two vectors * angle - find the angle between two vectors * newtonnu - find the mean anomaly * * references : * vallado 2013, 113, alg 9, ex 2-5 * --------------------------------------------------------------------------- */ static rv2coe( r: Vec3Flat, v: Vec3Flat, mus: number, ): { p: number; a: number; ecc: number; incl: number; omega: number; argp: number; nu: number; m: number; arglat: number; truelon: number; lonper: number; } { const nbar: Vec3Flat = [0, 0, 0]; const ebar: Vec3Flat = [0, 0, 0]; let p: number; let a: number; let ecc: number; let incl: number; let omega: number; let argp: number; let nu: number; let m = 0; let arglat: number; let truelon: number; let lonper: number; let rdotv: number; let magn: number; let hk: number; let sme: number; let i; /* * Switch this to an integer msvs seems to have probelms with this and strncpy_s * char typeorbit[2]; */ let typeorbit; /* * Here * typeorbit = 1 = 'ei' * typeorbit = 2 = 'ce' * typeorbit = 3 = 'ci' * typeorbit = 4 = 'ee' */ const halfpi = 0.5 * PI; const small = 0.00000001; const unknown = 999999.1; /** Ootk -- original undefined is illegal in JS */ const infinite = 999999.9; // ------------------------- implementation ----------------- const magr = Sgp4.mag_(r); const magv = Sgp4.mag_(v); // ------------------ find h n and e vectors ---------------- const hbar = Sgp4.cross_(r, v); const magh = Sgp4.mag_(hbar); if (magh > small) { nbar[0] = -hbar[1]; nbar[1] = hbar[0]; nbar[2] = 0.0; magn = Sgp4.mag_(nbar); const c1 = magv * magv - mus / magr; rdotv = Sgp4.dot_(r, v); for (i = 0; i <= 2; i++) { ebar[i] = (c1 * (r[i] as number) - rdotv * (v[i] as number)) / mus; } ecc = Sgp4.mag_(ebar); // ------------ find a e and semi-latus rectum ---------- sme = magv * magv * 0.5 - mus / magr; if (Math.abs(sme) > small) { a = -mus / (2.0 * sme); } else { a = infinite; } p = (magh * magh) / mus; // ----------------- find inclination ------------------- hk = hbar[2] / magh; incl = Math.acos(hk); /* * -------- determine type of orbit for later use -------- * ------ elliptical, parabolic, hyperbolic inclined ------- */ typeorbit = 1; if (ecc < small) { // ---------------- circular equatorial --------------- if (incl < small || Math.abs(incl - PI) < small) { typeorbit = 2; } else { // -------------- circular inclined --------------- typeorbit = 3; } // - elliptical, parabolic, hyperbolic equatorial -- } else if (incl < small || Math.abs(incl - PI) < small) { typeorbit = 4; } // ---------- find right ascension of the ascending node ------------ if (magn > small) { let temp = nbar[0] / magn; if (Math.abs(temp) > 1.0) { temp = Sgp4.sgn_(temp); } omega = Math.acos(temp); if (nbar[1] < 0.0) { omega = TAU - omega; } } else { omega = unknown; } // ---------------- f