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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,100 +1,100 @@
1
- import {BallisticPropagator} from "./ballisticPropagator.js";
2
- import {RungeKutta4Propagator,
3
- J2000,
4
- KeplerPropagator,
5
- EpochUTC,
6
- ClassicalElements} from "pious-squid";
7
- import {sgp4} from "satellite.js";
8
- import {NodeVector3D} from "./NodeVector3D.js";
9
- import {cartesianToKeplerian, checkTle} from "./astro.js";
10
- import {DEG2RAD} from "./constants.js";
11
-
12
- const ReferenceFrame = {
13
- ITRF: "ITRF",
14
- J2000: "J2000",
15
- TEME: "TEME",
16
- GCRF: "GCRF",
17
- };
18
-
19
- class PropagateUtils {
20
- static propagatorSelection(initialState, propagatorType, endTime) {
21
- let propVal = null;
22
- switch (propagatorType) {
23
- case "NUMERICAL":
24
- propVal = PropagateUtils.numericalPropagator(initialState, endTime);
25
- break;
26
- case "KEPLERIAN":
27
- propVal = PropagateUtils.keplerianPropagator(initialState, endTime);
28
- break;
29
- case "ANALYTICAL":
30
- propVal = PropagateUtils.analyticalPropagator(
31
- initialState, ReferenceFrame.J2000, endTime);
32
- break;
33
- case "TLE":
34
- propVal = PropagateUtils.tlePropagator(initialState, endTime);
35
- break;
36
- }
37
- return propVal;
38
- }
39
-
40
- static numericalPropagator(initialState, time) {
41
- const state0 = new J2000(
42
- new EpochUTC(initialState.epochUtc), initialState.position, initialState.velocity);
43
- const prop = new RungeKutta4Propagator(state0);
44
- const endTime = new EpochUTC(time);
45
- const result = prop.propagate(endTime);
46
- return result;
47
- }
48
-
49
- static analyticalPropagator(initialState, frame, time) {
50
- const state = {
51
- position: new NodeVector3D(
52
- initialState.position.x, initialState.position.y, initialState.position.z), // km
53
- velocity: new NodeVector3D(
54
- initialState.velocity.x, initialState.velocity.y, initialState.velocity.z), // km/s
55
- epochUtc: initialState.epochUtc,
56
- referenceFrame: frame,
57
- };
58
-
59
- const propagator = new BallisticPropagator(state);
60
- const result = propagator.propagate(time);
61
- return result;
62
- }
63
-
64
- static keplerianPropagator(initialState, time) {
65
- const elementsObjReal = cartesianToKeplerian(
66
- initialState.position, initialState.velocity);
67
-
68
- const elements = new ClassicalElements(
69
- new EpochUTC(initialState.epoch),
70
- elementsObjReal.a,
71
- elementsObjReal.e,
72
- elementsObjReal.i * DEG2RAD,
73
- elementsObjReal.raan * DEG2RAD,
74
- elementsObjReal.w * DEG2RAD,
75
- elementsObjReal.f * DEG2RAD,
76
- );
77
-
78
- // Now pass to KeplerPropagator:
79
- const prop = new KeplerPropagator(elements);
80
-
81
- const result = prop.propagate(new EpochUTC(time));
82
-
83
-
84
- return result;
85
- }
86
-
87
- static tlePropagator(tle, time) {
88
- const line1 = tle[0];
89
- const line2 = tle[1];
90
- const satrec = checkTle(line1, line2);
91
- if (!satrec) {
92
- throw new Error("Invalid TLE data");
93
- }
94
- const result = sgp4(satrec, time);
95
- return result;
96
- }
97
- }
98
-
99
- export {PropagateUtils};
100
-
1
+ import {BallisticPropagator} from "./ballisticPropagator.js";
2
+ import {RungeKutta4Propagator,
3
+ J2000,
4
+ KeplerPropagator,
5
+ EpochUTC,
6
+ ClassicalElements} from "pious-squid";
7
+ import {sgp4} from "satellite.js";
8
+ import {NodeVector3D} from "./NodeVector3D.js";
9
+ import {cartesianToKeplerian, checkTle} from "./astro.js";
10
+ import {DEG2RAD} from "./constants.js";
11
+
12
+ const ReferenceFrame = {
13
+ ITRF: "ITRF",
14
+ J2000: "J2000",
15
+ TEME: "TEME",
16
+ GCRF: "GCRF",
17
+ };
18
+
19
+ class PropagateUtils {
20
+ static propagatorSelection(initialState, propagatorType, endTime) {
21
+ let propVal = null;
22
+ switch (propagatorType) {
23
+ case "NUMERICAL":
24
+ propVal = PropagateUtils.numericalPropagator(initialState, endTime);
25
+ break;
26
+ case "KEPLERIAN":
27
+ propVal = PropagateUtils.keplerianPropagator(initialState, endTime);
28
+ break;
29
+ case "ANALYTICAL":
30
+ propVal = PropagateUtils.analyticalPropagator(
31
+ initialState, ReferenceFrame.J2000, endTime);
32
+ break;
33
+ case "TLE":
34
+ propVal = PropagateUtils.tlePropagator(initialState, endTime);
35
+ break;
36
+ }
37
+ return propVal;
38
+ }
39
+
40
+ static numericalPropagator(initialState, time) {
41
+ const state0 = new J2000(
42
+ new EpochUTC(initialState.epochUtc), initialState.position, initialState.velocity);
43
+ const prop = new RungeKutta4Propagator(state0);
44
+ const endTime = new EpochUTC(time);
45
+ const result = prop.propagate(endTime);
46
+ return result;
47
+ }
48
+
49
+ static analyticalPropagator(initialState, frame, time) {
50
+ const state = {
51
+ position: new NodeVector3D(
52
+ initialState.position.x, initialState.position.y, initialState.position.z), // km
53
+ velocity: new NodeVector3D(
54
+ initialState.velocity.x, initialState.velocity.y, initialState.velocity.z), // km/s
55
+ epochUtc: initialState.epochUtc,
56
+ referenceFrame: frame,
57
+ };
58
+
59
+ const propagator = new BallisticPropagator(state);
60
+ const result = propagator.propagate(time);
61
+ return result;
62
+ }
63
+
64
+ static keplerianPropagator(initialState, time) {
65
+ const elementsObjReal = cartesianToKeplerian(
66
+ initialState.position, initialState.velocity);
67
+
68
+ const elements = new ClassicalElements(
69
+ new EpochUTC(initialState.epoch),
70
+ elementsObjReal.a,
71
+ elementsObjReal.e,
72
+ elementsObjReal.i * DEG2RAD,
73
+ elementsObjReal.raan * DEG2RAD,
74
+ elementsObjReal.w * DEG2RAD,
75
+ elementsObjReal.f * DEG2RAD,
76
+ );
77
+
78
+ // Now pass to KeplerPropagator:
79
+ const prop = new KeplerPropagator(elements);
80
+
81
+ const result = prop.propagate(new EpochUTC(time));
82
+
83
+
84
+ return result;
85
+ }
86
+
87
+ static tlePropagator(tle, time) {
88
+ const line1 = tle[0];
89
+ const line2 = tle[1];
90
+ const satrec = checkTle(line1, line2);
91
+ if (!satrec) {
92
+ throw new Error("Invalid TLE data");
93
+ }
94
+ const result = sgp4(satrec, time);
95
+ return result;
96
+ }
97
+ }
98
+
99
+ export {PropagateUtils};
100
+
@@ -1,203 +1,203 @@
1
- import constants, {WGS84_EARTH_EQUATORIAL_RADIUS_KM,
2
- DEG2RAD, AU_KM as _AU_KM,
3
- GEO_ALTITUDE_KM, RAD2DEG} from "./constants.js";
4
- import solar from "solar-calculator";
5
- import {Vector3} from "three";
6
- import {wrapToRange} from "./utils.js";
7
- import {sunPosAt} from "./astro.js";
8
-
9
- class ShadowGEOCalculator {
10
- constructor() {
11
- this.constants = constants;
12
- this.solarCalculator = solar;
13
- this.sunRadiusKm = 695700; // Radius of the Sun in kilometers, (source: orekit, through resolution B3 from IAU 2015)
14
- // 695701 in astro library
15
- }
16
-
17
- static _calculateUmbraGeometry(deltaPS) {
18
- const Dp = 2 * WGS84_EARTH_EQUATORIAL_RADIUS_KM;
19
- const Ds = 2 * 695700; // sunRadiusKm
20
-
21
- // Distance from Earth center to umbra apex
22
- const Xu = (Dp * deltaPS) / (Ds - Dp);
23
-
24
- // Umbra subtended angle
25
- const alphaU = Math.asin(Dp / (2 * Xu));
26
-
27
- return {Xu, alphaU};
28
- }
29
-
30
- static _calculatePenumbraGeometry(deltaPS) {
31
- const Dp = 2 * WGS84_EARTH_EQUATORIAL_RADIUS_KM;
32
- const Ds = 2 * 695700; // sunRadiusKm
33
-
34
- // Distance from Earth center to penumbra apex
35
- const Xp = (Dp * deltaPS) / (Ds + Dp);
36
-
37
- // Penumbra subtended angle
38
- const alphaP = Math.asin(Dp / (2 * Xp));
39
-
40
- return {Xp, alphaP};
41
- }
42
-
43
- static _umbraRadiusDistance(distance, Xu, alphaU) {
44
- if (distance > Xu) {
45
- throw new Error("Distance must be less than or equal to the umbra apex distance.");
46
- }
47
- // Calculate the radius of the umbra at a given distance from the apex
48
- return (Xu - distance) * Math.tan(alphaU);
49
- }
50
-
51
- static _penumbraRadiusDistance(distance, Xp, alphaP) {
52
- // Calculate the radius of the penumbra at a given distance from the apex
53
- return (distance + Math.abs(Xp)) * Math.tan(alphaP);
54
- }
55
-
56
- static _sunUnitVector(sunLongitude, sunDeclination) {
57
- const sunVector = new Vector3();
58
- sunVector.x = Math.cos(sunDeclination * DEG2RAD)
59
- * Math.cos(sunLongitude * DEG2RAD);
60
- sunVector.y = Math.cos(sunDeclination * DEG2RAD)
61
- * Math.sin(sunLongitude * DEG2RAD);
62
- sunVector.z = Math.sin(sunDeclination * DEG2RAD);
63
- return sunVector; // returns the unit vector of the Sun's position
64
- }
65
-
66
- static _calculateSunDistance(sunLongitude, sunDeclination) {
67
- // Longitude and declination (latitude) describe direction, not distance.
68
- // To get the Earth-Sun distance in km, use astronomical data (mean ≈ 149,597,870.7 km).
69
- // If you want the position vector in km, use the distance and direction:
70
- const AU_KM = _AU_KM; // Astronomical Unit in kilometers
71
- // AstroLibrary uses 149597900
72
- const r = AU_KM; // Use actual Earth-Sun distance if available, otherwise mean AU
73
-
74
- // Convert longitude and declination to a position vector in km
75
- const x = r * Math.cos(sunDeclination * DEG2RAD)
76
- * Math.cos(sunLongitude * DEG2RAD);
77
- const y = r * Math.cos(sunDeclination * DEG2RAD)
78
- * Math.sin(sunLongitude * DEG2RAD);
79
- const z = r * Math.sin(sunDeclination * DEG2RAD);
80
-
81
- return {x, y, z, distance: r};
82
- }
83
-
84
- static geoBeltShadowIntersection(date) {
85
- const [sunDeclination, sunLongitude] = sunPosAt(date);
86
- // Calculate sun position
87
- let deltaPS;
88
- if (sunLongitude !== null && sunDeclination !== null) {
89
- // convert sunLongitude and sunDeclination to distance in km
90
- deltaPS = ShadowGEOCalculator._calculateSunDistance(
91
- sunLongitude, sunDeclination)["distance"];
92
- } else {
93
- const time = new Date();
94
- const [newSunDeclination, newSunLongitude] = sunPosAt(time);
95
- const sunPositionData = ShadowGEOCalculator._calculateSunDistance(
96
- newSunLongitude, newSunDeclination);
97
- deltaPS = sunPositionData.distance;
98
- }
99
-
100
- // Calculate shadow geometry
101
- const {Xu, alphaU} = ShadowGEOCalculator._calculateUmbraGeometry(deltaPS);
102
- const {Xp, alphaP} = ShadowGEOCalculator._calculatePenumbraGeometry(deltaPS);
103
-
104
- const geoRadius = WGS84_EARTH_EQUATORIAL_RADIUS_KM + GEO_ALTITUDE_KM;
105
-
106
- // Calculate Shadow radii at GEO altitude
107
- const umbraRadiusGEO = ShadowGEOCalculator._umbraRadiusDistance(geoRadius, Xu, alphaU);
108
- const penumbraRadiusGEO = ShadowGEOCalculator._penumbraRadiusDistance(
109
- geoRadius, Xp, alphaP);
110
-
111
- // Calculate sun unit vector
112
- const sunVector = ShadowGEOCalculator._sunUnitVector(sunLongitude, sunDeclination);
113
-
114
- // Calculate shadow axis
115
- const shadowAxis = [-sunVector.x, -sunVector.y, -sunVector.z];
116
-
117
- // Finding shadow axis center in x, y, z coordinates
118
-
119
- const shadowCenter = [
120
- shadowAxis[0] * geoRadius,
121
- shadowAxis[1] * geoRadius,
122
- shadowAxis[2] * geoRadius,
123
- ];
124
-
125
- // Converting shadow axis to latitude and longitude
126
- const shadowCenterLat = Math.asin(shadowCenter[2] / geoRadius) * RAD2DEG;
127
- const shadowCenterLon = Math.atan2(shadowCenter[1], shadowCenter[0]) * RAD2DEG;
128
-
129
- // Check if GEO belt intersects the shadow
130
- const equatorialDistance = Math.abs(shadowCenter[2]); // Distance from the equator to the shadow center
131
- const intersectsUmbra = umbraRadiusGEO > equatorialDistance;
132
- const intersectsPenumbra = penumbraRadiusGEO > equatorialDistance;
133
-
134
- const intersectionPoints = [];
135
-
136
- if (intersectsPenumbra) {
137
- // angle from the shadow center to the intersection point
138
- const alpha = Math.sqrt(penumbraRadiusGEO**2 - equatorialDistance**2);
139
- const theta = Math.atan2(alpha, geoRadius);
140
-
141
- // Project shadow center onto the GEO belt
142
- const shadowAxisEq = [shadowAxis[0], shadowAxis[1], 0];
143
- const shadowAxisNorm = Math.sqrt(shadowAxisEq[0]**2 + shadowAxisEq[1]**2);
144
- shadowAxisEq[0] /= shadowAxisNorm;
145
- shadowAxisEq[1] /= shadowAxisNorm;
146
-
147
- // Intersection points in the GEO belt
148
- const centerLon = shadowCenterLon; // You already have this!
149
-
150
- let lon1 = centerLon + theta * RAD2DEG;
151
- let lon2 = centerLon - theta * RAD2DEG;
152
-
153
- // Ensure longitudes are within -180 to 180 degrees
154
- lon1 = wrapToRange(lon1, -180, 180);
155
- lon2 = wrapToRange(lon2, -180, 180);
156
-
157
- intersectionPoints.push({
158
- type: "penumbra",
159
- longitudeRange: [lon1, lon2],
160
- latitude: 0, // Equatorial belt
161
- });
162
- }
163
-
164
- if (intersectsUmbra) {
165
- // angle from the shadow center to the intersection point
166
- const alpha = Math.sqrt(umbraRadiusGEO**2 - equatorialDistance**2);
167
- const theta = Math.atan2(alpha, geoRadius);
168
-
169
- // Project shadow center onto the GEO belt
170
- const shadowAxisEq = [shadowAxis[0], shadowAxis[1], 0];
171
- const shadowAxisNorm = Math.sqrt(shadowAxisEq[0]**2 + shadowAxisEq[1]**2);
172
- shadowAxisEq[0] /= shadowAxisNorm;
173
- shadowAxisEq[1] /= shadowAxisNorm;
174
-
175
- // Intersection points in the GEO belt
176
- const centerLon = shadowCenterLon; // You already have this!
177
-
178
- let lon1 = centerLon + theta * RAD2DEG;
179
- let lon2 = centerLon - theta * RAD2DEG;
180
-
181
- // Ensure longitudes are within -180 to 180 degrees
182
- lon1 = ((lon1 + 180) % 360 + 360) % 360 - 180;
183
- lon2 = ((lon2 + 180) % 360 + 360) % 360 - 180;
184
-
185
- intersectionPoints.push({
186
- type: "umbra",
187
- longitudeRange: [lon1, lon2],
188
- latitude: 0, // Equatorial belt
189
- });
190
- }
191
-
192
- return {
193
- shadowCenter: shadowCenter,
194
- shadowCenterLat: shadowCenterLat,
195
- shadowCenterLon: shadowCenterLon,
196
- umbraRadiusGEO: umbraRadiusGEO,
197
- penumbraRadiusGEO: penumbraRadiusGEO,
198
- intersectionPoints: intersectionPoints,
199
- };
200
- }
201
- }
202
-
203
- export {ShadowGEOCalculator};
1
+ import constants, {WGS84_EARTH_EQUATORIAL_RADIUS_KM,
2
+ DEG2RAD, AU_KM as _AU_KM,
3
+ GEO_ALTITUDE_KM, RAD2DEG} from "./constants.js";
4
+ import solar from "solar-calculator";
5
+ import {Vector3} from "three";
6
+ import {wrapToRange} from "./utils.js";
7
+ import {sunPosAt} from "./astro.js";
8
+
9
+ class ShadowGEOCalculator {
10
+ constructor() {
11
+ this.constants = constants;
12
+ this.solarCalculator = solar;
13
+ this.sunRadiusKm = 695700; // Radius of the Sun in kilometers, (source: orekit, through resolution B3 from IAU 2015)
14
+ // 695701 in astro library
15
+ }
16
+
17
+ static _calculateUmbraGeometry(deltaPS) {
18
+ const Dp = 2 * WGS84_EARTH_EQUATORIAL_RADIUS_KM;
19
+ const Ds = 2 * 695700; // sunRadiusKm
20
+
21
+ // Distance from Earth center to umbra apex
22
+ const Xu = (Dp * deltaPS) / (Ds - Dp);
23
+
24
+ // Umbra subtended angle
25
+ const alphaU = Math.asin(Dp / (2 * Xu));
26
+
27
+ return {Xu, alphaU};
28
+ }
29
+
30
+ static _calculatePenumbraGeometry(deltaPS) {
31
+ const Dp = 2 * WGS84_EARTH_EQUATORIAL_RADIUS_KM;
32
+ const Ds = 2 * 695700; // sunRadiusKm
33
+
34
+ // Distance from Earth center to penumbra apex
35
+ const Xp = (Dp * deltaPS) / (Ds + Dp);
36
+
37
+ // Penumbra subtended angle
38
+ const alphaP = Math.asin(Dp / (2 * Xp));
39
+
40
+ return {Xp, alphaP};
41
+ }
42
+
43
+ static _umbraRadiusDistance(distance, Xu, alphaU) {
44
+ if (distance > Xu) {
45
+ throw new Error("Distance must be less than or equal to the umbra apex distance.");
46
+ }
47
+ // Calculate the radius of the umbra at a given distance from the apex
48
+ return (Xu - distance) * Math.tan(alphaU);
49
+ }
50
+
51
+ static _penumbraRadiusDistance(distance, Xp, alphaP) {
52
+ // Calculate the radius of the penumbra at a given distance from the apex
53
+ return (distance + Math.abs(Xp)) * Math.tan(alphaP);
54
+ }
55
+
56
+ static _sunUnitVector(sunLongitude, sunDeclination) {
57
+ const sunVector = new Vector3();
58
+ sunVector.x = Math.cos(sunDeclination * DEG2RAD)
59
+ * Math.cos(sunLongitude * DEG2RAD);
60
+ sunVector.y = Math.cos(sunDeclination * DEG2RAD)
61
+ * Math.sin(sunLongitude * DEG2RAD);
62
+ sunVector.z = Math.sin(sunDeclination * DEG2RAD);
63
+ return sunVector; // returns the unit vector of the Sun's position
64
+ }
65
+
66
+ static _calculateSunDistance(sunLongitude, sunDeclination) {
67
+ // Longitude and declination (latitude) describe direction, not distance.
68
+ // To get the Earth-Sun distance in km, use astronomical data (mean ≈ 149,597,870.7 km).
69
+ // If you want the position vector in km, use the distance and direction:
70
+ const AU_KM = _AU_KM; // Astronomical Unit in kilometers
71
+ // AstroLibrary uses 149597900
72
+ const r = AU_KM; // Use actual Earth-Sun distance if available, otherwise mean AU
73
+
74
+ // Convert longitude and declination to a position vector in km
75
+ const x = r * Math.cos(sunDeclination * DEG2RAD)
76
+ * Math.cos(sunLongitude * DEG2RAD);
77
+ const y = r * Math.cos(sunDeclination * DEG2RAD)
78
+ * Math.sin(sunLongitude * DEG2RAD);
79
+ const z = r * Math.sin(sunDeclination * DEG2RAD);
80
+
81
+ return {x, y, z, distance: r};
82
+ }
83
+
84
+ static geoBeltShadowIntersection(date) {
85
+ const [sunDeclination, sunLongitude] = sunPosAt(date);
86
+ // Calculate sun position
87
+ let deltaPS;
88
+ if (sunLongitude !== null && sunDeclination !== null) {
89
+ // convert sunLongitude and sunDeclination to distance in km
90
+ deltaPS = ShadowGEOCalculator._calculateSunDistance(
91
+ sunLongitude, sunDeclination)["distance"];
92
+ } else {
93
+ const time = new Date();
94
+ const [newSunDeclination, newSunLongitude] = sunPosAt(time);
95
+ const sunPositionData = ShadowGEOCalculator._calculateSunDistance(
96
+ newSunLongitude, newSunDeclination);
97
+ deltaPS = sunPositionData.distance;
98
+ }
99
+
100
+ // Calculate shadow geometry
101
+ const {Xu, alphaU} = ShadowGEOCalculator._calculateUmbraGeometry(deltaPS);
102
+ const {Xp, alphaP} = ShadowGEOCalculator._calculatePenumbraGeometry(deltaPS);
103
+
104
+ const geoRadius = WGS84_EARTH_EQUATORIAL_RADIUS_KM + GEO_ALTITUDE_KM;
105
+
106
+ // Calculate Shadow radii at GEO altitude
107
+ const umbraRadiusGEO = ShadowGEOCalculator._umbraRadiusDistance(geoRadius, Xu, alphaU);
108
+ const penumbraRadiusGEO = ShadowGEOCalculator._penumbraRadiusDistance(
109
+ geoRadius, Xp, alphaP);
110
+
111
+ // Calculate sun unit vector
112
+ const sunVector = ShadowGEOCalculator._sunUnitVector(sunLongitude, sunDeclination);
113
+
114
+ // Calculate shadow axis
115
+ const shadowAxis = [-sunVector.x, -sunVector.y, -sunVector.z];
116
+
117
+ // Finding shadow axis center in x, y, z coordinates
118
+
119
+ const shadowCenter = [
120
+ shadowAxis[0] * geoRadius,
121
+ shadowAxis[1] * geoRadius,
122
+ shadowAxis[2] * geoRadius,
123
+ ];
124
+
125
+ // Converting shadow axis to latitude and longitude
126
+ const shadowCenterLat = Math.asin(shadowCenter[2] / geoRadius) * RAD2DEG;
127
+ const shadowCenterLon = Math.atan2(shadowCenter[1], shadowCenter[0]) * RAD2DEG;
128
+
129
+ // Check if GEO belt intersects the shadow
130
+ const equatorialDistance = Math.abs(shadowCenter[2]); // Distance from the equator to the shadow center
131
+ const intersectsUmbra = umbraRadiusGEO > equatorialDistance;
132
+ const intersectsPenumbra = penumbraRadiusGEO > equatorialDistance;
133
+
134
+ const intersectionPoints = [];
135
+
136
+ if (intersectsPenumbra) {
137
+ // angle from the shadow center to the intersection point
138
+ const alpha = Math.sqrt(penumbraRadiusGEO**2 - equatorialDistance**2);
139
+ const theta = Math.atan2(alpha, geoRadius);
140
+
141
+ // Project shadow center onto the GEO belt
142
+ const shadowAxisEq = [shadowAxis[0], shadowAxis[1], 0];
143
+ const shadowAxisNorm = Math.sqrt(shadowAxisEq[0]**2 + shadowAxisEq[1]**2);
144
+ shadowAxisEq[0] /= shadowAxisNorm;
145
+ shadowAxisEq[1] /= shadowAxisNorm;
146
+
147
+ // Intersection points in the GEO belt
148
+ const centerLon = shadowCenterLon; // You already have this!
149
+
150
+ let lon1 = centerLon + theta * RAD2DEG;
151
+ let lon2 = centerLon - theta * RAD2DEG;
152
+
153
+ // Ensure longitudes are within -180 to 180 degrees
154
+ lon1 = wrapToRange(lon1, -180, 180);
155
+ lon2 = wrapToRange(lon2, -180, 180);
156
+
157
+ intersectionPoints.push({
158
+ type: "penumbra",
159
+ longitudeRange: [lon1, lon2],
160
+ latitude: 0, // Equatorial belt
161
+ });
162
+ }
163
+
164
+ if (intersectsUmbra) {
165
+ // angle from the shadow center to the intersection point
166
+ const alpha = Math.sqrt(umbraRadiusGEO**2 - equatorialDistance**2);
167
+ const theta = Math.atan2(alpha, geoRadius);
168
+
169
+ // Project shadow center onto the GEO belt
170
+ const shadowAxisEq = [shadowAxis[0], shadowAxis[1], 0];
171
+ const shadowAxisNorm = Math.sqrt(shadowAxisEq[0]**2 + shadowAxisEq[1]**2);
172
+ shadowAxisEq[0] /= shadowAxisNorm;
173
+ shadowAxisEq[1] /= shadowAxisNorm;
174
+
175
+ // Intersection points in the GEO belt
176
+ const centerLon = shadowCenterLon; // You already have this!
177
+
178
+ let lon1 = centerLon + theta * RAD2DEG;
179
+ let lon2 = centerLon - theta * RAD2DEG;
180
+
181
+ // Ensure longitudes are within -180 to 180 degrees
182
+ lon1 = ((lon1 + 180) % 360 + 360) % 360 - 180;
183
+ lon2 = ((lon2 + 180) % 360 + 360) % 360 - 180;
184
+
185
+ intersectionPoints.push({
186
+ type: "umbra",
187
+ longitudeRange: [lon1, lon2],
188
+ latitude: 0, // Equatorial belt
189
+ });
190
+ }
191
+
192
+ return {
193
+ shadowCenter: shadowCenter,
194
+ shadowCenterLat: shadowCenterLat,
195
+ shadowCenterLon: shadowCenterLon,
196
+ umbraRadiusGEO: umbraRadiusGEO,
197
+ penumbraRadiusGEO: penumbraRadiusGEO,
198
+ intersectionPoints: intersectionPoints,
199
+ };
200
+ }
201
+ }
202
+
203
+ export {ShadowGEOCalculator};