@saber-usa/node-common 1.7.7-alpha.2 → 1.7.7

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/src/OrbitUtils.js CHANGED
@@ -1,309 +1,309 @@
1
- import {MU, RAD2DEG} from "./constants.js";
2
- import {cross, norm} from "mathjs";
3
- import {wrapHalfRevUnsigned, wrapOneRevUnsigned} from "./utils.js";
4
-
5
- const EARTH_MU_KM = MU; // km³/s²
6
-
7
- class OrbitUtils {
8
- /**
9
- * Get inclination based on azimuth and latitude.
10
- * @param {number} azimuthRad - Azimuth in radians
11
- * @param {number} latitudeRad - Latitude in radians
12
- * @return {number} Inclination in radians
13
- */
14
- static getInclination(azimuthRad, latitudeRad) {
15
- const inclination = Math.acos(Math.cos(latitudeRad) * Math.sin(azimuthRad));
16
- return wrapHalfRevUnsigned(inclination);
17
- }
18
-
19
- /**
20
- * Get the azimuth based on inclination and latitude.
21
- * Two results will be possible, based on whether you are Ascending from South to North or Descending from North to South.
22
- * @param {number} inclinationRad - Inclination in radians
23
- * @param {number} latitudeRad - Latitude in radians
24
- * @param {boolean} ascending - Ascending (true) or descending (false)
25
- * @return {number} Azimuth in radians
26
- */
27
- static getAzimuth(inclinationRad, latitudeRad, ascending = true) {
28
- const azimuth = Math.asin(Math.cos(inclinationRad) / Math.cos(latitudeRad));
29
- if (!ascending) {
30
- return wrapOneRevUnsigned(Math.PI - azimuth);
31
- }
32
- return wrapOneRevUnsigned(azimuth);
33
- }
34
-
35
- /**
36
- * Calculate the specific angular momentum (h) vector of an orbit.
37
- * @param {Array} r - Position vector [x, y, z]
38
- * @param {Array} v - Velocity vector [vx, vy, vz]
39
- * @return {Array} Angular momentum vector
40
- */
41
- static getAngularMomentum(r, v) {
42
- return cross(r, v);
43
- }
44
-
45
- /**
46
- * Get mean motion (rad/sec).
47
- * @param {number} a - Semi Major Axis (km)
48
- * @param {number} mu - Standard gravitational parameter (km^3/s^2)
49
- * @return {number} Mean motion in rad/sec
50
- */
51
- static getMeanMotion(a, mu = EARTH_MU_KM) {
52
- return Math.sqrt(mu / Math.pow(a, 3));
53
- }
54
-
55
- /**
56
- * Get the period of an orbit in seconds.
57
- * @param {number} a - Semi Major Axis (km)
58
- * @param {number} mu - Standard gravitational parameter (km^3/s^2)
59
- * @return {number} Period in seconds
60
- */
61
- static getPeriod(a, mu = EARTH_MU_KM) {
62
- if (a <= 0) return null;
63
- return 2 * Math.PI * Math.sqrt(Math.pow(a, 3) / mu);
64
- }
65
-
66
- /**
67
- * Transform an orbit to elliptical with target parameters using Brent's method.
68
- * Accurate port of the C# TransformElliptical method.
69
- * @param {Object} svInitial - Initial state vector {position: [x,y,z], velocity: [vx,vy,vz], epoch: Date}
70
- * @param {number} incRad - Inclination in radians
71
- * @param {number} raanRad - RAAN in radians
72
- * @param {number} targetEcc - Target eccentricity
73
- * @param {number} targetArgOfPeriapsisRad - Target argument of periapsis in radians
74
- * @param {number} targetPerigeeKm - Target perigee in km
75
- * @return {Object} Satellite object with new orbital elements
76
- */
77
- static transformElliptical(svInitial, incRad, raanRad,
78
- targetEcc, targetArgOfPeriapsisRad, targetPerigeeKm) {
79
- // Normalize the position vector
80
- const rNorm = norm(svInitial.position);
81
- const r = svInitial.position.map((x) => x / rNorm);
82
-
83
- // Project the position vector onto the orbital plane
84
- const projectedX = r[0] * Math.cos(raanRad) + r[1] * Math.sin(raanRad);
85
- const projectedY = r[1] * Math.cos(raanRad) - r[0] * Math.sin(raanRad);
86
-
87
- // Calculate the initial true anomaly using the projected coordinates and known argument of periapsis
88
- let nu0 = Math.atan2(projectedY, projectedX) - targetArgOfPeriapsisRad;
89
- nu0 = (nu0 + 2 * Math.PI) % (2 * Math.PI);
90
-
91
- // Define the objective function that we want to minimize
92
- const objectiveFunction = (nu) => {
93
- nu = (nu + 2 * Math.PI) % (2 * Math.PI);
94
-
95
- // Create orbital elements with the trial true anomaly
96
- const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
97
- const elements = {
98
- semiMajorAxis: semiMajorAxis,
99
- eccentricity: targetEcc,
100
- inclination: incRad,
101
- raan: raanRad,
102
- argOfPeriapsis: targetArgOfPeriapsisRad,
103
- trueAnomaly: nu,
104
- epoch: svInitial.epoch,
105
- };
106
-
107
- // Convert elements to state vector
108
- const trialState = this.elementsToStateVector(elements);
109
-
110
- // Normalize the trial position vector
111
- const trialRNorm = norm(trialState.position);
112
- const trialR = trialState.position.map((x) => x / trialRNorm);
113
-
114
- // Scale trial position to match initial position magnitude
115
- const pScaled = trialR.map((x) => x * rNorm);
116
-
117
- // Return the distance between initial and trial positions
118
- return norm(svInitial.position.map((x, i) => x - pScaled[i]));
119
- };
120
-
121
- // Use Brent's method for minimization
122
- const tolerance = 1e-6; // radians
123
- const maxIterations = 1000;
124
-
125
- // Initial bracket around the solution
126
- let a = nu0 - Math.PI;
127
- let b = nu0 + Math.PI;
128
- let c = nu0;
129
-
130
- let fa = objectiveFunction(a);
131
- let fb = objectiveFunction(b);
132
- let fc = objectiveFunction(c);
133
-
134
- let d = 0; let e = 0; let min1; let min2;
135
- let p; let q; let r1; let tol1; let xm;
136
-
137
- for (let iter = 0; iter < maxIterations; iter++) {
138
- if (Math.abs(b - a) < tolerance) {
139
- // Found minimum
140
- const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
141
- return [semiMajorAxis, targetEcc, incRad * RAD2DEG,
142
- raanRad * RAD2DEG, targetArgOfPeriapsisRad * RAD2DEG, c * RAD2DEG];
143
- }
144
-
145
- xm = 0.5 * (a + b);
146
- tol1 = tolerance * Math.abs(c) + 1e-10;
147
- min1 = Math.abs(c - a);
148
- min2 = Math.abs(b - c);
149
-
150
- if (min1 < tol1 || min2 < tol1) {
151
- // Found minimum
152
- const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
153
- return [semiMajorAxis, targetEcc, incRad * RAD2DEG,
154
- raanRad * RAD2DEG, targetArgOfPeriapsisRad * RAD2DEG, c * RAD2DEG];
155
- }
156
-
157
- // Construct a trial parabolic fit
158
- if (Math.abs(e) >= tol1) {
159
- r1 = (c - a) * (fb - fc);
160
- q = (c - b) * (fa - fc);
161
- p = (c - b) * q - (c - a) * r1;
162
- q = 2.0 * (q - r1);
163
- if (q > 0.0) p = -p;
164
- q = Math.abs(q);
165
- min1 = 3.0 * q * Math.abs(xm);
166
- min2 = Math.abs(e * q);
167
- if (2.0 * p < (min1 < min2 ? min1 : min2)) {
168
- e = d;
169
- d = p / q;
170
- } else {
171
- d = xm;
172
- e = d;
173
- }
174
- } else {
175
- d = xm;
176
- e = d;
177
- }
178
-
179
- a = b;
180
- fa = fb;
181
- if (Math.abs(d) > tol1) {
182
- b += d;
183
- } else {
184
- b += (xm >= 0 ? Math.abs(tol1) : -Math.abs(tol1));
185
- }
186
- fb = objectiveFunction(b);
187
- if ((fb <= fc && c > b) || (fb >= fc && c < b)) {
188
- c = a;
189
- fc = fa;
190
- a = b;
191
- fa = fb;
192
- b = c;
193
- fb = fc;
194
- }
195
- }
196
-
197
- throw new Error("Did not converge within maximum iterations");
198
- }
199
-
200
- /**
201
- * Convert orbital elements to state vector
202
- * @param {Object} elements - Orbital elements
203
- * @return {Object} State vector {position, velocity}
204
- */
205
- static elementsToStateVector(elements) {
206
- const {semiMajorAxis, eccentricity, inclination,
207
- raan, argOfPeriapsis, trueAnomaly} = elements;
208
- const mu = EARTH_MU_KM;
209
-
210
- // Calculate radius
211
- const rMag = semiMajorAxis * (1 - eccentricity * eccentricity)
212
- / (1 + eccentricity * Math.cos(trueAnomaly));
213
-
214
- // Position in orbital plane
215
- const rOrbital = [
216
- rMag * Math.cos(trueAnomaly),
217
- rMag * Math.sin(trueAnomaly),
218
- 0,
219
- ];
220
-
221
- // Velocity in orbital plane
222
- const h = Math.sqrt(mu * semiMajorAxis * (1 - eccentricity * eccentricity));
223
- const vOrbital = [
224
- -mu / h * Math.sin(trueAnomaly),
225
- mu / h * (eccentricity + Math.cos(trueAnomaly)),
226
- 0,
227
- ];
228
-
229
- // Rotation matrices
230
- const cosW = Math.cos(argOfPeriapsis);
231
- const sinW = Math.sin(argOfPeriapsis);
232
- const cosO = Math.cos(raan);
233
- const sinO = Math.sin(raan);
234
- const cosI = Math.cos(inclination);
235
- const sinI = Math.sin(inclination);
236
-
237
- // Transform to inertial frame
238
- const r = [
239
- rOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
240
- - rOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
241
- rOrbital[0] * (cosW * sinO + sinW * cosI * cosO) + rOrbital[1]
242
- * (cosW * cosI * cosO - sinW * sinO),
243
- rOrbital[0] * (sinW * sinI) + rOrbital[1] * (cosW * sinI),
244
- ];
245
-
246
- const v = [
247
- vOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
248
- - vOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
249
- vOrbital[0] * (cosW * sinO + sinW * cosI * cosO)
250
- + vOrbital[1] * (cosW * cosI * cosO - sinW * sinO),
251
- vOrbital[0] * (sinW * sinI) + vOrbital[1] * (cosW * sinI),
252
- ];
253
-
254
- return {position: r, velocity: v};
255
- }
256
-
257
- /**
258
- * Convert true anomaly to eccentric anomaly
259
- * @param {number} nu - true anomaly
260
- * @param {number} e - eccentricity
261
- * @return {number} Eccentric Anomaly
262
- */
263
- static trueAnomalyToEccentricAnomaly(nu, e) {
264
- const cosE = (e + Math.cos(nu)) / (1 + e * Math.cos(nu));
265
- const sinE = Math.sqrt(1 - e * e) * Math.sin(nu) / (1 + e * Math.cos(nu));
266
- return Math.atan2(sinE, cosE);
267
- }
268
-
269
- /**
270
- * Convert eccentric anomaly to mean anomaly
271
- * @param {number} E - Eccentric anomaly
272
- * @param {number} e - eccentricity
273
- * @return {number} Mean Anomaly
274
- */
275
- static eccentricAnomalyToMeanAnomaly(E, e) {
276
- return E - e * Math.sin(E);
277
- }
278
-
279
- /**
280
- * Convert mean anomaly to eccentric anomaly (Kepler's equation)
281
- * @param {number} M - Mean anomaly
282
- * @param {number} e - eccentricity
283
- * @param {number} tolerance - numerical tolerance
284
- * @return {number} Eccentric Anomaly
285
- */
286
- static meanAnomalyToEccentricAnomaly(M, e, tolerance = 1e-8) {
287
- let E = M;
288
- let delta = 1;
289
- while (Math.abs(delta) > tolerance) {
290
- delta = (M - E + e * Math.sin(E)) / (1 - e * Math.cos(E));
291
- E += delta;
292
- }
293
- return E;
294
- }
295
-
296
- /**
297
- * Convert eccentric anomaly to true anomaly
298
- * @param {number} E - Eccentric anomaly
299
- * @param {number} e - eccentricity
300
- * @return {number} true anomaly
301
- */
302
- static eccentricAnomalyToTrueAnomaly(E, e) {
303
- const cosNu = (Math.cos(E) - e) / (1 - e * Math.cos(E));
304
- const sinNu = Math.sqrt(1 - e * e) * Math.sin(E) / (1 - e * Math.cos(E));
305
- return Math.atan2(sinNu, cosNu);
306
- }
307
- }
308
-
309
- export {OrbitUtils};
1
+ import {MU, RAD2DEG} from "./constants.js";
2
+ import {cross, norm} from "mathjs";
3
+ import {wrapHalfRevUnsigned, wrapOneRevUnsigned} from "./utils.js";
4
+
5
+ const EARTH_MU_KM = MU; // km³/s²
6
+
7
+ class OrbitUtils {
8
+ /**
9
+ * Get inclination based on azimuth and latitude.
10
+ * @param {number} azimuthRad - Azimuth in radians
11
+ * @param {number} latitudeRad - Latitude in radians
12
+ * @return {number} Inclination in radians
13
+ */
14
+ static getInclination(azimuthRad, latitudeRad) {
15
+ const inclination = Math.acos(Math.cos(latitudeRad) * Math.sin(azimuthRad));
16
+ return wrapHalfRevUnsigned(inclination);
17
+ }
18
+
19
+ /**
20
+ * Get the azimuth based on inclination and latitude.
21
+ * Two results will be possible, based on whether you are Ascending from South to North or Descending from North to South.
22
+ * @param {number} inclinationRad - Inclination in radians
23
+ * @param {number} latitudeRad - Latitude in radians
24
+ * @param {boolean} ascending - Ascending (true) or descending (false)
25
+ * @return {number} Azimuth in radians
26
+ */
27
+ static getAzimuth(inclinationRad, latitudeRad, ascending = true) {
28
+ const azimuth = Math.asin(Math.cos(inclinationRad) / Math.cos(latitudeRad));
29
+ if (!ascending) {
30
+ return wrapOneRevUnsigned(Math.PI - azimuth);
31
+ }
32
+ return wrapOneRevUnsigned(azimuth);
33
+ }
34
+
35
+ /**
36
+ * Calculate the specific angular momentum (h) vector of an orbit.
37
+ * @param {Array} r - Position vector [x, y, z]
38
+ * @param {Array} v - Velocity vector [vx, vy, vz]
39
+ * @return {Array} Angular momentum vector
40
+ */
41
+ static getAngularMomentum(r, v) {
42
+ return cross(r, v);
43
+ }
44
+
45
+ /**
46
+ * Get mean motion (rad/sec).
47
+ * @param {number} a - Semi Major Axis (km)
48
+ * @param {number} mu - Standard gravitational parameter (km^3/s^2)
49
+ * @return {number} Mean motion in rad/sec
50
+ */
51
+ static getMeanMotion(a, mu = EARTH_MU_KM) {
52
+ return Math.sqrt(mu / Math.pow(a, 3));
53
+ }
54
+
55
+ /**
56
+ * Get the period of an orbit in seconds.
57
+ * @param {number} a - Semi Major Axis (km)
58
+ * @param {number} mu - Standard gravitational parameter (km^3/s^2)
59
+ * @return {number} Period in seconds
60
+ */
61
+ static getPeriod(a, mu = EARTH_MU_KM) {
62
+ if (a <= 0) return null;
63
+ return 2 * Math.PI * Math.sqrt(Math.pow(a, 3) / mu);
64
+ }
65
+
66
+ /**
67
+ * Transform an orbit to elliptical with target parameters using Brent's method.
68
+ * Accurate port of the C# TransformElliptical method.
69
+ * @param {Object} svInitial - Initial state vector {position: [x,y,z], velocity: [vx,vy,vz], epoch: Date}
70
+ * @param {number} incRad - Inclination in radians
71
+ * @param {number} raanRad - RAAN in radians
72
+ * @param {number} targetEcc - Target eccentricity
73
+ * @param {number} targetArgOfPeriapsisRad - Target argument of periapsis in radians
74
+ * @param {number} targetPerigeeKm - Target perigee in km
75
+ * @return {Object} Satellite object with new orbital elements
76
+ */
77
+ static transformElliptical(svInitial, incRad, raanRad,
78
+ targetEcc, targetArgOfPeriapsisRad, targetPerigeeKm) {
79
+ // Normalize the position vector
80
+ const rNorm = norm(svInitial.position);
81
+ const r = svInitial.position.map((x) => x / rNorm);
82
+
83
+ // Project the position vector onto the orbital plane
84
+ const projectedX = r[0] * Math.cos(raanRad) + r[1] * Math.sin(raanRad);
85
+ const projectedY = r[1] * Math.cos(raanRad) - r[0] * Math.sin(raanRad);
86
+
87
+ // Calculate the initial true anomaly using the projected coordinates and known argument of periapsis
88
+ let nu0 = Math.atan2(projectedY, projectedX) - targetArgOfPeriapsisRad;
89
+ nu0 = (nu0 + 2 * Math.PI) % (2 * Math.PI);
90
+
91
+ // Define the objective function that we want to minimize
92
+ const objectiveFunction = (nu) => {
93
+ nu = (nu + 2 * Math.PI) % (2 * Math.PI);
94
+
95
+ // Create orbital elements with the trial true anomaly
96
+ const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
97
+ const elements = {
98
+ semiMajorAxis: semiMajorAxis,
99
+ eccentricity: targetEcc,
100
+ inclination: incRad,
101
+ raan: raanRad,
102
+ argOfPeriapsis: targetArgOfPeriapsisRad,
103
+ trueAnomaly: nu,
104
+ epoch: svInitial.epoch,
105
+ };
106
+
107
+ // Convert elements to state vector
108
+ const trialState = this.elementsToStateVector(elements);
109
+
110
+ // Normalize the trial position vector
111
+ const trialRNorm = norm(trialState.position);
112
+ const trialR = trialState.position.map((x) => x / trialRNorm);
113
+
114
+ // Scale trial position to match initial position magnitude
115
+ const pScaled = trialR.map((x) => x * rNorm);
116
+
117
+ // Return the distance between initial and trial positions
118
+ return norm(svInitial.position.map((x, i) => x - pScaled[i]));
119
+ };
120
+
121
+ // Use Brent's method for minimization
122
+ const tolerance = 1e-6; // radians
123
+ const maxIterations = 1000;
124
+
125
+ // Initial bracket around the solution
126
+ let a = nu0 - Math.PI;
127
+ let b = nu0 + Math.PI;
128
+ let c = nu0;
129
+
130
+ let fa = objectiveFunction(a);
131
+ let fb = objectiveFunction(b);
132
+ let fc = objectiveFunction(c);
133
+
134
+ let d = 0; let e = 0; let min1; let min2;
135
+ let p; let q; let r1; let tol1; let xm;
136
+
137
+ for (let iter = 0; iter < maxIterations; iter++) {
138
+ if (Math.abs(b - a) < tolerance) {
139
+ // Found minimum
140
+ const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
141
+ return [semiMajorAxis, targetEcc, incRad * RAD2DEG,
142
+ raanRad * RAD2DEG, targetArgOfPeriapsisRad * RAD2DEG, c * RAD2DEG];
143
+ }
144
+
145
+ xm = 0.5 * (a + b);
146
+ tol1 = tolerance * Math.abs(c) + 1e-10;
147
+ min1 = Math.abs(c - a);
148
+ min2 = Math.abs(b - c);
149
+
150
+ if (min1 < tol1 || min2 < tol1) {
151
+ // Found minimum
152
+ const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
153
+ return [semiMajorAxis, targetEcc, incRad * RAD2DEG,
154
+ raanRad * RAD2DEG, targetArgOfPeriapsisRad * RAD2DEG, c * RAD2DEG];
155
+ }
156
+
157
+ // Construct a trial parabolic fit
158
+ if (Math.abs(e) >= tol1) {
159
+ r1 = (c - a) * (fb - fc);
160
+ q = (c - b) * (fa - fc);
161
+ p = (c - b) * q - (c - a) * r1;
162
+ q = 2.0 * (q - r1);
163
+ if (q > 0.0) p = -p;
164
+ q = Math.abs(q);
165
+ min1 = 3.0 * q * Math.abs(xm);
166
+ min2 = Math.abs(e * q);
167
+ if (2.0 * p < (min1 < min2 ? min1 : min2)) {
168
+ e = d;
169
+ d = p / q;
170
+ } else {
171
+ d = xm;
172
+ e = d;
173
+ }
174
+ } else {
175
+ d = xm;
176
+ e = d;
177
+ }
178
+
179
+ a = b;
180
+ fa = fb;
181
+ if (Math.abs(d) > tol1) {
182
+ b += d;
183
+ } else {
184
+ b += (xm >= 0 ? Math.abs(tol1) : -Math.abs(tol1));
185
+ }
186
+ fb = objectiveFunction(b);
187
+ if ((fb <= fc && c > b) || (fb >= fc && c < b)) {
188
+ c = a;
189
+ fc = fa;
190
+ a = b;
191
+ fa = fb;
192
+ b = c;
193
+ fb = fc;
194
+ }
195
+ }
196
+
197
+ throw new Error("Did not converge within maximum iterations");
198
+ }
199
+
200
+ /**
201
+ * Convert orbital elements to state vector
202
+ * @param {Object} elements - Orbital elements
203
+ * @return {Object} State vector {position, velocity}
204
+ */
205
+ static elementsToStateVector(elements) {
206
+ const {semiMajorAxis, eccentricity, inclination,
207
+ raan, argOfPeriapsis, trueAnomaly} = elements;
208
+ const mu = EARTH_MU_KM;
209
+
210
+ // Calculate radius
211
+ const rMag = semiMajorAxis * (1 - eccentricity * eccentricity)
212
+ / (1 + eccentricity * Math.cos(trueAnomaly));
213
+
214
+ // Position in orbital plane
215
+ const rOrbital = [
216
+ rMag * Math.cos(trueAnomaly),
217
+ rMag * Math.sin(trueAnomaly),
218
+ 0,
219
+ ];
220
+
221
+ // Velocity in orbital plane
222
+ const h = Math.sqrt(mu * semiMajorAxis * (1 - eccentricity * eccentricity));
223
+ const vOrbital = [
224
+ -mu / h * Math.sin(trueAnomaly),
225
+ mu / h * (eccentricity + Math.cos(trueAnomaly)),
226
+ 0,
227
+ ];
228
+
229
+ // Rotation matrices
230
+ const cosW = Math.cos(argOfPeriapsis);
231
+ const sinW = Math.sin(argOfPeriapsis);
232
+ const cosO = Math.cos(raan);
233
+ const sinO = Math.sin(raan);
234
+ const cosI = Math.cos(inclination);
235
+ const sinI = Math.sin(inclination);
236
+
237
+ // Transform to inertial frame
238
+ const r = [
239
+ rOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
240
+ - rOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
241
+ rOrbital[0] * (cosW * sinO + sinW * cosI * cosO) + rOrbital[1]
242
+ * (cosW * cosI * cosO - sinW * sinO),
243
+ rOrbital[0] * (sinW * sinI) + rOrbital[1] * (cosW * sinI),
244
+ ];
245
+
246
+ const v = [
247
+ vOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
248
+ - vOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
249
+ vOrbital[0] * (cosW * sinO + sinW * cosI * cosO)
250
+ + vOrbital[1] * (cosW * cosI * cosO - sinW * sinO),
251
+ vOrbital[0] * (sinW * sinI) + vOrbital[1] * (cosW * sinI),
252
+ ];
253
+
254
+ return {position: r, velocity: v};
255
+ }
256
+
257
+ /**
258
+ * Convert true anomaly to eccentric anomaly
259
+ * @param {number} nu - true anomaly
260
+ * @param {number} e - eccentricity
261
+ * @return {number} Eccentric Anomaly
262
+ */
263
+ static trueAnomalyToEccentricAnomaly(nu, e) {
264
+ const cosE = (e + Math.cos(nu)) / (1 + e * Math.cos(nu));
265
+ const sinE = Math.sqrt(1 - e * e) * Math.sin(nu) / (1 + e * Math.cos(nu));
266
+ return Math.atan2(sinE, cosE);
267
+ }
268
+
269
+ /**
270
+ * Convert eccentric anomaly to mean anomaly
271
+ * @param {number} E - Eccentric anomaly
272
+ * @param {number} e - eccentricity
273
+ * @return {number} Mean Anomaly
274
+ */
275
+ static eccentricAnomalyToMeanAnomaly(E, e) {
276
+ return E - e * Math.sin(E);
277
+ }
278
+
279
+ /**
280
+ * Convert mean anomaly to eccentric anomaly (Kepler's equation)
281
+ * @param {number} M - Mean anomaly
282
+ * @param {number} e - eccentricity
283
+ * @param {number} tolerance - numerical tolerance
284
+ * @return {number} Eccentric Anomaly
285
+ */
286
+ static meanAnomalyToEccentricAnomaly(M, e, tolerance = 1e-8) {
287
+ let E = M;
288
+ let delta = 1;
289
+ while (Math.abs(delta) > tolerance) {
290
+ delta = (M - E + e * Math.sin(E)) / (1 - e * Math.cos(E));
291
+ E += delta;
292
+ }
293
+ return E;
294
+ }
295
+
296
+ /**
297
+ * Convert eccentric anomaly to true anomaly
298
+ * @param {number} E - Eccentric anomaly
299
+ * @param {number} e - eccentricity
300
+ * @return {number} true anomaly
301
+ */
302
+ static eccentricAnomalyToTrueAnomaly(E, e) {
303
+ const cosNu = (Math.cos(E) - e) / (1 - e * Math.cos(E));
304
+ const sinNu = Math.sqrt(1 - e * e) * Math.sin(E) / (1 - e * Math.cos(E));
305
+ return Math.atan2(sinNu, cosNu);
306
+ }
307
+ }
308
+
309
+ export {OrbitUtils};