@saber-usa/node-common 1.7.3 → 1.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -41
- package/package.json +51 -51
- package/src/FrameConverter.js +1120 -1120
- package/src/LLA.js +179 -179
- package/src/LaunchNominalClass.js +774 -774
- package/src/NodeVector3D.js +71 -71
- package/src/OrbitUtils.js +490 -490
- package/src/PropagateUtils.js +100 -100
- package/src/ShadowGEOCalculator.js +203 -203
- package/src/TimeConverter.js +309 -309
- package/src/astro.js +3217 -3214
- package/src/ballisticPropagator.js +1037 -1037
- package/src/checkNetwork.cjs +20 -20
- package/src/constants.js +30 -30
- package/src/fixDate.js +62 -62
- package/src/index.js +47 -47
- package/src/launchNominal.js +208 -208
- package/src/loggerFactory.cjs +98 -98
- package/src/s3.js +59 -59
- package/src/transform.js +35 -35
- package/src/udl.js +116 -116
- package/src/utils.js +406 -406
package/src/OrbitUtils.js
CHANGED
|
@@ -1,490 +1,490 @@
|
|
|
1
|
-
import {MU} from "./constants.js";
|
|
2
|
-
import {cross, norm} from "mathjs";
|
|
3
|
-
import {wrapHalfRevUnsigned, wrapOneRevUnsigned} from "./utils.js";
|
|
4
|
-
|
|
5
|
-
const EARTH_MU_KM = MU / 1e9;
|
|
6
|
-
|
|
7
|
-
class OrbitUtils {
|
|
8
|
-
constructor() {
|
|
9
|
-
// Prevent instantiation
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Get inclination based on azimuth and latitude.
|
|
13
|
-
* @param {number} azimuthRad - Azimuth in radians
|
|
14
|
-
* @param {number} latitudeRad - Latitude in radians
|
|
15
|
-
* @return {number} Inclination in radians
|
|
16
|
-
*/
|
|
17
|
-
static getInclination(azimuthRad, latitudeRad) {
|
|
18
|
-
const inclination = Math.acos(Math.cos(latitudeRad) * Math.sin(azimuthRad));
|
|
19
|
-
return wrapHalfRevUnsigned(inclination);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Get the azimuth based on inclination and latitude.
|
|
24
|
-
* Two results will be possible, based on whether you are Ascending from South to North or Descending from North to South.
|
|
25
|
-
* @param {number} inclinationRad - Inclination in radians
|
|
26
|
-
* @param {number} latitudeRad - Latitude in radians
|
|
27
|
-
* @param {boolean} ascending - Ascending (true) or descending (false)
|
|
28
|
-
* @return {number} Azimuth in radians
|
|
29
|
-
*/
|
|
30
|
-
static getAzimuth(inclinationRad, latitudeRad, ascending = true) {
|
|
31
|
-
const azimuth = Math.asin(Math.cos(inclinationRad) / Math.cos(latitudeRad));
|
|
32
|
-
if (!ascending) {
|
|
33
|
-
return wrapOneRevUnsigned(Math.PI - azimuth);
|
|
34
|
-
}
|
|
35
|
-
return wrapOneRevUnsigned(azimuth);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Calculate the specific angular momentum (h) vector of an orbit.
|
|
40
|
-
* @param {Array} r - Position vector [x, y, z]
|
|
41
|
-
* @param {Array} v - Velocity vector [vx, vy, vz]
|
|
42
|
-
* @return {Array} Angular momentum vector
|
|
43
|
-
*/
|
|
44
|
-
static getAngularMomentum(r, v) {
|
|
45
|
-
return cross(r, v);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Get mean motion (rad/sec).
|
|
50
|
-
* @param {number} a - Semi Major Axis (km)
|
|
51
|
-
* @param {number} mu - Standard gravitational parameter (km^3/s^2)
|
|
52
|
-
* @return {number} Mean motion in rad/sec
|
|
53
|
-
*/
|
|
54
|
-
static getMeanMotion(a, mu = EARTH_MU_KM) {
|
|
55
|
-
return Math.sqrt(mu / Math.pow(a, 3));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Get the period of an orbit in seconds.
|
|
60
|
-
* @param {number} a - Semi Major Axis (km)
|
|
61
|
-
* @param {number} mu - Standard gravitational parameter (km^3/s^2)
|
|
62
|
-
* @return {number} Period in seconds
|
|
63
|
-
*/
|
|
64
|
-
static getPeriod(a, mu = EARTH_MU_KM) {
|
|
65
|
-
if (a <= 0) return null;
|
|
66
|
-
return 2 * Math.PI * Math.sqrt(Math.pow(a, 3) / mu);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Transform an orbit to elliptical with target parameters using Brent's method.
|
|
71
|
-
* Accurate port of the C# TransformElliptical method.
|
|
72
|
-
* @param {Object} svInitial - Initial state vector {position: [x,y,z], velocity: [vx,vy,vz], epoch: Date}
|
|
73
|
-
* @param {number} incRad - Inclination in radians
|
|
74
|
-
* @param {number} raanRad - RAAN in radians
|
|
75
|
-
* @param {number} targetEcc - Target eccentricity
|
|
76
|
-
* @param {number} targetArgOfPeriapsisRad - Target argument of periapsis in radians
|
|
77
|
-
* @param {number} targetPerigeeKm - Target perigee in km
|
|
78
|
-
* @return {Object} Satellite object with new orbital elements
|
|
79
|
-
*/
|
|
80
|
-
static transformElliptical(svInitial, incRad, raanRad,
|
|
81
|
-
targetEcc, targetArgOfPeriapsisRad, targetPerigeeKm) {
|
|
82
|
-
// Normalize the position vector
|
|
83
|
-
const rNorm = norm(svInitial.position);
|
|
84
|
-
const r = svInitial.position.map((x) => x / rNorm);
|
|
85
|
-
|
|
86
|
-
// Project the position vector onto the orbital plane
|
|
87
|
-
const projectedX = r[0] * Math.cos(raanRad) + r[1] * Math.sin(raanRad);
|
|
88
|
-
const projectedY = r[1] * Math.cos(raanRad) - r[0] * Math.sin(raanRad);
|
|
89
|
-
|
|
90
|
-
// Calculate the initial true anomaly using the projected coordinates and known argument of periapsis
|
|
91
|
-
let nu0 = Math.atan2(projectedY, projectedX) - targetArgOfPeriapsisRad;
|
|
92
|
-
nu0 = (nu0 + 2 * Math.PI) % (2 * Math.PI);
|
|
93
|
-
|
|
94
|
-
// Define the objective function that we want to minimize
|
|
95
|
-
const objectiveFunction = (nu) => {
|
|
96
|
-
nu = (nu + 2 * Math.PI) % (2 * Math.PI);
|
|
97
|
-
|
|
98
|
-
// Create orbital elements with the trial true anomaly
|
|
99
|
-
const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
|
|
100
|
-
const elements = {
|
|
101
|
-
semiMajorAxis: semiMajorAxis,
|
|
102
|
-
eccentricity: targetEcc,
|
|
103
|
-
inclination: incRad,
|
|
104
|
-
raan: raanRad,
|
|
105
|
-
argOfPeriapsis: targetArgOfPeriapsisRad,
|
|
106
|
-
trueAnomaly: nu,
|
|
107
|
-
epoch: svInitial.epoch,
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// Convert elements to state vector
|
|
111
|
-
const trialState = this.elementsToStateVector(elements);
|
|
112
|
-
|
|
113
|
-
// Normalize the trial position vector
|
|
114
|
-
const trialRNorm = norm(trialState.position);
|
|
115
|
-
const trialR = trialState.position.map((x) => x / trialRNorm);
|
|
116
|
-
|
|
117
|
-
// Scale trial position to match initial position magnitude
|
|
118
|
-
const pScaled = trialR.map((x) => x * rNorm);
|
|
119
|
-
|
|
120
|
-
// Return the distance between initial and trial positions
|
|
121
|
-
return norm(svInitial.position.map((x, i) => x - pScaled[i]));
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// Use Brent's method for minimization
|
|
125
|
-
const tolerance = 1e-6; // radians
|
|
126
|
-
const maxIterations = 1000;
|
|
127
|
-
|
|
128
|
-
// Initial bracket around the solution
|
|
129
|
-
let a = nu0 - Math.PI;
|
|
130
|
-
let b = nu0 + Math.PI;
|
|
131
|
-
let c = nu0;
|
|
132
|
-
|
|
133
|
-
let fa = objectiveFunction(a);
|
|
134
|
-
let fb = objectiveFunction(b);
|
|
135
|
-
let fc = objectiveFunction(c);
|
|
136
|
-
|
|
137
|
-
let d = 0; let e = 0; let min1; let min2;
|
|
138
|
-
let p; let q; let r1; let tol1; let xm;
|
|
139
|
-
|
|
140
|
-
for (let iter = 0; iter < maxIterations; iter++) {
|
|
141
|
-
if (Math.abs(b - a) < tolerance) {
|
|
142
|
-
// Found minimum
|
|
143
|
-
const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
|
|
144
|
-
return {
|
|
145
|
-
semiMajorAxis: semiMajorAxis,
|
|
146
|
-
eccentricity: targetEcc,
|
|
147
|
-
inclination: incRad,
|
|
148
|
-
raan: raanRad,
|
|
149
|
-
argOfPeriapsis: targetArgOfPeriapsisRad,
|
|
150
|
-
trueAnomaly: c,
|
|
151
|
-
epoch: svInitial.epoch,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
xm = 0.5 * (a + b);
|
|
156
|
-
tol1 = tolerance * Math.abs(c) + 1e-10;
|
|
157
|
-
min1 = Math.abs(c - a);
|
|
158
|
-
min2 = Math.abs(b - c);
|
|
159
|
-
|
|
160
|
-
if (min1 < tol1 || min2 < tol1) {
|
|
161
|
-
// Found minimum
|
|
162
|
-
const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
|
|
163
|
-
return {
|
|
164
|
-
semiMajorAxis: semiMajorAxis,
|
|
165
|
-
eccentricity: targetEcc,
|
|
166
|
-
inclination: incRad,
|
|
167
|
-
raan: raanRad,
|
|
168
|
-
argOfPeriapsis: targetArgOfPeriapsisRad,
|
|
169
|
-
trueAnomaly: c,
|
|
170
|
-
epoch: svInitial.epoch,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Construct a trial parabolic fit
|
|
175
|
-
if (Math.abs(e) >= tol1) {
|
|
176
|
-
r1 = (c - a) * (fb - fc);
|
|
177
|
-
q = (c - b) * (fa - fc);
|
|
178
|
-
p = (c - b) * q - (c - a) * r1;
|
|
179
|
-
q = 2.0 * (q - r1);
|
|
180
|
-
if (q > 0.0) p = -p;
|
|
181
|
-
q = Math.abs(q);
|
|
182
|
-
min1 = 3.0 * q * Math.abs(xm);
|
|
183
|
-
min2 = Math.abs(e * q);
|
|
184
|
-
if (2.0 * p < (min1 < min2 ? min1 : min2)) {
|
|
185
|
-
e = d;
|
|
186
|
-
d = p / q;
|
|
187
|
-
} else {
|
|
188
|
-
d = xm;
|
|
189
|
-
e = d;
|
|
190
|
-
}
|
|
191
|
-
} else {
|
|
192
|
-
d = xm;
|
|
193
|
-
e = d;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
a = b;
|
|
197
|
-
fa = fb;
|
|
198
|
-
if (Math.abs(d) > tol1) {
|
|
199
|
-
b += d;
|
|
200
|
-
} else {
|
|
201
|
-
b += (xm >= 0 ? Math.abs(tol1) : -Math.abs(tol1));
|
|
202
|
-
}
|
|
203
|
-
fb = objectiveFunction(b);
|
|
204
|
-
if ((fb <= fc && c > b) || (fb >= fc && c < b)) {
|
|
205
|
-
c = a;
|
|
206
|
-
fc = fa;
|
|
207
|
-
a = b;
|
|
208
|
-
fa = fb;
|
|
209
|
-
b = c;
|
|
210
|
-
fb = fc;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
throw new Error("Did not converge within maximum iterations");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Convert orbital elements to state vector
|
|
219
|
-
* @param {Object} elements - Orbital elements
|
|
220
|
-
* @return {Object} State vector {position, velocity}
|
|
221
|
-
*/
|
|
222
|
-
static elementsToStateVector(elements) {
|
|
223
|
-
const {semiMajorAxis, eccentricity, inclination,
|
|
224
|
-
raan, argOfPeriapsis, trueAnomaly} = elements;
|
|
225
|
-
const mu = EARTH_MU_KM;
|
|
226
|
-
|
|
227
|
-
// Calculate radius
|
|
228
|
-
const rMag = semiMajorAxis * (1 - eccentricity * eccentricity)
|
|
229
|
-
/ (1 + eccentricity * Math.cos(trueAnomaly));
|
|
230
|
-
|
|
231
|
-
// Position in orbital plane
|
|
232
|
-
const rOrbital = [
|
|
233
|
-
rMag * Math.cos(trueAnomaly),
|
|
234
|
-
rMag * Math.sin(trueAnomaly),
|
|
235
|
-
0,
|
|
236
|
-
];
|
|
237
|
-
|
|
238
|
-
// Velocity in orbital plane
|
|
239
|
-
const h = Math.sqrt(mu * semiMajorAxis * (1 - eccentricity * eccentricity));
|
|
240
|
-
const vOrbital = [
|
|
241
|
-
-mu / h * Math.sin(trueAnomaly),
|
|
242
|
-
mu / h * (eccentricity + Math.cos(trueAnomaly)),
|
|
243
|
-
0,
|
|
244
|
-
];
|
|
245
|
-
|
|
246
|
-
// Rotation matrices
|
|
247
|
-
const cosW = Math.cos(argOfPeriapsis);
|
|
248
|
-
const sinW = Math.sin(argOfPeriapsis);
|
|
249
|
-
const cosO = Math.cos(raan);
|
|
250
|
-
const sinO = Math.sin(raan);
|
|
251
|
-
const cosI = Math.cos(inclination);
|
|
252
|
-
const sinI = Math.sin(inclination);
|
|
253
|
-
|
|
254
|
-
// Transform to inertial frame
|
|
255
|
-
const r = [
|
|
256
|
-
rOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
|
|
257
|
-
- rOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
|
|
258
|
-
rOrbital[0] * (cosW * sinO + sinW * cosI * cosO) + rOrbital[1]
|
|
259
|
-
* (cosW * cosI * cosO - sinW * sinO),
|
|
260
|
-
rOrbital[0] * (sinW * sinI) + rOrbital[1] * (cosW * sinI),
|
|
261
|
-
];
|
|
262
|
-
|
|
263
|
-
const v = [
|
|
264
|
-
vOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
|
|
265
|
-
- vOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
|
|
266
|
-
vOrbital[0] * (cosW * sinO + sinW * cosI * cosO)
|
|
267
|
-
+ vOrbital[1] * (cosW * cosI * cosO - sinW * sinO),
|
|
268
|
-
vOrbital[0] * (sinW * sinI) + vOrbital[1] * (cosW * sinI),
|
|
269
|
-
];
|
|
270
|
-
|
|
271
|
-
return {position: r, velocity: v};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Convert state vector (position, velocity) to classical orbital elements
|
|
276
|
-
* @param {Array} r - Position vector [x, y, z] in km
|
|
277
|
-
* @param {Array} v - Velocity vector [vx, vy, vz] in km/s
|
|
278
|
-
* @param {number} mu - Standard gravitational parameter (default: Earth)
|
|
279
|
-
* @return {Object} Orbital elements
|
|
280
|
-
*/
|
|
281
|
-
static stateVectorToElements(r, v, mu = EARTH_MU_KM) {
|
|
282
|
-
const tol = 1e-9;
|
|
283
|
-
|
|
284
|
-
if (mu < 1e-30) {
|
|
285
|
-
throw new Error("Mu must be greater than 1e-30.");
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Helper functions
|
|
289
|
-
function cross(a, b) {
|
|
290
|
-
return [
|
|
291
|
-
a[1] * b[2] - a[2] * b[1],
|
|
292
|
-
a[2] * b[0] - a[0] * b[2],
|
|
293
|
-
a[0] * b[1] - a[1] * b[0],
|
|
294
|
-
];
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function dot(a, b) {
|
|
298
|
-
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function norm(v) {
|
|
302
|
-
return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function clamp(value, min, max) {
|
|
306
|
-
return Math.min(Math.max(value, min), max);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Calculate basic vectors
|
|
310
|
-
const h = cross(r, v);
|
|
311
|
-
const n = cross([0, 0, 1], h);
|
|
312
|
-
|
|
313
|
-
const rLength = norm(r);
|
|
314
|
-
const vLength = norm(v);
|
|
315
|
-
|
|
316
|
-
if (rLength === 0) throw new Error("Position vector must not be zero.");
|
|
317
|
-
if (vLength === 0) throw new Error("Velocity vector must not be zero.");
|
|
318
|
-
|
|
319
|
-
// Eccentricity vector calculation (corrected formula)
|
|
320
|
-
const vLengthSq = vLength * vLength;
|
|
321
|
-
const muOverR = mu / rLength;
|
|
322
|
-
const rvDot = dot(r, v);
|
|
323
|
-
|
|
324
|
-
const e = [
|
|
325
|
-
(1 / mu) * ((vLengthSq - muOverR) * r[0] - rvDot * v[0]),
|
|
326
|
-
(1 / mu) * ((vLengthSq - muOverR) * r[1] - rvDot * v[1]),
|
|
327
|
-
(1 / mu) * ((vLengthSq - muOverR) * r[2] - rvDot * v[2]),
|
|
328
|
-
];
|
|
329
|
-
|
|
330
|
-
const zeta = 0.5 * vLengthSq - muOverR;
|
|
331
|
-
|
|
332
|
-
if (zeta === 0) throw new Error("Zeta cannot be zero.");
|
|
333
|
-
|
|
334
|
-
const eLength = norm(e);
|
|
335
|
-
if (Math.abs(1.0 - eLength) <= tol) {
|
|
336
|
-
throw new Error("Parabolic orbit conversion is not supported.");
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const a = -mu / (zeta * 2);
|
|
340
|
-
|
|
341
|
-
if (Math.abs(a * (1 - eLength)) < 1e-3) {
|
|
342
|
-
throw new Error("The state results in a singular conic section "
|
|
343
|
-
+ "with radius of periapsis less than 1 m.");
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const hLength = norm(h);
|
|
347
|
-
if (hLength === 0) {
|
|
348
|
-
throw new Error(`Cannot convert from Cartesian to Keplerian
|
|
349
|
-
- angular momentum is zero.`);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const i = Math.acos(h[2] / hLength);
|
|
353
|
-
|
|
354
|
-
if (i >= Math.PI - tol) {
|
|
355
|
-
throw new Error("Cannot convert orbit with inclination of 180 degrees.");
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
let raan = 0;
|
|
359
|
-
let w = 0;
|
|
360
|
-
let f = 0;
|
|
361
|
-
|
|
362
|
-
const nLength = norm(n);
|
|
363
|
-
|
|
364
|
-
// CASE 1: Non-circular, Inclined Orbit
|
|
365
|
-
if (eLength >= 1e-11 && i >= 1e-11 && i <= Math.PI - 1e-11) {
|
|
366
|
-
if (nLength === 0.0) {
|
|
367
|
-
throw new Error("Cannot convert from Cartesian to Keplerian "
|
|
368
|
-
+ "- line-of-nodes vector is a zero vector.");
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
raan = Math.acos(n[0] / nLength);
|
|
372
|
-
if (n[1] < 0) raan = 2 * Math.PI - raan;
|
|
373
|
-
|
|
374
|
-
w = Math.acos(dot(n, e) / (nLength * eLength));
|
|
375
|
-
if (e[2] < 0) w = 2 * Math.PI - w;
|
|
376
|
-
|
|
377
|
-
f = Math.acos(clamp(dot(e, r) / (eLength * rLength), -1, 1));
|
|
378
|
-
if (rvDot < 0) f = 2 * Math.PI - f;
|
|
379
|
-
} else if (eLength >= 1e-11 && (i < 1e-11 || i > Math.PI - 1e-11)) { // CASE 2: Non-circular, Equatorial Orbit
|
|
380
|
-
if (eLength === 0.0) {
|
|
381
|
-
throw new Error(`Cannot convert from Cartesian to Keplerian
|
|
382
|
-
- eccentricity is zero.`);
|
|
383
|
-
}
|
|
384
|
-
raan = 0;
|
|
385
|
-
w = Math.acos(e[0] / eLength);
|
|
386
|
-
if (e[1] < 0) w = 2 * Math.PI - w;
|
|
387
|
-
|
|
388
|
-
// For GMT-4446 fix (LOJ: 2014.03.21)
|
|
389
|
-
if (i > Math.PI - 1e-11) w *= -1.0;
|
|
390
|
-
if (w < 0.0) w += 2 * Math.PI;
|
|
391
|
-
|
|
392
|
-
f = Math.acos(clamp(dot(e, r) / (eLength * rLength), -1, 1));
|
|
393
|
-
if (rvDot < 0) f = 2 * Math.PI - f;
|
|
394
|
-
} else if (eLength < 1e-11 && i >= 1e-11 && i <= Math.PI - 1e-11) { // CASE 3: Circular, Inclined Orbit
|
|
395
|
-
if (nLength === 0.0) {
|
|
396
|
-
throw new Error("Cannot convert from Cartesian to Keplerian "
|
|
397
|
-
+ "- line-of-nodes vector is a zero vector.");
|
|
398
|
-
}
|
|
399
|
-
raan = Math.acos(n[0] / nLength);
|
|
400
|
-
if (n[1] < 0) raan = 2 * Math.PI - raan;
|
|
401
|
-
|
|
402
|
-
w = 0;
|
|
403
|
-
|
|
404
|
-
f = Math.acos(clamp(dot(n, r) / (nLength * rLength), -1, 1));
|
|
405
|
-
if (r[2] < 0) f = 2 * Math.PI - f;
|
|
406
|
-
} else if (eLength < 1e-11 && (i < 1e-11 || i > Math.PI - 1e-11)) { // CASE 4: Circular, Equatorial Orbit
|
|
407
|
-
raan = 0;
|
|
408
|
-
w = 0;
|
|
409
|
-
f = Math.acos(clamp(r[0] / rLength, -1, 1));
|
|
410
|
-
if (r[1] < 0) f = 2 * Math.PI - f;
|
|
411
|
-
|
|
412
|
-
// For GMT-4446 fix (LOJ: 2014.03.21)
|
|
413
|
-
if (i > Math.PI - 1e-11) f *= -1.0;
|
|
414
|
-
if (f < 0.0) f += 2 * Math.PI;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Calculate additional orbital parameters
|
|
418
|
-
const period = 2 * Math.PI * Math.sqrt(Math.pow(Math.abs(a), 3) / mu);
|
|
419
|
-
const meanMotion = 2 * Math.PI / period;
|
|
420
|
-
|
|
421
|
-
// Convert true anomaly to mean anomaly properly
|
|
422
|
-
const eccentricAnomaly = this.trueAnomalyToEccentricAnomaly(f, eLength);
|
|
423
|
-
const meanAnomaly = this.eccentricAnomalyToMeanAnomaly(eccentricAnomaly, eLength);
|
|
424
|
-
|
|
425
|
-
return {
|
|
426
|
-
semiMajorAxis: a, // km
|
|
427
|
-
eccentricity: eLength, // dimensionless
|
|
428
|
-
inclination: i, // radians
|
|
429
|
-
raan: raan, // radians (Right Ascension of Ascending Node)
|
|
430
|
-
argOfPeriapsis: w, // radians (Argument of Periapsis)
|
|
431
|
-
trueAnomaly: f, // radians
|
|
432
|
-
meanAnomaly: meanAnomaly, // radians (simplified)
|
|
433
|
-
period: period, // seconds
|
|
434
|
-
meanMotion: meanMotion, // rad/s
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Convert true anomaly to eccentric anomaly
|
|
440
|
-
* @param {number} nu - true anomaly
|
|
441
|
-
* @param {number} e - eccentricity
|
|
442
|
-
* @return {number} Eccentric Anomaly
|
|
443
|
-
*/
|
|
444
|
-
static trueAnomalyToEccentricAnomaly(nu, e) {
|
|
445
|
-
const cosE = (e + Math.cos(nu)) / (1 + e * Math.cos(nu));
|
|
446
|
-
const sinE = Math.sqrt(1 - e * e) * Math.sin(nu) / (1 + e * Math.cos(nu));
|
|
447
|
-
return Math.atan2(sinE, cosE);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Convert eccentric anomaly to mean anomaly
|
|
452
|
-
* @param {number} E - Eccentric anomaly
|
|
453
|
-
* @param {number} e - eccentricity
|
|
454
|
-
* @return {number} Mean Anomaly
|
|
455
|
-
*/
|
|
456
|
-
static eccentricAnomalyToMeanAnomaly(E, e) {
|
|
457
|
-
return E - e * Math.sin(E);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Convert mean anomaly to eccentric anomaly (Kepler's equation)
|
|
462
|
-
* @param {number} M - Mean anomaly
|
|
463
|
-
* @param {number} e - eccentricity
|
|
464
|
-
* @param {number} tolerance - numerical tolerance
|
|
465
|
-
* @return {number} Eccentric Anomaly
|
|
466
|
-
*/
|
|
467
|
-
static meanAnomalyToEccentricAnomaly(M, e, tolerance = 1e-8) {
|
|
468
|
-
let E = M;
|
|
469
|
-
let delta = 1;
|
|
470
|
-
while (Math.abs(delta) > tolerance) {
|
|
471
|
-
delta = (M - E + e * Math.sin(E)) / (1 - e * Math.cos(E));
|
|
472
|
-
E += delta;
|
|
473
|
-
}
|
|
474
|
-
return E;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Convert eccentric anomaly to true anomaly
|
|
479
|
-
* @param {number} E - Eccentric anomaly
|
|
480
|
-
* @param {number} e - eccentricity
|
|
481
|
-
* @return {number} true anomaly
|
|
482
|
-
*/
|
|
483
|
-
static eccentricAnomalyToTrueAnomaly(E, e) {
|
|
484
|
-
const cosNu = (Math.cos(E) - e) / (1 - e * Math.cos(E));
|
|
485
|
-
const sinNu = Math.sqrt(1 - e * e) * Math.sin(E) / (1 - e * Math.cos(E));
|
|
486
|
-
return Math.atan2(sinNu, cosNu);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
export {OrbitUtils};
|
|
1
|
+
import {MU} from "./constants.js";
|
|
2
|
+
import {cross, norm} from "mathjs";
|
|
3
|
+
import {wrapHalfRevUnsigned, wrapOneRevUnsigned} from "./utils.js";
|
|
4
|
+
|
|
5
|
+
const EARTH_MU_KM = MU / 1e9;
|
|
6
|
+
|
|
7
|
+
class OrbitUtils {
|
|
8
|
+
constructor() {
|
|
9
|
+
// Prevent instantiation
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get inclination based on azimuth and latitude.
|
|
13
|
+
* @param {number} azimuthRad - Azimuth in radians
|
|
14
|
+
* @param {number} latitudeRad - Latitude in radians
|
|
15
|
+
* @return {number} Inclination in radians
|
|
16
|
+
*/
|
|
17
|
+
static getInclination(azimuthRad, latitudeRad) {
|
|
18
|
+
const inclination = Math.acos(Math.cos(latitudeRad) * Math.sin(azimuthRad));
|
|
19
|
+
return wrapHalfRevUnsigned(inclination);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get the azimuth based on inclination and latitude.
|
|
24
|
+
* Two results will be possible, based on whether you are Ascending from South to North or Descending from North to South.
|
|
25
|
+
* @param {number} inclinationRad - Inclination in radians
|
|
26
|
+
* @param {number} latitudeRad - Latitude in radians
|
|
27
|
+
* @param {boolean} ascending - Ascending (true) or descending (false)
|
|
28
|
+
* @return {number} Azimuth in radians
|
|
29
|
+
*/
|
|
30
|
+
static getAzimuth(inclinationRad, latitudeRad, ascending = true) {
|
|
31
|
+
const azimuth = Math.asin(Math.cos(inclinationRad) / Math.cos(latitudeRad));
|
|
32
|
+
if (!ascending) {
|
|
33
|
+
return wrapOneRevUnsigned(Math.PI - azimuth);
|
|
34
|
+
}
|
|
35
|
+
return wrapOneRevUnsigned(azimuth);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Calculate the specific angular momentum (h) vector of an orbit.
|
|
40
|
+
* @param {Array} r - Position vector [x, y, z]
|
|
41
|
+
* @param {Array} v - Velocity vector [vx, vy, vz]
|
|
42
|
+
* @return {Array} Angular momentum vector
|
|
43
|
+
*/
|
|
44
|
+
static getAngularMomentum(r, v) {
|
|
45
|
+
return cross(r, v);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get mean motion (rad/sec).
|
|
50
|
+
* @param {number} a - Semi Major Axis (km)
|
|
51
|
+
* @param {number} mu - Standard gravitational parameter (km^3/s^2)
|
|
52
|
+
* @return {number} Mean motion in rad/sec
|
|
53
|
+
*/
|
|
54
|
+
static getMeanMotion(a, mu = EARTH_MU_KM) {
|
|
55
|
+
return Math.sqrt(mu / Math.pow(a, 3));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the period of an orbit in seconds.
|
|
60
|
+
* @param {number} a - Semi Major Axis (km)
|
|
61
|
+
* @param {number} mu - Standard gravitational parameter (km^3/s^2)
|
|
62
|
+
* @return {number} Period in seconds
|
|
63
|
+
*/
|
|
64
|
+
static getPeriod(a, mu = EARTH_MU_KM) {
|
|
65
|
+
if (a <= 0) return null;
|
|
66
|
+
return 2 * Math.PI * Math.sqrt(Math.pow(a, 3) / mu);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Transform an orbit to elliptical with target parameters using Brent's method.
|
|
71
|
+
* Accurate port of the C# TransformElliptical method.
|
|
72
|
+
* @param {Object} svInitial - Initial state vector {position: [x,y,z], velocity: [vx,vy,vz], epoch: Date}
|
|
73
|
+
* @param {number} incRad - Inclination in radians
|
|
74
|
+
* @param {number} raanRad - RAAN in radians
|
|
75
|
+
* @param {number} targetEcc - Target eccentricity
|
|
76
|
+
* @param {number} targetArgOfPeriapsisRad - Target argument of periapsis in radians
|
|
77
|
+
* @param {number} targetPerigeeKm - Target perigee in km
|
|
78
|
+
* @return {Object} Satellite object with new orbital elements
|
|
79
|
+
*/
|
|
80
|
+
static transformElliptical(svInitial, incRad, raanRad,
|
|
81
|
+
targetEcc, targetArgOfPeriapsisRad, targetPerigeeKm) {
|
|
82
|
+
// Normalize the position vector
|
|
83
|
+
const rNorm = norm(svInitial.position);
|
|
84
|
+
const r = svInitial.position.map((x) => x / rNorm);
|
|
85
|
+
|
|
86
|
+
// Project the position vector onto the orbital plane
|
|
87
|
+
const projectedX = r[0] * Math.cos(raanRad) + r[1] * Math.sin(raanRad);
|
|
88
|
+
const projectedY = r[1] * Math.cos(raanRad) - r[0] * Math.sin(raanRad);
|
|
89
|
+
|
|
90
|
+
// Calculate the initial true anomaly using the projected coordinates and known argument of periapsis
|
|
91
|
+
let nu0 = Math.atan2(projectedY, projectedX) - targetArgOfPeriapsisRad;
|
|
92
|
+
nu0 = (nu0 + 2 * Math.PI) % (2 * Math.PI);
|
|
93
|
+
|
|
94
|
+
// Define the objective function that we want to minimize
|
|
95
|
+
const objectiveFunction = (nu) => {
|
|
96
|
+
nu = (nu + 2 * Math.PI) % (2 * Math.PI);
|
|
97
|
+
|
|
98
|
+
// Create orbital elements with the trial true anomaly
|
|
99
|
+
const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
|
|
100
|
+
const elements = {
|
|
101
|
+
semiMajorAxis: semiMajorAxis,
|
|
102
|
+
eccentricity: targetEcc,
|
|
103
|
+
inclination: incRad,
|
|
104
|
+
raan: raanRad,
|
|
105
|
+
argOfPeriapsis: targetArgOfPeriapsisRad,
|
|
106
|
+
trueAnomaly: nu,
|
|
107
|
+
epoch: svInitial.epoch,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Convert elements to state vector
|
|
111
|
+
const trialState = this.elementsToStateVector(elements);
|
|
112
|
+
|
|
113
|
+
// Normalize the trial position vector
|
|
114
|
+
const trialRNorm = norm(trialState.position);
|
|
115
|
+
const trialR = trialState.position.map((x) => x / trialRNorm);
|
|
116
|
+
|
|
117
|
+
// Scale trial position to match initial position magnitude
|
|
118
|
+
const pScaled = trialR.map((x) => x * rNorm);
|
|
119
|
+
|
|
120
|
+
// Return the distance between initial and trial positions
|
|
121
|
+
return norm(svInitial.position.map((x, i) => x - pScaled[i]));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Use Brent's method for minimization
|
|
125
|
+
const tolerance = 1e-6; // radians
|
|
126
|
+
const maxIterations = 1000;
|
|
127
|
+
|
|
128
|
+
// Initial bracket around the solution
|
|
129
|
+
let a = nu0 - Math.PI;
|
|
130
|
+
let b = nu0 + Math.PI;
|
|
131
|
+
let c = nu0;
|
|
132
|
+
|
|
133
|
+
let fa = objectiveFunction(a);
|
|
134
|
+
let fb = objectiveFunction(b);
|
|
135
|
+
let fc = objectiveFunction(c);
|
|
136
|
+
|
|
137
|
+
let d = 0; let e = 0; let min1; let min2;
|
|
138
|
+
let p; let q; let r1; let tol1; let xm;
|
|
139
|
+
|
|
140
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
141
|
+
if (Math.abs(b - a) < tolerance) {
|
|
142
|
+
// Found minimum
|
|
143
|
+
const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
|
|
144
|
+
return {
|
|
145
|
+
semiMajorAxis: semiMajorAxis,
|
|
146
|
+
eccentricity: targetEcc,
|
|
147
|
+
inclination: incRad,
|
|
148
|
+
raan: raanRad,
|
|
149
|
+
argOfPeriapsis: targetArgOfPeriapsisRad,
|
|
150
|
+
trueAnomaly: c,
|
|
151
|
+
epoch: svInitial.epoch,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
xm = 0.5 * (a + b);
|
|
156
|
+
tol1 = tolerance * Math.abs(c) + 1e-10;
|
|
157
|
+
min1 = Math.abs(c - a);
|
|
158
|
+
min2 = Math.abs(b - c);
|
|
159
|
+
|
|
160
|
+
if (min1 < tol1 || min2 < tol1) {
|
|
161
|
+
// Found minimum
|
|
162
|
+
const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
|
|
163
|
+
return {
|
|
164
|
+
semiMajorAxis: semiMajorAxis,
|
|
165
|
+
eccentricity: targetEcc,
|
|
166
|
+
inclination: incRad,
|
|
167
|
+
raan: raanRad,
|
|
168
|
+
argOfPeriapsis: targetArgOfPeriapsisRad,
|
|
169
|
+
trueAnomaly: c,
|
|
170
|
+
epoch: svInitial.epoch,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Construct a trial parabolic fit
|
|
175
|
+
if (Math.abs(e) >= tol1) {
|
|
176
|
+
r1 = (c - a) * (fb - fc);
|
|
177
|
+
q = (c - b) * (fa - fc);
|
|
178
|
+
p = (c - b) * q - (c - a) * r1;
|
|
179
|
+
q = 2.0 * (q - r1);
|
|
180
|
+
if (q > 0.0) p = -p;
|
|
181
|
+
q = Math.abs(q);
|
|
182
|
+
min1 = 3.0 * q * Math.abs(xm);
|
|
183
|
+
min2 = Math.abs(e * q);
|
|
184
|
+
if (2.0 * p < (min1 < min2 ? min1 : min2)) {
|
|
185
|
+
e = d;
|
|
186
|
+
d = p / q;
|
|
187
|
+
} else {
|
|
188
|
+
d = xm;
|
|
189
|
+
e = d;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
d = xm;
|
|
193
|
+
e = d;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
a = b;
|
|
197
|
+
fa = fb;
|
|
198
|
+
if (Math.abs(d) > tol1) {
|
|
199
|
+
b += d;
|
|
200
|
+
} else {
|
|
201
|
+
b += (xm >= 0 ? Math.abs(tol1) : -Math.abs(tol1));
|
|
202
|
+
}
|
|
203
|
+
fb = objectiveFunction(b);
|
|
204
|
+
if ((fb <= fc && c > b) || (fb >= fc && c < b)) {
|
|
205
|
+
c = a;
|
|
206
|
+
fc = fa;
|
|
207
|
+
a = b;
|
|
208
|
+
fa = fb;
|
|
209
|
+
b = c;
|
|
210
|
+
fb = fc;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw new Error("Did not converge within maximum iterations");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Convert orbital elements to state vector
|
|
219
|
+
* @param {Object} elements - Orbital elements
|
|
220
|
+
* @return {Object} State vector {position, velocity}
|
|
221
|
+
*/
|
|
222
|
+
static elementsToStateVector(elements) {
|
|
223
|
+
const {semiMajorAxis, eccentricity, inclination,
|
|
224
|
+
raan, argOfPeriapsis, trueAnomaly} = elements;
|
|
225
|
+
const mu = EARTH_MU_KM;
|
|
226
|
+
|
|
227
|
+
// Calculate radius
|
|
228
|
+
const rMag = semiMajorAxis * (1 - eccentricity * eccentricity)
|
|
229
|
+
/ (1 + eccentricity * Math.cos(trueAnomaly));
|
|
230
|
+
|
|
231
|
+
// Position in orbital plane
|
|
232
|
+
const rOrbital = [
|
|
233
|
+
rMag * Math.cos(trueAnomaly),
|
|
234
|
+
rMag * Math.sin(trueAnomaly),
|
|
235
|
+
0,
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
// Velocity in orbital plane
|
|
239
|
+
const h = Math.sqrt(mu * semiMajorAxis * (1 - eccentricity * eccentricity));
|
|
240
|
+
const vOrbital = [
|
|
241
|
+
-mu / h * Math.sin(trueAnomaly),
|
|
242
|
+
mu / h * (eccentricity + Math.cos(trueAnomaly)),
|
|
243
|
+
0,
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
// Rotation matrices
|
|
247
|
+
const cosW = Math.cos(argOfPeriapsis);
|
|
248
|
+
const sinW = Math.sin(argOfPeriapsis);
|
|
249
|
+
const cosO = Math.cos(raan);
|
|
250
|
+
const sinO = Math.sin(raan);
|
|
251
|
+
const cosI = Math.cos(inclination);
|
|
252
|
+
const sinI = Math.sin(inclination);
|
|
253
|
+
|
|
254
|
+
// Transform to inertial frame
|
|
255
|
+
const r = [
|
|
256
|
+
rOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
|
|
257
|
+
- rOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
|
|
258
|
+
rOrbital[0] * (cosW * sinO + sinW * cosI * cosO) + rOrbital[1]
|
|
259
|
+
* (cosW * cosI * cosO - sinW * sinO),
|
|
260
|
+
rOrbital[0] * (sinW * sinI) + rOrbital[1] * (cosW * sinI),
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const v = [
|
|
264
|
+
vOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
|
|
265
|
+
- vOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
|
|
266
|
+
vOrbital[0] * (cosW * sinO + sinW * cosI * cosO)
|
|
267
|
+
+ vOrbital[1] * (cosW * cosI * cosO - sinW * sinO),
|
|
268
|
+
vOrbital[0] * (sinW * sinI) + vOrbital[1] * (cosW * sinI),
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
return {position: r, velocity: v};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Convert state vector (position, velocity) to classical orbital elements
|
|
276
|
+
* @param {Array} r - Position vector [x, y, z] in km
|
|
277
|
+
* @param {Array} v - Velocity vector [vx, vy, vz] in km/s
|
|
278
|
+
* @param {number} mu - Standard gravitational parameter (default: Earth)
|
|
279
|
+
* @return {Object} Orbital elements
|
|
280
|
+
*/
|
|
281
|
+
static stateVectorToElements(r, v, mu = EARTH_MU_KM) {
|
|
282
|
+
const tol = 1e-9;
|
|
283
|
+
|
|
284
|
+
if (mu < 1e-30) {
|
|
285
|
+
throw new Error("Mu must be greater than 1e-30.");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Helper functions
|
|
289
|
+
function cross(a, b) {
|
|
290
|
+
return [
|
|
291
|
+
a[1] * b[2] - a[2] * b[1],
|
|
292
|
+
a[2] * b[0] - a[0] * b[2],
|
|
293
|
+
a[0] * b[1] - a[1] * b[0],
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function dot(a, b) {
|
|
298
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function norm(v) {
|
|
302
|
+
return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function clamp(value, min, max) {
|
|
306
|
+
return Math.min(Math.max(value, min), max);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Calculate basic vectors
|
|
310
|
+
const h = cross(r, v);
|
|
311
|
+
const n = cross([0, 0, 1], h);
|
|
312
|
+
|
|
313
|
+
const rLength = norm(r);
|
|
314
|
+
const vLength = norm(v);
|
|
315
|
+
|
|
316
|
+
if (rLength === 0) throw new Error("Position vector must not be zero.");
|
|
317
|
+
if (vLength === 0) throw new Error("Velocity vector must not be zero.");
|
|
318
|
+
|
|
319
|
+
// Eccentricity vector calculation (corrected formula)
|
|
320
|
+
const vLengthSq = vLength * vLength;
|
|
321
|
+
const muOverR = mu / rLength;
|
|
322
|
+
const rvDot = dot(r, v);
|
|
323
|
+
|
|
324
|
+
const e = [
|
|
325
|
+
(1 / mu) * ((vLengthSq - muOverR) * r[0] - rvDot * v[0]),
|
|
326
|
+
(1 / mu) * ((vLengthSq - muOverR) * r[1] - rvDot * v[1]),
|
|
327
|
+
(1 / mu) * ((vLengthSq - muOverR) * r[2] - rvDot * v[2]),
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
const zeta = 0.5 * vLengthSq - muOverR;
|
|
331
|
+
|
|
332
|
+
if (zeta === 0) throw new Error("Zeta cannot be zero.");
|
|
333
|
+
|
|
334
|
+
const eLength = norm(e);
|
|
335
|
+
if (Math.abs(1.0 - eLength) <= tol) {
|
|
336
|
+
throw new Error("Parabolic orbit conversion is not supported.");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const a = -mu / (zeta * 2);
|
|
340
|
+
|
|
341
|
+
if (Math.abs(a * (1 - eLength)) < 1e-3) {
|
|
342
|
+
throw new Error("The state results in a singular conic section "
|
|
343
|
+
+ "with radius of periapsis less than 1 m.");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const hLength = norm(h);
|
|
347
|
+
if (hLength === 0) {
|
|
348
|
+
throw new Error(`Cannot convert from Cartesian to Keplerian
|
|
349
|
+
- angular momentum is zero.`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const i = Math.acos(h[2] / hLength);
|
|
353
|
+
|
|
354
|
+
if (i >= Math.PI - tol) {
|
|
355
|
+
throw new Error("Cannot convert orbit with inclination of 180 degrees.");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let raan = 0;
|
|
359
|
+
let w = 0;
|
|
360
|
+
let f = 0;
|
|
361
|
+
|
|
362
|
+
const nLength = norm(n);
|
|
363
|
+
|
|
364
|
+
// CASE 1: Non-circular, Inclined Orbit
|
|
365
|
+
if (eLength >= 1e-11 && i >= 1e-11 && i <= Math.PI - 1e-11) {
|
|
366
|
+
if (nLength === 0.0) {
|
|
367
|
+
throw new Error("Cannot convert from Cartesian to Keplerian "
|
|
368
|
+
+ "- line-of-nodes vector is a zero vector.");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
raan = Math.acos(n[0] / nLength);
|
|
372
|
+
if (n[1] < 0) raan = 2 * Math.PI - raan;
|
|
373
|
+
|
|
374
|
+
w = Math.acos(dot(n, e) / (nLength * eLength));
|
|
375
|
+
if (e[2] < 0) w = 2 * Math.PI - w;
|
|
376
|
+
|
|
377
|
+
f = Math.acos(clamp(dot(e, r) / (eLength * rLength), -1, 1));
|
|
378
|
+
if (rvDot < 0) f = 2 * Math.PI - f;
|
|
379
|
+
} else if (eLength >= 1e-11 && (i < 1e-11 || i > Math.PI - 1e-11)) { // CASE 2: Non-circular, Equatorial Orbit
|
|
380
|
+
if (eLength === 0.0) {
|
|
381
|
+
throw new Error(`Cannot convert from Cartesian to Keplerian
|
|
382
|
+
- eccentricity is zero.`);
|
|
383
|
+
}
|
|
384
|
+
raan = 0;
|
|
385
|
+
w = Math.acos(e[0] / eLength);
|
|
386
|
+
if (e[1] < 0) w = 2 * Math.PI - w;
|
|
387
|
+
|
|
388
|
+
// For GMT-4446 fix (LOJ: 2014.03.21)
|
|
389
|
+
if (i > Math.PI - 1e-11) w *= -1.0;
|
|
390
|
+
if (w < 0.0) w += 2 * Math.PI;
|
|
391
|
+
|
|
392
|
+
f = Math.acos(clamp(dot(e, r) / (eLength * rLength), -1, 1));
|
|
393
|
+
if (rvDot < 0) f = 2 * Math.PI - f;
|
|
394
|
+
} else if (eLength < 1e-11 && i >= 1e-11 && i <= Math.PI - 1e-11) { // CASE 3: Circular, Inclined Orbit
|
|
395
|
+
if (nLength === 0.0) {
|
|
396
|
+
throw new Error("Cannot convert from Cartesian to Keplerian "
|
|
397
|
+
+ "- line-of-nodes vector is a zero vector.");
|
|
398
|
+
}
|
|
399
|
+
raan = Math.acos(n[0] / nLength);
|
|
400
|
+
if (n[1] < 0) raan = 2 * Math.PI - raan;
|
|
401
|
+
|
|
402
|
+
w = 0;
|
|
403
|
+
|
|
404
|
+
f = Math.acos(clamp(dot(n, r) / (nLength * rLength), -1, 1));
|
|
405
|
+
if (r[2] < 0) f = 2 * Math.PI - f;
|
|
406
|
+
} else if (eLength < 1e-11 && (i < 1e-11 || i > Math.PI - 1e-11)) { // CASE 4: Circular, Equatorial Orbit
|
|
407
|
+
raan = 0;
|
|
408
|
+
w = 0;
|
|
409
|
+
f = Math.acos(clamp(r[0] / rLength, -1, 1));
|
|
410
|
+
if (r[1] < 0) f = 2 * Math.PI - f;
|
|
411
|
+
|
|
412
|
+
// For GMT-4446 fix (LOJ: 2014.03.21)
|
|
413
|
+
if (i > Math.PI - 1e-11) f *= -1.0;
|
|
414
|
+
if (f < 0.0) f += 2 * Math.PI;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Calculate additional orbital parameters
|
|
418
|
+
const period = 2 * Math.PI * Math.sqrt(Math.pow(Math.abs(a), 3) / mu);
|
|
419
|
+
const meanMotion = 2 * Math.PI / period;
|
|
420
|
+
|
|
421
|
+
// Convert true anomaly to mean anomaly properly
|
|
422
|
+
const eccentricAnomaly = this.trueAnomalyToEccentricAnomaly(f, eLength);
|
|
423
|
+
const meanAnomaly = this.eccentricAnomalyToMeanAnomaly(eccentricAnomaly, eLength);
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
semiMajorAxis: a, // km
|
|
427
|
+
eccentricity: eLength, // dimensionless
|
|
428
|
+
inclination: i, // radians
|
|
429
|
+
raan: raan, // radians (Right Ascension of Ascending Node)
|
|
430
|
+
argOfPeriapsis: w, // radians (Argument of Periapsis)
|
|
431
|
+
trueAnomaly: f, // radians
|
|
432
|
+
meanAnomaly: meanAnomaly, // radians (simplified)
|
|
433
|
+
period: period, // seconds
|
|
434
|
+
meanMotion: meanMotion, // rad/s
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Convert true anomaly to eccentric anomaly
|
|
440
|
+
* @param {number} nu - true anomaly
|
|
441
|
+
* @param {number} e - eccentricity
|
|
442
|
+
* @return {number} Eccentric Anomaly
|
|
443
|
+
*/
|
|
444
|
+
static trueAnomalyToEccentricAnomaly(nu, e) {
|
|
445
|
+
const cosE = (e + Math.cos(nu)) / (1 + e * Math.cos(nu));
|
|
446
|
+
const sinE = Math.sqrt(1 - e * e) * Math.sin(nu) / (1 + e * Math.cos(nu));
|
|
447
|
+
return Math.atan2(sinE, cosE);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Convert eccentric anomaly to mean anomaly
|
|
452
|
+
* @param {number} E - Eccentric anomaly
|
|
453
|
+
* @param {number} e - eccentricity
|
|
454
|
+
* @return {number} Mean Anomaly
|
|
455
|
+
*/
|
|
456
|
+
static eccentricAnomalyToMeanAnomaly(E, e) {
|
|
457
|
+
return E - e * Math.sin(E);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Convert mean anomaly to eccentric anomaly (Kepler's equation)
|
|
462
|
+
* @param {number} M - Mean anomaly
|
|
463
|
+
* @param {number} e - eccentricity
|
|
464
|
+
* @param {number} tolerance - numerical tolerance
|
|
465
|
+
* @return {number} Eccentric Anomaly
|
|
466
|
+
*/
|
|
467
|
+
static meanAnomalyToEccentricAnomaly(M, e, tolerance = 1e-8) {
|
|
468
|
+
let E = M;
|
|
469
|
+
let delta = 1;
|
|
470
|
+
while (Math.abs(delta) > tolerance) {
|
|
471
|
+
delta = (M - E + e * Math.sin(E)) / (1 - e * Math.cos(E));
|
|
472
|
+
E += delta;
|
|
473
|
+
}
|
|
474
|
+
return E;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Convert eccentric anomaly to true anomaly
|
|
479
|
+
* @param {number} E - Eccentric anomaly
|
|
480
|
+
* @param {number} e - eccentricity
|
|
481
|
+
* @return {number} true anomaly
|
|
482
|
+
*/
|
|
483
|
+
static eccentricAnomalyToTrueAnomaly(E, e) {
|
|
484
|
+
const cosNu = (Math.cos(E) - e) / (1 - e * Math.cos(E));
|
|
485
|
+
const sinNu = Math.sqrt(1 - e * e) * Math.sin(E) / (1 - e * Math.cos(E));
|
|
486
|
+
return Math.atan2(sinNu, cosNu);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export {OrbitUtils};
|