ootk-core
Version:
Orbital Object Toolkit. A modern typed replacement for satellite.js including SGP4 propagation, TLE parsing, Sun and Moon calculations, and more.
562 lines (493 loc) • 18.4 kB
text/typescript
/* eslint-disable require-jsdoc */
import { AngularDiameterMethod } from '../enums/AngularDiameterMethod.js';
import { AngularDistanceMethod } from '../enums/AngularDistanceMethod.js';
import { EcfVec3, Kilometers, Radians, SpaceObjectType } from '../types/types.js';
import { angularVelocityOfEarth, cKmPerSec } from './constants.js';
/**
* Calculates the factorial of a given number.
* @param n - The number to calculate the factorial for.
* @returns The factorial of the given number.
*/
export function factorial(n: number): number {
const nAbs = Math.abs(n);
let result = 1;
for (let i = 2; i <= nAbs; i++) {
result *= i;
}
return result;
}
/**
* Calculates the base 10 logarithm of a number.
* @param x - The number to calculate the logarithm for.
* @returns The base 10 logarithm of the input number.
*/
export function log10(x: number): number {
return Math.log(x) / Math.LN10;
}
/**
* Calculates the hyperbolic secant of a number.
* @param x - The number to calculate the hyperbolic secant of.
* @returns The hyperbolic secant of the given number.
*/
export function sech(x: number): number {
return 1 / Math.cosh(x);
}
/**
* Calculates the hyperbolic cosecant of a number.
* @param x - The number for which to calculate the hyperbolic cosecant.
* @returns The hyperbolic cosecant of the given number.
*/
export function csch(x: number): number {
return 1 / Math.sinh(x);
}
/**
* Returns the inverse hyperbolic cosecant of a number.
* @param x - The number to calculate the inverse hyperbolic cosecant of.
* @returns The inverse hyperbolic cosecant of the given number.
*/
export function acsch(x: number): number {
return Math.log(1 / x + Math.sqrt(1 / (x * x) + 1));
}
/**
* Calculates the inverse hyperbolic secant (asech) of a number.
* @param x - The number to calculate the inverse hyperbolic secant of.
* @returns The inverse hyperbolic secant of the given number.
*/
export function asech(x: number): number {
return Math.log(1 / x + Math.sqrt(1 / (x * x) - 1));
}
/**
* Calculates the inverse hyperbolic cotangent (acoth) of a number.
* @param x - The number to calculate the acoth of.
* @returns The inverse hyperbolic cotangent of the given number.
*/
export function acoth(x: number): number {
return 0.5 * Math.log((x + 1) / (x - 1));
}
/**
* Copies the sign of the second number to the first number.
* @param mag - The magnitude of the number.
* @param sgn - The sign of the number.
* @returns The number with the magnitude of `mag` and the sign of `sgn`.
*/
export function copySign(mag: number, sgn: number): number {
return Math.abs(mag) * Math.sign(sgn);
}
/**
* Evaluates a polynomial function at a given value.
* @param x - The value at which to evaluate the polynomial.
* @param coeffs - The coefficients of the polynomial.
* @returns The result of evaluating the polynomial at the given value.
*/
export function evalPoly(x: number, coeffs: Float64Array): number {
let result: number = coeffs[0] as number;
for (let i = 1; i < coeffs.length; i++) {
result = result * x + coeffs[i]!;
}
return result;
}
/**
* Concatenates two Float64Arrays into a new Float64Array.
* @param a - The first Float64Array.
* @param b - The second Float64Array.
* @returns A new Float64Array containing the concatenated values of `a` and `b`.
*/
export function concat(a: Float64Array, b: Float64Array): Float64Array {
const result = new Float64Array(a.length + b.length);
result.set(a);
result.set(b, a.length);
return result;
}
/**
* Calculates the angle in the half-plane that best matches the given angle.
* @param angle - The angle to be matched.
* @param match - The angle to be matched against.
* @returns The angle in the half-plane that best matches the given angle.
*/
export function matchHalfPlane(angle: number, match: number): number {
const a1 = angle;
const a2 = 2 * Math.PI - angle;
const d1 = Math.atan2(Math.sin(a1 - match), Math.cos(a1 - match));
const d2 = Math.atan2(Math.sin(a2 - match), Math.cos(a2 - match));
return Math.abs(d1) < Math.abs(d2) ? a1 : a2;
}
/**
* Wraps an angle to the range [-π, π].
* @param theta - The angle to wrap.
* @returns The wrapped angle.
*/
export function wrapAngle(theta: Radians): Radians {
const result = ((theta + Math.PI) % (2 * Math.PI)) - Math.PI;
if (result === -Math.PI) {
return Math.PI as Radians;
}
return result as Radians;
}
/**
* Calculates the angular distance between two points on a sphere using the cosine formula.
* @param lam1 - The longitude of the first point in radians.
* @param phi1 - The latitude of the first point in radians.
* @param lam2 - The longitude of the second point in radians.
* @param phi2 - The latitude of the second point in radians.
* @returns The angular distance between the two points in radians.
*/
function angularDistanceCosine_(lam1: number, phi1: number, lam2: number, phi2: number): Radians {
const a = Math.sin(phi1) * Math.sin(phi2);
const b = Math.cos(phi1) * Math.cos(phi2) * Math.cos(lam2 - lam1);
return Math.acos(a + b) as Radians;
}
/**
* Calculates the angular distance between two points on a sphere using the Haversine formula.
* @param lam1 - The longitude of the first point in radians.
* @param phi1 - The latitude of the first point in radians.
* @param lam2 - The longitude of the second point in radians.
* @param phi2 - The latitude of the second point in radians.
* @returns The angular distance between the two points in radians.
*/
function angularDistanceHaversine_(lam1: number, phi1: number, lam2: number, phi2: number): Radians {
const dlam = lam2 - lam1;
const dphi = phi2 - phi1;
const sdlam = Math.sin(0.5 * dlam);
const sdphi = Math.sin(0.5 * dphi);
const a = sdphi * sdphi + Math.cos(phi1) * Math.cos(phi2) * sdlam * sdlam;
return 2.0 * Math.asin(Math.min(1.0, Math.sqrt(a))) as Radians;
}
/**
* Calculates the angular distance between two points on a sphere.
* @param lam1 The longitude of the first point.
* @param phi1 The latitude of the first point.
* @param lam2 The longitude of the second point.
* @param phi2 The latitude of the second point.
* @param method The method to use for calculating the angular distance. Defaults to AngularDistanceMethod.Cosine.
* @returns The angular distance between the two points.
* @throws Error if an invalid angular distance method is provided.
*/
export function angularDistance(
lam1: number,
phi1: number,
lam2: number,
phi2: number,
method: AngularDistanceMethod = AngularDistanceMethod.Cosine,
): Radians {
switch (method) {
case AngularDistanceMethod.Cosine:
return angularDistanceCosine_(lam1, phi1, lam2, phi2);
case AngularDistanceMethod.Haversine:
return angularDistanceHaversine_(lam1, phi1, lam2, phi2);
default:
throw new Error('Invalid angular distance method.');
}
}
/**
* Calculates the angular diameter of an object.
* @param diameter - The diameter of the object.
* @param distance - The distance to the object.
* @param method - The method used to calculate the angular diameter. Defaults to AngularDiameterMethod.Sphere.
* @returns The angular diameter of the object.
* @throws Error if an invalid angular diameter method is provided.
*/
export function angularDiameter(
diameter: number,
distance: number,
method: AngularDiameterMethod = AngularDiameterMethod.Sphere,
): number {
switch (method) {
case AngularDiameterMethod.Circle:
return 2 * Math.atan(diameter / (2 * distance));
case AngularDiameterMethod.Sphere:
return 2 * Math.asin(diameter / (2 * distance));
default:
throw new Error('Invalid angular diameter method.');
}
}
/**
* Performs linear interpolation between two points.
* @param x - The x-coordinate to interpolate.
* @param x0 - The x-coordinate of the first point.
* @param y0 - The y-coordinate of the first point.
* @param x1 - The x-coordinate of the second point.
* @param y1 - The y-coordinate of the second point.
* @returns The interpolated y-coordinate corresponding to the given x-coordinate.
*/
export function linearInterpolate(x: number, x0: number, y0: number, x1: number, y1: number): number {
return (y0 * (x1 - x) + y1 * (x - x0)) / (x1 - x0);
}
/**
* Calculates the mean value of an array of numbers.
* @param values - The array of numbers.
* @returns The mean value of the numbers.
*/
export function mean(values: number[]): number {
const n = values.length;
let sum = 0.0;
for (const v of values) {
sum += v;
}
return sum / n;
}
/**
* Calculates the standard deviation of an array of numbers.
* @param values - The array of numbers.
* @param isSample - Optional. Specifies whether the array represents a sample. Default is false.
* @returns The standard deviation of the array.
*/
export function std(values: number[], isSample = false): number {
const mu = mean(values);
const n = values.length;
let sum = 0.0;
for (const v of values) {
const sub = v - mu;
sum += sub * sub;
}
const m = isSample ? 1 : 0;
return Math.sqrt((1.0 / (n - m)) * sum);
}
/**
* Calculates the covariance between two arrays.
* @param a - The first array.
* @param b - The second array.
* @param isSample - Optional. Specifies whether the arrays represent a sample. Default is false.
* @returns The covariance between the two arrays.
*/
export function covariance(a: number[], b: number[], isSample = false): number {
const n = a.length;
const am = mean(a);
const bm = mean(b);
let result = 0.0;
for (let i = 0; i < n; i++) {
result += (a[i]! - am) * (b[i]! - bm);
}
const m = isSample ? 1 : 0;
return result / (n - m);
}
/**
* Calculates the gamma function of a number.
* @param n - The input number.
* @returns The gamma function value.
*/
export function gamma(n: number): number {
return factorial(n - 1);
}
/**
* Calculates the eccentric anomaly (e0) and true anomaly (nu) using Newton's method
* for a given eccentricity (ecc) and mean anomaly (m).
* @param ecc - The eccentricity of the orbit.
* @param m - The mean anomaly.
* @returns An object containing the eccentric anomaly (e0) and true anomaly (nu).
*/
export function newtonM(ecc: number, m: number): { e0: number; nu: number } {
const numiter = 50;
const small = 1e-8;
let e0;
let nu;
if (ecc > small) {
if ((m < 0.0 && m > -Math.PI) || m > Math.PI) {
e0 = m - ecc;
} else {
e0 = m + ecc;
}
let ktr = 1;
let e1 = e0 + (m - e0 + ecc * Math.sin(e0)) / (1.0 - ecc * Math.cos(e0));
while (Math.abs(e1 - e0) > small && ktr <= numiter) {
ktr++;
e0 = e1;
e1 = e0 + (m - e0 + ecc * Math.sin(e0)) / (1.0 - ecc * Math.cos(e0));
}
const sinv = (Math.sqrt(1.0 - ecc * ecc) * Math.sin(e1)) / (1.0 - ecc * Math.cos(e1));
const cosv = (Math.cos(e1) - ecc) / (1.0 - ecc * Math.cos(e1));
nu = Math.atan2(sinv, cosv);
} else {
nu = m;
e0 = m;
}
return { e0, nu };
}
/**
* Calculates the eccentric anomaly (e0) and mean anomaly (m) using Newton's method
* for a given eccentricity (ecc) and true anomaly (nu).
* @param ecc - The eccentricity of the orbit.
* @param nu - The true anomaly.
* @returns An object containing the calculated eccentric anomaly (e0) and mean anomaly (m).
*/
export function newtonNu(ecc: number, nu: number): { e0: number; m: Radians } {
const small = 1e-8;
let e0 = 0.0;
let m = 0.0;
if (Math.abs(ecc) < small) {
m = nu;
e0 = nu;
} else if (ecc < 1.0 - small) {
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);
}
if (ecc < 1.0) {
m -= Math.floor(m / (2 * Math.PI)) * (2 * Math.PI);
if (m < 0.0) {
m += 2.0 * Math.PI;
}
e0 -= Math.floor(e0 / (2 * Math.PI)) * (2 * Math.PI);
}
return { e0, m: m as Radians };
}
/**
* Creates a 2D array with the specified number of rows and columns, filled with the same given value.
* @template T The type of elements in the array.
* @param rows The number of rows in the 2D array.
* @param columns The number of columns in the 2D array.
* @param value The value to fill the array with.
* @returns The 2D array with the specified number of rows and columns, filled with the given value.
*/
export function array2d<T>(rows: number, columns: number, value: T): T[][] {
const output: T[][] = [];
for (let i = 0; i < rows; i++) {
output.push(Array(columns).fill(value));
}
return output;
}
/**
* Clamps a number between a minimum and maximum value.
* @param x The number to clamp.
* @param min The minimum value.
* @param max The maximum value.
* @returns The clamped number.
*/
export function clamp(x: number, min: number, max: number): number {
return Math.max(min, Math.min(x, max));
}
/**
* Determines whether a given year is a leap year.
* @param dateIn The date to check.
* @returns `true` if the year is a leap year, `false` otherwise.
*/
export function isLeapYear(dateIn: Date): boolean {
const year = dateIn.getUTCFullYear();
if ((year & 3) !== 0) {
return false;
}
return year % 100 !== 0 || year % 400 === 0;
}
/**
* An array of the day of the year for each month.
*/
type MonthDaysArray = [number, number, number, number, number, number, number, number, number, number, number, number];
/**
* Calculates the day of the year for a given date.
* If no date is provided, the current date is used.
*
* This is sometimes referred to as the Jday, but is
* very different from the Julian day used in astronomy.
* @param date - The date for which to calculate the day of the year.
* @returns The day of the year as a number.
*/
export function getDayOfYear(date = new Date()): number {
const dayCount = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334] as MonthDaysArray;
const mn = date.getUTCMonth() as 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
const dn = date.getUTCDate();
let dayOfYear = dayCount[mn] + dn;
if (mn > 1 && isLeapYear(date)) {
dayOfYear++;
}
return dayOfYear;
}
/**
* Rounds a number to a specified number of decimal places.
* @param value - The number to round.
* @param places - The number of decimal places to round to.
* @returns The rounded number.
*/
export function toPrecision(value: number, places: number): number {
return Number(value.toFixed(places));
}
/**
* Returns the sign of a number.
* @param value - The number to determine the sign of.
* @returns 1 if the number is positive, -1 if the number is negative.
*/
export function sign(value: number) {
return value >= 0 ? 1 : -1;
}
const spaceObjTypeStrMap_ = {
[SpaceObjectType.UNKNOWN]: 'Unknown',
[SpaceObjectType.PAYLOAD]: 'Payload',
[SpaceObjectType.ROCKET_BODY]: 'Rocket Body',
[SpaceObjectType.DEBRIS]: 'Debris',
[SpaceObjectType.SPECIAL]: 'Special',
[SpaceObjectType.BALLISTIC_MISSILE]: 'Ballistic Missile',
[SpaceObjectType.STAR]: 'Star',
[SpaceObjectType.INTERGOVERNMENTAL_ORGANIZATION]: 'Intergovernmental Organization',
[SpaceObjectType.SUBORBITAL_PAYLOAD_OPERATOR]: 'Suborbital Payload Operator',
[SpaceObjectType.PAYLOAD_OWNER]: 'Payload Owner',
[SpaceObjectType.METEOROLOGICAL_ROCKET_LAUNCH_AGENCY_OR_MANUFACTURER]:
'Meteorological Rocket Launch Agency or Manufacturer',
[SpaceObjectType.PAYLOAD_MANUFACTURER]: 'Payload Manufacturer',
[SpaceObjectType.LAUNCH_AGENCY]: 'Launch Agency',
[SpaceObjectType.LAUNCH_SITE]: 'Launch Site',
[SpaceObjectType.LAUNCH_POSITION]: 'Launch Position',
[SpaceObjectType.LAUNCH_FACILITY]: 'Launch Facility',
[SpaceObjectType.CONTROL_FACILITY]: 'Control Facility',
[SpaceObjectType.GROUND_SENSOR_STATION]: 'Ground Sensor Station',
[SpaceObjectType.OPTICAL]: 'Optical',
[SpaceObjectType.MECHANICAL]: 'Mechanical',
[SpaceObjectType.PHASED_ARRAY_RADAR]: 'Phased Array Radar',
[SpaceObjectType.OBSERVER]: 'Observer',
[SpaceObjectType.BISTATIC_RADIO_TELESCOPE]: 'Bi-static Radio Telescope',
[SpaceObjectType.COUNTRY]: 'Country',
[SpaceObjectType.LAUNCH_VEHICLE_MANUFACTURER]: 'Launch Vehicle Manufacturer',
[SpaceObjectType.ENGINE_MANUFACTURER]: 'Engine Manufacturer',
[SpaceObjectType.NOTIONAL]: 'Notional',
[SpaceObjectType.FRAGMENT]: 'Fragment',
[SpaceObjectType.SHORT_TERM_FENCE]: 'Short Term Fence',
[SpaceObjectType.MAX_SPACE_OBJECT_TYPE]: 'Max Space Object Type',
};
/**
* Converts a SpaceObjectType to a string representation.
* @param spaceObjType - The SpaceObjectType to convert.
* @returns The string representation of the SpaceObjectType.
*/
export const spaceObjType2Str = (spaceObjType: SpaceObjectType): string =>
spaceObjTypeStrMap_[spaceObjType] || 'Unknown';
/**
* Calculates the Doppler factor for a given location, position, and velocity.
* The Doppler factor is a measure of the change in frequency or wavelength of a wave
* as observed by an observer moving relative to the source of the wave.
* @param location - The location vector of the observer.
* @param position - The position vector of the source.
* @param velocity - The velocity vector of the source.
* @returns The calculated Doppler factor.
*/
export const dopplerFactor = (
location: EcfVec3<Kilometers>,
position: EcfVec3<Kilometers>,
velocity: EcfVec3<Kilometers>,
): number => {
const range = <EcfVec3>{
x: position.x - location.x,
y: position.y - location.y,
z: position.z - location.z,
};
const distance = Math.sqrt(range.x ** 2 + range.y ** 2 + range.z ** 2);
const rangeVel = <EcfVec3>{
x: velocity.x + angularVelocityOfEarth * location.y,
y: velocity.y - angularVelocityOfEarth * location.x,
z: velocity.z,
};
const rangeRate = (range.x * rangeVel.x + range.y * rangeVel.y + range.z * rangeVel.z) / distance;
const dopplerFactor = 1 - rangeRate / cKmPerSec;
return dopplerFactor;
};
/**
* Creates an array of numbers from start to stop (inclusive) with the specified step.
* @param start The starting number.
* @param stop The ending number.
* @param step The step value.
* @returns An array of numbers.
*/
export function createVec(start: number, stop: number, step: number): number[] {
const array = [] as number[];
for (let i = start; i <= stop; i += step) {
array.push(i);
}
return array;
}