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
text/typescript
/**
* @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