UNPKG

s2-tools

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

450 lines 13.8 kB
import { days2mdhms, jday } from './util/time'; import { deg2rad, minutesPerDay, pi } from './util/constants'; import { sgp4, sgp4init } from './propagation'; /** * # Satellite Orbit Class * * Input TLE example * STARLINK-1007 * 1 44713C 19074A 23048.53451389 -.00009219 00000+0 -61811-3 0 482 * 2 44713 53.0512 157.2379 0001140 81.3827 74.7980 15.06382459 15 */ export class Satellite { init = false; // Line 0 name = 'default'; // Line 1 number = 0; // (satnum) Satellite catalog number or NORAD (North American Aerospace Defense) Catalog Number class = 'U'; // Classification (U: unclassified, C: classified, S: secret) id = 'null'; // International Designator date = new Date(); // (epochyr + epochdays) epochyr = 0; epochdays = 0; jdsatepoch; fdmm = 0; // (ndot) First derivative of mean motion; the ballistic coefficient sdmm = 0; // (nddot) Second derivative of mean motion (decimal point assumed) drag = 0; // (bstar) B*, the drag term, or radiation pressure coefficient (decimal point assumed) ephemeris = 0; // Ephemeris type (always zero; only used in undistributed TLE data) esn = 0; // Element set number. Incremented when a new TLE is generated for this object. // Line 2 inclination = 0; // Inclination (degrees) ascension = 0; // Right ascension of the ascending node (degrees) eccentricity = 0; // Eccentricity (decimal point assumed) perigee = 0; // Argument of perigee (degrees) anomaly = 0; // Mean anomaly (degrees) motion = 0; // Mean motion (revolutions per day) revolution = 0; // Revolution number at epoch (revolutions) // extra opsmode = 'i'; rms; // ----------- all near earth variables ------------ isimp = 0; method = 'n'; aycof = 0; con41 = 0; cc1 = 0; cc4 = 0; cc5 = 0; d2 = 0; d3 = 0; d4 = 0; delmo = 0; eta = 0; argpdot = 0; omgcof = 0; sinmao = 0; t2cof = 0; t3cof = 0; t4cof = 0; t5cof = 0; x1mth2 = 0; x7thm1 = 0; mdot = 0; nodedot = 0; xlcof = 0; xmcof = 0; nodecf = 0; // ----------- all deep space variables ------------ irez = 0; d2201 = 0; d2211 = 0; d3210 = 0; d3222 = 0; d4410 = 0; d4422 = 0; d5220 = 0; d5232 = 0; d5421 = 0; d5433 = 0; dedt = 0; del1 = 0; del2 = 0; del3 = 0; didt = 0; dmdt = 0; dnodt = 0; domdt = 0; e3 = 0; ee2 = 0; peo = 0; pgho = 0; pho = 0; pinco = 0; plo = 0; se2 = 0; se3 = 0; sgh2 = 0; sgh3 = 0; sgh4 = 0; sh2 = 0; sh3 = 0; si2 = 0; si3 = 0; sl2 = 0; sl3 = 0; sl4 = 0; gsto = 0; xfact = 0; xgh2 = 0; xgh3 = 0; xgh4 = 0; xh2 = 0; xh3 = 0; xi2 = 0; xi3 = 0; xl2 = 0; xl3 = 0; xl4 = 0; xlamo = 0; zmol = 0; zmos = 0; atime = 0; xli = 0; xni = 0; /** * Constructor * @param data - TLE data or TLE string * @param initialize - initialize the object on creation */ constructor(data, initialize = true) { if (typeof data === 'string') this.#parseTLE(data); else this.#parseJSON(data); // convert degrees to rad this.inclination *= deg2rad; this.ascension *= deg2rad; this.perigee *= deg2rad; this.anomaly *= deg2rad; // convert revolution from deg/day to rad/minute this.motion /= 1440 / (2 * pi); // rad/min (229.1831180523293) // find sgp4epoch time of element set // remember that sgp4 uses units of days from 0 jan 1950 (sgp4epoch) // and minutes from the epoch (time) const year = this.epochyr < 57 ? this.epochyr + 2000 : this.epochyr + 1900; const mdhmsResult = days2mdhms(year, this.epochdays); const { mon, day, hr, minute, sec } = mdhmsResult; this.jdsatepoch = jday(year, mon, day, hr, minute, sec, 0); if (initialize) sgp4init(this); } /** API */ /** * propagate the satellite's position and velocity given a Date input * @param time - Date object * @returns - SGP4ErrorOutput or SGP4Output */ propagate(time) { const j = jday(time); return sgp4(this, (j - this.jdsatepoch) * minutesPerDay); } /** * time in minutes since epoch * @param time - time in minutes * @returns - satellite state at that time */ sgp4(time) { return sgp4(this, time); } /** * Converts satellite state to an array that is readable by the GPU * @returns - satellite state in an array */ gpu() { return [ this.anomaly, this.motion, this.eccentricity, this.inclination, this.method === 'd' ? 0 : 1, // 0 -> 'd', 1 -> 'n' this.opsmode === 'a' ? 0 : 1, // 0 -> 'a'; 1 -> 'i' this.drag, this.mdot, this.perigee, this.argpdot, this.ascension, this.nodedot, this.nodecf, this.cc1, this.cc4, this.cc5, this.t2cof, this.isimp, this.omgcof, this.eta, this.xmcof, this.delmo, this.d2, this.d3, this.d4, this.sinmao, this.t3cof, this.t4cof, this.t5cof, this.irez, this.d2201, this.d2211, this.d3210, this.d3222, this.d4410, this.d4422, this.d5220, this.d5232, this.d5421, this.d5433, this.dedt, this.del1, this.del2, this.del3, this.didt, this.dmdt, this.dnodt, this.domdt, this.gsto, this.xfact, this.xlamo, this.atime, this.xli, this.xni, this.aycof, this.xlcof, this.con41, this.x1mth2, this.x7thm1, this.zmos, this.zmol, this.se2, this.se3, this.si2, this.si3, this.sl2, this.sl3, this.sl4, this.sgh2, this.sgh3, this.sgh4, this.sh2, this.sh3, this.ee2, this.e3, this.xi2, this.xi3, this.xl2, this.xl3, this.xl4, this.xgh2, this.xgh3, this.xgh4, this.xh2, this.xh3, this.peo, this.pinco, this.plo, this.pgho, this.pho, ]; } /** INTERNAL */ /** @param data - TLE data object */ #parseJSON(data) { if (typeof data.date === 'string') data.date = new Date(data.date); for (const [key, value] of Object.entries(data)) { if (key in this) this[key] = value; } this.epochyr = this.date.getFullYear() % 100; const start = new Date(Date.UTC(this.date.getFullYear(), 0, 0)); this.epochdays = jday(this.date) - jday(start); } /** * Parse TLE string * https://en.wikipedia.org/wiki/Two-line_element_set * @param value - TLE string */ #parseTLE(value) { const lines = trim(String(value)).split(/\r?\n/g); let line; let checksum; // Line 0 if (lines.length === 3) { this.name = trim(lines.shift()); if (this.name.substring(0, 2) === '0 ') this.name = this.name.substring(2); } // Line 1 line = lines.shift(); checksum = check(line); if (checksum !== Number(line.substring(68, 69))) { throw new Error(`Line 1 checksum mismatch: ${checksum} != ${line.substring(68, 69)}: ${line}`); } this.number = this.#parseFloat(alpha5Converter(line.substring(2, 7))); this.class = trim(line.substring(7, 9)); this.id = trim(line.substring(9, 18)); this.date = this.#parseDate(line.substring(18, 33)); this.fdmm = this.#parseFloat(line.substring(33, 44)); this.sdmm = this.#parseFloat(line.substring(44, 53)); this.drag = this.#parseDrag(line.substring(53, 62)); this.ephemeris = this.#parseFloat(line.substring(62, 64)); this.esn = this.#parseFloat(line.substring(64, 68)); // Line 2 line = lines.shift(); checksum = check(line); if (checksum !== Number(line.substring(68, 69))) { throw new Error(`Line 2 checksum mismatch: ${checksum} != ${line.substring(68, 69)}: ${line}`); } this.inclination = this.#parseFloat(line.substring(8, 17)); this.ascension = this.#parseFloat(line.substring(17, 26)); this.eccentricity = this.#parseFloat('0.' + line.substring(26, 34)); this.perigee = this.#parseFloat(line.substring(34, 43)); this.anomaly = this.#parseFloat(line.substring(43, 52)); this.motion = this.#parseFloat(line.substring(52, 63)); this.revolution = this.#parseFloat(line.substring(63, 68)); } /** * Parse a float number from a string * @param value - string * @returns - the parsed float number */ #parseFloat(value) { const pattern = /([-])?([.\d]+)([+-]\d+)?/; const match = pattern.exec(value); if (match !== null) { const sign = match[1] === '-' ? -1 : 1; const power = match[3] !== undefined ? 'e' + match[3] : 'e0'; const value = match[2]; return sign * parseFloat(value + power); } return NaN; } /** * Parse a drag coefficient * @param value - string * @returns - the parsed drag coefficient */ #parseDrag(value) { const pattern = /([-])?([.\d]+)([+-]\d+)?/; const match = pattern.exec(value); if (match !== null) { const sign = match[1] === '-' ? -1 : 1; const power = match[3] !== undefined ? 'e' + match[3] : 'e0'; const value = !match[2].includes('.') ? '0.' + match[2] : match[2]; return sign * parseFloat(value + power); } return NaN; } /** * Parse a date * @param value - string of the date * @returns - the parsed date */ #parseDate(value) { value = String(value).replace(/^\s+|\s+$/, ''); const epoch = (this.epochyr = parseInt(value.substring(0, 2), 10)); const days = (this.epochdays = parseFloat(value.substring(2))); let year = new Date().getFullYear(); const currentEpoch = year % 100; const century = year - currentEpoch; year = epoch > currentEpoch + 1 ? century - 100 + epoch : century + epoch; const day = Math.floor(days); const hours = 24 * (days - day); const hour = Math.floor(hours); const minutes = 60 * (hours - hour); const minute = Math.floor(minutes); const seconds = 60 * (minutes - minute); const second = Math.floor(seconds); const millisecond = 1000 * (seconds - second); const utc = Date.UTC(year, 0, day, hour, minute, second, millisecond); return new Date(utc); } } /** * Covnert Celestrak TLE data to a standard TLE data object * [JSON example](https://celestrak.org/NORAD/elements/supplemental/index.php?FORMAT=json) * @param data - Celestrak TLE data * @returns - TLE data */ export function convertCelestrak(data) { // convert date to UTC to avoid javascripts localization issues const date = new Date(data.EPOCH); const utc = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()); return { name: data.OBJECT_NAME, number: data.NORAD_CAT_ID, class: data.CLASSIFICATION_TYPE, id: data.OBJECT_ID, date: new Date(utc), fdmm: data.MEAN_MOTION_DOT, sdmm: data.MEAN_MOTION_DDOT, drag: data.BSTAR, ephemeris: data.EPHEMERIS_TYPE, esn: data.ELEMENT_SET_NO, inclination: data.INCLINATION, ascension: data.RA_OF_ASC_NODE, eccentricity: data.ECCENTRICITY, perigee: data.ARG_OF_PERICENTER, anomaly: data.MEAN_ANOMALY, motion: data.MEAN_MOTION, revolution: data.REV_AT_EPOCH, rms: parseFloat(data.RMS), }; } /** * Check TLE checksum * @param line - TLE line * @returns - TLE checksum */ function check(line) { let sum = 0; for (const char of line.substring(0, 68)) { // deno-lint-ignore no-explicit-any if (!isNaN(char)) sum += Number(char); else if (char === '-') sum++; } return sum % 10; } /** * Converts alpha5 to alpha2 * NOTE: Alpha5 skips I and O * @param str - alpha5 input string * @returns - alpha2 string */ function alpha5Converter(str) { const firstChar = str.charAt(0); // deno-lint-ignore no-explicit-any if (!isNaN(firstChar)) return str; const alpha5Index = 'ABCDEFGHJKLMNPQRSTUVWXYZ'.indexOf(firstChar); return String(alpha5Index + 10) + str.slice(1); } /** * Trim leading and trailing whitespace * @param str - string to trim * @returns - trimmed string */ function trim(str) { return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); } //# sourceMappingURL=sat.js.map