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.

410 lines 17.6 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. */ import { J2000 } from '../coordinate/J2000.js'; import { RIC } from '../coordinate/RIC.js'; import { Tle } from '../coordinate/Tle.js'; import { RAE } from '../observation/RAE.js'; import { Vector3D } from '../operations/Vector3D.js'; import { EpochUTC } from '../time/EpochUTC.js'; import { ecf2rae, eci2ecf, eci2lla, jday } from '../transforms/index.js'; import { DEG2RAD, MILLISECONDS_TO_DAYS, MINUTES_PER_DAY, RAD2DEG } from '../utils/constants.js'; import { dopplerFactor } from './../utils/functions.js'; import { BaseObject } from './BaseObject.js'; import { Sgp4 } from '../main.js'; /** * Represents a satellite object with orbital information and methods for * calculating its position and other properties. */ export class Satellite extends BaseObject { apogee; argOfPerigee; bstar; eccentricity; epochDay; epochYear; inclination; intlDes; meanAnomaly; meanMoDev1; meanMoDev2; meanMotion; options; perigee; period; rightAscension; satrec; /** The satellite catalog number as listed in the TLE. */ sccNum; /** The 5 digit alpha-numeric satellite catalog number. */ sccNum5; /** The 6 digit numeric satellite catalog number. */ sccNum6; tle1; tle2; /** The semi-major axis of the satellite's orbit. */ semiMajorAxis; /** The semi-minor axis of the satellite's orbit. */ semiMinorAxis; constructor(info, options) { super(info); if (info.tle1 && info.tle2) { this.parseTleAndUpdateOrbit_(info.tle1, info.tle2, info.sccNum); } else if (info.omm) { this.parseOmmAndUpdateOrbit_(info.omm); } else { throw new Error('tle1 and tle2 or omm must be provided to create a Satellite object.'); } this.options = options ?? { notes: '', }; } parseTleAndUpdateOrbit_(tle1, tle2, sccNum) { const tleData = Tle.parse(tle1, tle2); this.tle1 = tle1; this.tle2 = tle2; this.sccNum = sccNum ?? tleData.satNum.toString(); this.sccNum5 = Tle.convert6DigitToA5(this.sccNum); this.sccNum6 = Tle.convertA5to6Digit(this.sccNum5); this.intlDes = tleData.intlDes; this.epochYear = tleData.epochYear; this.epochDay = tleData.epochDay; this.meanMoDev1 = tleData.meanMoDev1; this.meanMoDev2 = tleData.meanMoDev2; this.bstar = tleData.bstar; this.inclination = tleData.inclination; this.rightAscension = tleData.rightAscension; this.eccentricity = tleData.eccentricity; this.argOfPerigee = tleData.argOfPerigee; this.meanAnomaly = tleData.meanAnomaly; this.meanMotion = tleData.meanMotion; this.period = tleData.period; this.semiMajorAxis = ((8681663.653 / this.meanMotion) ** (2 / 3)); this.semiMinorAxis = (this.semiMajorAxis * Math.sqrt(1 - this.eccentricity ** 2)); this.apogee = (this.semiMajorAxis * (1 + this.eccentricity) - 6371); this.perigee = (this.semiMajorAxis * (1 - this.eccentricity) - 6371); this.satrec = Sgp4.createSatrec(tle1, tle2); } parseOmmAndUpdateOrbit_(omm) { this.sccNum = omm.NORAD_CAT_ID.padStart(5, '0'); this.sccNum5 = Tle.convert6DigitToA5(omm.NORAD_CAT_ID); this.sccNum6 = Tle.convertA5to6Digit(this.sccNum5); this.intlDes = omm.OBJECT_ID; const YYYY = omm.EPOCH.slice(0, 4); const MM = omm.EPOCH.slice(5, 7); const DD = omm.EPOCH.slice(8, 10); const hh = omm.EPOCH.slice(11, 13); const mm = omm.EPOCH.slice(14, 16); const ss = omm.EPOCH.slice(17, 23); const epochDateObj = Date.UTC(Number(YYYY), Number(MM) - 1, Number(DD), Number(hh), Number(mm), Number(ss)); const dayOfYear = (epochDateObj - Date.UTC(Number(YYYY), 0, 0)) / 86400000; const ommParsed = { ...omm, epoch: { year: Number(YYYY), month: Number(MM), day: Number(DD), hour: Number(hh), minute: Number(mm), second: Number(ss), doy: dayOfYear, }, }; this.epochYear = parseInt(YYYY.slice(2, 4)); this.epochDay = dayOfYear; this.meanMoDev1 = parseFloat(omm.MEAN_MOTION_DOT); this.meanMoDev2 = parseFloat(omm.MEAN_MOTION_DDOT); this.bstar = parseFloat(omm.BSTAR); this.inclination = parseFloat(omm.INCLINATION); this.rightAscension = parseFloat(omm.RA_OF_ASC_NODE); this.eccentricity = parseFloat(omm.ECCENTRICITY); this.argOfPerigee = parseFloat(omm.ARG_OF_PERICENTER); this.meanAnomaly = parseFloat(omm.MEAN_ANOMALY); this.meanMotion = parseFloat(omm.MEAN_MOTION); this.period = 1440 / this.meanMotion; this.semiMajorAxis = ((8681663.653 / this.meanMotion) ** (2 / 3)); this.semiMinorAxis = (this.semiMajorAxis * Math.sqrt(1 - this.eccentricity ** 2)); this.apogee = (this.semiMajorAxis * (1 + this.eccentricity) - 6371); this.perigee = (this.semiMajorAxis * (1 - this.eccentricity) - 6371); this.satrec = Sgp4.createSatrecFromOmm(ommParsed); } /** * Checks if the object is a satellite. * @returns True if the object is a satellite, false otherwise. */ isSatellite() { return true; } /** * Returns whether the satellite is static or not. * @returns True if the satellite is static, false otherwise. */ isStatic() { return false; } /** * Checks if the given SatelliteRecord object is valid by checking if its properties are all numbers. * @param satrec - The SatelliteRecord object to check. * @returns True if the SatelliteRecord object is valid, false otherwise. */ static isValidSatrec(satrec) { if (isNaN(satrec.a) || isNaN(satrec.am) || isNaN(satrec.alta) || isNaN(satrec.em) || isNaN(satrec.mo) || isNaN(satrec.ecco) || isNaN(satrec.no)) { return false; } return true; } ageOfElset(nowInput, outputUnits = 'days') { return Tle.calcElsetAge(this, nowInput, outputUnits); } editTle(tle1, tle2, sccNum) { this.parseTleAndUpdateOrbit_(tle1, tle2, sccNum); } /** * Calculates the azimuth angle of the satellite relative to the given sensor at the specified date. If no date is * provided, the current time of the satellite is used. * @variation optimized * @param observer - The observer's position on the ground. * @param date - The date at which to calculate the azimuth angle. Optional, defaults to the current date. * @returns The azimuth angle of the satellite relative to the given sensor at the specified date. */ az(observer, date = new Date()) { return (this.rae(observer, date).az * RAD2DEG); } /** * Calculates the RAE (Range, Azimuth, Elevation) values for a given sensor and date. If no date is provided, the * current time is used. * @variation expanded * @param observer - The observer's position on the ground. * @param date - The date at which to calculate the RAE values. Optional, defaults to the current date. * @returns The RAE values for the given sensor and date. */ toRae(observer, date = new Date()) { const rae = this.rae(observer, date); const rae2 = this.rae(observer, new Date(date.getTime() + 1000)); const epoch = new EpochUTC(date.getTime() / 1000); const rangeRate = rae2.rng - rae.rng; const azimuthRate = rae2.az - rae.az; const elevationRate = rae2.el - rae.el; return new RAE(epoch, rae.rng, (rae.az * DEG2RAD), (rae.el * DEG2RAD), rangeRate, azimuthRate, elevationRate); } /** * Calculates ECF position at a given time. * @variation optimized * @param date - The date at which to calculate the ECF position. Optional, defaults to the current date. * @returns The ECF position at the specified date. */ ecf(date = new Date()) { const { gmst } = Satellite.calculateTimeVariables(date); return eci2ecf(this.eci(date).position, gmst); } /** * Calculates ECI position at a given time. * @variation optimized * @param date - The date at which to calculate the ECI position. Optional, defaults to the current date. * @param j - Julian date. Optional, defaults to null. * @param gmst - Greenwich Mean Sidereal Time. Optional, defaults to null. * @returns The ECI position at the specified date. */ eci(date, j, gmst) { date ??= new Date(); const { m } = Satellite.calculateTimeVariables(date, this.satrec, j, gmst); if (!m) { throw new Error('Propagation failed!'); } const pv = Sgp4.propagate(this.satrec, m); if (!pv) { throw new Error('Propagation failed!'); } else { return pv; } } /** * Calculates the J2000 coordinates for a given date. If no date is provided, the current time is used. * @variation expanded * @param date - The date for which to calculate the J2000 coordinates, defaults to the current date. * @returns The J2000 coordinates for the specified date. * @throws Error if propagation fails. */ toJ2000(date = new Date()) { const { m } = Satellite.calculateTimeVariables(date, this.satrec); if (!m) { throw new Error('Propagation failed!'); } const pv = Sgp4.propagate(this.satrec, m); if (!pv.position) { throw new Error('Propagation failed!'); } else { const p = pv.position; const v = pv.velocity; const epoch = new EpochUTC(date.getTime() / 1000); const pos = new Vector3D(p.x, p.y, p.z); const vel = new Vector3D(v.x, v.y, v.z); return new J2000(epoch, pos, vel); } } /** * Returns the elevation angle of the satellite as seen by the given sensor at the specified time. * @variation optimized * @param observer - The observer's position on the ground. * @param date - The date at which to calculate the elevation angle. Optional, defaults to the current date. * @returns The elevation angle of the satellite as seen by the given sensor at the specified time. */ el(observer, date = new Date()) { return (this.rae(observer, date).el * RAD2DEG); } /** * Calculates LLA position at a given time. * @variation optimized * @param date - The date at which to calculate the LLA position. Optional, defaults to the current date. * @param j - Julian date. Optional, defaults to null. * @param gmst - Greenwich Mean Sidereal Time. Optional, defaults to null. * @returns The LLA position at the specified date. */ lla(date, j, gmst) { date ??= new Date(); if (!j || !gmst) { const timeVar = Satellite.calculateTimeVariables(date, this.satrec); j = timeVar.j; gmst = timeVar.gmst; } const pos = this.eci(date, j, gmst).position; const lla = eci2lla(pos, gmst); return lla; } /** * Converts the satellite's position to geodetic coordinates. * @variation expanded * @param date The date for which to calculate the geodetic coordinates. Defaults to the current date. * @returns The geodetic coordinates of the satellite. */ toGeodetic(date = new Date()) { return this.toJ2000(date).toITRF().toGeodetic(); } /** * Converts the satellite's position to the International Terrestrial Reference Frame (ITRF) at the specified date. * If no date is provided, the current date is used. * @variation expanded * @param date The date for which to convert the position. Defaults to the current date. * @returns The satellite's position in the ITRF at the specified date. */ toITRF(date = new Date()) { return this.toJ2000(date).toITRF(); } /** * Converts the current satellite's position to the Reference-Inertial-Celestial (RIC) frame * relative to the specified reference satellite at the given date. * @variation expanded * @param reference The reference satellite. * @param date The date for which to calculate the RIC frame. Defaults to the current date. * @returns The RIC frame representing the current satellite's position relative to the reference satellite. */ toRIC(reference, date = new Date()) { return RIC.fromJ2000(this.toJ2000(date), reference.toJ2000(date)); } /** * Converts the satellite object to a TLE (Two-Line Element) object. * @returns The TLE object representing the satellite. */ toTle() { return new Tle(this.tle1, this.tle2); } /** * Converts the satellite's position to classical orbital elements. * @param date The date for which to calculate the classical elements. Defaults to the current date. * @returns The classical orbital elements of the satellite. */ toClassicalElements(date = new Date()) { return this.toJ2000(date).toClassicalElements(); } /** * Calculates the RAE (Range, Azimuth, Elevation) vector for a given sensor and time. * @variation optimized * @param observer - The observer's position on the ground. * @param date - The date at which to calculate the RAE vector. Optional, defaults to the current date. * @param j - Julian date. Optional, defaults to null. * @param gmst - Greenwich Mean Sidereal Time. Optional, defaults to null. * @returns The RAE vector for the given sensor and time. */ rae(observer, date, j, gmst) { date ??= new Date(); gmst ??= Satellite.calculateTimeVariables(date, this.satrec).gmst; const eci = this.eci(date, j, gmst).position; const ecf = eci2ecf(eci, gmst); return ecf2rae(observer, ecf); } /** * Returns the range of the satellite from the given sensor at the specified time. * @variation optimized * @param observer - The observer's position on the ground. * @param date - The date at which to calculate the range. Optional, defaults to the current date. * @returns The range of the satellite from the given sensor at the specified time. */ rng(observer, date = new Date()) { return this.rae(observer, date).rng; } /** * Applies the Doppler effect to the given frequency based on the observer's position and the date. * @param freq - The frequency to apply the Doppler effect to. * @param observer - The observer's position on the ground. * @param date - The date at which to calculate the Doppler effect. Optional, defaults to the current date. * @returns The frequency after applying the Doppler effect. */ applyDoppler(freq, observer, date) { const doppler = this.dopplerFactor(observer, date); return freq * doppler; } /** * Calculates the Doppler factor for the satellite. * @param observer The observer's ground position. * @param date The optional date for which to calculate the Doppler factor. If not provided, the current date is used. * @returns The calculated Doppler factor. */ dopplerFactor(observer, date) { const position = this.eci(date); return dopplerFactor(observer.eci(date), position.position, position.velocity); } /** * Calculates the time variables for a given date relative to the TLE epoch. * @param date Date to calculate * @param satrec Satellite orbital information * @param j Julian date * @param gmst Greenwich Mean Sidereal Time * @returns Time variables */ static calculateTimeVariables(date, satrec, j, gmst) { j ??= jday(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()) + date.getUTCMilliseconds() * MILLISECONDS_TO_DAYS; gmst ??= Sgp4.gstime(j); const m = satrec ? (j - satrec.jdsatepoch) * MINUTES_PER_DAY : null; return { gmst, m, j }; } } //# sourceMappingURL=Satellite.js.map