@saber-usa/node-common 1.6.207
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 +42 -0
- package/package.json +51 -0
- package/src/FrameConverter.js +1071 -0
- package/src/LLA.js +181 -0
- package/src/LaunchNominalClass.js +822 -0
- package/src/NodeVector3D.js +71 -0
- package/src/OrbitUtils.js +491 -0
- package/src/ShadowGEOCalculator.js +201 -0
- package/src/TimeConverter.js +311 -0
- package/src/astro.js +3169 -0
- package/src/ballisticPropagator/ballisticPropagator.js +1047 -0
- package/src/checkNetwork.js +19 -0
- package/src/constants.js +33 -0
- package/src/fixDate.js +69 -0
- package/src/index.js +11 -0
- package/src/launchNominal.js +215 -0
- package/src/loggerFactory.js +98 -0
- package/src/s3.js +59 -0
- package/src/transform.js +40 -0
- package/src/udl.js +115 -0
- package/src/utils.js +406 -0
package/src/astro.js
ADDED
|
@@ -0,0 +1,3169 @@
|
|
|
1
|
+
const {cross, dot, norm, multiply, subtract, inv, polynomialRoot, add} = require("mathjs");
|
|
2
|
+
const {dateToMySqlDate, dtStrtoJsDt, parseDate} = require("./fixDate.js");
|
|
3
|
+
const solar = require("solar-calculator");
|
|
4
|
+
const {
|
|
5
|
+
twoline2satrec,
|
|
6
|
+
propagate,
|
|
7
|
+
sgp4,
|
|
8
|
+
gstime,
|
|
9
|
+
eciToGeodetic,
|
|
10
|
+
degreesToRadians,
|
|
11
|
+
radiansToDegrees,
|
|
12
|
+
eciToEcf,
|
|
13
|
+
ecfToLookAngles,
|
|
14
|
+
degreesLong,
|
|
15
|
+
degreesLat,
|
|
16
|
+
} = require("satellite.js");
|
|
17
|
+
const {lowerCaseObjectKeys} = require("./transform.js");
|
|
18
|
+
const squid = require("pious-squid");
|
|
19
|
+
const {
|
|
20
|
+
normalize,
|
|
21
|
+
transpose,
|
|
22
|
+
multiplyVector,
|
|
23
|
+
dist,
|
|
24
|
+
julianToGregorian,
|
|
25
|
+
epochToDate,
|
|
26
|
+
isValidDataMode,
|
|
27
|
+
isBoolean,
|
|
28
|
+
isNonEmptyString,
|
|
29
|
+
removeNullUndefined,
|
|
30
|
+
getTimeDifference,
|
|
31
|
+
isDefined,
|
|
32
|
+
getAngle,
|
|
33
|
+
wrapToRange,
|
|
34
|
+
getAngleDiffSigned,
|
|
35
|
+
posToArray,
|
|
36
|
+
} = require("./utils.js");
|
|
37
|
+
const {
|
|
38
|
+
DEG2RAD,
|
|
39
|
+
RAD2DEG,
|
|
40
|
+
SEC2RAD,
|
|
41
|
+
ARCSEC2RAD,
|
|
42
|
+
AU_KM,
|
|
43
|
+
SUN_RADIUS_KM,
|
|
44
|
+
REGIMES,
|
|
45
|
+
GRAV_CONST,
|
|
46
|
+
EARTH_MASS,
|
|
47
|
+
WGS72_EARTH_EQUATORIAL_RADIUS_KM,
|
|
48
|
+
WGS84_EARTH_EQUATORIAL_RADIUS_KM,
|
|
49
|
+
MILLIS_PER_DAY,
|
|
50
|
+
GEO_ALTITUDE_KM,
|
|
51
|
+
} = require("./constants.js");
|
|
52
|
+
|
|
53
|
+
// Solar Terminator
|
|
54
|
+
// Returns sun latitude and longitude as -180/180
|
|
55
|
+
const sunPosAt = (dt) => {
|
|
56
|
+
const day = new Date(+dt).setUTCHours(0, 0, 0, 0);
|
|
57
|
+
const t = solar.century(dt);
|
|
58
|
+
const longitude = ((day - dt) / 864e5) * 360 - 180;
|
|
59
|
+
return [
|
|
60
|
+
solar.declination(t),
|
|
61
|
+
(longitude - solar.equationOfTime(t) / 4) % 360,
|
|
62
|
+
];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const checkTle = (line1, line2) => {
|
|
66
|
+
try {
|
|
67
|
+
// Initialize a satellite record
|
|
68
|
+
const satrec = twoline2satrec(line1, line2);
|
|
69
|
+
satrec.epoch = dateToMySqlDate(julianToGregorian(satrec.jdsatepoch)); // Add the epoch
|
|
70
|
+
const pv = sgp4(satrec, 0);
|
|
71
|
+
if (Number.isNaN(pv.position.x) || Number.isNaN(pv.position.y) || Number.isNaN(pv.position.z)) {
|
|
72
|
+
return false;
|
|
73
|
+
} else {
|
|
74
|
+
return satrec;
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return false; // TLE invalid
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Calculates the semi-major axis in kilometers from a satellite record.
|
|
83
|
+
* @param {Object} satrec The satellite record from satellite.js
|
|
84
|
+
* @return {number} Semi-major axis in kilometers
|
|
85
|
+
*/
|
|
86
|
+
const getSemiMajorAxis = (satrec) => {
|
|
87
|
+
// satrec.a is in non-dimensionalized units (Earth radii)
|
|
88
|
+
// Convert to kilometers using WGS72 Earth equatorial radius (SGP4 standard)
|
|
89
|
+
return satrec.a * WGS72_EARTH_EQUATORIAL_RADIUS_KM;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const getRaanPrecession = (line1, line2) => {
|
|
93
|
+
try {
|
|
94
|
+
// Initialize a satellite record
|
|
95
|
+
const satrec = twoline2satrec(line1, line2);
|
|
96
|
+
// satrec nodedot is in radians per minute
|
|
97
|
+
// 1400 minutes / day (60 min/hr * 24 hr/day)
|
|
98
|
+
// result is in degrees per day
|
|
99
|
+
return satrec.nodedot * 1440 * RAD2DEG;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const getLonAndDrift = (line1, line2, datetime) => {
|
|
106
|
+
try {
|
|
107
|
+
// Initialize a satellite record
|
|
108
|
+
const satrec = twoline2satrec(line1, line2);
|
|
109
|
+
|
|
110
|
+
let pv;
|
|
111
|
+
let pv2;
|
|
112
|
+
|
|
113
|
+
if (datetime) {
|
|
114
|
+
pv = propagate(satrec, datetime);
|
|
115
|
+
pv2 = propagate(
|
|
116
|
+
satrec,
|
|
117
|
+
new Date(datetime.getTime() + 1440 * 60000),
|
|
118
|
+
);
|
|
119
|
+
} else {
|
|
120
|
+
// Propagate satellite using time since epoch (in minutes).
|
|
121
|
+
pv = sgp4(satrec, 0);
|
|
122
|
+
pv2 = sgp4(satrec, 1440); // One day later
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// You will need GMST for some of the coordinate transforms.
|
|
126
|
+
// http://en.wikipedia.org/wiki/Sidereal_time#Definition
|
|
127
|
+
const gmst = gstime(datetime ?? satrec.jdsatepoch); // GMST at epoch
|
|
128
|
+
const gmst2 = gstime(
|
|
129
|
+
datetime
|
|
130
|
+
? new Date(datetime.getTime() + 1440 * 60000)
|
|
131
|
+
: satrec.jdsatepoch + 1,
|
|
132
|
+
); // GMST One day later
|
|
133
|
+
|
|
134
|
+
// You can get ECF, Geodetic, Look Angles, and Doppler Factor.
|
|
135
|
+
const positionGd = eciToGeodetic(pv.position, gmst);
|
|
136
|
+
const positionGd2 = eciToGeodetic(pv2.position, gmst2);
|
|
137
|
+
|
|
138
|
+
// Geodetic coords are accessed via `longitude`, `latitude`, `height`.
|
|
139
|
+
const lon = degreesLong(positionGd.longitude);
|
|
140
|
+
const lon2 = degreesLong(positionGd2.longitude);
|
|
141
|
+
const lat = degreesLat(positionGd.latitude);
|
|
142
|
+
|
|
143
|
+
let diff = lon2 - lon;
|
|
144
|
+
if (diff < -180) {
|
|
145
|
+
diff = (diff % 180) + 180;
|
|
146
|
+
} else if (diff > 180) {
|
|
147
|
+
diff = (diff % 180) - 180;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
latitude: lat,
|
|
152
|
+
longitude: lon,
|
|
153
|
+
longitude360: (lon+ 360) % 360,
|
|
154
|
+
lonDriftDegreesPerDay: diff,
|
|
155
|
+
};
|
|
156
|
+
} catch (e) {
|
|
157
|
+
return {
|
|
158
|
+
latitude: 0,
|
|
159
|
+
longitude: 0,
|
|
160
|
+
longitude360: 0,
|
|
161
|
+
lonDriftDegreesPerDay: 0,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Given eccentricity, inclination, meanmotion, and period
|
|
168
|
+
* Calculates & returns type of orbit to include LEO, MEO, HEO, GEO Drifter, GEO Inclined,GEO Stationary, and undetermined if applicable
|
|
169
|
+
* @param {Object} orbit Object with keys eccentricity, inclination (deg), meanmotion (rev/day) and period (min)
|
|
170
|
+
* @return {number} the regime index
|
|
171
|
+
*/
|
|
172
|
+
const calcRegime = (orbit) => {
|
|
173
|
+
if (!isDefined(orbit)) {
|
|
174
|
+
return REGIMES.Undetermined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const {eccentricity, inclination, meanmotion, period}
|
|
178
|
+
= lowerCaseObjectKeys(orbit);
|
|
179
|
+
|
|
180
|
+
if (!isDefined(meanmotion) && (!isDefined(period) || !isDefined(eccentricity))) {
|
|
181
|
+
return REGIMES.Undetermined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (meanmotion >= 0.99 && meanmotion <= 1.01 && inclination <= 0.01) {
|
|
185
|
+
return REGIMES.GeoStationary;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (meanmotion >= 0.99 && meanmotion <= 1.01 && inclination > 0.01) {
|
|
189
|
+
return REGIMES.GeoInclined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (meanmotion >= 0.94 && meanmotion <= 1.06) {
|
|
193
|
+
return REGIMES.GeoDrifter;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (period >= 600 && period <= 800 && eccentricity < 0.25) {
|
|
197
|
+
return REGIMES.Meo;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (meanmotion > 11.25 && eccentricity < 0.25) {
|
|
201
|
+
return REGIMES.Leo;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return eccentricity > 0.25 ? REGIMES.Heo : REGIMES.Undetermined;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Given altitude, calculates & returns type of orbit to include LEO, MEO, HEO, GEO Drifter, GEO Inclined,GEO Stationary, and undetermined if applicable
|
|
209
|
+
* @param {Number} alt Altitude in km
|
|
210
|
+
* @param {Number} buffer altitude buffer in km, default 0 km
|
|
211
|
+
* @return {Array} Array of the regime indices
|
|
212
|
+
*/
|
|
213
|
+
const altToRegime = (alt, buffer = 0) => {
|
|
214
|
+
const regimes = [];
|
|
215
|
+
if (alt >= 35786 - buffer) {
|
|
216
|
+
regimes.push([
|
|
217
|
+
REGIMES.GeoStationary,
|
|
218
|
+
REGIMES.GeoInclined,
|
|
219
|
+
REGIMES.GeoDrifter,
|
|
220
|
+
]);
|
|
221
|
+
}
|
|
222
|
+
if (alt < 35786 + buffer && alt >= 2000 - buffer) {
|
|
223
|
+
regimes.push([REGIMES.Meo]);
|
|
224
|
+
}
|
|
225
|
+
if (alt < 2000 + buffer && alt >= 100 - buffer) {
|
|
226
|
+
regimes.push([REGIMES.Leo, REGIMES.Sso, REGIMES.Polar]);
|
|
227
|
+
}
|
|
228
|
+
if (alt < 100) {
|
|
229
|
+
return [REGIMES.Undetermined];
|
|
230
|
+
}
|
|
231
|
+
return regimes.flat();
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Converts cartesian coordinates to polar coordinates
|
|
236
|
+
*
|
|
237
|
+
* @param {Object} r 3D point
|
|
238
|
+
* @param {Object} v 3D vector
|
|
239
|
+
*
|
|
240
|
+
* @return {Object} Corresponding 2D vector (polar coordinates)
|
|
241
|
+
*/
|
|
242
|
+
const cartesianToRIC = (r, v) => {
|
|
243
|
+
const rArray = [r.x, r.y, r.z];
|
|
244
|
+
const vArray = [v.x, v.y, v.z];
|
|
245
|
+
const rCrossV = cross(rArray, vArray);
|
|
246
|
+
let R = normalize(r);
|
|
247
|
+
R = [R.x, R.y, R.z];
|
|
248
|
+
let W = normalize({x: rCrossV[0], y: rCrossV[1], z: rCrossV[2]});
|
|
249
|
+
W = [W.x, W.y, W.z];
|
|
250
|
+
const S = cross(W, R);
|
|
251
|
+
const convMatrix = transpose([R, S, W]);
|
|
252
|
+
return inv(convMatrix);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @param {Object} coord1 1st (3D) coordinate
|
|
257
|
+
* @param {Object} coord2 2nd (3D) coordinate
|
|
258
|
+
*
|
|
259
|
+
* @return {boolean} Whether coordinates are equal
|
|
260
|
+
*/
|
|
261
|
+
const areCoordsEqual = (coord1, coord2) => {
|
|
262
|
+
return coord1.x === coord2.x && coord1.y === coord2.y && coord1.z === coord2.z;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Calculate angle between three points in 3D space.
|
|
267
|
+
* Note: assumes we want one vector to run from coord1 -> coord2, and the other
|
|
268
|
+
* from coord3 -> coord2.
|
|
269
|
+
*
|
|
270
|
+
* @param {Object} coord1 1st (3D) coordinate
|
|
271
|
+
* @param {Object} coord2 2nd (3D) coordinate
|
|
272
|
+
* @param {Object} coord3 3rd (3D) coordinate
|
|
273
|
+
*
|
|
274
|
+
* @return {number} Angle between the 3 points
|
|
275
|
+
*/
|
|
276
|
+
const angleBetween3DCoords = (coord1, coord2, coord3) => {
|
|
277
|
+
// Calculate vector between points 1 and 2
|
|
278
|
+
const v1 = {
|
|
279
|
+
x: coord1.x - coord2.x,
|
|
280
|
+
y: coord1.y - coord2.y,
|
|
281
|
+
z: coord1.z - coord2.z,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Calculate vector between points 2 and 3
|
|
285
|
+
const v2 = {
|
|
286
|
+
x: coord3.x - coord2.x,
|
|
287
|
+
y: coord3.y - coord2.y,
|
|
288
|
+
z: coord3.z - coord2.z,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// The dot product of vectors v1 & v2 is a function of the cosine of the
|
|
292
|
+
// angle between them (it's scaled by the product of their magnitudes).
|
|
293
|
+
|
|
294
|
+
// Normalize v1
|
|
295
|
+
const v1mag = Math.sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z);
|
|
296
|
+
const v1norm = {
|
|
297
|
+
x: v1.x / v1mag,
|
|
298
|
+
y: v1.y / v1mag,
|
|
299
|
+
z: v1.z / v1mag,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Normalize v2
|
|
303
|
+
const v2mag = Math.sqrt(v2.x * v2.x + v2.y * v2.y + v2.z * v2.z);
|
|
304
|
+
const v2norm = {
|
|
305
|
+
x: v2.x / v2mag,
|
|
306
|
+
y: v2.y / v2mag,
|
|
307
|
+
z: v2.z / v2mag,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Calculate the dot products of vectors v1 and v2
|
|
311
|
+
const dotProducts
|
|
312
|
+
= v1norm.x * v2norm.x + v1norm.y * v2norm.y + v1norm.z * v2norm.z;
|
|
313
|
+
|
|
314
|
+
// Extract the angle from the dot products
|
|
315
|
+
const angle = (Math.acos(dotProducts) * 180.0) / Math.PI;
|
|
316
|
+
|
|
317
|
+
// Round result to 3 decimal points and return
|
|
318
|
+
return Math.round(angle * 1000) / 1000;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/** Calculates the Minimum Delta-V required for sat1 to plane match to sat2,
|
|
322
|
+
* with a single impulsive maneuver at one of the two mutual modal points of
|
|
323
|
+
* sat1.
|
|
324
|
+
*
|
|
325
|
+
* This maneuver, when performed at one of these points, and when its components
|
|
326
|
+
* are properly computed, will only modify the inclination and RAAN of sat1 to
|
|
327
|
+
* match those of sat2 while the position vector and velocity magnitude
|
|
328
|
+
* (but not direction) after the impulsive burn will be the same as
|
|
329
|
+
* pre-maneuver!
|
|
330
|
+
*
|
|
331
|
+
* The intersection of two arbitrary Keplerian orbits is a single line
|
|
332
|
+
* defined by the cross product of the orbits' angular momenta.
|
|
333
|
+
*
|
|
334
|
+
* The Ascending Mutual node of sat1 is the true anomaly of sat1 at which the
|
|
335
|
+
* mutual nodal vector is positive. The Descending Mutual node of sat1 is the
|
|
336
|
+
* true anomaly of the ascending mutual node plus 180 degrees, modulo 360 (to
|
|
337
|
+
* stay within 0 to 360 bounds).
|
|
338
|
+
*
|
|
339
|
+
* This function evaluates the Dv required at the Ascending Mutual Node and
|
|
340
|
+
* Descending Mutual Node, and chooses the one with the smallest Dv.
|
|
341
|
+
*
|
|
342
|
+
* The direction and components of this minimum Dv depend on two factors:
|
|
343
|
+
* (a) The relative inclination of sat1 and sat2 with respect to the equatorial
|
|
344
|
+
* plane.
|
|
345
|
+
* (b) Whether the burn is performed at the ascending or descending mutual node.
|
|
346
|
+
*
|
|
347
|
+
* The Dv magnitude and direction also depends on the flight path angle at each
|
|
348
|
+
* of those mutual node points, which is generally not the same.
|
|
349
|
+
*
|
|
350
|
+
* Therefore, the process steps are:
|
|
351
|
+
* 1. Compute the angular momentum of each orbit from the pos vel vectors.
|
|
352
|
+
* 2. Compute the mutual line of nodes vector from the orbits' angular momenta.
|
|
353
|
+
* 3. Compute the eccentricity vector of sat1 (i.e. the line of apsides with a
|
|
354
|
+
* direction from apo to periapsis), in the inertial frame
|
|
355
|
+
* 4. Compute the true anomalies of ascending and descending mutual node points
|
|
356
|
+
* for sat1, based on the mutual nodal vector and eccentricity vector.
|
|
357
|
+
* 5. Compute the flight path angle at each of these points, using the Keplerian
|
|
358
|
+
* representation of sat1 (requires conversion of pos, vel to Kepler elements)
|
|
359
|
+
* 6. Compute the magnitude of Dv required at each point.
|
|
360
|
+
* 7. By evaluating the relative inclination of start and final orbit, as well
|
|
361
|
+
* as the nodal point on which to burn, compute the exact components.
|
|
362
|
+
*
|
|
363
|
+
* Note: The burn components are expressed in the Radial,Along-Track,Cross-Track
|
|
364
|
+
* frame, known as RSW by Vallado notation. This is distinct from the NTW frame,
|
|
365
|
+
* which uses the In-track,Cross-Track axes to instantiate it.
|
|
366
|
+
* @param {Object} pv1
|
|
367
|
+
* @param {Object} pv2
|
|
368
|
+
* @return {Object} Delta-v in m/s in RSW frame to perform a single impulsive
|
|
369
|
+
* plane match maneuver of sat1 to sat2.
|
|
370
|
+
*/
|
|
371
|
+
const planeChangeDeltaV = (pv1, pv2) => {
|
|
372
|
+
const mu = 3.986004418e14; // (m^3)/(s^2) WGS-84 Earth Mu
|
|
373
|
+
|
|
374
|
+
// 1. Compute the angular momentum of each orbit from the pos vel vectors
|
|
375
|
+
const r = multiply([pv1.position.x, pv1.position.y, pv1.position.z],
|
|
376
|
+
1000.0);
|
|
377
|
+
const v = multiply([pv1.velocity.x, pv1.velocity.y, pv1.velocity.z],
|
|
378
|
+
1000.0);
|
|
379
|
+
const h1 = cross(r, v);
|
|
380
|
+
|
|
381
|
+
const vMag = norm(v);
|
|
382
|
+
|
|
383
|
+
const r2 = multiply([pv2.position.x, pv2.position.y, pv2.position.z],
|
|
384
|
+
1000.0);
|
|
385
|
+
const v2 = multiply([pv2.velocity.x, pv2.velocity.y, pv2.velocity.z],
|
|
386
|
+
1000.0);
|
|
387
|
+
const h2 = cross(r2, v2);
|
|
388
|
+
|
|
389
|
+
// 2. Compute the mutual line of nodes vector
|
|
390
|
+
const n = cross(h1, h2);
|
|
391
|
+
|
|
392
|
+
// 3. Compute the eccentricity vector of sat1
|
|
393
|
+
const e = multiply(
|
|
394
|
+
(1 / mu),
|
|
395
|
+
subtract(
|
|
396
|
+
(multiply(Math.pow(norm(v, 2), 2) - (mu / norm(r, 2)), r)),
|
|
397
|
+
multiply(dot(r, v), v)),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// 4. Compute the true anomalies of ascending and descending mutual node
|
|
401
|
+
const vAsc = Math.acos((dot(n, e))/(norm(n)* norm(e)));
|
|
402
|
+
const vDesc = (vAsc + Math.PI) % (2* Math.PI);
|
|
403
|
+
|
|
404
|
+
// 5. Compute the flight path angle at each of these points
|
|
405
|
+
const el = cartesianToKeplerian(r, v);
|
|
406
|
+
const gammaAsc = Math.atan(el.e* Math.sin(vAsc)/(1+ el.e* Math.cos(vAsc)));
|
|
407
|
+
const gammaDesc = Math.atan(el.e* Math.sin(vDesc)/(1+ el.e* Math.cos(vDesc)));
|
|
408
|
+
|
|
409
|
+
// 6. Compute the magnitude of Dv required at each point.
|
|
410
|
+
// The absolute inclination difference
|
|
411
|
+
const dihedralAngle = Math.abs(angleBetweenPlanes(pv1, pv2)*DEG2RAD);
|
|
412
|
+
|
|
413
|
+
const dvAsc = 2* vMag * Math.cos(gammaAsc) * Math.sin(dihedralAngle/2);
|
|
414
|
+
const dvDesc = 2* vMag * Math.cos(gammaDesc) * Math.sin(dihedralAngle/2);
|
|
415
|
+
|
|
416
|
+
const el2 = cartesianToKeplerian(r2, v2);
|
|
417
|
+
|
|
418
|
+
// 7. Choose minimum dv, and compute the exact components.
|
|
419
|
+
|
|
420
|
+
// The Along-track component. We need to perform this slight retrograde burn
|
|
421
|
+
// in order to cancel out the eccentricity a pure cross-track burn
|
|
422
|
+
// would introduce.
|
|
423
|
+
let dvI = 0;
|
|
424
|
+
// The Cross-track component
|
|
425
|
+
let dvC = 0;
|
|
426
|
+
|
|
427
|
+
// If sat 1 is higer inc than sat2
|
|
428
|
+
if (el.i >= el2.i) {
|
|
429
|
+
// In-track component is always retrograde to correct for sma and e increase
|
|
430
|
+
dvI = (-1) * vMag * Math.cos(gammaAsc) * (1 - Math.cos(dihedralAngle));
|
|
431
|
+
if (Math.abs(dvAsc) <= Math.abs(dvDesc)) {
|
|
432
|
+
// Burn at ASC node is opposite of the sat1 cross-track axis!
|
|
433
|
+
dvC = (-1) * vMag * Math.cos(gammaAsc) * Math.sin(dihedralAngle);
|
|
434
|
+
} else {
|
|
435
|
+
// Burn at DESC node is aligned with sat1 cross-track
|
|
436
|
+
dvC = (+1) * vMag * Math.cos(gammaAsc) * Math.sin(dihedralAngle);
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
// In-track component is always retrograde to correct for sma and e increase
|
|
440
|
+
dvI = (-1) * vMag * Math.cos(gammaDesc) * (1 - Math.cos(dihedralAngle));
|
|
441
|
+
if (Math.abs(dvAsc) <= Math.abs(dvDesc)) {
|
|
442
|
+
// Burn at ASC node is aligned with sat1 cross-track
|
|
443
|
+
dvC = (+1) * vMag * Math.cos(gammaDesc) * Math.sin(dihedralAngle);
|
|
444
|
+
} else {
|
|
445
|
+
// Burn at DESC node is opposite of the sat1 cross-track axis
|
|
446
|
+
dvC = (-1) * vMag * Math.cos(gammaDesc) * Math.sin(dihedralAngle);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 5. Return the RSW components of the required burn!
|
|
451
|
+
// Note that there's no radial component for this type of burn.
|
|
452
|
+
return {
|
|
453
|
+
r: 0,
|
|
454
|
+
i: dvI,
|
|
455
|
+
c: dvC,
|
|
456
|
+
mag: (dvAsc <= dvDesc) ? dvAsc: dvDesc,
|
|
457
|
+
node: (dvAsc <= dvDesc) ? "asc": "desc",
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
/** Calculates the Minimum delta-v required for sat1 to match the inclination of sat2.
|
|
462
|
+
* This will generally not match the plane of sat1 with that of sat2, but only their inclination.
|
|
463
|
+
*
|
|
464
|
+
* For a maneuver that perform a plane match between sat1 and sat2, see the
|
|
465
|
+
* function planeChangeDeltaV, which accounts for the RAAN AND inclination change,
|
|
466
|
+
* to exactly align the orbital plane with the least Dv.
|
|
467
|
+
*
|
|
468
|
+
* Reference: https://ai-solutions.com/_freeflyeruniversityguide/plane_change_maneuver.htm
|
|
469
|
+
*
|
|
470
|
+
* The delta-v calculation is performed under the assumption that it will generate a
|
|
471
|
+
* new orbit for sat1 whose Orbital elements are all the same except for inclination,
|
|
472
|
+
* which will match that of sat2!
|
|
473
|
+
*
|
|
474
|
+
* This assumption is possible only by additionally assuming that such am impulsive
|
|
475
|
+
* maneuver will be performed at a sat1 nodal crossing. This algorithm specifies the node at which
|
|
476
|
+
* the plane change maneuver will require the least amount of fuel.
|
|
477
|
+
*
|
|
478
|
+
* The algorithm is applicable for all closed, planar, elliptical orbits.
|
|
479
|
+
* Note the contribution of the flight path angle in the calculations,
|
|
480
|
+
* which guarantees this algorithm's applicability for eccentic orbits.
|
|
481
|
+
* The flight path angle is separately computed for the ascending and descending node.
|
|
482
|
+
*
|
|
483
|
+
* For more information, consult Vallado's Algorithm 39 and Example 6-4 in Edition 4.
|
|
484
|
+
*
|
|
485
|
+
* @param {Object} pv1 Position and Velocity Vector of Satelltie 1 at Time = x
|
|
486
|
+
* @param {Object} pv2 Position and velocity Vector of Satellite 2 at Time = x
|
|
487
|
+
* @return {Object} Delta-v in m/s to perform a pure inclination plane change at
|
|
488
|
+
* the sat1 asc or desc node.
|
|
489
|
+
*/
|
|
490
|
+
const planeChangePureInclinationDeltaV = (pv1, pv2)=>{
|
|
491
|
+
// 1. Get position and velocity vectors and magnitudes.
|
|
492
|
+
// Note that the magnitude of the final velocity for sat1 will be EQUAL
|
|
493
|
+
// to this initial velocity magnitude of sat1!
|
|
494
|
+
const r = multiply([pv1.position.x, pv1.position.y, pv1.position.z], 1000.0);
|
|
495
|
+
const v = multiply([pv1.velocity.x, pv1.velocity.y, pv1.velocity.z], 1000.0);
|
|
496
|
+
|
|
497
|
+
// Velocity Magnitude in m/s
|
|
498
|
+
const vMag = norm(v);
|
|
499
|
+
|
|
500
|
+
const r2 = multiply([pv2.position.x, pv2.position.y, pv2.position.z], 1000.0);
|
|
501
|
+
const v2 = multiply([pv2.velocity.x, pv2.velocity.y, pv2.velocity.z], 1000.0);
|
|
502
|
+
|
|
503
|
+
// 2. Compute the Keplerian Elements of both satellites.
|
|
504
|
+
const el = cartesianToKeplerian(r, v);
|
|
505
|
+
const el2 = cartesianToKeplerian(r2, v2);
|
|
506
|
+
|
|
507
|
+
// 3. Compute the desired inclination change, based on sat1 starting orbit and sat2 orbit
|
|
508
|
+
// CAREFUL: Do not use the function angleBetweenPlanes. It would not be correct
|
|
509
|
+
// in this context, because the result of that function integrates the changes
|
|
510
|
+
// in inclination AND raan, since it simply computes the
|
|
511
|
+
// angular momentum vector angle!!!
|
|
512
|
+
const incRad = Math.abs(el.i - el2.i)*DEG2RAD;
|
|
513
|
+
|
|
514
|
+
// 4a. Compute the flight path angle and DV at ascending node.
|
|
515
|
+
// The flight path angle in this context is the angle between the inertial
|
|
516
|
+
// velocity vector and local horizontal plane.
|
|
517
|
+
|
|
518
|
+
// The true anomaly of a satellite at the ascending node is equal to the negative of the
|
|
519
|
+
// argument of periapsis. We use this trick to compute the flight path angle at that location.
|
|
520
|
+
const fAtAsc = (-1) * Math.abs(el.w);
|
|
521
|
+
const gammaAscending = Math.atan(
|
|
522
|
+
el.e* Math.sin(fAtAsc*DEG2RAD)/(1+ el.e* Math.cos(fAtAsc*DEG2RAD)));
|
|
523
|
+
const dvAsc = 2* vMag * Math.cos(gammaAscending) * Math.sin(incRad/2);
|
|
524
|
+
|
|
525
|
+
// 4b. Compute the flight path angle and DV at descending node
|
|
526
|
+
const fAtDesc = fAtAsc + 180.0;
|
|
527
|
+
const gammaDescending = Math.atan(
|
|
528
|
+
el.e* Math.sin(fAtDesc*DEG2RAD)/(1+ el.e* Math.cos(fAtDesc*DEG2RAD)));
|
|
529
|
+
const dvDesc = 2* vMag * Math.cos(gammaDescending) * Math.sin(incRad/2);
|
|
530
|
+
|
|
531
|
+
// The Along-track component. We need to perform this slight retrograde burn
|
|
532
|
+
// in order to cancel out the eccentricity a pure cross-track burn
|
|
533
|
+
// would introduce.
|
|
534
|
+
let dvI = 0;
|
|
535
|
+
// The Cross-track component
|
|
536
|
+
let dvC = 0;
|
|
537
|
+
|
|
538
|
+
// 5. Compute the cross-track and along-track burn components in the satellite's
|
|
539
|
+
// RSW frame (Radial, Along-Track, Cross-track)
|
|
540
|
+
// Note, in general, Along-Track is NOT the same as In-Track!
|
|
541
|
+
|
|
542
|
+
// The direction of the along and cross-track components will depend on the relative inclination of sat1 to sat2,
|
|
543
|
+
// In addition, we try to find the node with the smaller burn, in each of those cases of relative incliantion.
|
|
544
|
+
|
|
545
|
+
// If sat 1 is higer inc than sat2
|
|
546
|
+
// In-track component is always retrograde to correct for sma and e increase
|
|
547
|
+
if (el.i >= el2.i) {
|
|
548
|
+
if (Math.abs(dvAsc) <= Math.abs(dvDesc)) {
|
|
549
|
+
// Burn at ASC node is opposite of the sat1 cross-track axis!
|
|
550
|
+
dvC = (-1) * vMag * Math.cos(gammaAscending) * Math.sin(incRad);
|
|
551
|
+
dvI = (-1) * vMag * Math.cos(gammaAscending) * (1 - Math.cos(incRad));
|
|
552
|
+
} else {
|
|
553
|
+
// Burn at DESC node is aligned with sat1 cross-track
|
|
554
|
+
dvC = (+1) * vMag * Math.cos(gammaDescending) * Math.sin(incRad);
|
|
555
|
+
dvI = (-1) * vMag * Math.cos(gammaDescending) * (1 - Math.cos(incRad));
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
if (Math.abs(dvAsc) <= Math.abs(dvDesc)) {
|
|
559
|
+
// Burn at ASC node is aligned with sat1 cross-track
|
|
560
|
+
dvC = (+1) * vMag * Math.cos(gammaAscending) * Math.sin(incRad);
|
|
561
|
+
dvI = (-1) * vMag * Math.cos(gammaAscending) * (1 - Math.cos(incRad));
|
|
562
|
+
} else {
|
|
563
|
+
// Burn at DESC node is opposite of the sat1 cross-track axis
|
|
564
|
+
dvC = (-1) * vMag * Math.cos(gammaDescending) * Math.sin(incRad);
|
|
565
|
+
dvI = (-1) * vMag * Math.cos(gammaDescending) * (1 - Math.cos(incRad));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// 5. Return the RSW components of the required burn!
|
|
570
|
+
// Note that there's no radial component for this type of burn.
|
|
571
|
+
return {
|
|
572
|
+
r: 0,
|
|
573
|
+
i: dvI,
|
|
574
|
+
c: dvC,
|
|
575
|
+
mag: (dvAsc <= dvDesc) ? dvAsc: dvDesc,
|
|
576
|
+
node: (dvAsc <= dvDesc) ? "asc": "desc",
|
|
577
|
+
};
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Takes two StateVectors from two satellits at the same time, and calculates the angle between their orbit planes.
|
|
582
|
+
* This angle DOES NOT directly translate to an inclination delta, but a combined effect of inclination AND raan difference between
|
|
583
|
+
* the two states, since an orbit's plane orientation is defined by both of those parameters!
|
|
584
|
+
* To do this, we need to find vector normal to each plane and find the cross product (angle) between them instead.
|
|
585
|
+
* Step 1: Calculate RxV for each satellite.
|
|
586
|
+
* Step 2: Normalize the RxV vectors to a unit vector
|
|
587
|
+
* Step 3: Theta = Perform the dot product between the two RxV normalized vectors
|
|
588
|
+
* Step 4: ArcCos(Theta) = angle between the planes
|
|
589
|
+
* @param {Object} pv1 Position and Velocity Vector of Satelltie 1 at Time = x
|
|
590
|
+
* @param {Object} pv2 Position and velocity Vector of Satellite 2 at Time = x
|
|
591
|
+
* @return {Number} Angle between the two planes in radians
|
|
592
|
+
*/
|
|
593
|
+
const angleBetweenPlanes = (pv1, pv2) => {
|
|
594
|
+
// Calculate Cross Products and Normalize
|
|
595
|
+
const cross1 = cross(
|
|
596
|
+
[pv1.position.x, pv1.position.y, pv1.position.z],
|
|
597
|
+
[pv1.velocity.x, pv1.velocity.y, pv1.velocity.z],
|
|
598
|
+
);
|
|
599
|
+
const cross1mag = Math.sqrt(
|
|
600
|
+
cross1[0] * cross1[0]
|
|
601
|
+
+ cross1[1] * cross1[1]
|
|
602
|
+
+ cross1[2] * cross1[2]);
|
|
603
|
+
|
|
604
|
+
const cross1Norm = {
|
|
605
|
+
x: cross1[0] / cross1mag,
|
|
606
|
+
y: cross1[1] / cross1mag,
|
|
607
|
+
z: cross1[2] / cross1mag,
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const cross2 = cross(
|
|
611
|
+
[pv2.position.x, pv2.position.y, pv2.position.z],
|
|
612
|
+
[pv2.velocity.x, pv2.velocity.y, pv2.velocity.z],
|
|
613
|
+
);
|
|
614
|
+
const cross2Mag = Math.sqrt(
|
|
615
|
+
cross2[0] * cross2[0]
|
|
616
|
+
+ cross2[1] * cross2[1]
|
|
617
|
+
+ cross2[2] * cross2[2]);
|
|
618
|
+
|
|
619
|
+
const cross2Norm = {
|
|
620
|
+
x: cross2[0] / cross2Mag,
|
|
621
|
+
y: cross2[1] / cross2Mag,
|
|
622
|
+
z: cross2[2] / cross2Mag,
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// Calculate the dot products
|
|
626
|
+
const dotProducts = cross1Norm.x * cross2Norm.x
|
|
627
|
+
+ cross1Norm.y * cross2Norm.y
|
|
628
|
+
+ cross1Norm.z * cross2Norm.z;
|
|
629
|
+
|
|
630
|
+
// If the values are outside -1 to 1, it will clamp it to -1 or 1.
|
|
631
|
+
const clamped = Math.max(-1, Math.min(1, dotProducts));
|
|
632
|
+
|
|
633
|
+
// Extract the angle from the dot products in degrees
|
|
634
|
+
const angle = Math.acos(clamped) * (180.0 / Math.PI);
|
|
635
|
+
|
|
636
|
+
// Round result to 3 decimal points and return
|
|
637
|
+
return Math.round(angle * 1000) / 1000;
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Calculates the position and velocity of a satellite at a point in time using SGP4
|
|
642
|
+
*
|
|
643
|
+
* @param {Object} satRec twoline2satrec
|
|
644
|
+
* @param {Integer} time UNIX time stamp to evaluate at
|
|
645
|
+
* @return {Object} Tracking position and velocity of satellite at given point of time, or null on error
|
|
646
|
+
*/
|
|
647
|
+
const propTo = (satRec, time) => {
|
|
648
|
+
const pv = propagate(satRec, new Date(time));
|
|
649
|
+
if ( !isDefined(pv))
|
|
650
|
+
return null;
|
|
651
|
+
return {
|
|
652
|
+
p: pv.position,
|
|
653
|
+
v: pv.velocity,
|
|
654
|
+
t: time,
|
|
655
|
+
meanElements: {
|
|
656
|
+
semimajor: pv.meanElements.am,
|
|
657
|
+
eccentricity: pv.meanElements.em,
|
|
658
|
+
inclination: pv.meanElements.im,
|
|
659
|
+
raan: pv.meanElements.Om,
|
|
660
|
+
argp: pv.meanElements.om,
|
|
661
|
+
meanmotion: pv.meanElements.nm,
|
|
662
|
+
meananomaly: pv.meanElements.mm,
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Calculates the position and velocity of a satellite over a period of time (start, end) using SGP4
|
|
669
|
+
*
|
|
670
|
+
* @param {Object} elset {Line1, Line2}
|
|
671
|
+
* @param {Integer} start UNIX time stamp
|
|
672
|
+
* @param {Integer} end UNIX time stamp
|
|
673
|
+
* @param {Integer} stepMs The step size in ms
|
|
674
|
+
* @return {Array} Array of objects tracking positions and velocities of a satellite at given points of time (stepMs apart)
|
|
675
|
+
*/
|
|
676
|
+
const prop = (elset, start, end, stepMs = 1000) => {
|
|
677
|
+
const ephem = [];
|
|
678
|
+
const sat = twoline2satrec(elset.Line1, elset.Line2);
|
|
679
|
+
for (let i = start; i < end; i = i + stepMs) {
|
|
680
|
+
const pv = propTo(sat, i);
|
|
681
|
+
if (!isDefined(pv)) {
|
|
682
|
+
return [];
|
|
683
|
+
}
|
|
684
|
+
ephem.push(pv);
|
|
685
|
+
}
|
|
686
|
+
return ephem; // Ephemeris
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Calculates the geodetic position of a satellite over a period of time (start, end) using SGP4
|
|
691
|
+
* @param {Object} elset {Line1, Line2}
|
|
692
|
+
* @param {Integer} start UNIX time stamp
|
|
693
|
+
* @param {Integer} end UNIX time stamp, inclusive
|
|
694
|
+
* @param {Integer} stepMs The step size in ms
|
|
695
|
+
* @return {Array} Array of objects tracking geodetic positions of a satellite at given points of time (stepMs apart)
|
|
696
|
+
*/
|
|
697
|
+
const propGeodetic = (elset, start, end, stepMs = 60000) => {
|
|
698
|
+
const positions = [];
|
|
699
|
+
const sat = twoline2satrec(elset.Line1, elset.Line2);
|
|
700
|
+
for (let t = start; t <= end; t = t + stepMs) {
|
|
701
|
+
const pv = propagate(sat, new Date(t));
|
|
702
|
+
if (!isDefined(pv.position)) return [];
|
|
703
|
+
const gmst = gstime(new Date(t));
|
|
704
|
+
const positionGd = eciToGeodetic(pv.position, gmst);
|
|
705
|
+
positions.push({lat: positionGd.latitude*RAD2DEG, lon: positionGd.longitude*RAD2DEG, t: t});
|
|
706
|
+
}
|
|
707
|
+
return positions;
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const doesLineSegmentSphereIntersect = (linePoint0, linePoint1, circleCenter, circleRadius) => {
|
|
711
|
+
// From Space Cockpit
|
|
712
|
+
// http://www.codeproject.com/Articles/19799/Simple-Ray-Tracing-in-C-Part-II-Triangles-Intersec
|
|
713
|
+
|
|
714
|
+
// Input validation
|
|
715
|
+
if (circleRadius < 0) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const cx = circleCenter.x;
|
|
720
|
+
const cy = circleCenter.y;
|
|
721
|
+
const cz = circleCenter.z;
|
|
722
|
+
|
|
723
|
+
const px = linePoint0.x;
|
|
724
|
+
const py = linePoint0.y;
|
|
725
|
+
const pz = linePoint0.z;
|
|
726
|
+
|
|
727
|
+
const vx = linePoint1.x - px;
|
|
728
|
+
const vy = linePoint1.y - py;
|
|
729
|
+
const vz = linePoint1.z - pz;
|
|
730
|
+
|
|
731
|
+
const a = vx * vx + vy * vy + vz * vz;
|
|
732
|
+
|
|
733
|
+
// Check for zero-length line segment
|
|
734
|
+
if (Math.abs(a) < Number.EPSILON) {
|
|
735
|
+
// If point is on sphere surface, count as intersection
|
|
736
|
+
const distanceSquared
|
|
737
|
+
= (px - cx) * (px - cx) + (py - cy) * (py - cy) + (pz - cz) * (pz - cz);
|
|
738
|
+
return Math.abs(distanceSquared - circleRadius * circleRadius) < Number.EPSILON;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const b = 2.0 * (px * vx + py * vy + pz * vz - vx * cx - vy * cy - vz * cz);
|
|
742
|
+
const c = px * px - 2 * px * cx + cx * cx + py * py - 2 * py * cy + cy * cy
|
|
743
|
+
+ pz * pz - 2 * pz * cz + cz * cz - circleRadius * circleRadius;
|
|
744
|
+
|
|
745
|
+
// discriminant
|
|
746
|
+
const d = b * b - 4 * a * c;
|
|
747
|
+
|
|
748
|
+
if (d < 0) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const sqrtD = Math.sqrt(d);
|
|
753
|
+
const t1 = (-b - sqrtD) / (2.0 * a);
|
|
754
|
+
const t2 = (-b + sqrtD) / (2.0 * a);
|
|
755
|
+
|
|
756
|
+
// Check if either intersection point lies within the line segment bounds [0,1]
|
|
757
|
+
return (t1 >= 0 && t1 <= 1) || (t2 >= 0 && t2 <= 1);
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const doesLineSegmentIntersectEarth = (start, end) => {
|
|
761
|
+
return doesLineSegmentSphereIntersect(start, end,
|
|
762
|
+
{x: 0, y: 0, z: 0}, WGS72_EARTH_EQUATORIAL_RADIUS_KM);
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Given primary and target ephems, calculate the time-based, radial, intrack, and crosstrack data
|
|
767
|
+
*
|
|
768
|
+
* @param {Array} pEphem primary ephems [{p{x,y,z}, v, t}]
|
|
769
|
+
* @param {Array} tEphem target ephems [{p{x,y,z}, v, t}]
|
|
770
|
+
* @return {Object} Time, Radial, intrack, crosstrack, absolute distances, sun angles
|
|
771
|
+
*/
|
|
772
|
+
const getTRIC = (pEphem, tEphem) => {
|
|
773
|
+
// Relative positions in RIC format.
|
|
774
|
+
const t = []; // Time
|
|
775
|
+
const r = []; // Radial (X)
|
|
776
|
+
const i = []; // Intrack (y)
|
|
777
|
+
const c = []; // CrossTrack (z)
|
|
778
|
+
const d = []; // Relative Absolute distance
|
|
779
|
+
const s = []; // Sun Angles
|
|
780
|
+
const targetVisibility = []; // Target visibility from primary
|
|
781
|
+
const primaryVisibility = []; // Primary visibility from target
|
|
782
|
+
const isPrimaryEpoch = []; // Whether point is primary epoch
|
|
783
|
+
const isTargetEpoch = []; // Whether point is target epoch
|
|
784
|
+
const sources = []; // Sources
|
|
785
|
+
|
|
786
|
+
for (let j = 0; j < pEphem.length; j++) {
|
|
787
|
+
const prim = pEphem[j];
|
|
788
|
+
const target = tEphem[j];
|
|
789
|
+
|
|
790
|
+
t.push(prim.t);
|
|
791
|
+
isPrimaryEpoch.push((!!prim.isEpoch)+""); // it can't be a boolean for some reason
|
|
792
|
+
isTargetEpoch.push((!!target.isEpoch)+"");
|
|
793
|
+
sources.push(prim.source + ", " + target.source);
|
|
794
|
+
|
|
795
|
+
// Calculate RIC values.
|
|
796
|
+
const deltaR = {
|
|
797
|
+
x: prim.p.x - target.p.x,
|
|
798
|
+
y: prim.p.y - target.p.y,
|
|
799
|
+
z: prim.p.z - target.p.z,
|
|
800
|
+
};
|
|
801
|
+
const cartToRICMatrix = cartesianToRIC(prim.p, prim.v);
|
|
802
|
+
const ricP = multiplyVector(deltaR, cartToRICMatrix);
|
|
803
|
+
r.push(Math.round(ricP.x * 10000) / 10000);
|
|
804
|
+
i.push(Math.round(ricP.y * 10000) / 10000);
|
|
805
|
+
c.push(Math.round(ricP.z * 10000) / 10000);
|
|
806
|
+
|
|
807
|
+
const time = new Date(prim.t);
|
|
808
|
+
s.push(
|
|
809
|
+
angleBetween3DCoords(
|
|
810
|
+
prim.p,
|
|
811
|
+
target.p,
|
|
812
|
+
getSunDirection(time),
|
|
813
|
+
),
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
const earthEclipsed = doesLineSegmentIntersectEarth(prim.p, target.p);
|
|
817
|
+
const primaryShadow = getEclipseStatus(time, posToArray(prim.p));
|
|
818
|
+
const targetShadow = getEclipseStatus(time, posToArray(target.p));
|
|
819
|
+
targetVisibility.push(earthEclipsed ? "EARTH ECLIPSED" : targetShadow);
|
|
820
|
+
primaryVisibility.push(earthEclipsed ? "EARTH ECLIPSED" : primaryShadow);
|
|
821
|
+
|
|
822
|
+
d.push(Math.round(dist(prim.p, target.p) * 1000) / 1000);
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
t,
|
|
826
|
+
r,
|
|
827
|
+
i,
|
|
828
|
+
c,
|
|
829
|
+
d,
|
|
830
|
+
s,
|
|
831
|
+
isPrimaryEpoch,
|
|
832
|
+
isTargetEpoch,
|
|
833
|
+
sources,
|
|
834
|
+
targetVisibility,
|
|
835
|
+
primaryVisibility,
|
|
836
|
+
};
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Calculates the sun's direction/distance based on a given time to return position relative to center of earth
|
|
841
|
+
*
|
|
842
|
+
* @param {Object} time new Date() object generated using the time of interest
|
|
843
|
+
*
|
|
844
|
+
* @return {Object} 3 point coordinate in 3D space
|
|
845
|
+
*/
|
|
846
|
+
const getSunDirection = (time) => {
|
|
847
|
+
const year = time.getUTCFullYear();
|
|
848
|
+
const month = time.getUTCMonth() + 1; // index 0
|
|
849
|
+
const day = time.getUTCDate();
|
|
850
|
+
const hour = time.getUTCHours();
|
|
851
|
+
const minute = time.getUTCMinutes();
|
|
852
|
+
const seconds = time.getUTCSeconds();
|
|
853
|
+
const JD
|
|
854
|
+
= 367 * year
|
|
855
|
+
- Math.floor((7.0 * (year + Math.floor((month + 9.0) / 12.0))) / 4.0)
|
|
856
|
+
+ Math.floor((275.0 * month) / 9.0)
|
|
857
|
+
+ day
|
|
858
|
+
+ 1721013.5
|
|
859
|
+
+ hour / 24.0
|
|
860
|
+
+ minute / 1440.0
|
|
861
|
+
+ seconds / 86400.0;
|
|
862
|
+
const UT1 = (JD - 2451545) / 36525;
|
|
863
|
+
const longMSUN = 280.4606184 + 36000.77005361 * UT1;
|
|
864
|
+
const mSUN = 357.5277233 + 35999.05034 * UT1;
|
|
865
|
+
const ecliptic
|
|
866
|
+
= longMSUN
|
|
867
|
+
+ 1.914666471 * Math.sin(mSUN * DEG2RAD)
|
|
868
|
+
+ 0.918994643 * Math.sin(2 * mSUN * DEG2RAD);
|
|
869
|
+
const eccen = 23.439291 - 0.0130042 * UT1;
|
|
870
|
+
|
|
871
|
+
// Direction
|
|
872
|
+
let x = Math.cos(ecliptic * DEG2RAD);
|
|
873
|
+
let y = Math.cos(eccen * DEG2RAD) * Math.sin(ecliptic * DEG2RAD);
|
|
874
|
+
let z = Math.sin(eccen * DEG2RAD) * Math.sin(ecliptic * DEG2RAD);
|
|
875
|
+
|
|
876
|
+
// Distance
|
|
877
|
+
const sunDistance = 0.989 * 1.496e8;
|
|
878
|
+
x = x * sunDistance;
|
|
879
|
+
y = y * sunDistance;
|
|
880
|
+
z = z * sunDistance;
|
|
881
|
+
return {x, y, z};
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Converts a Right Ascension & Declination in Sensor Centered Coordinates
|
|
886
|
+
* into the Latitude and Longitude of a satellite.
|
|
887
|
+
* Slight error as we are not accounting for polar motion of the Earth, it's about 0.01 deg lat and long and 100km range error.
|
|
888
|
+
* @param {String} ObTimeUtc, date-time string of the observation
|
|
889
|
+
* @param {Number} Ra, Right Ascension of the observation
|
|
890
|
+
* @param {Number} Dec, Declination of the observation
|
|
891
|
+
* @param {Number} senLat, Sensor's Latitude
|
|
892
|
+
* @param {Number} senLon, Sensor's Longitude
|
|
893
|
+
* @param {Number} senAltKm, Sensor's Altitude in Km (defaults to sea-level)
|
|
894
|
+
* @param {Number} rangeKm, The Range of the observation in Km from the observer
|
|
895
|
+
* @return {Object} {latitude, longitude, altitude}
|
|
896
|
+
*/
|
|
897
|
+
const RaDecToGeodetic = (
|
|
898
|
+
ObTimeUtc,
|
|
899
|
+
Ra,
|
|
900
|
+
Dec,
|
|
901
|
+
senLat,
|
|
902
|
+
senLon,
|
|
903
|
+
senAltKm,
|
|
904
|
+
rangeKm,
|
|
905
|
+
) => {
|
|
906
|
+
// check for nulls
|
|
907
|
+
if (
|
|
908
|
+
!isDefined(ObTimeUtc)
|
|
909
|
+
|| !isDefined(Ra)
|
|
910
|
+
|| !isDefined(Dec)
|
|
911
|
+
|| !isDefined(senLat)
|
|
912
|
+
|| !isDefined(senLon)
|
|
913
|
+
|| !isDefined(senAltKm)
|
|
914
|
+
|| !isDefined(rangeKm)
|
|
915
|
+
) {
|
|
916
|
+
return {Altitude: null, Latitude: null, Longitude: null};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const time = squid.EpochUTC.fromDateString(
|
|
920
|
+
dtStrtoJsDt(ObTimeUtc).toISOString(),
|
|
921
|
+
);
|
|
922
|
+
const sensorGeo = new squid.Geodetic(
|
|
923
|
+
senLat * DEG2RAD,
|
|
924
|
+
senLon * DEG2RAD,
|
|
925
|
+
senAltKm,
|
|
926
|
+
);
|
|
927
|
+
const sensorItrf = sensorGeo.toITRF(time);
|
|
928
|
+
const sensorGcrf = sensorItrf.toJ2000();
|
|
929
|
+
const sat = new squid.J2000(
|
|
930
|
+
time,
|
|
931
|
+
new squid.Vector3D(
|
|
932
|
+
rangeKm * Math.cos(Ra * DEG2RAD) * Math.cos(Dec * DEG2RAD),
|
|
933
|
+
rangeKm * Math.sin(Ra * DEG2RAD) * Math.cos(Dec * DEG2RAD),
|
|
934
|
+
rangeKm * Math.sin(Dec * DEG2RAD),
|
|
935
|
+
),
|
|
936
|
+
);
|
|
937
|
+
const satGcrf = new squid.J2000(
|
|
938
|
+
time,
|
|
939
|
+
sensorGcrf.position.add(sat.position),
|
|
940
|
+
);
|
|
941
|
+
const satItrf = satGcrf.toITRF();
|
|
942
|
+
const satLatLng = satItrf.toGeodetic();
|
|
943
|
+
return {
|
|
944
|
+
Altitude: satLatLng.altitude,
|
|
945
|
+
Latitude: satLatLng.latitude * RAD2DEG,
|
|
946
|
+
Longitude: satLatLng.longitude * RAD2DEG,
|
|
947
|
+
};
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* This function estimates the slant range from a sensor to an observed satellite in GEO.
|
|
952
|
+
*
|
|
953
|
+
* It uses the sine law to first solve for the sat-sensor-earth angle, and then uses
|
|
954
|
+
* the cosine law to solve for the sat-earth-sensor angle.
|
|
955
|
+
*
|
|
956
|
+
* Finally, it uses the sine law again to get the range from sensor to satellite (i.e. the slant range)
|
|
957
|
+
*
|
|
958
|
+
* @param {String} obTime The ISO string of the observation time
|
|
959
|
+
* @param {Number} ra Topocentric Right Ascension of the observation, in degrees
|
|
960
|
+
* @param {Number} dec Topocentric Declination of the observation, in degrees
|
|
961
|
+
* @param {Number} senLat Sensor Latitude, in degrees
|
|
962
|
+
* @param {Number} senLon Sensor Lontitude, in degrees
|
|
963
|
+
* @param {Number} senAltKm Sensor altitude, in degrees
|
|
964
|
+
* @return {Number} range The range from the sensor to the satellite
|
|
965
|
+
*/
|
|
966
|
+
const estimateSlantRange = (obTime, ra, dec, senLat, senLon, senAltKm) => {
|
|
967
|
+
// A very rough initial guess, akso initialization
|
|
968
|
+
let range = 35786;
|
|
969
|
+
|
|
970
|
+
// Convert ra/dec to rectangular coordinates.
|
|
971
|
+
// It is assumed that ra/dec is topocentric, non-null, and well defined.
|
|
972
|
+
|
|
973
|
+
const phi = Math.PI/2 - dec * DEG2RAD;
|
|
974
|
+
const theta = ra* DEG2RAD;
|
|
975
|
+
const x = Math.sin(phi)* Math.cos(theta);
|
|
976
|
+
const y = Math.sin(phi)* Math.sin(theta);
|
|
977
|
+
const z = Math.cos(phi);
|
|
978
|
+
|
|
979
|
+
// A unit vector representing the line-of-site from sensor to sat
|
|
980
|
+
const slantU = [-x, -y, -z];
|
|
981
|
+
|
|
982
|
+
// Sensor coordinates
|
|
983
|
+
const sensorGeo = new squid.Geodetic(
|
|
984
|
+
senLat * DEG2RAD,
|
|
985
|
+
senLon * DEG2RAD,
|
|
986
|
+
senAltKm,
|
|
987
|
+
);
|
|
988
|
+
const time = squid.EpochUTC.fromDateString(obTime);
|
|
989
|
+
const sensorItrf = sensorGeo.toITRF(time);
|
|
990
|
+
const sensorGcrf = sensorItrf.toJ2000();
|
|
991
|
+
|
|
992
|
+
// Compute the distance of the sensor from the Earth's center
|
|
993
|
+
const a = norm([sensorGcrf.position.x, sensorGcrf.position.y, sensorGcrf.position.z], 2);
|
|
994
|
+
|
|
995
|
+
// Normalize the sensor position to a unit vector
|
|
996
|
+
const cVec = normalize(
|
|
997
|
+
{x: sensorGcrf.position.x, y: sensorGcrf.position.y, z: sensorGcrf.position.z});
|
|
998
|
+
// Get the angle between the slant vector and the sensor vector
|
|
999
|
+
const beta = getAngle(slantU, [cVec.x, cVec.y, cVec.z]);
|
|
1000
|
+
|
|
1001
|
+
// Distance of a GEO satellite wrt to the Earth's center.
|
|
1002
|
+
const b = 42164;
|
|
1003
|
+
// Law of Sines ratio
|
|
1004
|
+
const delta = b / Math.sin(beta);
|
|
1005
|
+
// Using Law of cosines, isolate and solve the sensor, earth, satellite angle. Always two solutions exist.
|
|
1006
|
+
const roots = polynomialRoot(
|
|
1007
|
+
(Math.pow(a, 2) + Math.pow(b, 2) - Math.pow(delta, 2)), (-2*a*b), Math.pow(delta, 2));
|
|
1008
|
+
|
|
1009
|
+
// The two solutions, only one is valid. Validity check is based on the fact that the sum of triangle angles is 180.
|
|
1010
|
+
const gamma1 = Math.acos(roots[0]);
|
|
1011
|
+
const gamma2 = Math.acos(roots[1]);
|
|
1012
|
+
|
|
1013
|
+
// Compute the sensor-satellite-earth angle
|
|
1014
|
+
const alpha = Math.sin(delta/a);
|
|
1015
|
+
// alpha + beta + gamma NEEDS to be 180
|
|
1016
|
+
// Choose the gamma that best achieves this
|
|
1017
|
+
const targetGamma = Math.PI-alpha-beta;
|
|
1018
|
+
if (Math.abs(targetGamma-gamma1) < Math.abs(targetGamma-gamma2)) {
|
|
1019
|
+
range = delta* Math.sin(gamma1);
|
|
1020
|
+
} else {
|
|
1021
|
+
range = delta* Math.sin(gamma2);
|
|
1022
|
+
}
|
|
1023
|
+
return range;
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Converts the Azimuth (az) and Elevation (el) of an object to its Right Ascension
|
|
1028
|
+
* (Ra) and Declination (Declination)
|
|
1029
|
+
* @param {*} ObTimeUtc The datetimestamp in UTC format as a string
|
|
1030
|
+
* @param {*} az Azimuth in degrees
|
|
1031
|
+
* @param {*} el Elevation in degrees
|
|
1032
|
+
* @param {*} lat Latitude of observer in degrees
|
|
1033
|
+
* @param {*} lon Longitude of observer in degrees
|
|
1034
|
+
* @return {Object} {ra, dec} Right Ascension and Declination in degrees.
|
|
1035
|
+
*/
|
|
1036
|
+
const AzElToRaDec = (ObTimeUtc, az, el, lat, lon) => {
|
|
1037
|
+
// check for nulls
|
|
1038
|
+
if (!isDefined(ObTimeUtc)
|
|
1039
|
+
|| !isDefined(az)
|
|
1040
|
+
|| !isDefined(el)
|
|
1041
|
+
|| !isDefined(lat)
|
|
1042
|
+
|| !isDefined(lon)) {
|
|
1043
|
+
return {ra: null, dec: null};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const time = squid.EpochUTC.fromDateString(ObTimeUtc);
|
|
1047
|
+
|
|
1048
|
+
// Convert all angles to radians
|
|
1049
|
+
az = az * DEG2RAD;
|
|
1050
|
+
el = el * DEG2RAD;
|
|
1051
|
+
lat = lat * DEG2RAD;
|
|
1052
|
+
lon = lon * DEG2RAD;
|
|
1053
|
+
|
|
1054
|
+
const dec = Math.asin(
|
|
1055
|
+
Math.sin(el) * Math.sin(lat)
|
|
1056
|
+
+ Math.cos(el) * Math.cos(lat) * Math.cos(az),
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
const gmst = time.gmstAngle();
|
|
1060
|
+
const lmst = lon + gmst;
|
|
1061
|
+
|
|
1062
|
+
const H = Math.atan2(
|
|
1063
|
+
-Math.sin(az) * Math.cos(el) * Math.cos(lat),
|
|
1064
|
+
Math.sin(el) - Math.sin(lat) * Math.sin(dec),
|
|
1065
|
+
);
|
|
1066
|
+
|
|
1067
|
+
const ra = (lmst - H) % (Math.PI * 2);
|
|
1068
|
+
|
|
1069
|
+
// Return the right ascension and declination as an object
|
|
1070
|
+
return {ra: ra * RAD2DEG, dec: dec * RAD2DEG};
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Convert the Right Ascension (RA) and Declination (Dec) of an object in space to its
|
|
1075
|
+
* Azimuth (Az) and Elevation (El) given the object's RaDec, the latitude and longitude of the observer's location,
|
|
1076
|
+
* and the current local sidereal time (LST). The function will return an object with properties az and el representing the
|
|
1077
|
+
* Azimuth and Elevation of the object in degrees.}
|
|
1078
|
+
* @param {*} ObTime The time of the observation in Utc as a string in YYYY-MM-DDThh:mm:ss.ssssssZ
|
|
1079
|
+
* @param {*} Ra Right Ascension in degrees
|
|
1080
|
+
* @param {*} Dec Declination in degrees
|
|
1081
|
+
* @param {*} SenLat Latitude of observer in degrees
|
|
1082
|
+
* @param {*} SenLon Longitude of observer in degrees
|
|
1083
|
+
* @return {Object} {Az, El}
|
|
1084
|
+
*/
|
|
1085
|
+
const RaDecToAzEl = (ObTime, Ra, Dec, SenLat, SenLon) => {
|
|
1086
|
+
if (!isDefined(ObTime)
|
|
1087
|
+
|| !isDefined(Ra)
|
|
1088
|
+
|| !isDefined(Dec)
|
|
1089
|
+
|| !isDefined(SenLat)
|
|
1090
|
+
|| !isDefined(SenLon)) {
|
|
1091
|
+
return {Az: null, El: null};
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// senAlt and slantRange are ambiguous
|
|
1095
|
+
const SenAlt = 0;
|
|
1096
|
+
const SlantRangeKm = 1;
|
|
1097
|
+
|
|
1098
|
+
const time = squid.EpochUTC.fromDateString(ObTime);
|
|
1099
|
+
const sensorGeo = new squid.Geodetic(
|
|
1100
|
+
SenLat * DEG2RAD,
|
|
1101
|
+
SenLon * DEG2RAD,
|
|
1102
|
+
SenAlt,
|
|
1103
|
+
);
|
|
1104
|
+
const sensorITRF = sensorGeo.toITRF(time);
|
|
1105
|
+
const sensorGCRF = sensorITRF.toJ2000();
|
|
1106
|
+
const satellite = new squid.J2000(
|
|
1107
|
+
time,
|
|
1108
|
+
new squid.Vector3D(
|
|
1109
|
+
SlantRangeKm * Math.cos(Ra * DEG2RAD) * Math.cos(Dec * DEG2RAD),
|
|
1110
|
+
SlantRangeKm * Math.sin(Ra * DEG2RAD) * Math.cos(Dec * DEG2RAD),
|
|
1111
|
+
SlantRangeKm * Math.sin(Dec * DEG2RAD),
|
|
1112
|
+
),
|
|
1113
|
+
);
|
|
1114
|
+
const satGCRF = new squid.J2000(
|
|
1115
|
+
time,
|
|
1116
|
+
sensorGCRF.position.add(satellite.position),
|
|
1117
|
+
);
|
|
1118
|
+
const satITRF = satGCRF.toITRF();
|
|
1119
|
+
const result = satITRF.toLookAngle(sensorGeo);
|
|
1120
|
+
return {
|
|
1121
|
+
Az: (result.azimuth * 180) / Math.PI,
|
|
1122
|
+
El: (result.elevation * 180) / Math.PI,
|
|
1123
|
+
};
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Algorithm to calculate the residual error in Azimuth and Elevation for an array
|
|
1128
|
+
* of Observations from a given TLE.
|
|
1129
|
+
* @param {Array} obs Array of Observations
|
|
1130
|
+
* @param {Object} tle The satellites TLE
|
|
1131
|
+
* @return {Array} Array of residual data objects
|
|
1132
|
+
*/
|
|
1133
|
+
const GetResiduals = (obs, tle) => {
|
|
1134
|
+
const sat = twoline2satrec(tle.Line1, tle.Line2);
|
|
1135
|
+
const residuals = [];
|
|
1136
|
+
const epochDate = epochToDate(sat.epochdays, 2000 + sat.epochyr);
|
|
1137
|
+
|
|
1138
|
+
if (sat.error > 0) {
|
|
1139
|
+
console.log(sat.error);
|
|
1140
|
+
return residuals; // Can't propegate TLE
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
for (let i = 0; i < obs.length; i++) {
|
|
1144
|
+
const ob = obs[i];
|
|
1145
|
+
const obTimeUtc = dtStrtoJsDt(ob.ObTime);
|
|
1146
|
+
const dt
|
|
1147
|
+
= (obTimeUtc.getTime() - dtStrtoJsDt(epochDate).getTime())
|
|
1148
|
+
/ 1000
|
|
1149
|
+
/ 60;
|
|
1150
|
+
const pv = sgp4(sat, dt);
|
|
1151
|
+
const gmst = gstime(obTimeUtc);
|
|
1152
|
+
const observerGd = {
|
|
1153
|
+
latitude: degreesToRadians(ob.SenLat),
|
|
1154
|
+
longitude: degreesToRadians(ob.SenLon),
|
|
1155
|
+
height: ob.SenAlt,
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
const pECEF = eciToEcf(pv.position, gmst);
|
|
1159
|
+
const angles = ecfToLookAngles(observerGd, pECEF);
|
|
1160
|
+
residuals.push({
|
|
1161
|
+
SatNo: ob.SatNo,
|
|
1162
|
+
ElErr: (ob.Elevation!==undefined && ob.Elevation!==null)
|
|
1163
|
+
? ob.Elevation - radiansToDegrees(angles.elevation): null,
|
|
1164
|
+
AzErr: (ob.Azimuth!==undefined && ob.Azimuth!==null)
|
|
1165
|
+
? getAngleDiffSigned(radiansToDegrees(angles.azimuth), ob.Azimuth): null,
|
|
1166
|
+
RangeErr: (ob.Range!==undefined && ob.Range!==null) ? ob.Range - angles.rangeSat : null,
|
|
1167
|
+
ObTime: ob.ObTime,
|
|
1168
|
+
Source: ob.Source,
|
|
1169
|
+
SensorId: ob.IdSensor,
|
|
1170
|
+
Type: (ob.Type!==undefined && ob.Type!==null) ? ob.Type : null,
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
return residuals;
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
/** Convert a TLE's two lines to a UDL-compatible Elset_Ingested object, ready to be ingested to UDL.
|
|
1177
|
+
*
|
|
1178
|
+
* @param {String} l1 Line 1 of a TLE
|
|
1179
|
+
* @param {String} l2 Line 2 of a TLE
|
|
1180
|
+
* @param {String} dataMode The data mode corresonding to the TLE. Validation is performed,
|
|
1181
|
+
* but it should be one of the allowed UDL enumerated values (REAL, TEST, EXERCISE, SIMULATED).
|
|
1182
|
+
* @param {Boolean} isUct The flag indicating if the object is catalogued or not.
|
|
1183
|
+
* If not sure, leave it empty, UDL allows this to be null, but it is not good practice!
|
|
1184
|
+
* @param {String} descriptor A string with a helpful description message for the consumers.
|
|
1185
|
+
* @return {Object} The Elset json. It should be strictly validated against Elset_Ingested schema.
|
|
1186
|
+
*/
|
|
1187
|
+
const GetElsetUdlFromTle = (
|
|
1188
|
+
l1,
|
|
1189
|
+
l2,
|
|
1190
|
+
dataMode,
|
|
1191
|
+
isUct = null,
|
|
1192
|
+
descriptor = null,
|
|
1193
|
+
) => {
|
|
1194
|
+
try {
|
|
1195
|
+
// Check the TLE lines, or exit early
|
|
1196
|
+
if (!checkTle(l1, l2)) {
|
|
1197
|
+
throw new Error("Input TLE lines have bad format.");
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
const elset = {};
|
|
1201
|
+
|
|
1202
|
+
// Validate and add uct. If not boolean, check if null, since UDL allows it,
|
|
1203
|
+
// and we may be unsure of entering true or false for any reason.
|
|
1204
|
+
if (isBoolean(isUct)) {
|
|
1205
|
+
elset.uct = isUct;
|
|
1206
|
+
} else if (isUct === null || typeof isUct === "undefined") {
|
|
1207
|
+
// Do nothing, do not populate nor put null.
|
|
1208
|
+
} else {
|
|
1209
|
+
throw new Error("Input uct flag is not of type boolean.");
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Validate and add dataMode
|
|
1213
|
+
if (isValidDataMode(dataMode)) {
|
|
1214
|
+
elset.dataMode = dataMode;
|
|
1215
|
+
} else {
|
|
1216
|
+
throw new Error("Input dataMode argument is not an allowed type.");
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Add U classification marking. Hardcoded Unclassified for now.
|
|
1220
|
+
// Otherwise, CAPCO marking validation shall be performed.
|
|
1221
|
+
// Classification is a required field in the Elset and almost all UDL schemas.
|
|
1222
|
+
elset.classificationMarking = "U";
|
|
1223
|
+
|
|
1224
|
+
// Add the descriptor string, for any comments etc.
|
|
1225
|
+
if (isNonEmptyString(descriptor)) {
|
|
1226
|
+
elset.descriptor = descriptor;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Initialize a satellite record
|
|
1230
|
+
const satrec = twoline2satrec(l1, l2);
|
|
1231
|
+
|
|
1232
|
+
// Now, Populate the elset properties from the satellite record
|
|
1233
|
+
|
|
1234
|
+
// Epoch to ISO-8601
|
|
1235
|
+
const date = julianToGregorian(satrec.jdsatepoch);
|
|
1236
|
+
elset.epoch = date.toISOString();
|
|
1237
|
+
|
|
1238
|
+
elset.satNo = parseInt(satrec.satnum);
|
|
1239
|
+
|
|
1240
|
+
// Eccentricity
|
|
1241
|
+
elset.eccentricity = satrec.ecco;
|
|
1242
|
+
|
|
1243
|
+
// Mean motion conversion from radians per minute to revs per day does not match
|
|
1244
|
+
// the value of mean motion on line 2 exactly! This is an issue of the satellite.js library
|
|
1245
|
+
// So we directly extract this number from line 2.
|
|
1246
|
+
elset.meanMotion = parseFloat(l2.slice(52, 63));
|
|
1247
|
+
|
|
1248
|
+
// According to UDL, period field is the inverse of mean motion, in minutes!
|
|
1249
|
+
// This is added for convenience of the consumer. It is not needed for a successful POST operation.
|
|
1250
|
+
elset.period = (1 / elset.meanMotion) * 24.0 * 60.0;
|
|
1251
|
+
|
|
1252
|
+
// For convenience, we report the ephemeris Type, which is NOT automatically populated by UDL.
|
|
1253
|
+
// Again, this field is not mandatory and POST will succeed if it is ommitted. It is offered for convenience!
|
|
1254
|
+
// UDL suggests to use "SGP4" if orbital period < 225 minutes, and SDP4 otherwise (see ephemType field description in UDL Elset schema)
|
|
1255
|
+
if (elset.period < 225.0) {
|
|
1256
|
+
elset.ephemType = 0;
|
|
1257
|
+
} else {
|
|
1258
|
+
elset.ephemType = 3;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Inclination, converted from radians to degrees
|
|
1262
|
+
elset.inclination = satrec.inclo * RAD2DEG;
|
|
1263
|
+
|
|
1264
|
+
// Source Always saber
|
|
1265
|
+
elset.source = "saber";
|
|
1266
|
+
|
|
1267
|
+
// Raan, converted from radians to degrees
|
|
1268
|
+
elset.raan = satrec.nodeo * RAD2DEG;
|
|
1269
|
+
|
|
1270
|
+
// Argument of perigee, converted from radians to degrees
|
|
1271
|
+
elset.argOfPerigee = satrec.argpo * RAD2DEG;
|
|
1272
|
+
|
|
1273
|
+
// Mean anomaly, converted from radians to degrees
|
|
1274
|
+
elset.meanAnomaly = satrec.mo * RAD2DEG;
|
|
1275
|
+
|
|
1276
|
+
// B-star in inverse earth radii
|
|
1277
|
+
elset.bStar = satrec.bstar;
|
|
1278
|
+
|
|
1279
|
+
// First derivative of mean motion
|
|
1280
|
+
elset.meanMotionDot = satrec.ndot;
|
|
1281
|
+
|
|
1282
|
+
// Second derivative of mean motion
|
|
1283
|
+
elset.meanMotionDDot = satrec.nddot;
|
|
1284
|
+
|
|
1285
|
+
// Revolution number, parsed directly from line 2
|
|
1286
|
+
elset.revNo = parseInt(l2.slice(63, 68));
|
|
1287
|
+
|
|
1288
|
+
// sma
|
|
1289
|
+
elset.semiMajorAxis = (
|
|
1290
|
+
(elset.period * 60)**2 * GRAV_CONST * EARTH_MASS / (4 * Math.PI**2)
|
|
1291
|
+
)**(1/3) / 1000; // km
|
|
1292
|
+
|
|
1293
|
+
// apogee
|
|
1294
|
+
elset.apogee = elset.semiMajorAxis * (1 + elset.eccentricity); // km
|
|
1295
|
+
|
|
1296
|
+
// perigee
|
|
1297
|
+
elset.perigee = elset.semiMajorAxis * (1 - elset.eccentricity); // km
|
|
1298
|
+
|
|
1299
|
+
// Cleanup in the end
|
|
1300
|
+
removeNullUndefined(elset);
|
|
1301
|
+
return elset;
|
|
1302
|
+
} catch (e) {
|
|
1303
|
+
// Return an empty object, which is guaranteed to be invalid
|
|
1304
|
+
// against UDL Elset_Ingested schema.
|
|
1305
|
+
return {};
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
/** Calculates the distance between two Geodetic coordinates.
|
|
1310
|
+
* @param {Number} lat1 The latitude of the first coordinate in degrees
|
|
1311
|
+
* @param {Number} lon1 The longitude of the first coordinate in degrees
|
|
1312
|
+
* @param {Number} lat2 The latitude of the second coordinate in degrees
|
|
1313
|
+
* @param {Number} lon2 The longitude of the second coordinate in degrees
|
|
1314
|
+
* @return {Number} The distance between the two coordinates in meters
|
|
1315
|
+
*
|
|
1316
|
+
* Source: https://www.movable-type.co.uk/scripts/latlong.html
|
|
1317
|
+
*/
|
|
1318
|
+
const distGeodetic = (lat1, lon1, lat2, lon2) => {
|
|
1319
|
+
const R = WGS84_EARTH_EQUATORIAL_RADIUS_KM * 1000.0; // metres
|
|
1320
|
+
const phi1 = lat1 * DEG2RAD; // φ1 in formula
|
|
1321
|
+
const phi2 = lat2 * DEG2RAD; // φ2 in formula
|
|
1322
|
+
const deltaPhi = (lat2 - lat1) * DEG2RAD; // Δφ in formula
|
|
1323
|
+
const deltaLambda = (lon2 - lon1) * DEG2RAD; // Δλ in formula
|
|
1324
|
+
const a
|
|
1325
|
+
= Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2)
|
|
1326
|
+
+ Math.cos(phi1)
|
|
1327
|
+
* Math.cos(phi2)
|
|
1328
|
+
* Math.sin(deltaLambda / 2)
|
|
1329
|
+
* Math.sin(deltaLambda / 2);
|
|
1330
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
1331
|
+
|
|
1332
|
+
const d = R * c; // in metres
|
|
1333
|
+
|
|
1334
|
+
return d;
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
/** Convert cartesian coordinates in an ECI (J2000, TEME, GCRF) frame to keplerian elements formatted as [ a, e, i, Ω, ω, θ ].
|
|
1338
|
+
* The gravitational parameter is assumed to be Earth's mu.
|
|
1339
|
+
* The function is a translation of Astro Library's OrbitalElementConverter.CartesianToKeplerian method.
|
|
1340
|
+
*
|
|
1341
|
+
* NOTE 1: raan, argument of periapsis, and true anomaly are ALL adjusted to have a domain from 0 to 360!!!
|
|
1342
|
+
* NOTE 2: raan, argument of periapsis, and true anomaly are ALL in degrees!
|
|
1343
|
+
* NOTE 3: SMA (a) is in km
|
|
1344
|
+
*
|
|
1345
|
+
* @param {Array} r Position, in meters
|
|
1346
|
+
* @param {Array} v Velocity, in m/s
|
|
1347
|
+
* @return {Object} An object containing the Keplerian Elements, if successful. Otherwise, an empty object.
|
|
1348
|
+
*/
|
|
1349
|
+
const cartesianToKeplerian = (r, v) => {
|
|
1350
|
+
try {
|
|
1351
|
+
const mu = 3.986004418e14; // (m^3)/(s^2) WGS-84 Earth Mu
|
|
1352
|
+
const tol = 1e-9;
|
|
1353
|
+
|
|
1354
|
+
const h = cross(r, v);
|
|
1355
|
+
const K = [0, 0, 1];
|
|
1356
|
+
const n = cross(K, h);
|
|
1357
|
+
|
|
1358
|
+
if (norm(r, 2) === 0) throw new Error("Position vector must not be zero.");
|
|
1359
|
+
if (norm(v, 2) === 0) throw new Error("Velocity vector must not be zero.");
|
|
1360
|
+
|
|
1361
|
+
const e = multiply(
|
|
1362
|
+
(1 / mu),
|
|
1363
|
+
subtract(
|
|
1364
|
+
(multiply(Math.pow(norm(v, 2), 2) - (mu / norm(r, 2)), r)),
|
|
1365
|
+
multiply(dot(r, v), v)),
|
|
1366
|
+
);
|
|
1367
|
+
|
|
1368
|
+
const zeta = 0.5 * Math.pow(norm(v), 2) - (mu / norm(r));
|
|
1369
|
+
|
|
1370
|
+
if (zeta === 0) throw new Error("Zeta cannot be zero.");
|
|
1371
|
+
if (Math.abs(1.0 - norm(e)) <= tol) {
|
|
1372
|
+
throw new Error("Parabolic orbit conversion is not supported.");
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const a = -mu / zeta / 2;
|
|
1376
|
+
|
|
1377
|
+
if (Math.abs(a * (1 - norm(e))) < 1e-3) {
|
|
1378
|
+
throw new Error(`The state results in a singular conic section with
|
|
1379
|
+
radius of periapsis less than 1 m.`);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const i = Math.acos(h[2] / norm(h));
|
|
1383
|
+
|
|
1384
|
+
if (i >= Math.PI - tol) {
|
|
1385
|
+
throw new Error("Cannot convert orbit with inclination of 180 degrees.");
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
let raan = 0;
|
|
1389
|
+
let w = 0;
|
|
1390
|
+
let f = 0;
|
|
1391
|
+
|
|
1392
|
+
// CASE 1: Non-circular, Inclined Orbit
|
|
1393
|
+
if (norm(e) >= 1E-11 && i >= 1E-11 && i <= Math.PI - 1E-11) {
|
|
1394
|
+
if (norm(n) === 0.0) {
|
|
1395
|
+
throw new Error(`Cannot convert from Cartesian to Keplerian,
|
|
1396
|
+
line-of-nodes vector is a zero vector.`);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
raan = Math.acos(n[0] / norm(n));
|
|
1400
|
+
if (n[1] < 0) raan = 2 * Math.PI - raan;
|
|
1401
|
+
|
|
1402
|
+
w = Math.acos(dot(n, e) / (norm(n) * norm(e)));
|
|
1403
|
+
if (e[2] < 0) w = (2 * Math.PI) - w;
|
|
1404
|
+
|
|
1405
|
+
f = Math.acos(dot(e, r) / (norm(e) * norm(r)));
|
|
1406
|
+
if (dot(r, v) < 0) f = 2 * Math.PI - f;
|
|
1407
|
+
}
|
|
1408
|
+
// CASE 2: Non-circular, Equatorial Orbit
|
|
1409
|
+
if (norm(e) >= 1E-11 && (i < 1E-11 || i > Math.PI - 1E-11)) {
|
|
1410
|
+
if (norm(e) === 0.0) {
|
|
1411
|
+
throw new Error(`Cannot convert from Cartesian to Keplerian,
|
|
1412
|
+
eccentricity is zero.`);
|
|
1413
|
+
}
|
|
1414
|
+
raan = 0;
|
|
1415
|
+
w = Math.acos(e[0] / norm(e));
|
|
1416
|
+
if (e[1] < 0) {
|
|
1417
|
+
w = 2 * Math.PI - w;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// For GMT-4446 fix (LOJ: 2014.03.21)
|
|
1421
|
+
if (i > Math.PI - 1E-11) {
|
|
1422
|
+
w *= -1.0;
|
|
1423
|
+
}
|
|
1424
|
+
if (w < 0.0) {
|
|
1425
|
+
w += 2 * Math.PI;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
f = Math.acos(dot(e, r) / (norm(e) * norm(r)));
|
|
1429
|
+
if (r * v < 0) {
|
|
1430
|
+
f = 2 * Math.PI - f;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
// CASE 3: Circular, Inclined Orbit
|
|
1434
|
+
if (norm(e) < 1E-11 && i >= 1E-11 && i <= Math.PI - 1E-11) {
|
|
1435
|
+
if (norm(n) === 0.0) {
|
|
1436
|
+
throw new Error(`Cannot convert from Cartesian to Keplerian,
|
|
1437
|
+
line-of-nodes vector is a zero vector.`);
|
|
1438
|
+
}
|
|
1439
|
+
raan = Math.acos(n[0] / norm(n));
|
|
1440
|
+
if (n[1] < 0) {
|
|
1441
|
+
raan = 2 * Math.PI - raan;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
w = 0;
|
|
1445
|
+
f = Math.acos((dot(n, r) / (norm(n) * norm(r))));
|
|
1446
|
+
if (r[3] < 0) {
|
|
1447
|
+
f = 2 * Math.PI - f;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
// CASE 4: Circular, Equatorial Orbit
|
|
1451
|
+
if (norm(e) < 1E-11 && (i < 1E-11 || i > Math.PI - 1E-11)) {
|
|
1452
|
+
raan = 0;
|
|
1453
|
+
w = 0;
|
|
1454
|
+
f = Math.acos(r[0] / norm(r));
|
|
1455
|
+
if (r[1] < 0) f = 2 * Math.PI - f;
|
|
1456
|
+
|
|
1457
|
+
// For GMT-4446 fix (LOJ: 2014.03.21)
|
|
1458
|
+
if (i > Math.PI - 1E-11) f *= -1.0;
|
|
1459
|
+
if (f < 0.0) {
|
|
1460
|
+
f += 2 * Math.PI;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
return {
|
|
1465
|
+
a: a /1000.0, // km
|
|
1466
|
+
e: norm(e),
|
|
1467
|
+
i: i * RAD2DEG, // deg
|
|
1468
|
+
raan: raan * RAD2DEG, // deg
|
|
1469
|
+
w: w * RAD2DEG, // deg
|
|
1470
|
+
f: f * RAD2DEG, // deg
|
|
1471
|
+
};
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
console.error(err);
|
|
1474
|
+
return {};
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
/** Convert cartesian coordinates in an ECI (J2000, TEME, GCRF) frame to keplerian elements formatted as [ a, e, i, Ω, ω, θ ].
|
|
1479
|
+
* The gravitational parameter is assumed to be Earth's mu.
|
|
1480
|
+
* The function is a translation of Astro Library's OrbitalElementConverter.KeplerianToCartesian method.
|
|
1481
|
+
*
|
|
1482
|
+
* NOTE 1: input raan, argument of periapsis, and true anomaly are ALL adjusted to have a domain from 0 to 360!!!
|
|
1483
|
+
* NOTE 2: raan, argument of periapsis, and true anomaly are ALL in degrees!
|
|
1484
|
+
* NOTE 3: SMA (a) is in km
|
|
1485
|
+
*
|
|
1486
|
+
* @param {Array} elset Keplerian elements
|
|
1487
|
+
* @param {Number} mu The gravitational parameter for the celestial object that we orbit, in km based units
|
|
1488
|
+
* @return {Object} An object containing the position and velocity in *meter* based units
|
|
1489
|
+
*/
|
|
1490
|
+
const keplerianToCartesian = (elset, mu = 398600.4418) => {
|
|
1491
|
+
const INFINITE_TOL = 1e-10;
|
|
1492
|
+
const ORBIT_TOL = 1e-10;
|
|
1493
|
+
try {
|
|
1494
|
+
const [a, e, iDeg, raanDeg, wDeg, fDeg] = elset;
|
|
1495
|
+
const i = iDeg * DEG2RAD;
|
|
1496
|
+
const raan = raanDeg * DEG2RAD;
|
|
1497
|
+
const w = wDeg * DEG2RAD;
|
|
1498
|
+
const f = fDeg * DEG2RAD;
|
|
1499
|
+
|
|
1500
|
+
const p = a * (1 - Math.pow(e, 2));
|
|
1501
|
+
|
|
1502
|
+
if (Math.abs(p) < INFINITE_TOL) {
|
|
1503
|
+
throw new Error("Cannot convert parabolic orbit.");
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const onePlusECos = 1 + e * Math.cos(f);
|
|
1507
|
+
|
|
1508
|
+
if (onePlusECos < ORBIT_TOL) {
|
|
1509
|
+
throw new Error("Orbital radius is large and may cause singularity.");
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const rad = p / onePlusECos;
|
|
1513
|
+
const cosPerAnom = Math.cos(w + f);
|
|
1514
|
+
const sinPerAnom = Math.sin(w + f);
|
|
1515
|
+
const cosInc = Math.cos(i);
|
|
1516
|
+
const sinInc = Math.sin(i);
|
|
1517
|
+
const cosRaan = Math.cos(raan);
|
|
1518
|
+
const sinRaan = Math.sin(raan);
|
|
1519
|
+
const sqrtGravP = Math.sqrt(mu / p);
|
|
1520
|
+
const cosAnomPlusE = Math.cos(f) + e;
|
|
1521
|
+
const sinAnom = Math.sin(f);
|
|
1522
|
+
const cosPer = Math.cos(w);
|
|
1523
|
+
const sinPer = Math.sin(w);
|
|
1524
|
+
|
|
1525
|
+
const r = {
|
|
1526
|
+
x: 1000.0 * rad * (cosPerAnom * cosRaan - cosInc * sinPerAnom * sinRaan),
|
|
1527
|
+
y: 1000.0 * rad * (cosPerAnom * sinRaan + cosInc * sinPerAnom * cosRaan),
|
|
1528
|
+
z: 1000.0 * rad * sinPerAnom * sinInc,
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
const v = {
|
|
1532
|
+
x: 1000.0 * (sqrtGravP * cosAnomPlusE * (-sinPer * cosRaan - cosInc * sinRaan * cosPer)
|
|
1533
|
+
- sqrtGravP * sinAnom * (cosPer * cosRaan - cosInc * sinRaan * sinPer)),
|
|
1534
|
+
y: 1000.0 * (sqrtGravP * cosAnomPlusE * (-sinPer * sinRaan + cosInc * cosRaan * cosPer)
|
|
1535
|
+
- sqrtGravP * sinAnom * (cosPer * sinRaan + cosInc * cosRaan * sinPer)),
|
|
1536
|
+
z: 1000.0 * (sqrtGravP * (cosAnomPlusE * sinInc * cosPer - sinAnom * sinInc * sinPer)),
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
return {r, v};
|
|
1540
|
+
} catch (err) {
|
|
1541
|
+
return {};
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* Get LEO RPO data for a given target satellite and a set of potential threat satellites.
|
|
1547
|
+
* @param {String} line1, line 1 of the target satellite
|
|
1548
|
+
* @param {String} line2, line 2 of the target satellite
|
|
1549
|
+
* @param {Array<Objects>} sats, array of potential threat satellites and their Elsets
|
|
1550
|
+
* @param {Integer} startTime, start time of the analysis, Unix milliseconds
|
|
1551
|
+
* @param {Integer} endTime, end time of the analysis, Unix milliseconds
|
|
1552
|
+
* @return {Array} Array of RPO data objects
|
|
1553
|
+
*/
|
|
1554
|
+
const getLeoRpoData = (line1, line2, sats, startTime, endTime) => {
|
|
1555
|
+
const results = [];
|
|
1556
|
+
const pSatRec = checkTle(line1, line2);
|
|
1557
|
+
if (!isDefined(pSatRec)) return results;
|
|
1558
|
+
|
|
1559
|
+
const start = new Date(startTime).getTime();
|
|
1560
|
+
const end = new Date(endTime).getTime();
|
|
1561
|
+
const pElset = {
|
|
1562
|
+
Line1: line1,
|
|
1563
|
+
Line2: line2,
|
|
1564
|
+
};
|
|
1565
|
+
const pEphem = prop(pElset, start, end, 10000);
|
|
1566
|
+
|
|
1567
|
+
sats.forEach( (s) => {
|
|
1568
|
+
const sEphem = prop(s, start, end, 10000);
|
|
1569
|
+
if (!isDefined(pEphem)
|
|
1570
|
+
|| !isDefined(sEphem)
|
|
1571
|
+
|| pEphem.length !== sEphem.length) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
const aResult = {
|
|
1576
|
+
line1: s.Line1,
|
|
1577
|
+
line2: s.Line2,
|
|
1578
|
+
epoch: s.Epoch,
|
|
1579
|
+
name: s.CommonName,
|
|
1580
|
+
rank: s.Rank ?? "",
|
|
1581
|
+
satNo: s.SatNo,
|
|
1582
|
+
inclination: s.Inclination,
|
|
1583
|
+
raan: s.Raan,
|
|
1584
|
+
source: s.Source,
|
|
1585
|
+
sma: s.SemiMajorAxis,
|
|
1586
|
+
country: s.CountryId,
|
|
1587
|
+
flag: s.Flag,
|
|
1588
|
+
poca: 9999999999,
|
|
1589
|
+
toca: "",
|
|
1590
|
+
tocaString: "",
|
|
1591
|
+
planeDiff: 0,
|
|
1592
|
+
incDiff: s.incDiff,
|
|
1593
|
+
raanDiff: s.raanDiff,
|
|
1594
|
+
semiMajorDiff: s.semiMajorDiff,
|
|
1595
|
+
raanDrift: s.RaanPrecessionDegreesPerDay,
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
const pv1 = {
|
|
1599
|
+
position: pEphem[0].p,
|
|
1600
|
+
velocity: pEphem[0].v,
|
|
1601
|
+
};
|
|
1602
|
+
const pv2 = {
|
|
1603
|
+
position: sEphem[0].p,
|
|
1604
|
+
velocity: sEphem[0].v,
|
|
1605
|
+
};
|
|
1606
|
+
aResult.di = angleBetweenPlanes(pv1, pv2);
|
|
1607
|
+
aResult.dv = planeChangeDeltaV(pv1, pv2);
|
|
1608
|
+
aResult.dv = {
|
|
1609
|
+
i: Math.round(aResult.dv.i*1000)/1000,
|
|
1610
|
+
c: Math.round(aResult.dv.c*1000)/1000,
|
|
1611
|
+
}; // Round to 3 decimals
|
|
1612
|
+
|
|
1613
|
+
// Find the distance at each time step
|
|
1614
|
+
for (let i=0; i<pEphem.length; i++) {
|
|
1615
|
+
const distKm = dist(pEphem[i].p, sEphem[i].p);
|
|
1616
|
+
if (distKm < aResult.poca) {
|
|
1617
|
+
aResult.poca = distKm;
|
|
1618
|
+
aResult.toca = new Date(pEphem[i].t).toISOString();
|
|
1619
|
+
aResult.tocaString = getTimeDifference(aResult.toca);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// Calc danger score (account for di of 0)
|
|
1624
|
+
aResult.danger = (aResult.di === 0
|
|
1625
|
+
|| aResult.poca === 0) ? 1000 : 1/(aResult.di*aResult.poca);
|
|
1626
|
+
results.push(aResult);
|
|
1627
|
+
});
|
|
1628
|
+
return results;
|
|
1629
|
+
};
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Get GEO RPO data for a given target satellite and a set of potential threat satellites.
|
|
1634
|
+
* @param {String} line1, line 1 of the target satellite
|
|
1635
|
+
* @param {String} line2, line 2 of the target satellite
|
|
1636
|
+
* @param {Array<Objects>} sats, array of potential threat satellites and their Elsets
|
|
1637
|
+
* @param {Integer} startTime, start time of the analysis, Unix milliseconds
|
|
1638
|
+
* @param {Integer} endTime, end time of the analysis, Unix milliseconds
|
|
1639
|
+
* @param {Integer} lonTime, datetime to analyze longitude and lon drift at, Unix milliseconds
|
|
1640
|
+
* @return {Array<Objects>}, Array of RPO data objects
|
|
1641
|
+
*/
|
|
1642
|
+
const getGeoRpoData = (line1, line2, sats, startTime, endTime, lonTime) => {
|
|
1643
|
+
const start = new Date(startTime).getTime();
|
|
1644
|
+
const end = new Date(endTime).getTime();
|
|
1645
|
+
const pEphem = prop({
|
|
1646
|
+
Line1: line1,
|
|
1647
|
+
Line2: line2,
|
|
1648
|
+
}, start, end, 60000);
|
|
1649
|
+
|
|
1650
|
+
const lonEvalTime = lonTime ? new Date(lonTime) : new Date(end);
|
|
1651
|
+
|
|
1652
|
+
const pLonAndDrift = getLonAndDrift(line1, line2, lonEvalTime);
|
|
1653
|
+
|
|
1654
|
+
const getLonDiff = (lon1, drift1, lon2, drift2) => {
|
|
1655
|
+
const closing = lon1 > lon2
|
|
1656
|
+
? (drift1 * drift2 > 0 && drift1 < drift2) || (drift1 * drift2 < 0 && drift1 < 0) // Sat1 is more Eastward
|
|
1657
|
+
: lon2 > lon1
|
|
1658
|
+
? (drift1 * drift2 > 0 && drift2 < drift1) || (drift1 * drift2 < 0 && drift1 > 0) // Sat2 is more Eastward
|
|
1659
|
+
: false;
|
|
1660
|
+
const difference = drift1 - drift2;
|
|
1661
|
+
return closing ? -1 * Math.abs(difference) : Math.abs(difference);
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
const results = [];
|
|
1665
|
+
sats.forEach((s) => {
|
|
1666
|
+
const aResult = {
|
|
1667
|
+
line1: s.Line1,
|
|
1668
|
+
line2: s.Line2,
|
|
1669
|
+
epoch: s.Epoch,
|
|
1670
|
+
name: s.CommonName,
|
|
1671
|
+
rank: s.Rank ?? "",
|
|
1672
|
+
satNo: s.SatNo,
|
|
1673
|
+
source: s.Source,
|
|
1674
|
+
country: s.CountryId,
|
|
1675
|
+
flag: s.Flag,
|
|
1676
|
+
poca: 9999999999,
|
|
1677
|
+
toca: "",
|
|
1678
|
+
tocaString: "",
|
|
1679
|
+
incDiff: s.incDiff,
|
|
1680
|
+
longitude: null,
|
|
1681
|
+
lonDiff: null,
|
|
1682
|
+
relativeDrift: null,
|
|
1683
|
+
di: null,
|
|
1684
|
+
dv: null,
|
|
1685
|
+
danger: null,
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
const sEphem = prop({
|
|
1689
|
+
Line1: aResult.line1,
|
|
1690
|
+
Line2: aResult.line2,
|
|
1691
|
+
}, start, end, 60000);
|
|
1692
|
+
if (!isDefined(pEphem)
|
|
1693
|
+
|| !isDefined(sEphem)
|
|
1694
|
+
|| pEphem.length !== sEphem.length) {
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
const sLonAndDrift = getLonAndDrift(s.Line1, s.Line2, lonEvalTime);
|
|
1698
|
+
aResult.longitude = (sLonAndDrift.longitude + 360) % 360; // Normalize to 0-360
|
|
1699
|
+
|
|
1700
|
+
const lonDiff = Math.abs(
|
|
1701
|
+
(pLonAndDrift.longitude + 360.0) % 360
|
|
1702
|
+
- (sLonAndDrift.longitude + 360.0) % 360.0,
|
|
1703
|
+
);
|
|
1704
|
+
// To ensure that the smallest "short-way" lon diff is considered
|
|
1705
|
+
aResult.lonDiff = Math.min(lonDiff, 360 - lonDiff);
|
|
1706
|
+
|
|
1707
|
+
aResult.relativeDrift = getLonDiff(
|
|
1708
|
+
pLonAndDrift.longitude,
|
|
1709
|
+
pLonAndDrift.lonDriftDegreesPerDay,
|
|
1710
|
+
sLonAndDrift.longitude,
|
|
1711
|
+
sLonAndDrift.lonDriftDegreesPerDay);
|
|
1712
|
+
const pv1 = {
|
|
1713
|
+
position: pEphem[0].p,
|
|
1714
|
+
velocity: pEphem[0].v,
|
|
1715
|
+
};
|
|
1716
|
+
const pv2 = {
|
|
1717
|
+
position: sEphem[0].p,
|
|
1718
|
+
velocity: sEphem[0].v,
|
|
1719
|
+
};
|
|
1720
|
+
aResult.di = angleBetweenPlanes(pv1, pv2);
|
|
1721
|
+
aResult.dv = {
|
|
1722
|
+
i: Math.round(planeChangeDeltaV(pv1, pv2).i*1000)/1000,
|
|
1723
|
+
c: Math.round(planeChangeDeltaV(pv1, pv2).c*1000)/1000,
|
|
1724
|
+
}; // Round to 3 decimals
|
|
1725
|
+
|
|
1726
|
+
for (let i=0; i<pEphem.length; i++) {
|
|
1727
|
+
const distKm = dist(pEphem[i].p, sEphem[i].p);
|
|
1728
|
+
if (distKm < aResult.poca) {
|
|
1729
|
+
aResult.poca = distKm;
|
|
1730
|
+
aResult.toca = new Date(pEphem[i].t).toISOString();
|
|
1731
|
+
aResult.tocaString = getTimeDifference(aResult.toca);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
// Calc danger score (account for di of 0)
|
|
1735
|
+
aResult.danger = (aResult.di === 0
|
|
1736
|
+
|| aResult.poca === 0) ? 1000 : 1/(aResult.di*aResult.poca);
|
|
1737
|
+
|
|
1738
|
+
results.push(aResult);
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
return results;
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1744
|
+
const getGeoShadowZones = (time, accuracySecondsDeg=0.00416*100) => {
|
|
1745
|
+
// Accuracy is 86400 pooints on a 360 circle (i.e. 0.00416 deg per second)
|
|
1746
|
+
|
|
1747
|
+
// Define the TLE parameters of a template TLE in GEO
|
|
1748
|
+
const tleLine1 = "1 00000U 00000A 24079.98445361 -.00000000 00000-0 -00000-0 0 00000";
|
|
1749
|
+
const tleLine2Temp = "2 00000 000.0000 000.0000 0000000 000.0000 XXX.XXXX 01.00000000000000";
|
|
1750
|
+
// Number of steps for the mean anomaly
|
|
1751
|
+
const steps = 360 / accuracySecondsDeg;
|
|
1752
|
+
// Loop over the range of mean anomalies
|
|
1753
|
+
const res = [];
|
|
1754
|
+
for (let i = 0; i < steps; i++) {
|
|
1755
|
+
// Calculate the mean anomaly for this step
|
|
1756
|
+
const meanAnomaly = i * accuracySecondsDeg;
|
|
1757
|
+
// Format the mean anomaly into the TLE line
|
|
1758
|
+
// Round the mean anomaly to two decimal places
|
|
1759
|
+
const roundedMeanAnomaly = meanAnomaly.toFixed(4);
|
|
1760
|
+
// Format the mean anomaly into the TLE line
|
|
1761
|
+
const tleLine2 = tleLine2Temp.replace("XXX.XXXX", roundedMeanAnomaly.padStart(8, "0"));
|
|
1762
|
+
// Generate the satrec object
|
|
1763
|
+
const p = propagate(twoline2satrec(tleLine1, tleLine2), time).position;
|
|
1764
|
+
res.push({
|
|
1765
|
+
ecl: getEclipseStatus(time, [p.x, p.y, p.z]),
|
|
1766
|
+
geolon: eciToGeodetic(p, gstime(time)).longitude*RAD2DEG,
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
// Find the sun zone, umbra zone, and two penumbra zones.
|
|
1770
|
+
const ints = [];
|
|
1771
|
+
|
|
1772
|
+
// Open the first zone. Note that this may not be the start of this zone but somewhere in the middle.
|
|
1773
|
+
// It's just the start of the mean anomaly seen as a circular buffer.
|
|
1774
|
+
ints.push({
|
|
1775
|
+
ecl: res[0].ecl,
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
for (let i=1; i<=res.length-1; i++) {
|
|
1779
|
+
// Compare with the previous eclipse state
|
|
1780
|
+
if (res[i].ecl!==res[i-1].ecl) {
|
|
1781
|
+
// Close the previous zone
|
|
1782
|
+
ints[ints.length-1].stopDeg = res[i-1].geolon;
|
|
1783
|
+
|
|
1784
|
+
// Open a new zone
|
|
1785
|
+
ints.push({
|
|
1786
|
+
ecl: res[i].ecl,
|
|
1787
|
+
startDeg: res[i].geolon,
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
// With this process, the first interval will have a stop lon but not a start lon, and
|
|
1792
|
+
// the final interval will have a start lon but not a stop lon.
|
|
1793
|
+
// Stitch the first and last intervals together.
|
|
1794
|
+
|
|
1795
|
+
// If not dates are pushed on the first interval, it means it never closed, so it's a fully lit GEO belt.
|
|
1796
|
+
// If there's only sun, return null values
|
|
1797
|
+
if ( ints.length===1
|
|
1798
|
+
&& ints.filter((x)=>x.ecl === "SUN").length===1) {
|
|
1799
|
+
return {
|
|
1800
|
+
penStartWestLon: null,
|
|
1801
|
+
penStartEastLon: null,
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
ints[0].startDeg = ints[ints.length-1].startDeg;
|
|
1806
|
+
ints.pop();
|
|
1807
|
+
|
|
1808
|
+
if (
|
|
1809
|
+
ints.filter((x)=>x.ecl === "UMBRA").length===0
|
|
1810
|
+
&& ints.filter((x)=>x.ecl === "PENUMBRA").length>0) {
|
|
1811
|
+
// If there's no umbra but penumbra exists, return the perumbra values
|
|
1812
|
+
|
|
1813
|
+
// Extract the penumbra interval. In the absence of umbra, a single penumbra interval should exist.
|
|
1814
|
+
const penumbraInt = ints.filter((x)=>x.ecl === "PENUMBRA")[0];
|
|
1815
|
+
return {
|
|
1816
|
+
penStartWestLon: wrapToRange(penumbraInt.startDeg, 0, 360),
|
|
1817
|
+
penStartEastLon: wrapToRange(penumbraInt.stopDeg, 0, 360),
|
|
1818
|
+
};
|
|
1819
|
+
} else {
|
|
1820
|
+
// If sun, umbra, penumbra intervals exist
|
|
1821
|
+
|
|
1822
|
+
const umbra = ints.filter((x)=>x.ecl === "UMBRA")[0];
|
|
1823
|
+
|
|
1824
|
+
const umbraStart360 = wrapToRange(umbra.startDeg, 0, 360);
|
|
1825
|
+
const umbraStop360 = wrapToRange(umbra.stopDeg, 0, 360);
|
|
1826
|
+
// Assuming a spherical earth for this purpose, the two penumbra intervals should be equal.
|
|
1827
|
+
// Compute the angle of one of the penumbra intervals, the one that does not contain the disconituity.
|
|
1828
|
+
const penumbraInts = ints.filter((x)=>x.ecl === "PENUMBRA");
|
|
1829
|
+
const chosenPenumbraInt = penumbraInts[0].startDeg > penumbraInts[0].stopDeg
|
|
1830
|
+
? penumbraInts[1] : penumbraInts[0];
|
|
1831
|
+
const penumbraAngle = chosenPenumbraInt.stopDeg - chosenPenumbraInt.startDeg;
|
|
1832
|
+
const penumbraStart = umbraStart360 - penumbraAngle;
|
|
1833
|
+
const penumbraStop = umbraStop360 + penumbraAngle;
|
|
1834
|
+
|
|
1835
|
+
return {
|
|
1836
|
+
penStartWestLon: penumbraStart,
|
|
1837
|
+
penStartEastLon: penumbraStop,
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1842
|
+
/** Returns the light intervals of an arbitrary satellite in strict GEO orbit.
|
|
1843
|
+
*
|
|
1844
|
+
* These intervals are generally equal in duration for all other GEO satellites.
|
|
1845
|
+
*
|
|
1846
|
+
* The use-case for this function is to help with the drawing of umbra/penumbra zones in the waterfall plot.
|
|
1847
|
+
*
|
|
1848
|
+
* @param {Date} time The time in UTC
|
|
1849
|
+
* @param {Number} durationSeconds The duration in seconds of the analysis interval. Defaults to 1 day
|
|
1850
|
+
* @param {Number} accuracySeconds The accuracy of the calulations, defaults to 1 sec. The higher, the faster.
|
|
1851
|
+
* @return {Array} An array of objects, each of which represents a contiguous interval of sunlight, penumbra, or umbra.
|
|
1852
|
+
*/
|
|
1853
|
+
const getGeoLightIntervals = (time, durationSeconds=86400, accuracySeconds=10.0) => {
|
|
1854
|
+
const endTime = new Date(time.getTime() + 1000.0*durationSeconds);
|
|
1855
|
+
// Create an artificial satellite on GEO
|
|
1856
|
+
const satrec = twoline2satrec(
|
|
1857
|
+
"1 00000U 00000A 24079.98445361 -.00000000 00000-0 -00000-0 0 00000",
|
|
1858
|
+
"2 00000 000.0000 000.0000 0000000 000.0000 000.0000 01.00299372000000",
|
|
1859
|
+
);
|
|
1860
|
+
const res = [];
|
|
1861
|
+
while (time.getTime() <= endTime.getTime()) {
|
|
1862
|
+
const p = propagate(satrec, time).position;
|
|
1863
|
+
res.push({
|
|
1864
|
+
t: time.toISOString(),
|
|
1865
|
+
ecl: getEclipseStatus(time, [p.x, p.y, p.z]),
|
|
1866
|
+
geolon: eciToGeodetic(p, gstime(time)).longitude,
|
|
1867
|
+
});
|
|
1868
|
+
time = new Date(time.getTime() + accuracySeconds*1000);
|
|
1869
|
+
}
|
|
1870
|
+
// Group to intervals
|
|
1871
|
+
const ints = [];
|
|
1872
|
+
|
|
1873
|
+
// Create and open the first interval
|
|
1874
|
+
ints.push({
|
|
1875
|
+
ecl: res[0].ecl,
|
|
1876
|
+
start: res[0].t,
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
for (let i=1; i<=res.length-1; i++) {
|
|
1880
|
+
if (res[i].ecl!==res[i-1].ecl) {
|
|
1881
|
+
// Close the previous interval
|
|
1882
|
+
ints[ints.length-1].end = res[i-1].t;
|
|
1883
|
+
// Create a new open interval
|
|
1884
|
+
ints.push({
|
|
1885
|
+
ecl: res[i].ecl,
|
|
1886
|
+
start: res[i].t,
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
// Close the last interval
|
|
1891
|
+
ints[ints.length-1].end = res[res.length-1].t;
|
|
1892
|
+
|
|
1893
|
+
return {
|
|
1894
|
+
ints: ints,
|
|
1895
|
+
meanLonDeg: res.reduce((acc, obj) => acc + obj.geolon, 0) / res.length,
|
|
1896
|
+
};
|
|
1897
|
+
};
|
|
1898
|
+
|
|
1899
|
+
/** Find if a satellite is in Sun, Umbra, or Penumbra.
|
|
1900
|
+
*
|
|
1901
|
+
* @param {Date} time The time instant of the satellite state
|
|
1902
|
+
* @param {Array} pos The satellite position vector (assumed to be in an inertial frame)
|
|
1903
|
+
* @return {String} SUN if in sunlight, UMBRA if in umbra, PENUMBRA if in penumbra
|
|
1904
|
+
*/
|
|
1905
|
+
const getEclipseStatus = (time, pos) => {
|
|
1906
|
+
let shadow = "SUN";
|
|
1907
|
+
const aPen = 0.269007205 * DEG2RAD;
|
|
1908
|
+
const aUmb = 0.264121687 * DEG2RAD;
|
|
1909
|
+
const rp = WGS72_EARTH_EQUATORIAL_RADIUS_KM; // Using WGS72 Earth equatorial radius (SGP4 standard) because this function is predominently used by Elset calculations.
|
|
1910
|
+
const rS = getSunDirection(time);
|
|
1911
|
+
const rSun = [rS.x, rS.y, rS.z];
|
|
1912
|
+
const r = pos;
|
|
1913
|
+
const rMag = norm(r);
|
|
1914
|
+
|
|
1915
|
+
if (dot(rSun, r) < 0) {
|
|
1916
|
+
const rSunN = multiply(rSun, -1);
|
|
1917
|
+
const rSunNnorm = normalize({x: rSunN[0], y: rSunN[1], z: rSunN[2]});
|
|
1918
|
+
const rNorm = normalize({x: r[0], y: r[1], z: r[2]});
|
|
1919
|
+
const angle = getAngle(
|
|
1920
|
+
[rSunNnorm.x, rSunNnorm.y, rSunNnorm.z],
|
|
1921
|
+
[rNorm.x, rNorm.y, rNorm.z],
|
|
1922
|
+
);
|
|
1923
|
+
const satHoriz = rMag * Math.cos(angle);
|
|
1924
|
+
const satVert = rMag * Math.sin(angle);
|
|
1925
|
+
const x = rp / Math.sin(aUmb);
|
|
1926
|
+
const penVert = Math.tan(aPen)*(x+satHoriz);
|
|
1927
|
+
if (satVert <= penVert) {
|
|
1928
|
+
shadow = "PENUMBRA";
|
|
1929
|
+
const y = rp / Math.sin(aUmb);
|
|
1930
|
+
const umbVert = Math.tan(aUmb)*(y-satHoriz);
|
|
1931
|
+
if (satVert <= umbVert) {
|
|
1932
|
+
shadow = "UMBRA";
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
return shadow;
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* Brute-forces GEO crossing points where satellite crosses GEO altitude
|
|
1941
|
+
* @param {*} propagateBetween async propagation function
|
|
1942
|
+
* @param {*} start start time
|
|
1943
|
+
* @param {*} end end time
|
|
1944
|
+
* @param {*} stepMs step in milliseconds
|
|
1945
|
+
* @return {Promise<Array>} array of objects with time (ms), altitude, and direction of crossing
|
|
1946
|
+
*/
|
|
1947
|
+
const calculateGeoCrossingTimes = async (propagateBetween, start, end, stepMs = 1000) => {
|
|
1948
|
+
const startTime = new Date(start).getTime();
|
|
1949
|
+
const endTime = new Date(end).getTime();
|
|
1950
|
+
|
|
1951
|
+
let lastAltState = 0; // -1 for below, 0 for none, 1 for above
|
|
1952
|
+
|
|
1953
|
+
const crossings = [];
|
|
1954
|
+
const points = await propagateBetween(startTime, endTime, stepMs);
|
|
1955
|
+
|
|
1956
|
+
for (const pv of points) {
|
|
1957
|
+
if (!isDefined(pv)) continue;
|
|
1958
|
+
const time = pv.time;
|
|
1959
|
+
|
|
1960
|
+
const dist = norm(posToArray(pv.position)); // distance from center of the earth, km
|
|
1961
|
+
const altitude = dist - WGS72_EARTH_EQUATORIAL_RADIUS_KM; // altitude, km
|
|
1962
|
+
|
|
1963
|
+
const newAltState = altitude >= GEO_ALTITUDE_KM ? 1 : -1;
|
|
1964
|
+
|
|
1965
|
+
if (lastAltState !== 0 && lastAltState !== newAltState) { // crossed GEO alt
|
|
1966
|
+
const lon = wrapToRange(
|
|
1967
|
+
eciToGeodetic(
|
|
1968
|
+
pv.position,
|
|
1969
|
+
gstime(new Date(time))).longitude*RAD2DEG,
|
|
1970
|
+
0, 360);
|
|
1971
|
+
|
|
1972
|
+
crossings.push({
|
|
1973
|
+
t: time,
|
|
1974
|
+
alt: altitude,
|
|
1975
|
+
direction: newAltState > 0 ? 1 : -1, // 1 means we crossed going above, -1 crossed going below1
|
|
1976
|
+
lon,
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
lastAltState = newAltState;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
return crossings;
|
|
1984
|
+
};
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
/** A function that calculates the next time of an apogee and perigee for an orbit,
|
|
1988
|
+
* and the geodetic longitude at which those occur.
|
|
1989
|
+
*
|
|
1990
|
+
* The 'next' times are with respect to an input date.
|
|
1991
|
+
*
|
|
1992
|
+
* Steps:
|
|
1993
|
+
* 1. Propagate to the reference time.
|
|
1994
|
+
* 2. Get the Cartesian and Keplerian state at the reference time.
|
|
1995
|
+
* 3. At the reference time, compute Eccentric and Mean anomalies, and the time since periapsis.
|
|
1996
|
+
* 4. Compute the time of the next periapsis. Repeat for apoapsis.
|
|
1997
|
+
*
|
|
1998
|
+
* @param {Object} pv Initial position/velocity object
|
|
1999
|
+
* @param {Function<Object>} propagateTo async Date -> PV propagation method for SV or TLE
|
|
2000
|
+
* @param {Date} time The time after which to compute the imediately next apogee and perigee times
|
|
2001
|
+
* @param {Boolean} findApogee Propagate for apogee longitude and state
|
|
2002
|
+
* @param {Boolean} findPerigee Propagate for perigee longitude and state
|
|
2003
|
+
* @return {Object} The object with the next apoapsis/periapsis times, and the geodetic longitudes on which they occur.
|
|
2004
|
+
*/
|
|
2005
|
+
const calculateNextApogeePerigeeTimesWithPropagation
|
|
2006
|
+
= async (pv, propagateTo, time, findApogee=true, findPerigee=true) => {
|
|
2007
|
+
const r = multiply([pv.position.x, pv.position.y, pv.position.z],
|
|
2008
|
+
1000.0);
|
|
2009
|
+
const v = multiply([pv.velocity.x, pv.velocity.y, pv.velocity.z],
|
|
2010
|
+
1000.0);
|
|
2011
|
+
const el = cartesianToKeplerian(r, v);
|
|
2012
|
+
|
|
2013
|
+
// Compute Eccentric Anomaly from True Anomaly and Eccentricity
|
|
2014
|
+
const E = 2 * Math.atan2(Math.tan(el.f/2 * DEG2RAD) * Math.sqrt(1-el.e), Math.sqrt(1+el.e));
|
|
2015
|
+
|
|
2016
|
+
// Compute Mean Anomaly from Eccentric Anomaly and Eccentricity
|
|
2017
|
+
const M = E - (el.e)*Math.sin(E);
|
|
2018
|
+
|
|
2019
|
+
// Mean motion in radians per second
|
|
2020
|
+
const mu = 3.986004418e14; // (m^3)/(s^2) WGS-84 Earth Mu
|
|
2021
|
+
const n = Math.sqrt(mu/Math.pow((el.a*1000.0), 3));
|
|
2022
|
+
|
|
2023
|
+
// Orbit Period
|
|
2024
|
+
const periodSecs = 2*Math.PI/n;
|
|
2025
|
+
|
|
2026
|
+
// Compute Time of Flight (ToF) since last Periapsis
|
|
2027
|
+
const tofSincePeriapsisSecs = M / n;
|
|
2028
|
+
|
|
2029
|
+
// Time until next periapsis
|
|
2030
|
+
let timeToNextPeriapsisSecs = periodSecs - Math.abs(tofSincePeriapsisSecs);
|
|
2031
|
+
|
|
2032
|
+
if (tofSincePeriapsisSecs < 0) {
|
|
2033
|
+
timeToNextPeriapsisSecs = Math.abs(tofSincePeriapsisSecs);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// For next apoapsis, we check the true anomaly of the reference date
|
|
2037
|
+
let timeToNextApoapsisSecs = 0;
|
|
2038
|
+
if (el.f >= 180.0) {
|
|
2039
|
+
// Satellite is past the apoapsis and before the next periapsis
|
|
2040
|
+
timeToNextApoapsisSecs = timeToNextPeriapsisSecs + periodSecs/2;
|
|
2041
|
+
} else {
|
|
2042
|
+
// Satellite is past the periapsis and before the next apoapsis
|
|
2043
|
+
timeToNextApoapsisSecs = periodSecs/2 - tofSincePeriapsisSecs;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
const nextPeriapsisTimeUtc = new Date(time);
|
|
2047
|
+
nextPeriapsisTimeUtc.setSeconds(new Date(time).getSeconds() + timeToNextPeriapsisSecs);
|
|
2048
|
+
|
|
2049
|
+
let periapsisPV = null;
|
|
2050
|
+
let periLon = null;
|
|
2051
|
+
if (findPerigee) {
|
|
2052
|
+
periapsisPV = await propagateTo(nextPeriapsisTimeUtc);
|
|
2053
|
+
periLon = wrapToRange(
|
|
2054
|
+
eciToGeodetic(
|
|
2055
|
+
periapsisPV.position,
|
|
2056
|
+
gstime(nextPeriapsisTimeUtc)).longitude*RAD2DEG,
|
|
2057
|
+
0, 360);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
const nextApoapsisTimeUtc
|
|
2061
|
+
= new Date(new Date(time).getTime() + (timeToNextApoapsisSecs * 1000));
|
|
2062
|
+
|
|
2063
|
+
let apoapsisPV = null;
|
|
2064
|
+
let apoLon = null;
|
|
2065
|
+
if (findApogee) {
|
|
2066
|
+
apoapsisPV = await propagateTo(nextApoapsisTimeUtc);
|
|
2067
|
+
apoLon = wrapToRange(
|
|
2068
|
+
eciToGeodetic(
|
|
2069
|
+
apoapsisPV.position,
|
|
2070
|
+
gstime(nextApoapsisTimeUtc)).longitude*RAD2DEG,
|
|
2071
|
+
0, 360);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
const res = {
|
|
2075
|
+
nextApoapsisTimeUtc: nextApoapsisTimeUtc.toISOString(),
|
|
2076
|
+
nextApoapsisLonDeg: apoLon,
|
|
2077
|
+
nextApoapsisPV: apoapsisPV,
|
|
2078
|
+
nextPeriapsisTimeUtc: nextPeriapsisTimeUtc.toISOString(),
|
|
2079
|
+
nextPeriapsisLonDeg: periLon,
|
|
2080
|
+
nextPeriapsisPV: periapsisPV,
|
|
2081
|
+
};
|
|
2082
|
+
return res;
|
|
2083
|
+
};
|
|
2084
|
+
|
|
2085
|
+
/** A function that calculates the next time of an apogee and perigee for an orbit,
|
|
2086
|
+
* and the geodetic longitude at which those occur.
|
|
2087
|
+
*
|
|
2088
|
+
* The 'next' times are with respect to an input date.
|
|
2089
|
+
*
|
|
2090
|
+
* Steps:
|
|
2091
|
+
* 1. Propagate the TLE to the reference time.
|
|
2092
|
+
* 2. Get the Cartesian and Keplerian state at the reference time.
|
|
2093
|
+
* 3. At the reference time, compute Eccentric and Mean anomalies, and the time since periapsis.
|
|
2094
|
+
* 4. Compute the time of the next periapsis. Repeat for apoapsis.
|
|
2095
|
+
*
|
|
2096
|
+
* @param {String} line1 TLE line 1
|
|
2097
|
+
* @param {String} line2 TLE line 2
|
|
2098
|
+
* @param {Date} time The time after which to compute the imediately next apogee and perigee times
|
|
2099
|
+
* @param {Boolean} findApogee Propagate for apogee longitude and state
|
|
2100
|
+
* @param {Boolean} findPerigee Propagate for perigee longitude and state
|
|
2101
|
+
* @return {Object} The object with the next apoapsis/periapsis times, and the geodetic longitudes on which they occur.
|
|
2102
|
+
*/
|
|
2103
|
+
const calculateNextApogeePerigeeTimes
|
|
2104
|
+
= async (line1, line2, time, findApogee=true, findPerigee=true) => {
|
|
2105
|
+
const satrec = twoline2satrec(line1, line2);
|
|
2106
|
+
const propagateTo = (t) => propagate(satrec, new Date(t));
|
|
2107
|
+
const pv = propagateTo(time);
|
|
2108
|
+
return await calculateNextApogeePerigeeTimesWithPropagation(
|
|
2109
|
+
pv, propagateTo, time, findApogee, findPerigee,
|
|
2110
|
+
);
|
|
2111
|
+
};
|
|
2112
|
+
|
|
2113
|
+
/**
|
|
2114
|
+
* Computes whether target satellite is leading the primary satellite
|
|
2115
|
+
* @param {*} primaryEphem propagated ephemeris for primary satellite
|
|
2116
|
+
* @param {*} targetEphem propagated ephemeris for target satellite
|
|
2117
|
+
* @return {Boolean} true if target is leading primary
|
|
2118
|
+
*/
|
|
2119
|
+
const isTargetLeading = (primaryEphem, targetEphem) => {
|
|
2120
|
+
const pPos = posToArray(primaryEphem.p);
|
|
2121
|
+
const tPos = posToArray(targetEphem.p);
|
|
2122
|
+
|
|
2123
|
+
const angularMomentum = cross(pPos, posToArray(primaryEphem.v)); // up if ref moving CCW, down if CW
|
|
2124
|
+
const refTargetCross = cross(pPos, tPos); // up if CCW, down if CW (target from ref)
|
|
2125
|
+
|
|
2126
|
+
if (norm(refTargetCross) === 0) {
|
|
2127
|
+
return 1;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// if angular momentum and ref target cross product are in same hemisphere (angle < 90 deg)
|
|
2131
|
+
// then the target is leading the ref, otherwise it is trailing
|
|
2132
|
+
const targetLeading = getAngle(angularMomentum, refTargetCross) <= Math.PI / 2;
|
|
2133
|
+
return targetLeading;
|
|
2134
|
+
};
|
|
2135
|
+
|
|
2136
|
+
/**
|
|
2137
|
+
* Computes phase offset (in degrees) for a given primary (reference) satellite and a target satellite.
|
|
2138
|
+
* Positive phase difference means the target is leading the satellite, negative means trailing
|
|
2139
|
+
* @param {*} pSatRec satRec for primary satellite
|
|
2140
|
+
* @param {*} tSatRec satRec for target satellite
|
|
2141
|
+
* @param {*} time time to evaluate offset at
|
|
2142
|
+
* @return {Number} phase offset in degrees
|
|
2143
|
+
*/
|
|
2144
|
+
const computePhaseDiff = (pSatRec, tSatRec, time) => {
|
|
2145
|
+
const pEphem = propTo(pSatRec, time);
|
|
2146
|
+
const tEphem = propTo(tSatRec, time);
|
|
2147
|
+
|
|
2148
|
+
if (!isDefined(pEphem) || !isDefined(tEphem)) {
|
|
2149
|
+
return null;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
const pPos = posToArray(pEphem.p);
|
|
2153
|
+
const tPos = posToArray(tEphem.p);
|
|
2154
|
+
|
|
2155
|
+
const targetLeading = isTargetLeading(pEphem, tEphem);
|
|
2156
|
+
|
|
2157
|
+
const phaseAngleAbs = getAngle(pPos, tPos) * RAD2DEG;
|
|
2158
|
+
const phaseDiff = phaseAngleAbs * (targetLeading ? 1 : -1); // positive for leading
|
|
2159
|
+
|
|
2160
|
+
return phaseDiff;
|
|
2161
|
+
};
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* Computes orbital period in minutes, given TLE line 2
|
|
2165
|
+
* @param {String} line2 TLE line 2 containing mean motion
|
|
2166
|
+
* @return {Number} orbital period in minutes
|
|
2167
|
+
*/
|
|
2168
|
+
const getOrbitalPeriod = (line2) => {
|
|
2169
|
+
// apparently mean motion should be obtained through TLE instead of satellite.js SatRec (see getElsetUdlFromTle)
|
|
2170
|
+
const meanMotion = parseFloat(line2.slice(52, 63)); // revs per day
|
|
2171
|
+
const period = (1 / meanMotion) * 24.0 * 60.0; // period in minutes
|
|
2172
|
+
return period;
|
|
2173
|
+
};
|
|
2174
|
+
|
|
2175
|
+
/**
|
|
2176
|
+
* Computes phase offset, delta phase offset, time offset, and delta time offset
|
|
2177
|
+
* data for a given primary (reference) satellite and a target satellite.
|
|
2178
|
+
* Positive phase difference means the target is leading the satellite, negative means trailing
|
|
2179
|
+
* @param {String} pLine1, line 1 of the primary (reference) satellite
|
|
2180
|
+
* @param {String} pLine2, line 2 of the primary (reference) satellite
|
|
2181
|
+
* @param {String} tLine1, line 1 of the target satellite
|
|
2182
|
+
* @param {String} tLine2, line 2 of the target satellite
|
|
2183
|
+
* @param {Integer} time, time of the analysis, Unix milliseconds
|
|
2184
|
+
* @return {Object} Data object
|
|
2185
|
+
*/
|
|
2186
|
+
const calculateLeoPhaseDifference = (pLine1, pLine2, tLine1, tLine2, time) => {
|
|
2187
|
+
const pSatRec = twoline2satrec(pLine1, pLine2);
|
|
2188
|
+
const tSatRec = twoline2satrec(tLine1, tLine2);
|
|
2189
|
+
|
|
2190
|
+
const diffNow = computePhaseDiff(pSatRec, tSatRec, time); // degrees
|
|
2191
|
+
const diffFuture = computePhaseDiff(pSatRec, tSatRec, time + MILLIS_PER_DAY);
|
|
2192
|
+
|
|
2193
|
+
if (!isDefined(diffNow) || !isDefined(diffFuture)) {
|
|
2194
|
+
return null;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
const targetLeading = diffNow > 0;
|
|
2198
|
+
|
|
2199
|
+
const period = getOrbitalPeriod(pLine2) * 60; // period in seconds
|
|
2200
|
+
|
|
2201
|
+
// since time is how long target will take to catch up to reference's current phase,
|
|
2202
|
+
// positive phase (target leading ref) means negative time
|
|
2203
|
+
let timeOffset = -diffNow * (period / 360); // period/360 gives seconds per degree
|
|
2204
|
+
let timeOffsetFuture = -diffFuture * (period / 360);
|
|
2205
|
+
|
|
2206
|
+
let deltaPhase = diffFuture - diffNow;
|
|
2207
|
+
|
|
2208
|
+
// if we go from 170 to 190, it becomes (-170) - 170 = -340 when the actual change is +20
|
|
2209
|
+
// so add 360 if < -180
|
|
2210
|
+
// if we go from 190 to 170, it becomes 170 - (-170) = 340 when the actual change is -20
|
|
2211
|
+
// so subtract 360 if > 180
|
|
2212
|
+
if (deltaPhase < -180) {
|
|
2213
|
+
deltaPhase += 360;
|
|
2214
|
+
timeOffsetFuture = -(diffFuture+360) * (period / 360);
|
|
2215
|
+
} else if (deltaPhase > 180) {
|
|
2216
|
+
deltaPhase -= 360;
|
|
2217
|
+
timeOffset = -(diffNow+360) * (period / 360);
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
const deltaTime = timeOffsetFuture - timeOffset;
|
|
2221
|
+
|
|
2222
|
+
return {
|
|
2223
|
+
phaseOffset: diffNow,
|
|
2224
|
+
deltaPhase,
|
|
2225
|
+
timeOffset,
|
|
2226
|
+
deltaTime,
|
|
2227
|
+
targetLeading,
|
|
2228
|
+
};
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
/**
|
|
2232
|
+
* Get LEO waterfall data for a given primary satellite and a set of secondary satellites.
|
|
2233
|
+
* Takes a list of elsets for each satellite, ordered in increasing epoch
|
|
2234
|
+
* @param {Array<Array<Object>>} elsets, list of satellites, each as a list of elsets, with primary sat as first element
|
|
2235
|
+
* @param {Integer} startTime, start time of the analysis, Unix milliseconds
|
|
2236
|
+
* @param {Integer} endTime, end time of the analysis, Unix milliseconds
|
|
2237
|
+
* @param {Integer} stepMs, step time of the analysis in milliseconds, default 10s
|
|
2238
|
+
* @return {Array} Array of data objects
|
|
2239
|
+
*/
|
|
2240
|
+
const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
|
|
2241
|
+
const results = [];
|
|
2242
|
+
|
|
2243
|
+
const start = new Date(startTime).getTime();
|
|
2244
|
+
const end = new Date(endTime).getTime();
|
|
2245
|
+
|
|
2246
|
+
const currentIndices = elsets.map((sat) => 0);
|
|
2247
|
+
const breakpoints = [{time: start, satIndex: -1}, {time: end, satIndex: -1}];
|
|
2248
|
+
elsets.forEach((satElsets, i) => {
|
|
2249
|
+
breakpoints.push(...satElsets.map((e, j) => ({
|
|
2250
|
+
time: new Date(e.Epoch).getTime(),
|
|
2251
|
+
satIndex: i,
|
|
2252
|
+
elsetIndex: j,
|
|
2253
|
+
})).filter((b) => b.time > start && b.time < end)); // ensure breakpoints are inside time range
|
|
2254
|
+
});
|
|
2255
|
+
breakpoints.sort((a, b) => a.time - b.time);
|
|
2256
|
+
|
|
2257
|
+
const satEphems = elsets.map((sat) => []);
|
|
2258
|
+
|
|
2259
|
+
for (let i = 0; i < breakpoints.length-1; i++) {
|
|
2260
|
+
const bkpoint = breakpoints[i];
|
|
2261
|
+
// if we start at breakpoints, it's irregular (since the epochs aren't in lockstep)
|
|
2262
|
+
// we can force it to Math.ceil(segmentStart/stepMs)*stepMs for even steps but since the discrepancy
|
|
2263
|
+
// is less than 10s in a plot over multiple days it's rather negligible
|
|
2264
|
+
// also this makes it easier since we can check if its an epoch based on the segmentStart
|
|
2265
|
+
const segmentStart = bkpoint.time;
|
|
2266
|
+
const segmentEnd = breakpoints[i+1].time;
|
|
2267
|
+
|
|
2268
|
+
if (bkpoint.satIndex >= 0) { // this is a satellite's elset's epoch
|
|
2269
|
+
currentIndices[bkpoint.satIndex] = bkpoint.elsetIndex;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
currentIndices.forEach((elsetIndex, satIndex) => {
|
|
2273
|
+
const elset = elsets[satIndex][elsetIndex];
|
|
2274
|
+
const epoch = new Date(elset.Epoch).getTime();
|
|
2275
|
+
const ephem = prop(elset, segmentStart, segmentEnd, stepMs);
|
|
2276
|
+
satEphems[satIndex].push(...ephem.map((point, pointInd) => {
|
|
2277
|
+
const osculatingElements = cartesianToKeplerian(
|
|
2278
|
+
multiply(posToArray(point.p), 1000.0), // km to m
|
|
2279
|
+
multiply(posToArray(point.v), 1000.0), // km/s to m/s
|
|
2280
|
+
);
|
|
2281
|
+
return {
|
|
2282
|
+
...point,
|
|
2283
|
+
isEpoch: pointInd === 0 && bkpoint.satIndex === satIndex,
|
|
2284
|
+
epoch: epoch,
|
|
2285
|
+
satNo: elset.SatNo,
|
|
2286
|
+
name: elset.CommonName,
|
|
2287
|
+
source: elset.Source,
|
|
2288
|
+
raan: osculatingElements.raan, // deg
|
|
2289
|
+
raanPrecession: elset.RaanPrecessionDegreesPerDay,
|
|
2290
|
+
inclination: osculatingElements.i, // deg
|
|
2291
|
+
};
|
|
2292
|
+
}));
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
const pEphem = satEphems[0];
|
|
2297
|
+
|
|
2298
|
+
satEphems.forEach( (sEphem, satIndex) => {
|
|
2299
|
+
// Find the distance at each time step
|
|
2300
|
+
for (let i = 0; i < pEphem.length; i++) {
|
|
2301
|
+
const pEphemPoint = pEphem[i];
|
|
2302
|
+
const sEphemPoint = sEphem[i];
|
|
2303
|
+
|
|
2304
|
+
const direction = isTargetLeading(pEphemPoint, sEphemPoint) ? 1 : -1;
|
|
2305
|
+
const distKm = direction * dist(pEphemPoint.p, sEphemPoint.p);
|
|
2306
|
+
|
|
2307
|
+
const raanDiff = getRaanDiff(sEphemPoint.raan, pEphemPoint.raan, false);
|
|
2308
|
+
const incDiff = getIncDiff(sEphemPoint.inclination, pEphemPoint.inclination, false);
|
|
2309
|
+
|
|
2310
|
+
const time = new Date(pEphemPoint.t);
|
|
2311
|
+
const epoch = new Date(sEphemPoint.epoch).toISOString();
|
|
2312
|
+
|
|
2313
|
+
let catsAngle = null;
|
|
2314
|
+
let targetVisibility = null;
|
|
2315
|
+
|
|
2316
|
+
if (satIndex > 0) { // not the primary
|
|
2317
|
+
if (areCoordsEqual(pEphemPoint.p, sEphemPoint.p)) {
|
|
2318
|
+
catsAngle = 180; // they're in the same spot, so just say secondary can see primary
|
|
2319
|
+
} else {
|
|
2320
|
+
catsAngle = angleBetween3DCoords(
|
|
2321
|
+
pEphemPoint.p,
|
|
2322
|
+
sEphemPoint.p,
|
|
2323
|
+
getSunDirection(time),
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
const earthEclipsed = doesLineSegmentIntersectEarth(pEphemPoint.p, sEphemPoint.p);
|
|
2328
|
+
const targetShadow = getEclipseStatus(time, posToArray(sEphemPoint.p));
|
|
2329
|
+
targetVisibility = earthEclipsed ? "EARTH ECLIPSED" : targetShadow;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
results.push({
|
|
2333
|
+
name: sEphemPoint.name,
|
|
2334
|
+
satNo: sEphemPoint.satNo,
|
|
2335
|
+
epoch: epoch,
|
|
2336
|
+
isEpoch: sEphemPoint.isEpoch,
|
|
2337
|
+
distance: distKm,
|
|
2338
|
+
time: time.toISOString(),
|
|
2339
|
+
source: sEphemPoint.source,
|
|
2340
|
+
raan: sEphemPoint.raan,
|
|
2341
|
+
raanDiff: raanDiff,
|
|
2342
|
+
raanPrecession: sEphemPoint.raanPrecession,
|
|
2343
|
+
inclination: sEphemPoint.inclination,
|
|
2344
|
+
incDiff: incDiff,
|
|
2345
|
+
primary: pEphemPoint,
|
|
2346
|
+
catsAngle,
|
|
2347
|
+
targetVisibility,
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
2351
|
+
return results;
|
|
2352
|
+
};
|
|
2353
|
+
|
|
2354
|
+
/**
|
|
2355
|
+
* Returns the minimum deltaV for a Lambert maneuver between two satellites,
|
|
2356
|
+
* within the given time range.
|
|
2357
|
+
* @param {*} sat1Tle The initial TLE satrec
|
|
2358
|
+
* @param {*} sat2Tle The final TLE satrec
|
|
2359
|
+
* @param {Date} startTime The start time of the maneuver as unix timestamp in milliseconds
|
|
2360
|
+
* @return {Object} An object containing the maneuver properties
|
|
2361
|
+
*/
|
|
2362
|
+
const getInterceptRendezvousMinDv = (sat1Tle, sat2Tle, startTime) =>{
|
|
2363
|
+
const mu = 3.986004415e5; // km based
|
|
2364
|
+
|
|
2365
|
+
let minDv = Infinity;
|
|
2366
|
+
let revNum = 0;
|
|
2367
|
+
let minIntercept = [0, 0, 0]; // The minimum dv to intercept the target.
|
|
2368
|
+
let minDt = 0; // The time of flight to intercept the target.
|
|
2369
|
+
let interceptPvEci = {p: [0, 0, 0], v: [0, 0, 0]}; // The position and velocity of the interceptor and intercept time.
|
|
2370
|
+
const sat1Pv = propagate(sat1Tle, new Date(startTime));
|
|
2371
|
+
|
|
2372
|
+
// Sweep across 1 orbit period
|
|
2373
|
+
const sat1Period = parseInt((120*Math.PI)/(sat1Tle.no));
|
|
2374
|
+
const sat2Period = parseInt((120*Math.PI)/(sat2Tle.no));
|
|
2375
|
+
const periodS = sat1Period > sat2Period ? sat1Period : sat2Period;
|
|
2376
|
+
|
|
2377
|
+
for (let rev=0; rev < 10; rev++) {
|
|
2378
|
+
for (let dt=100; dt<=periodS; dt+=100) { // Seconds
|
|
2379
|
+
const sat2Pv = propagate(sat2Tle, new Date(startTime + (dt*1000)));
|
|
2380
|
+
const r1 = [sat1Pv.position.x, sat1Pv.position.y, sat1Pv.position.z]; // km based
|
|
2381
|
+
const v1Before = [sat1Pv.velocity.x, sat1Pv.velocity.y, sat1Pv.velocity.z]; // km/s based
|
|
2382
|
+
const r2 = [sat2Pv.position.x, sat2Pv.position.y, sat2Pv.position.z]; // km based
|
|
2383
|
+
|
|
2384
|
+
|
|
2385
|
+
const {v1, v2, vH1} = lambertThomsonAlgorithm(r1, r2, dt, rev, 0, v1Before, mu);
|
|
2386
|
+
const deltaV1 = isDefined(v1) ? subtract(v1, v1Before) : [0, 0, 0];
|
|
2387
|
+
const deltaVH1 = isDefined(vH1) ? subtract(vH1, v1Before): [0, 0, 0];
|
|
2388
|
+
const v1Mag = norm(deltaV1);
|
|
2389
|
+
const vH1Mag = norm(deltaVH1);
|
|
2390
|
+
|
|
2391
|
+
const areArraysEqual = deltaV1.every((val, idx) => val === deltaVH1[idx]);
|
|
2392
|
+
const isZeroArray = deltaV1.every((val) => val === 0);
|
|
2393
|
+
|
|
2394
|
+
if (areArraysEqual && isZeroArray) {
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
const currentVector = v1Mag <= vH1Mag ? deltaV1 : deltaVH1;
|
|
2399
|
+
const currentNorm = v1Mag <= vH1Mag ? v1Mag : vH1Mag;
|
|
2400
|
+
|
|
2401
|
+
if (currentNorm < minDv) {
|
|
2402
|
+
revNum = rev;
|
|
2403
|
+
minDv = currentNorm;
|
|
2404
|
+
minIntercept = {
|
|
2405
|
+
x: currentVector[0],
|
|
2406
|
+
y: currentVector[1],
|
|
2407
|
+
z: currentVector[2],
|
|
2408
|
+
};
|
|
2409
|
+
minDt = dt;
|
|
2410
|
+
interceptPvEci = {
|
|
2411
|
+
p: {
|
|
2412
|
+
x: r2[0],
|
|
2413
|
+
y: r2[1],
|
|
2414
|
+
z: r2[2],
|
|
2415
|
+
},
|
|
2416
|
+
v: {
|
|
2417
|
+
x: v2[0],
|
|
2418
|
+
y: v2[1],
|
|
2419
|
+
z: v2[2],
|
|
2420
|
+
}}; // Position and velocity of interceptor and intercept time.
|
|
2421
|
+
}
|
|
2422
|
+
// As N increments, at some point either both or one of the two energy solutions will be NaN.
|
|
2423
|
+
// At that point, the N sweep can safely stop.
|
|
2424
|
+
if (isNaN(v1Mag) || isNaN(vH1Mag)) {
|
|
2425
|
+
break;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
// First Maneuver: Intercept Target
|
|
2431
|
+
const interceptRSW = multiply(
|
|
2432
|
+
cartesianToRIC( sat1Pv.position, sat1Pv.velocity), // Convert the intercept maneuver vector to RSW coordinates.
|
|
2433
|
+
[minIntercept.x, minIntercept.y, minIntercept.z],
|
|
2434
|
+
);
|
|
2435
|
+
const interceptDv = minDv*1000;
|
|
2436
|
+
|
|
2437
|
+
// Second Maneuver: Rendezvous
|
|
2438
|
+
const sat2PvAtIntercept = propagate(sat2Tle, new Date(startTime + (minDt*1000)));
|
|
2439
|
+
const rendezvousECIArray = subtract(
|
|
2440
|
+
[
|
|
2441
|
+
sat2PvAtIntercept.velocity.x,
|
|
2442
|
+
sat2PvAtIntercept.velocity.y,
|
|
2443
|
+
sat2PvAtIntercept.velocity.z,
|
|
2444
|
+
],
|
|
2445
|
+
[interceptPvEci.v.x, interceptPvEci.v.y, interceptPvEci.v.z],
|
|
2446
|
+
);
|
|
2447
|
+
const rendezvousECI = {
|
|
2448
|
+
x: rendezvousECIArray[0]*1000, // km/s to m/s
|
|
2449
|
+
y: rendezvousECIArray[1]*1000,
|
|
2450
|
+
z: rendezvousECIArray[2]*1000,
|
|
2451
|
+
};
|
|
2452
|
+
const rendezvousDv = norm(rendezvousECIArray)*1000; // km/s to m/s
|
|
2453
|
+
const rendezvousRSW = multiply(
|
|
2454
|
+
cartesianToRIC(interceptPvEci.p, interceptPvEci.v),
|
|
2455
|
+
rendezvousECIArray,
|
|
2456
|
+
);
|
|
2457
|
+
return {
|
|
2458
|
+
startTime: new Date(startTime).toISOString(),
|
|
2459
|
+
revNum,
|
|
2460
|
+
interceptDv_ms: interceptDv,
|
|
2461
|
+
interceptTof_s: minDt,
|
|
2462
|
+
interceptTime: new Date(startTime + (minDt*1000)).toISOString(),
|
|
2463
|
+
interceptManeuverECI_ms: {
|
|
2464
|
+
time: new Date(startTime).toISOString(),
|
|
2465
|
+
x: minIntercept.x*1000, // km/s to m/s
|
|
2466
|
+
y: minIntercept.y*1000,
|
|
2467
|
+
z: minIntercept.z*1000,
|
|
2468
|
+
},
|
|
2469
|
+
interceptManeuverRSW_ms: {
|
|
2470
|
+
time: new Date(startTime).toISOString(),
|
|
2471
|
+
x: interceptRSW[0]*1000,
|
|
2472
|
+
y: interceptRSW[1]*1000,
|
|
2473
|
+
z: interceptRSW[2]*1000,
|
|
2474
|
+
},
|
|
2475
|
+
rendezvousDv_ms: rendezvousDv,
|
|
2476
|
+
rendezvousManeuverECI_ms: rendezvousECI,
|
|
2477
|
+
rendezvousManeuverRSW_ms: {
|
|
2478
|
+
time: new Date(startTime + (minDt*1000)).toISOString(),
|
|
2479
|
+
x: rendezvousRSW[0]*1000,
|
|
2480
|
+
y: rendezvousRSW[1]*1000,
|
|
2481
|
+
z: rendezvousRSW[2]*1000,
|
|
2482
|
+
},
|
|
2483
|
+
totalDv_ms: interceptDv + rendezvousDv,
|
|
2484
|
+
};
|
|
2485
|
+
};
|
|
2486
|
+
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* A function that attempts to perform maneuver recovery (i.e. maneuver deltaV computation) between an initial and final TLE
|
|
2490
|
+
* and return the minimum deltaV possible to perform the maneuver.
|
|
2491
|
+
*
|
|
2492
|
+
* The initial TLE should be the pre-maneuver TLE, and the final TLE should be the post-maneuver TLE.
|
|
2493
|
+
* The computation will work as long as the initial TLE is before the final TLE in terms of epoch, and as long as the satNos match.
|
|
2494
|
+
* Otherwise, a zero vector is retuned.
|
|
2495
|
+
*
|
|
2496
|
+
* The function wraps a parameter sweep strategy and solves a series of Lambert problems.
|
|
2497
|
+
* Each Lambert problem is using the positions of the two TLEs at their respective epochs and the time difference as the time of flight
|
|
2498
|
+
* using the robust method suggested by Thomson:
|
|
2499
|
+
*
|
|
2500
|
+
* *AAS 18-074 Complete Solution of the Lambert Problem with Perturbations and Target State Sensitivity*
|
|
2501
|
+
* by Blair F. Thomson, Denise Brown, and Ryan Cobb
|
|
2502
|
+
*
|
|
2503
|
+
* An outer loop solves Lamberts problem for different values of the number of revolutions, starting from 0 and up to 10.
|
|
2504
|
+
* Inside the loop the core Lambert algorithm return a low-energy solution and a high-energy solution, of which the one with the smallest deltaV magnitude is chosen.
|
|
2505
|
+
*
|
|
2506
|
+
* Once the increments of N start producing NaN results for the deltaV, the loop is broken and the lowest deltaV vector thus far is fetched from the sweep results.
|
|
2507
|
+
*
|
|
2508
|
+
* This parameter sweep is performed because different satellites on various orbits and time of flight periods will yield different deltaV for the same positions and time of flight.
|
|
2509
|
+
*
|
|
2510
|
+
* By varying N lineary from 0 to its breaking point, a basic form of convex convergence on the minimum deltaV is guaranteed.
|
|
2511
|
+
*
|
|
2512
|
+
* @param {Object} initialTLE The initial TLE object
|
|
2513
|
+
* @param {Object} finalTLE The final TLE object
|
|
2514
|
+
* @return {number[]} The minimumDeltaV vector in ECI frame, in units of km/s
|
|
2515
|
+
*/
|
|
2516
|
+
const detectManeuverMinDv = (initialTLE, finalTLE) => {
|
|
2517
|
+
try {
|
|
2518
|
+
// Silently return zero vector if initial and final TLE have different satNo
|
|
2519
|
+
if (initialTLE.satnum !== finalTLE.satnum) {
|
|
2520
|
+
throw new Error(`Initial satNo ${initialTLE.satnum} and Final satNo ${finalTLE.satnum} do not match. `);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// Silently return zero vector if initial TLE is after final TLE
|
|
2524
|
+
if (initialTLE.jdsatepoch > finalTLE.jdsatepoch) {
|
|
2525
|
+
throw new Error(`Initial TLE epoch ${julianToGregorian(initialTLE.jdsatepoch)} is after Final TLE epoch ${julianToGregorian(finalTLE.jdsatepoch)}`);
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// Propagate initial TLE to its TLE epoch
|
|
2529
|
+
const pvInitial = propagate(initialTLE, julianToGregorian(initialTLE.jdsatepoch));
|
|
2530
|
+
|
|
2531
|
+
// Propagate final TLE to its own TLE epoch
|
|
2532
|
+
const pvFinal = propagate(finalTLE, julianToGregorian(finalTLE.jdsatepoch));
|
|
2533
|
+
|
|
2534
|
+
// Compute the epoch difference between the two TLEs
|
|
2535
|
+
// This will be the used as the time of flight between initial and final state.
|
|
2536
|
+
const dt = Math.abs((julianToGregorian(finalTLE.jdsatepoch)
|
|
2537
|
+
- julianToGregorian(initialTLE.jdsatepoch)) / 1000);
|
|
2538
|
+
|
|
2539
|
+
// Position of initial TLE at its epoch
|
|
2540
|
+
const r1 = [pvInitial.position.x, pvInitial.position.y, pvInitial.position.z]; // km based
|
|
2541
|
+
// Velocity of initial TLE at TLE epoch
|
|
2542
|
+
const v1Before = [pvInitial.velocity.x, pvInitial.velocity.y, pvInitial.velocity.z]; // km/s based
|
|
2543
|
+
|
|
2544
|
+
// Position of final TLE at its epoch
|
|
2545
|
+
const r2 = [pvFinal.position.x, pvFinal.position.y, pvFinal.position.z]; // km based
|
|
2546
|
+
|
|
2547
|
+
// Earth Gravitational Parameter
|
|
2548
|
+
const mu = 3.986004415e5; // km based
|
|
2549
|
+
|
|
2550
|
+
// Loop for N until NaN
|
|
2551
|
+
let minNorm = Infinity;
|
|
2552
|
+
// let minIndex = -1;
|
|
2553
|
+
let minVector = [0, 0, 0];
|
|
2554
|
+
for (let i=0; i<10; i++) { // loop up to 10 revolutions, it is a safe upper limit to capture impulsive maneuvers
|
|
2555
|
+
const {v1, vH1} = lambertThomsonAlgorithm(r1, r2, dt, i, 0, v1Before, mu);
|
|
2556
|
+
|
|
2557
|
+
const deltaV1 = isDefined(v1) ? subtract(v1, v1Before) : [0, 0, 0];
|
|
2558
|
+
const deltaVH1 = isDefined(vH1) ? subtract(vH1, v1Before): [0, 0, 0];
|
|
2559
|
+
const v1Mag = norm(deltaV1);
|
|
2560
|
+
const vH1Mag = norm(deltaVH1);
|
|
2561
|
+
|
|
2562
|
+
const isZeroArray = deltaV1.every((val) => val === 0);
|
|
2563
|
+
|
|
2564
|
+
if (deltaV1 === deltaVH1 && isZeroArray ) {
|
|
2565
|
+
continue;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
const currentVector = v1Mag <= vH1Mag ? deltaV1 : deltaVH1;
|
|
2569
|
+
const currentNorm = v1Mag <= vH1Mag ? v1Mag : vH1Mag;
|
|
2570
|
+
|
|
2571
|
+
if (currentNorm < minNorm) {
|
|
2572
|
+
minNorm = currentNorm;
|
|
2573
|
+
// minIndex = i;
|
|
2574
|
+
minVector = currentVector;
|
|
2575
|
+
}
|
|
2576
|
+
// As N increments, at some point either both or one of the two energy solutions will be NaN.
|
|
2577
|
+
// At that point, the N sweep can safely stop.
|
|
2578
|
+
if (isNaN(v1Mag) || isNaN(vH1Mag)) {
|
|
2579
|
+
break;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
return minVector;
|
|
2583
|
+
} catch (err) {
|
|
2584
|
+
console.warn(err);
|
|
2585
|
+
return [0, 0, 0];
|
|
2586
|
+
}
|
|
2587
|
+
};
|
|
2588
|
+
|
|
2589
|
+
/** This is an implementation of the Lambert Problem using the algorithm described by Thomson in:
|
|
2590
|
+
*
|
|
2591
|
+
* *AAS 18-074 Complete Solution of the Lambert Problem with Perturbations and Target State Sensitivity*
|
|
2592
|
+
* by Blair F. Thomson, Denise Brown, and Ryan Cobb
|
|
2593
|
+
*
|
|
2594
|
+
* The algorithm is a more numerically stable and robust implementation of the Battin algorithm or the Universal Variable formulation.
|
|
2595
|
+
* It is stable for transfers near or at 180 degrees. All benchmark cases of the algorithm are added as tests for this method.
|
|
2596
|
+
*
|
|
2597
|
+
* The algorithm can accept units in both km or meter based, as long as all of the inputs are consistent.
|
|
2598
|
+
* For example, if the initial position vector is in km, the the final position vector, as well as the initial velocity and gravitational parameter must be in km based units (and km/s respectively).
|
|
2599
|
+
*
|
|
2600
|
+
* @param {Array<Number>} r1 The initial position vector
|
|
2601
|
+
* @param {Array<Number>} r2 The final position vector
|
|
2602
|
+
* @param {Number} t The time of flight between initial and final position in seconds
|
|
2603
|
+
* @param {Integer} N The number of revolutions
|
|
2604
|
+
* @param {Integer} D A flag indicating the direction of the transfer. D = 1 for short way transfers, D = -1 for long way transfers. D = 0 will allow the algorithm to decide.
|
|
2605
|
+
* @param {Array<Number>} v1Minus The initial velocity vector prior to the application of the maneuver.
|
|
2606
|
+
* @param {Number} mu The gravitational parameter of the central body. Defaults to the value of the Earth's gravitational parameter in meter based units (so use meter based vectors if using the default mu!)
|
|
2607
|
+
* @param {Number} outOfPlaneError The out-of-plane error tolerance error to allow if the user wants to restrict the transfer to the initial orbital plane. Defaults to 0 for no initial plane restriction. Tweak this value if you suspect that the resulting deltaV is too large for a relatively small rendevous distance.
|
|
2608
|
+
* @return {Object} An object containing the following properties:
|
|
2609
|
+
* - {Array<Number>} v1: The initial velocity vector after the application of the maneuver.
|
|
2610
|
+
* - {Array<Number>} v2: The final velocity vector after the application of the maneuver.
|
|
2611
|
+
* - {Array<Number>} vH1: The high-energy velocity vector for the first leg of the transfer.
|
|
2612
|
+
* - {Array<Number>} vH2: The high-energy velocity vector for the second leg of the transfer.
|
|
2613
|
+
* - {Number} theta: The transfer angle in radians. This is only for information purposes. It does not consider revolutions.
|
|
2614
|
+
*
|
|
2615
|
+
*/
|
|
2616
|
+
const lambertThomsonAlgorithm
|
|
2617
|
+
= (r1, r2, t, N, D = 0, v1Minus, mu = 3.986004415e14, outOfPlaneError = 0) => {
|
|
2618
|
+
// Out-Of-Plane error check
|
|
2619
|
+
const r1Mag = norm(r1);
|
|
2620
|
+
const r2Mag = norm(r2);
|
|
2621
|
+
const r2Scaled = multiply(r2, r1Mag / r2Mag);
|
|
2622
|
+
const r2PlusR1Scaled = add(r2, r2Scaled);
|
|
2623
|
+
|
|
2624
|
+
if (norm(r2PlusR1Scaled) <= outOfPlaneError) {
|
|
2625
|
+
const crossProduct = cross(r1, v1Minus);
|
|
2626
|
+
const nH = normalize({x: crossProduct[0], y: crossProduct[1], z: crossProduct[2]});
|
|
2627
|
+
const f = dot(nH, subtract(r1, r2));
|
|
2628
|
+
r2 = multiply(normalize(add(r2, multiply([nH.x, nH.y, nH.z], f))), r2Mag);
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
// ----- Lines 2-5 -----
|
|
2632
|
+
let A = dot(r1, r2) / (r1Mag * r2Mag);
|
|
2633
|
+
if (A > 1.0) A = 1.0;
|
|
2634
|
+
if (A < -1.0) A = -1.0;
|
|
2635
|
+
let theta = Math.acos(A); // transfer angle
|
|
2636
|
+
|
|
2637
|
+
// ----- Lines 6-8: Determine transfer direction -----
|
|
2638
|
+
const crossR1V1 = cross(r1, v1Minus);
|
|
2639
|
+
const crossR1R2 = cross(r1, r2);
|
|
2640
|
+
if (D === -1 || (D === 0 && dot(crossR1V1, crossR1R2) < 0)) {
|
|
2641
|
+
theta = 2 * Math.PI - theta; // long way transfer
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
// ----- Lines 9-13 -----
|
|
2645
|
+
const c = norm(subtract(r2, r1)); // chord length
|
|
2646
|
+
const s = 0.5 * (r1Mag + r2Mag + c); // semiperimeter
|
|
2647
|
+
const lambda = (1.0 / s) * Math.sqrt(r1Mag * r2Mag) * Math.cos(theta / 2.0);
|
|
2648
|
+
const L = Math.pow((1 - lambda) / (1 + lambda), 2.0);
|
|
2649
|
+
const mVal = 8 * mu * t * t / (Math.pow(s, 3.0) * Math.pow(1 + lambda, 6.0));
|
|
2650
|
+
|
|
2651
|
+
// ----- Line 14: Initial x -----
|
|
2652
|
+
let x = (N > 0) ? (1 + 4 * L) : L;
|
|
2653
|
+
let xdiff = 1.0;
|
|
2654
|
+
let y = 0.0;
|
|
2655
|
+
|
|
2656
|
+
// ----- Lines 15-54: Iteration for x -----
|
|
2657
|
+
while (xdiff > 1e-6) {
|
|
2658
|
+
let h1; let h2;
|
|
2659
|
+
if (N > 0) {
|
|
2660
|
+
// ----- Lines 17-19: Multi-rev (N > 0) branch using Loechler's method -----
|
|
2661
|
+
const numerator = Math.pow(L + x, 2.0);
|
|
2662
|
+
const denominator = 4 * x * x * (1 + 2 * x + L);
|
|
2663
|
+
const sqrtx = Math.sqrt(x);
|
|
2664
|
+
const commonTerm = ((N * Math.PI / 2 + Math.atan(sqrtx)) / sqrtx);
|
|
2665
|
+
// The bracketed term (Loechler p.30 (4.3))
|
|
2666
|
+
const term1 = 3 * Math.pow(1 + x, 2.0) * commonTerm - (3 + 5 * x);
|
|
2667
|
+
h1 = (numerator / denominator) * term1;
|
|
2668
|
+
|
|
2669
|
+
// ----- Line 19: h2 (Loechler p.31 (4.4)) -----
|
|
2670
|
+
const term2 = (x * x - x * (1 + L) - 3 * L) * commonTerm + (3 * L + x);
|
|
2671
|
+
h2 = mVal / denominator * term2;
|
|
2672
|
+
} else {
|
|
2673
|
+
// ----- Lines 20-36: Single-rev branch -----
|
|
2674
|
+
const eta = x / Math.pow(Math.sqrt(1 + x) + 1, 2.0);
|
|
2675
|
+
let bn = 3; let dn = 1; let un = 8 * (Math.sqrt(1 + x) + 1) / bn;
|
|
2676
|
+
let xi = un;
|
|
2677
|
+
let i = 1;
|
|
2678
|
+
while (Math.abs(un) > 1e-12) {
|
|
2679
|
+
const bp = bn; const dp = dn; const up = un;
|
|
2680
|
+
i++;
|
|
2681
|
+
let an;
|
|
2682
|
+
if (i === 2) {
|
|
2683
|
+
an = -1;
|
|
2684
|
+
bn = 5 + eta;
|
|
2685
|
+
} else if (i === 3) {
|
|
2686
|
+
an = -9 * eta / 7;
|
|
2687
|
+
bn = 1;
|
|
2688
|
+
} else {
|
|
2689
|
+
an = -eta * i * i / (4 * i * i - 1);
|
|
2690
|
+
bn = 1;
|
|
2691
|
+
}
|
|
2692
|
+
dn = 1 / (1 - an * dp / bp / bn);
|
|
2693
|
+
un = up * (dn - 1);
|
|
2694
|
+
xi += un;
|
|
2695
|
+
}
|
|
2696
|
+
h1 = (Math.pow(L + x, 2.0) * (1 + 3 * x + xi))
|
|
2697
|
+
/ ((1 + 2 * x + L) * (4 * x + xi * (3 + x)));
|
|
2698
|
+
h2 = mVal * (x - L + xi) / ((1 + 2 * x + L) * (4 * x + xi * (3 + x)));
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
// ----- Lines 38-39: Compute B and solve for K -----
|
|
2702
|
+
const BVal = 27.0 * h2 / (4.0 * Math.pow(1 + h1, 3.0));
|
|
2703
|
+
const uVal = BVal / (2.0 * (Math.sqrt(1 + BVal) + 1.0));
|
|
2704
|
+
|
|
2705
|
+
const bn2 = 1.0; let dn2 = 1.0; let un2 = 1.0 / 3.0;
|
|
2706
|
+
let K = un2;
|
|
2707
|
+
let n = 0;
|
|
2708
|
+
let evenflag = 1;
|
|
2709
|
+
while (Math.abs(un2) > 1e-12) {
|
|
2710
|
+
const bp = bn2; const dp = dn2; const up = un2;
|
|
2711
|
+
let an;
|
|
2712
|
+
if (evenflag === 1) {
|
|
2713
|
+
an = -2.0 * uVal * (3 * n + 2) * (6 * n + 1) / (9 * (4 * n + 1) * (4 * n + 3));
|
|
2714
|
+
evenflag = 0;
|
|
2715
|
+
n++;
|
|
2716
|
+
} else {
|
|
2717
|
+
an = -2.0 * uVal * (3 * n + 1) * (6 * n - 1) / 9.0 / (4 * n - 1) / (4 * n + 1);
|
|
2718
|
+
evenflag = 1;
|
|
2719
|
+
}
|
|
2720
|
+
dn2 = 1.0 / (1 - an * dp / bp / bn2);
|
|
2721
|
+
un2 = up * (dn2 - 1.0);
|
|
2722
|
+
K += un2;
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
// ----- Line 50: Compute y -----
|
|
2726
|
+
y = (1 + h1) / 3.0 * (2 + Math.sqrt(1 + BVal) / (1.0 + 2.0 * uVal * K * K));
|
|
2727
|
+
// ----- Line 51: Update xnew (Battin p.335 (7.113))
|
|
2728
|
+
const xnew = Math.sqrt(Math.pow((1.0 - L)/2.0, 2.0) + mVal/(y*y)) - (1.0 + L)/2.0;
|
|
2729
|
+
// ----- Line 52: Compute difference -----
|
|
2730
|
+
xdiff = Math.abs(xnew - x);
|
|
2731
|
+
// ----- Line 53: Update x -----
|
|
2732
|
+
x = xnew;
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
// ----- Lines 55-57: Compute p and eccentricity -----
|
|
2736
|
+
const pVal = 2 * r1Mag * r2Mag * y * y * Math.pow(1 + x, 2.0)
|
|
2737
|
+
* Math.pow(Math.sin(theta / 2), 2.0)
|
|
2738
|
+
/ (mVal * s * Math.pow(1 + lambda, 2.0));
|
|
2739
|
+
const eps = (r2Mag - r1Mag) / r1Mag;
|
|
2740
|
+
const eVal = Math.sqrt(
|
|
2741
|
+
(Math.pow(eps, 2.0) + 4.0 * r2Mag / r1Mag
|
|
2742
|
+
* Math.pow(Math.sin(theta / 2.0), 2.0) * Math.pow((L - x) / (L + x), 2.0))
|
|
2743
|
+
/ (Math.pow(eps, 2.0) + (4.0 * r2Mag / r1Mag) * Math.pow(Math.sin(theta / 2.0), 2.0)),
|
|
2744
|
+
);
|
|
2745
|
+
|
|
2746
|
+
// ----- Line 58: Call hodograph velocity algorithm -----
|
|
2747
|
+
const {v1, v2} = hodographVelocityAlgorithm(r1, r2, t, v1Minus, theta, pVal, eVal, mu);
|
|
2748
|
+
|
|
2749
|
+
// ----- Lines 59-84: Multi-rev high-energy solution -----
|
|
2750
|
+
let vH1 = [0, 0, 0];
|
|
2751
|
+
let vH2 = [0, 0, 0];
|
|
2752
|
+
|
|
2753
|
+
if (N > 0) {
|
|
2754
|
+
// Line 61:
|
|
2755
|
+
x = 1e-20;
|
|
2756
|
+
xdiff = 1.0;
|
|
2757
|
+
while (xdiff > 1e-6) {
|
|
2758
|
+
// ----- Line 64: Compute h1 for high-energy solution (Loechler p.35 (5.5)) -----
|
|
2759
|
+
const h1He = (L + x) * (1 + 2 * x + L) / (2 * (L - x * x));
|
|
2760
|
+
// ----- Line 65: Compute h2 for high-energy solution (Loechler p.35 (5.6)) -----
|
|
2761
|
+
const h2He = mVal * Math.sqrt(x) / (2 * (L - x * x))
|
|
2762
|
+
* ((L - x * x) * ((N * Math.PI / 2 + Math.atan(Math.sqrt(x)))
|
|
2763
|
+
/ Math.sqrt(x)) - (L + x));
|
|
2764
|
+
// ----- Line 66: Compute B for high-energy solution (Loechler p.37 (5.9)) -----
|
|
2765
|
+
const BHe = 27 * h2He / (4 * Math.pow(Math.sqrt(x) * (1 + h1He), 3.0));
|
|
2766
|
+
let F;
|
|
2767
|
+
if (BHe < 0) {
|
|
2768
|
+
// ----- Line 68: Compute F for negative B -----
|
|
2769
|
+
F = 2 * Math.cos(1.0 / 3.0 * Math.acos(Math.sqrt(BHe + 1)));
|
|
2770
|
+
} else {
|
|
2771
|
+
// ----- Lines 70-71: Compute F for nonnegative B -----
|
|
2772
|
+
const AHe = Math.pow(Math.sqrt(BHe) + Math.sqrt(BHe + 1), 1.0 / 3.0);
|
|
2773
|
+
F = AHe + 1 / AHe;
|
|
2774
|
+
}
|
|
2775
|
+
// ----- Line 73: Compute y for high-energy solution (Loechler p.37 (5.10)) -----
|
|
2776
|
+
const yHe = (2.0 / 3.0) * Math.sqrt(x) * (1 + h1He) * (Math.sqrt(BHe+1) / F + 1);
|
|
2777
|
+
// ----- Line 74: Update xnew for high-energy solution (Loechler p.34 (5.3)) -----
|
|
2778
|
+
const temp = (mVal / (yHe * yHe)) - (1 + L);
|
|
2779
|
+
const xnewHe = 0.5 * (temp - Math.sqrt(temp * temp - 4 * L));
|
|
2780
|
+
xdiff = Math.abs(xnewHe - x);
|
|
2781
|
+
x = xnewHe;
|
|
2782
|
+
}
|
|
2783
|
+
// ----- Line 78: Compute semi-major axis a -----
|
|
2784
|
+
const eVal = s * Math.pow(1 + lambda, 2.0) * (1 + x) * (L + x) / (8 * x);
|
|
2785
|
+
// ----- Line 79: Compute p for high-energy solution -----
|
|
2786
|
+
const pHe = 2 * r1Mag * r2Mag * Math.pow(Math.sin(theta / 2), 2.0) * (1 + x)
|
|
2787
|
+
/ (s * Math.pow(1 + lambda, 2.0) * (L + x));
|
|
2788
|
+
// ----- Line 80: Compute eccentricity for high-energy solution -----
|
|
2789
|
+
const eHe = Math.sqrt(1 - pHe / eVal);
|
|
2790
|
+
// ----- Line 81: Compute high-energy velocities -----
|
|
2791
|
+
const highEnergyVels = hodographVelocityAlgorithm(r1, r2, t, v1Minus, theta, pHe, eHe, mu);
|
|
2792
|
+
vH1 = highEnergyVels.v1;
|
|
2793
|
+
vH2 = highEnergyVels.v2;
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
return {v1, v2, vH1, vH2, theta};
|
|
2797
|
+
};
|
|
2798
|
+
|
|
2799
|
+
/**
|
|
2800
|
+
* Implements the Hodograph Velocity Algorithm.
|
|
2801
|
+
* @param {Array<number>} r1 Initial position vector
|
|
2802
|
+
* @param {Array<number>} r2 Final position vector
|
|
2803
|
+
* @param {number} t Time of flight
|
|
2804
|
+
* @param {Array<number>} v1Minus Initial velocity vector
|
|
2805
|
+
* @param {number} theta Transfer angle in radians
|
|
2806
|
+
* @param {number} p Semi-latus rectum
|
|
2807
|
+
* @param {number} e Eccentricity
|
|
2808
|
+
* @param {number} mu Gravitational parameter
|
|
2809
|
+
* @return {{v1: Array<number>, v2: Array<number>}} Initial and final velocity vectors
|
|
2810
|
+
*/
|
|
2811
|
+
const hodographVelocityAlgorithm = (r1, r2, t, v1Minus, theta, p, e, mu) => {
|
|
2812
|
+
// Line 2: Define L180 (in meters)
|
|
2813
|
+
const L180 = 1.0;
|
|
2814
|
+
|
|
2815
|
+
const r1Mag = norm(r1);
|
|
2816
|
+
const r2Mag = norm(r2);
|
|
2817
|
+
|
|
2818
|
+
// Line 3: A = mu*(1/r1 - 1/p)
|
|
2819
|
+
const eVal = mu * (1.0 / r1Mag - 1.0 / p);
|
|
2820
|
+
|
|
2821
|
+
// Line 4: B = (mu*e/p)^2 - A^2
|
|
2822
|
+
const BVal = Math.pow(mu * e / p, 2.0) - (eVal * eVal);
|
|
2823
|
+
|
|
2824
|
+
// Line 5: if B <= 0 then x1 = 0 else x1 = -sqrt(B)
|
|
2825
|
+
let x1 = (BVal <= 0) ? 0.0 : -Math.sqrt(BVal);
|
|
2826
|
+
|
|
2827
|
+
let nHat; // unit normal vector
|
|
2828
|
+
|
|
2829
|
+
// Line 6: Check if |sin(theta)| < L180 / r2Mag
|
|
2830
|
+
if (Math.abs(Math.sin(theta)) < L180 / r2Mag) {
|
|
2831
|
+
// Line 7: nHat = (r1 x v1Minus) normalized
|
|
2832
|
+
const crossProduct = cross(r1, v1Minus);
|
|
2833
|
+
nHat = normalize({x: crossProduct[0], y: crossProduct[1], z: crossProduct[2]});
|
|
2834
|
+
nHat = [nHat.x, nHat.y, nHat.z];
|
|
2835
|
+
|
|
2836
|
+
// Line 8: If the orbit is elliptical (e < 1)
|
|
2837
|
+
if (e < 1.0) {
|
|
2838
|
+
// Line 9: P = 2π * sqrt( p^3 / [ mu * (1-e^2)^3 ] )
|
|
2839
|
+
const P = 2 * Math.PI * Math.sqrt(Math.pow(p, 3.0) / (mu * Math.pow(1 - e * e, 3.0)));
|
|
2840
|
+
// Line 10: If (t mod P) > (P/2) then reverse x1
|
|
2841
|
+
const tMod = t % P;
|
|
2842
|
+
if (tMod > P / 2) {
|
|
2843
|
+
x1 = -x1;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
} else {
|
|
2847
|
+
// Line 13: nHat = (r1 x r2) normalized
|
|
2848
|
+
const crossProduct = cross(r1, r2);
|
|
2849
|
+
nHat = normalize({x: crossProduct[0], y: crossProduct[1], z: crossProduct[2]});
|
|
2850
|
+
nHat = [nHat.x, nHat.y, nHat.z];
|
|
2851
|
+
|
|
2852
|
+
// Line 14: If (theta mod 2π) > π then reverse nHat
|
|
2853
|
+
const thetaMod = theta % (2 * Math.PI);
|
|
2854
|
+
if (thetaMod > Math.PI) {
|
|
2855
|
+
nHat = nHat.map((x) => -x);
|
|
2856
|
+
}
|
|
2857
|
+
// Line 15: y2a = mu/p - x1*sin(theta) + A*cos(theta)
|
|
2858
|
+
const y2a = mu / p - x1 * Math.sin(theta) + eVal * Math.cos(theta);
|
|
2859
|
+
// Line 16: y2b = mu/p + x1*sin(theta) + A*cos(theta)
|
|
2860
|
+
const y2b = mu / p + x1 * Math.sin(theta) + eVal * Math.cos(theta);
|
|
2861
|
+
// Line 17: if |mu/r2Mag - y2b| < |mu/r2Mag - y2a| then reverse x1
|
|
2862
|
+
if (Math.abs(mu / r2Mag - y2b) < Math.abs(mu / r2Mag - y2a)) {
|
|
2863
|
+
x1 = -x1;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// Line 19
|
|
2868
|
+
const term1V1 = multiply(r1, x1 / mu);
|
|
2869
|
+
const crossProduct1 = cross(nHat, r1);
|
|
2870
|
+
const term2V1 = multiply(crossProduct1, 1/r1Mag);
|
|
2871
|
+
const v1 = multiply(add(term1V1, term2V1), Math.sqrt(mu * p) / r1Mag);
|
|
2872
|
+
|
|
2873
|
+
// Line 20
|
|
2874
|
+
const x2 = x1 * Math.cos(theta) + eVal * Math.sin(theta);
|
|
2875
|
+
|
|
2876
|
+
// Line 21
|
|
2877
|
+
const term1V2 = multiply(r2, x2 / mu);
|
|
2878
|
+
const crossProduct2 = cross(nHat, r2);
|
|
2879
|
+
const term2V2 = multiply(crossProduct2, 1/r2Mag);
|
|
2880
|
+
const v2 = multiply(add(term1V2, term2V2), Math.sqrt(mu * p) / r2Mag);
|
|
2881
|
+
|
|
2882
|
+
return {v1, v2};
|
|
2883
|
+
};
|
|
2884
|
+
|
|
2885
|
+
const wrap360 = (r) => {
|
|
2886
|
+
if (r < 0) return r + 360;
|
|
2887
|
+
if (r >= 360) return r - 360;
|
|
2888
|
+
return r;
|
|
2889
|
+
};
|
|
2890
|
+
|
|
2891
|
+
const inWrappedRange = (val, low, high) => {
|
|
2892
|
+
if (low <= high) {
|
|
2893
|
+
return val >= low && val <= high;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
// e.g. 350 to 10 -> 350 to 360 OR 0 to 10
|
|
2897
|
+
// since wrapped value cannot be >360 nor <0 this simplifies to >350 or <10
|
|
2898
|
+
return val >= low || val <= high;
|
|
2899
|
+
};
|
|
2900
|
+
|
|
2901
|
+
const subtractWrapped = (a, b, range=360) => { // a - b, range = 360 for example for 0-360 range
|
|
2902
|
+
const diff = a - b;
|
|
2903
|
+
if (diff > range/2) { // 350 - 10 = 340 => 350 - 370 = -20
|
|
2904
|
+
return diff - range;
|
|
2905
|
+
} else if (diff < -range/2) { // 10 - 350 = -340 => 370 - 350 = 20
|
|
2906
|
+
return diff + range;
|
|
2907
|
+
}
|
|
2908
|
+
return diff;
|
|
2909
|
+
};
|
|
2910
|
+
|
|
2911
|
+
const propagateRaan = (raan, raanPrecessionPerDay, currentTime, epoch) => {
|
|
2912
|
+
const epochMs = parseDate(epoch).getTime();
|
|
2913
|
+
const currentTimeMs = new Date(currentTime).getTime();
|
|
2914
|
+
const currentRaan
|
|
2915
|
+
= wrap360(raan + (currentTimeMs - epochMs) / MILLIS_PER_DAY * raanPrecessionPerDay);
|
|
2916
|
+
return currentRaan;
|
|
2917
|
+
};
|
|
2918
|
+
|
|
2919
|
+
const getIncDiff = (incCurrent, incPrimary, absoluteValue=true) => {
|
|
2920
|
+
const diff = subtractWrapped(incCurrent, incPrimary, 180); // Inclination is 0 to 180
|
|
2921
|
+
return absoluteValue ? Math.abs(diff) : diff;
|
|
2922
|
+
};
|
|
2923
|
+
|
|
2924
|
+
const getRaanDiff = (raanCurrent, raanPrimary, absoluteValue=true) => {
|
|
2925
|
+
const diff = subtractWrapped(raanCurrent, raanPrimary, 360); // RAAN is 0 to 360
|
|
2926
|
+
return absoluteValue ? Math.abs(diff) : diff;
|
|
2927
|
+
};
|
|
2928
|
+
|
|
2929
|
+
const getRaanClosureRate = (raanPrecessionCurrent, raanPrecessionPrimary) => {
|
|
2930
|
+
return Math.abs(raanPrecessionCurrent - raanPrecessionPrimary); // closure rate should always be positive
|
|
2931
|
+
};
|
|
2932
|
+
|
|
2933
|
+
const getRaanAlignTime = (raan, raanDrift, raanTarget, raanTargetDrift, currentTime) => {
|
|
2934
|
+
// imagine a circle where positive is counterclockwise
|
|
2935
|
+
const netDrift = raanDrift - raanTargetDrift; // how fast we are drifting along the circle
|
|
2936
|
+
if (netDrift === 0) return null;
|
|
2937
|
+
if (raan === raanTarget) return new Date(currentTime);
|
|
2938
|
+
|
|
2939
|
+
let diff = raanTarget - raan; // the distance along the circle we need to drift to reach the target, might be negative or positive
|
|
2940
|
+
|
|
2941
|
+
if (Math.sign(netDrift) !== Math.sign(diff)) {
|
|
2942
|
+
// we need to transform the distance to be in the same direction as we are drifting
|
|
2943
|
+
if (diff < 0) diff += 360; // target is behind us, e.g. target is 10 and we are 350 (diff -340): get the other part of the circle (+20)
|
|
2944
|
+
else diff -= 360; // diff > 0, so target is ahead of us (+20), but we are going backwards, get the other part of the circle (-340)
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// diff and netdrift signs should match now
|
|
2948
|
+
const raanAlignMillis = (diff / netDrift) * MILLIS_PER_DAY; // drift is deg/day
|
|
2949
|
+
if (raanAlignMillis > MILLIS_PER_DAY * 365) {
|
|
2950
|
+
return null; // will take over a year to align
|
|
2951
|
+
}
|
|
2952
|
+
return new Date(new Date(currentTime).getTime() + raanAlignMillis);
|
|
2953
|
+
};
|
|
2954
|
+
|
|
2955
|
+
const getRaanAlignTimeStr = (raan, raanDrift, raanTarget, raanTargetDrift, currentTime) => {
|
|
2956
|
+
const alignTime = getRaanAlignTime(raan, raanDrift, raanTarget, raanTargetDrift, currentTime);
|
|
2957
|
+
return alignTime?.toISOString() ?? "Relatively stationary";
|
|
2958
|
+
};
|
|
2959
|
+
|
|
2960
|
+
const getRaanDetails = (raan, raanPrecessionDegreesPerDay, epoch,
|
|
2961
|
+
primaryRaan, primaryRaanPrecessionDegreesPerDay, primaryEpoch, currentTime,
|
|
2962
|
+
absoluteValueDiff=true,
|
|
2963
|
+
) => {
|
|
2964
|
+
const raanDrift = raanPrecessionDegreesPerDay;
|
|
2965
|
+
const currentRaan = propagateRaan(raan, raanDrift,
|
|
2966
|
+
currentTime, epoch);
|
|
2967
|
+
|
|
2968
|
+
const primaryRaanDrift = primaryRaanPrecessionDegreesPerDay;
|
|
2969
|
+
const currentPrimaryRaan = propagateRaan(primaryRaan, primaryRaanDrift,
|
|
2970
|
+
currentTime, primaryEpoch);
|
|
2971
|
+
|
|
2972
|
+
return {
|
|
2973
|
+
Raan: currentRaan,
|
|
2974
|
+
RaanDrift: raanDrift,
|
|
2975
|
+
RaanDiff: getRaanDiff(currentRaan, currentPrimaryRaan, absoluteValueDiff),
|
|
2976
|
+
RaanClosureRate: getRaanClosureRate(raanDrift, primaryRaanDrift),
|
|
2977
|
+
RaanAlignTime: getRaanAlignTimeStr(currentRaan, raanDrift,
|
|
2978
|
+
currentPrimaryRaan, primaryRaanDrift, currentTime),
|
|
2979
|
+
};
|
|
2980
|
+
};
|
|
2981
|
+
|
|
2982
|
+
/** Calculates if a satellite is in shadow.
|
|
2983
|
+
* Based on Astro Library's implementation of Algorithm 34 SHADOW from Vallado's Fundamentals of Astrodynamics and Applications.
|
|
2984
|
+
* Astrolibrary's implementation is adding up a few frame transformations from the GCRF to the J2000 frame for the sun position.
|
|
2985
|
+
* Those are implemented below.
|
|
2986
|
+
*
|
|
2987
|
+
* August 2025: An attempt was made to use the pious-squid shadow algorithm SunBody.shadow(...), but the function returns true/false and cannot distinguish between partial and full shadow.
|
|
2988
|
+
* Therefore the astrolibrary implementation is used instead.
|
|
2989
|
+
*
|
|
2990
|
+
* Returns a string:
|
|
2991
|
+
* - "None": if the satellite is in sunlight, or
|
|
2992
|
+
* - "Penumbra": if in partial sunlight, or
|
|
2993
|
+
* - "Umbra": if in full shadow
|
|
2994
|
+
*
|
|
2995
|
+
* @param {Date} epoch UTC time epoch of the measurement
|
|
2996
|
+
* @param {Object} satPos satellite position at the specified epoch
|
|
2997
|
+
* @return {String} type of shadow (None, Penumbra, Umbra)
|
|
2998
|
+
*/
|
|
2999
|
+
const isSatInShadow = (epoch, satPos) => {
|
|
3000
|
+
// Constants
|
|
3001
|
+
const re = WGS84_EARTH_EQUATORIAL_RADIUS_KM; // Earth radius in km
|
|
3002
|
+
const rs = SUN_RADIUS_KM; // Sun radius in km
|
|
3003
|
+
const arcsec2rad = ARCSEC2RAD;
|
|
3004
|
+
const sec2rad = SEC2RAD;
|
|
3005
|
+
const auDist = AU_KM; // 1 AU in km
|
|
3006
|
+
const alphaUmbra = Math.atan((rs - re) / auDist); // half angle of the umbra
|
|
3007
|
+
const alphaPenumbra = Math.atan((rs + re) / auDist); // half angle of the penumbra
|
|
3008
|
+
|
|
3009
|
+
// Begin with no shadow
|
|
3010
|
+
let shadowType = "None";
|
|
3011
|
+
|
|
3012
|
+
// Getting Sun's apperent longitude and declination at the current epoch
|
|
3013
|
+
const century = solar.century(epoch); // Convert date to Julian centuries since J2000 || equal to var tut1 in GetPosition()
|
|
3014
|
+
|
|
3015
|
+
// Variables to solve for Sun Pos in ___ frame
|
|
3016
|
+
const lamM = (280.460 + 36000.771*century % 360) * DEG2RAD;
|
|
3017
|
+
const M = (357.5291092 + 35999.05034*century % 360) * DEG2RAD;
|
|
3018
|
+
const lamEc = (((lamM * RAD2DEG)
|
|
3019
|
+
+ 1.914666471 * Math.sin(M)
|
|
3020
|
+
+ 0.019994643 * Math.sin(2*M))) * DEG2RAD;
|
|
3021
|
+
const rMag = 1.000140612
|
|
3022
|
+
- 0.016708617 * Math.cos(M)
|
|
3023
|
+
- 0.000139589 * Math.cos(2*M);
|
|
3024
|
+
const dec = ((23.439291 - 0.0130042 * century) % 360) * DEG2RAD;
|
|
3025
|
+
|
|
3026
|
+
// Mod variable
|
|
3027
|
+
const sunVec = {
|
|
3028
|
+
x: rMag * Math.cos(lamEc) * auDist,
|
|
3029
|
+
y: rMag * Math.cos(dec) * Math.sin(lamEc) * auDist,
|
|
3030
|
+
z: rMag * Math.sin(dec) * Math.sin(lamEc) * auDist,
|
|
3031
|
+
};
|
|
3032
|
+
|
|
3033
|
+
// Convert mod => GCRF using Precession FK5
|
|
3034
|
+
const zeta = (((0.017998 * century + 0.30188) * century + 2306.2181) * century) * sec2rad;
|
|
3035
|
+
const theta = (((-0.041833 * century - 0.42665) * century + 2004.3109) * century) * sec2rad;
|
|
3036
|
+
const z = (((0.018203 * century + 1.09468) * century + 2306.2181) * century) * sec2rad;
|
|
3037
|
+
|
|
3038
|
+
const coszeta = Math.cos(zeta);
|
|
3039
|
+
const sinzeta = Math.sin(zeta);
|
|
3040
|
+
const costheta = Math.cos(theta);
|
|
3041
|
+
const sintheta = Math.sin(theta);
|
|
3042
|
+
const cosz = Math.cos(z);
|
|
3043
|
+
const sinz = Math.sin(z);
|
|
3044
|
+
|
|
3045
|
+
const mod2gcrf = [
|
|
3046
|
+
[coszeta * costheta * cosz - sinzeta * sinz,
|
|
3047
|
+
coszeta * costheta * sinz + sinzeta * cosz,
|
|
3048
|
+
coszeta * sintheta],
|
|
3049
|
+
[-sinzeta * costheta * cosz - coszeta * sinz,
|
|
3050
|
+
-sinzeta * costheta * sinz + coszeta * cosz,
|
|
3051
|
+
-sinzeta * sintheta],
|
|
3052
|
+
[-sintheta * cosz, -sintheta * sinz, costheta],
|
|
3053
|
+
];
|
|
3054
|
+
|
|
3055
|
+
const sunPosGCRF = multiply(mod2gcrf, posToArray(sunVec)); // Sun Position in GCRF
|
|
3056
|
+
const sunVecGCRF = {
|
|
3057
|
+
x: sunPosGCRF[0],
|
|
3058
|
+
y: sunPosGCRF[1],
|
|
3059
|
+
z: sunPosGCRF[2],
|
|
3060
|
+
};
|
|
3061
|
+
|
|
3062
|
+
// Convert Sun Pos from GCRF to J2000
|
|
3063
|
+
const xi0 = -0.0166170 * arcsec2rad;
|
|
3064
|
+
const eta0 = -0.0068192 * arcsec2rad;
|
|
3065
|
+
const da0 = -0.01460 * arcsec2rad;
|
|
3066
|
+
|
|
3067
|
+
const gcrf2j2000 = [
|
|
3068
|
+
[1.0 - 0.5*(-da0*-da0 + xi0*xi0), da0, -xi0],
|
|
3069
|
+
[-da0, 1.0 - 0.5*(-da0*-da0 + eta0*eta0), -eta0],
|
|
3070
|
+
[xi0, eta0, 1.0 - 0.5*(eta0*eta0 + xi0*xi0)],
|
|
3071
|
+
];
|
|
3072
|
+
|
|
3073
|
+
const sunVecJ2000 = multiply(gcrf2j2000, posToArray(sunVecGCRF)); // Sun Position in J2000
|
|
3074
|
+
const sunPosVec = sunVecJ2000;
|
|
3075
|
+
|
|
3076
|
+
const sunPos = {
|
|
3077
|
+
x: sunPosVec[0],
|
|
3078
|
+
y: sunPosVec[1],
|
|
3079
|
+
z: sunPosVec[2],
|
|
3080
|
+
}; // create a vector object of the Sun's J2000 position vector
|
|
3081
|
+
|
|
3082
|
+
// perform algorithm 34 from Vallado's Fundamentals of Astrodynamics and Applications
|
|
3083
|
+
if (dot(posToArray(satPos), posToArray(sunPos)) < 0) { // ensure sat is not in the direction of the Sun, which would mean it is opposite the shadow
|
|
3084
|
+
const reverseSunVec = {
|
|
3085
|
+
x: -sunPos.x,
|
|
3086
|
+
y: -sunPos.y,
|
|
3087
|
+
z: -sunPos.z,
|
|
3088
|
+
}; // reverse the Sun vector to obtain the vector to the Umbra cone tip
|
|
3089
|
+
|
|
3090
|
+
// angle between sat position vector and shadow cone vector
|
|
3091
|
+
const theta = Math.acos(
|
|
3092
|
+
dot(posToArray(satPos), posToArray(reverseSunVec))
|
|
3093
|
+
/ (norm(posToArray(satPos)) * norm(posToArray(reverseSunVec))));
|
|
3094
|
+
|
|
3095
|
+
const satHorz = norm(posToArray(satPos)) * Math.cos(theta); // horizontal component of sat position
|
|
3096
|
+
const satVert = norm(posToArray(satPos)) * Math.sin(theta); // vertical component of the sat position
|
|
3097
|
+
const x = re / Math.sin(alphaPenumbra);
|
|
3098
|
+
const penumbraVert = Math.tan(alphaPenumbra) * (x + satHorz); // vertical component of the penumbra
|
|
3099
|
+
if (satVert <= penumbraVert) {
|
|
3100
|
+
shadowType = "Penumbra"; // if vertical satellite component < vertical penumbra component, shadow is at least within penumbra
|
|
3101
|
+
const y = re / Math.sin(alphaUmbra);
|
|
3102
|
+
const umbraVert = Math.tan(alphaUmbra) * (y - satHorz); // vertical component of the umbra
|
|
3103
|
+
if (satVert <= umbraVert) {
|
|
3104
|
+
shadowType = "Umbra"; // if vertical satellite component < umbra vertical component, shadow is within umbra
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
return shadowType; // return the shadow type
|
|
3109
|
+
};
|
|
3110
|
+
|
|
3111
|
+
module.exports.CONSTANTS = require("./constants.js");
|
|
3112
|
+
module.exports.REGIMES = REGIMES;
|
|
3113
|
+
module.exports.julianToGregorian = julianToGregorian;
|
|
3114
|
+
module.exports.calcRegime = calcRegime;
|
|
3115
|
+
module.exports.altToRegime = altToRegime;
|
|
3116
|
+
module.exports.cartesianToRIC = cartesianToRIC;
|
|
3117
|
+
module.exports.multiplyVector = multiplyVector;
|
|
3118
|
+
module.exports.dist = dist;
|
|
3119
|
+
module.exports.angleBetween3DCoords = angleBetween3DCoords;
|
|
3120
|
+
module.exports.prop = prop;
|
|
3121
|
+
module.exports.propGeodetic = propGeodetic;
|
|
3122
|
+
module.exports.getTRIC = getTRIC;
|
|
3123
|
+
module.exports.getSunDirection = getSunDirection;
|
|
3124
|
+
module.exports.getLonAndDrift = getLonAndDrift;
|
|
3125
|
+
module.exports.getRaanPrecession = getRaanPrecession;
|
|
3126
|
+
module.exports.checkTle = checkTle;
|
|
3127
|
+
module.exports.sunPosAt = sunPosAt;
|
|
3128
|
+
module.exports.raDecToGeodetic = RaDecToGeodetic;
|
|
3129
|
+
module.exports.getResiduals = GetResiduals;
|
|
3130
|
+
module.exports.raDecToAzEl = RaDecToAzEl;
|
|
3131
|
+
module.exports.azElToRaDec = AzElToRaDec;
|
|
3132
|
+
module.exports.getElsetUdlFromTle = GetElsetUdlFromTle;
|
|
3133
|
+
module.exports.satjs = require("satellite.js");
|
|
3134
|
+
module.exports.distGeodetic = distGeodetic;
|
|
3135
|
+
module.exports.getSemiMajorAxis = getSemiMajorAxis;
|
|
3136
|
+
module.exports.angleBetweenPlanes = angleBetweenPlanes;
|
|
3137
|
+
module.exports.propagate = propagate;
|
|
3138
|
+
module.exports.planeChangeDeltaV = planeChangeDeltaV;
|
|
3139
|
+
module.exports.planeChangePureInclinationDeltaV = planeChangePureInclinationDeltaV;
|
|
3140
|
+
module.exports.cartesianToKeplerian = cartesianToKeplerian;
|
|
3141
|
+
module.exports.keplerianToCartesian = keplerianToCartesian;
|
|
3142
|
+
module.exports.getLeoRpoData = getLeoRpoData;
|
|
3143
|
+
module.exports.getGeoRpoData = getGeoRpoData;
|
|
3144
|
+
module.exports.getGeoShadowZones = getGeoShadowZones;
|
|
3145
|
+
module.exports.getGeoLightIntervals = getGeoLightIntervals;
|
|
3146
|
+
module.exports.getEclipseStatus = getEclipseStatus;
|
|
3147
|
+
module.exports.estimateSlantRange = estimateSlantRange;
|
|
3148
|
+
module.exports.calculateNextApogeePerigeeTimes = calculateNextApogeePerigeeTimes;
|
|
3149
|
+
module.exports.calculateNextApogeePerigeeTimesWithPropagation
|
|
3150
|
+
= calculateNextApogeePerigeeTimesWithPropagation;
|
|
3151
|
+
module.exports.calculateLeoPhaseDifference = calculateLeoPhaseDifference;
|
|
3152
|
+
module.exports.getLeoWaterfallData = getLeoWaterfallData;
|
|
3153
|
+
module.exports.lambertThomsonAlgorithm = lambertThomsonAlgorithm;
|
|
3154
|
+
module.exports.detectManeuverMinDv = detectManeuverMinDv;
|
|
3155
|
+
module.exports.getInterceptRendezvousMinDv = getInterceptRendezvousMinDv;
|
|
3156
|
+
|
|
3157
|
+
module.exports.wrap360 = wrap360;
|
|
3158
|
+
module.exports.inWrappedRange = inWrappedRange;
|
|
3159
|
+
module.exports.subtractWrapped = subtractWrapped;
|
|
3160
|
+
module.exports.propagateRaan = propagateRaan;
|
|
3161
|
+
module.exports.getIncDiff = getIncDiff;
|
|
3162
|
+
module.exports.getRaanDiff = getRaanDiff;
|
|
3163
|
+
module.exports.getRaanClosureRate = getRaanClosureRate;
|
|
3164
|
+
module.exports.getRaanAlignTime = getRaanAlignTime;
|
|
3165
|
+
module.exports.getRaanAlignTimeStr = getRaanAlignTimeStr;
|
|
3166
|
+
module.exports.getRaanDetails = getRaanDetails;
|
|
3167
|
+
module.exports.isSatInShadow = isSatInShadow;
|
|
3168
|
+
|
|
3169
|
+
module.exports.calculateGeoCrossingTimes = calculateGeoCrossingTimes;
|