1   /* Copyright 2002-2025 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.propagation.events;
18  
19  import org.hipparchus.geometry.euclidean.threed.Rotation;
20  import org.hipparchus.geometry.euclidean.threed.Vector3D;
21  import org.hipparchus.geometry.spherical.twod.Edge;
22  import org.hipparchus.ode.events.Action;
23  import org.hipparchus.util.FastMath;
24  import org.hipparchus.util.MathUtils;
25  import org.junit.jupiter.api.Assertions;
26  import org.junit.jupiter.api.BeforeEach;
27  import org.junit.jupiter.api.Test;
28  import org.orekit.Utils;
29  import org.orekit.attitudes.AttitudeProvider;
30  import org.orekit.attitudes.BodyCenterPointing;
31  import org.orekit.attitudes.NadirPointing;
32  import org.orekit.bodies.BodyShape;
33  import org.orekit.bodies.CelestialBodyFactory;
34  import org.orekit.bodies.GeodeticPoint;
35  import org.orekit.bodies.OneAxisEllipsoid;
36  import org.orekit.errors.OrekitException;
37  import org.orekit.frames.Frame;
38  import org.orekit.frames.FramesFactory;
39  import org.orekit.frames.TopocentricFrame;
40  import org.orekit.geometry.fov.CircularFieldOfView;
41  import org.orekit.geometry.fov.DoubleDihedraFieldOfView;
42  import org.orekit.geometry.fov.EllipticalFieldOfView;
43  import org.orekit.geometry.fov.FieldOfView;
44  import org.orekit.geometry.fov.PolygonalFieldOfView;
45  import org.orekit.geometry.fov.PolygonalFieldOfView.DefiningConeType;
46  import org.orekit.orbits.EquinoctialOrbit;
47  import org.orekit.orbits.KeplerianOrbit;
48  import org.orekit.orbits.Orbit;
49  import org.orekit.orbits.PositionAngleType;
50  import org.orekit.propagation.Propagator;
51  import org.orekit.propagation.SpacecraftState;
52  import org.orekit.propagation.analytical.KeplerianPropagator;
53  import org.orekit.propagation.events.EventsLogger.LoggedEvent;
54  import org.orekit.propagation.events.handlers.ContinueOnEvent;
55  import org.orekit.propagation.events.handlers.EventHandler;
56  import org.orekit.time.AbsoluteDate;
57  import org.orekit.time.DateComponents;
58  import org.orekit.time.TimeComponents;
59  import org.orekit.time.TimeScale;
60  import org.orekit.time.TimeScalesFactory;
61  import org.orekit.utils.Constants;
62  import org.orekit.utils.IERSConventions;
63  import org.orekit.utils.PVCoordinates;
64  import org.orekit.utils.PVCoordinatesProvider;
65  
66  import java.util.ArrayList;
67  import java.util.List;
68  
69  public class FieldOfViewDetectorTest {
70  
71      // Body mu
72      private double mu;
73  
74      // Computation date
75      private AbsoluteDate initDate;
76  
77      // Orbit
78      private Orbit initialOrbit;
79  
80      // WGS84 Earth model
81      private OneAxisEllipsoid earth;
82  
83      // Earth center pointing attitude provider
84      private BodyCenterPointing earthCenterAttitudeLaw;
85  
86      // UTC time scale
87      private TimeScale utc;
88  
89      @Test
90      public void testDihedralFielOfView() {
91  
92          // Definition of initial conditions with position and velocity
93          //------------------------------------------------------------
94  
95          // Extrapolator definition
96          KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit, earthCenterAttitudeLaw);
97  
98          // Event definition : square field of view, along X axis, aperture 68°
99          final double halfAperture = FastMath.toRadians(0.5 * 68.0);
100         final double maxCheck  = 60.;
101         final double threshold = 1.0e-10;
102         final PVCoordinatesProvider sunPV = CelestialBodyFactory.getSun();
103         final Vector3D center = Vector3D.PLUS_I;
104         final Vector3D axis1  = Vector3D.PLUS_K;
105         final Vector3D axis2  = Vector3D.PLUS_J;
106         final double aperture1 = halfAperture;
107         final double aperture2 = halfAperture;
108 
109         final EventDetector sunVisi =
110                 new FieldOfViewDetector(sunPV, new DoubleDihedraFieldOfView(center, axis1, aperture1, axis2, aperture2, 0.0)).
111                 withMaxCheck(maxCheck).
112                 withThreshold(threshold).
113                 withHandler(new DihedralSunVisiHandler());
114 
115         Assertions.assertSame(sunPV, ((FieldOfViewDetector) sunVisi).getPVTarget());
116         Assertions.assertEquals(0, ((FieldOfViewDetector) sunVisi).getFOV().getMargin(), 1.0e-15);
117         double eta = FastMath.acos(FastMath.sin(aperture1) * FastMath.sin(aperture2));
118         double theoreticalArea = MathUtils.TWO_PI - 4 * eta;
119         Assertions.assertEquals(theoreticalArea,
120                             ((PolygonalFieldOfView) ((FieldOfViewDetector) sunVisi).getFOV()).getZone().getSize(),
121                             1.0e-15);
122 
123         // Add event to be detected
124         EventsLogger logger = new EventsLogger();
125         propagator.addEventDetector(logger.monitorDetector(sunVisi));
126 
127         // Extrapolate from the initial to the final date
128         propagator.propagate(initDate.shiftedBy(6000.));
129 
130         // Sun is in dihedra 1 between tB and tC and in dihedra1 between tA and tD
131         // dihedra 1 is entered and left from same side (dihedra angle increases from < -34° to about -31.6° max
132         // and then decreases again to < -34°)
133         // dihedra 2 is completely crossed (dihedra angle increases from < -34° to > +34°
134         final AbsoluteDate tA = new AbsoluteDate("1969-08-28T00:04:50.540686", utc);
135         final AbsoluteDate tB = new AbsoluteDate("1969-08-28T00:08:08.299196", utc);
136         final AbsoluteDate tC = new AbsoluteDate("1969-08-28T00:29:58.478894", utc);
137         final AbsoluteDate tD = new AbsoluteDate("1969-08-28T00:36:13.390275", utc);
138 
139         List<LoggedEvent>  events = logger.getLoggedEvents();
140         final AbsoluteDate t0     = events.get(0).getState().getDate();
141         final AbsoluteDate t1     = events.get(1).getState().getDate();
142         Assertions.assertEquals(2, events.size());
143         Assertions.assertEquals(0, t0.durationFrom(tB), 1.0e-6);
144         Assertions.assertEquals(0, t1.durationFrom(tC), 1.0e-6);
145 
146         for (double dt = 0; dt < 3600; dt += 10.0) {
147             AbsoluteDate t = initialOrbit.getDate().shiftedBy(dt);
148             double[] angles = dihedralAngles(center, axis1, axis2,
149                                              sunPV.getPVCoordinates(t, initialOrbit.getFrame()),
150                                              new KeplerianPropagator(initialOrbit, earthCenterAttitudeLaw).propagate(t));
151             if (t.compareTo(tA) < 0) {
152                 // before tA, we are outside of both dihedras
153                 Assertions.assertTrue(angles[0] < -halfAperture);
154                 Assertions.assertTrue(angles[1] < -halfAperture);
155             } else if (t.compareTo(tB) < 0) {
156                 // between tA and tB, we are inside dihedra 2 but still outside of dihedra 1
157                 Assertions.assertTrue(angles[0] < -halfAperture);
158                 Assertions.assertTrue(angles[1] > -halfAperture);
159                 Assertions.assertTrue(angles[1] < +halfAperture);
160             } else if (t.compareTo(tC) < 0) {
161                 // between tB and tC, we are inside both dihedra 1 and dihedra 2
162                 Assertions.assertTrue(angles[0] > -halfAperture);
163                 Assertions.assertTrue(angles[0] < +halfAperture);
164                 Assertions.assertTrue(angles[1] > -halfAperture);
165                 Assertions.assertTrue(angles[1] < +halfAperture);
166             } else if (t.compareTo(tD) < 0) {
167                 // between tC and tD, we are inside dihedra 2 but again outside of dihedra 1
168                 Assertions.assertTrue(angles[0] < -halfAperture);
169                 Assertions.assertTrue(angles[1] > -halfAperture);
170                 Assertions.assertTrue(angles[1] < +halfAperture);
171             } else {
172                 // after tD, we are outside of both dihedras
173                 Assertions.assertTrue(angles[0] < -halfAperture);
174                 Assertions.assertTrue(angles[1] > +halfAperture);
175             }
176         }
177 
178     }
179 
180     private double[] dihedralAngles(final Vector3D center, final Vector3D axis1, final Vector3D axis2,
181                                     final PVCoordinates target, final SpacecraftState s) {
182         final Rotation toInert     = s.getAttitude().getOrientation().getRotation().revert();
183         final Vector3D centerInert = toInert.applyTo(center);
184         final Vector3D axis1Inert  = toInert.applyTo(axis1);
185         final Vector3D axis2Inert  = toInert.applyTo(axis2);
186         final Vector3D direction   = target.getPosition().subtract(s.getPosition()).normalize();
187         return new double[] {
188             dihedralAngle(centerInert, axis1Inert, direction),
189             dihedralAngle(centerInert, axis2Inert, direction)
190         };
191     }
192 
193     private double dihedralAngle(final Vector3D center, final Vector3D axis, final Vector3D u) {
194         final Vector3D y = Vector3D.crossProduct(axis, center).normalize();
195         final Vector3D x = Vector3D.crossProduct(y, axis).normalize();
196         return FastMath.atan2(Vector3D.dotProduct(u, y), Vector3D.dotProduct(u, x));
197     }
198 
199     /** check the default behavior to stop propagation on FoV exit. */
200     @Test
201     public void testStopOnExit() {
202         //setup
203         double pi = FastMath.PI;
204         AbsoluteDate date = AbsoluteDate.J2000_EPOCH; //arbitrary date
205         AbsoluteDate endDate = date.shiftedBy(Constants.JULIAN_DAY);
206         Frame eci = FramesFactory.getGCRF();
207         Frame ecef = FramesFactory.getITRF(IERSConventions.IERS_2010, true);
208         BodyShape earth = new OneAxisEllipsoid(
209                 Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
210                 Constants.WGS84_EARTH_FLATTENING,
211                 ecef);
212         GeodeticPoint gp = new GeodeticPoint(
213                 FastMath.toRadians(39), FastMath.toRadians(77), 0);
214         TopocentricFrame topo = new TopocentricFrame(earth, gp, "topo");
215         //iss like orbit
216         KeplerianOrbit orbit = new KeplerianOrbit(
217                 6378137 + 400e3, 0, FastMath.toRadians(51.65), 0, 0, 0,
218                 PositionAngleType.TRUE, eci, date, Constants.EGM96_EARTH_MU);
219         AttitudeProvider attitude = new NadirPointing(eci, earth);
220 
221         //action
222         FieldOfView fov =
223                 new PolygonalFieldOfView(Vector3D.PLUS_K,
224                                          DefiningConeType.INSIDE_CONE_TOUCHING_POLYGON_AT_EDGES_MIDDLE,
225                                          Vector3D.PLUS_I, pi / 3, 16, 0);
226         FieldOfViewDetector fovDetector =
227                 new FieldOfViewDetector(topo, fov)
228                         .withMaxCheck(5.0);
229         EventsLogger logger = new EventsLogger();
230 
231         Propagator prop = new KeplerianPropagator(orbit, attitude);
232         prop.addEventDetector(logger.monitorDetector(fovDetector));
233         prop.propagate(endDate);
234         List<LoggedEvent> actual = logger.getLoggedEvents();
235 
236         //verify
237         // check we have an entry and an exit event.
238         Assertions.assertEquals(2, actual.size());
239     }
240 
241     @Test
242     public void testRadius() {
243 
244         // Definition of initial conditions with position and velocity
245         //------------------------------------------------------------
246 
247         // Extrapolator definition
248         KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit, earthCenterAttitudeLaw);
249 
250         // Event definition : square field of view, along X axis, aperture 68°
251         final double halfAperture = FastMath.toRadians(0.5 * 68.0);
252         final double maxCheck  = 60.;
253         final double threshold = 1.0e-10;
254         final PVCoordinatesProvider sun = CelestialBodyFactory.getSun();
255         final Vector3D center = Vector3D.PLUS_I;
256         final Vector3D axis1  = Vector3D.PLUS_K;
257         final Vector3D axis2  = Vector3D.PLUS_J;
258         final double aperture1 = halfAperture;
259         final double aperture2 = halfAperture;
260         final FieldOfView fov  = new DoubleDihedraFieldOfView(center, axis1, aperture1, axis2, aperture2, 0.0);
261 
262         final EventDetector sunCenter =
263                         new FieldOfViewDetector(sun, fov).
264                         withMaxCheck(maxCheck).
265                         withThreshold(threshold).
266                         withHandler(new ContinueOnEvent());
267 
268         final EventDetector sunFull =
269                         new FieldOfViewDetector(sun, Constants.SUN_RADIUS,
270                                                 VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV,
271                                                 fov).
272                         withMaxCheck(maxCheck).
273                         withThreshold(threshold).
274                         withHandler(new ContinueOnEvent());
275 
276         final EventDetector sunPartial =
277                         new FieldOfViewDetector(sun, Constants.SUN_RADIUS,
278                                                 VisibilityTrigger.VISIBLE_AS_SOON_AS_PARTIALLY_IN_FOV,
279                                                 fov).
280                         withMaxCheck(maxCheck).
281                         withThreshold(threshold).
282                         withHandler(new ContinueOnEvent());
283 
284         Assertions.assertSame(sun, ((FieldOfViewDetector) sunCenter).getPVTarget());
285         Assertions.assertEquals(0, ((FieldOfViewDetector) sunCenter).getFOV().getMargin(), 1.0e-15);
286         double eta = FastMath.acos(FastMath.sin(aperture1) * FastMath.sin(aperture2));
287         double theoreticalArea = MathUtils.TWO_PI - 4 * eta;
288         Assertions.assertEquals(theoreticalArea,
289                             ((PolygonalFieldOfView) ((FieldOfViewDetector) sunCenter).getFOV()).getZone().getSize(),
290                             1.0e-15);
291 
292         // Add event to be detected
293         EventsLogger logger = new EventsLogger();
294         propagator.addEventDetector(logger.monitorDetector(sunCenter));
295         propagator.addEventDetector(logger.monitorDetector(sunFull));
296         propagator.addEventDetector(logger.monitorDetector(sunPartial));
297 
298         // Extrapolate from the initial to the final date
299         propagator.propagate(initDate.shiftedBy(6000.));
300 
301         List<LoggedEvent>  events = logger.getLoggedEvents();
302         Assertions.assertEquals(6, events.size());
303         Assertions.assertSame(sunPartial, events.get(0).getEventDetector());
304         Assertions.assertEquals(460.876793, events.get(0).getState().getDate().durationFrom(initialOrbit.getDate()), 1.0e-6);
305         Assertions.assertSame(sunCenter, events.get(1).getEventDetector());
306         Assertions.assertEquals(488.299210, events.get(1).getState().getDate().durationFrom(initialOrbit.getDate()), 1.0e-6);
307         Assertions.assertSame(sunFull, events.get(2).getEventDetector());
308         Assertions.assertEquals(517.536353, events.get(2).getState().getDate().durationFrom(initialOrbit.getDate()), 1.0e-6);
309         Assertions.assertSame(sunFull, events.get(3).getEventDetector());
310         Assertions.assertEquals(1749.277930, events.get(3).getState().getDate().durationFrom(initialOrbit.getDate()), 1.0e-6);
311         Assertions.assertSame(sunCenter, events.get(4).getEventDetector());
312         Assertions.assertEquals(1798.478948, events.get(4).getState().getDate().durationFrom(initialOrbit.getDate()), 1.0e-6);
313         Assertions.assertSame(sunPartial, events.get(5).getEventDetector());
314         Assertions.assertEquals(1845.979622, events.get(5).getState().getDate().durationFrom(initialOrbit.getDate()), 1.0e-6);
315 
316     }
317 
318     @Test
319     public void testMatryoshka() {
320 
321         // Definition of initial conditions with position and velocity
322         //------------------------------------------------------------
323 
324         // Extrapolator definition
325         KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit, earthCenterAttitudeLaw);
326 
327         final PVCoordinatesProvider sun = CelestialBodyFactory.getSun();
328         final double maxCheck  = 60.;
329         final double threshold = 1.0e-10;
330         EventsLogger logger = new EventsLogger();
331 
332         // largest fov: circular, along X axis, aperture 68°, no margin
333         CircularFieldOfView circFov = new CircularFieldOfView(Vector3D.PLUS_I, FastMath.toRadians(0.5 * 68.0), 0.0);
334         List<EventDetector> detectors = new ArrayList<>();
335         for (int i = 0; i < 4; ++i) {
336 
337             // outer circular detector
338             final EventDetector circDetector =
339                             new FieldOfViewDetector(sun, circFov).
340                             withMaxCheck(maxCheck).
341                             withThreshold(threshold).
342                             withHandler(new ContinueOnEvent());
343             detectors.add(circDetector);
344             propagator.addEventDetector(logger.monitorDetector(circDetector));
345 
346             // inner polygonal detector
347             PolygonalFieldOfView polyFov = new PolygonalFieldOfView(circFov.getCenter(),
348                                                                     DefiningConeType.OUTSIDE_CONE_TOUCHING_POLYGON_AT_VERTICES,
349                                                                     circFov.getCenter().orthogonal(),
350                                                                     circFov.getHalfAperture(), 16, 0.0);
351             final EventDetector polyDetector =
352                             new FieldOfViewDetector(sun, polyFov).
353                             withMaxCheck(maxCheck).
354                             withThreshold(threshold).
355                             withHandler(new ContinueOnEvent());
356             detectors.add(polyDetector);
357             propagator.addEventDetector(logger.monitorDetector(polyDetector));
358 
359             // find another inner circular fov
360             final Edge     edge   = polyFov.getZone().getBoundaryLoops().get(0).getOutgoing();
361             final Vector3D middle = edge.getPointAt(0.5 * edge.getLength());
362             final double   innerRadius = Vector3D.angle(circFov.getCenter(), middle);
363             circFov = new CircularFieldOfView(circFov.getCenter(), innerRadius, 0.0);
364 
365         }
366 
367         // Extrapolate from the initial to the final date
368         propagator.propagate(initDate.shiftedBy(6000.));
369 
370         int n = detectors.size();
371         List<LoggedEvent>  events = logger.getLoggedEvents();
372         Assertions.assertEquals(2 * n, events.size());
373 
374         // series of Sun visibility start events, from outer to inner FoV
375         for (int i = 0; i < n; ++i) {
376             Assertions.assertSame(detectors.get(i), events.get(i).getEventDetector());
377         }
378 
379         // series of Sun visibility end events, from inner to outer FoV
380         for (int i = 0; i < n; ++i) {
381             Assertions.assertSame(detectors.get(n - 1 - i), events.get(n + i).getEventDetector());
382         }
383 
384     }
385 
386     @Test
387     public void testElliptical() {
388 
389         // Definition of initial conditions with position and velocity
390         //------------------------------------------------------------
391 
392         // Extrapolator definition
393         KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit, earthCenterAttitudeLaw);
394 
395         final PVCoordinatesProvider sun = CelestialBodyFactory.getSun();
396         final double maxCheck  = 60.;
397         final double threshold = 1.0e-10;
398         EventsLogger logger = new EventsLogger();
399 
400         EllipticalFieldOfView fov = new EllipticalFieldOfView(Vector3D.PLUS_I, Vector3D.PLUS_J,
401                                                               FastMath.toRadians(40), FastMath.toRadians(10),
402                                                               0.0);
403         propagator.addEventDetector(logger.monitorDetector(new FieldOfViewDetector(sun, fov).
404                                                            withMaxCheck(maxCheck).
405                                                            withThreshold(threshold).
406                                                            withHandler(new ContinueOnEvent())));
407 
408        // Extrapolate from the initial to the final date
409         propagator.propagate(initDate.shiftedBy(6000.));
410 
411         List<LoggedEvent>  events = logger.getLoggedEvents();
412         Assertions.assertEquals(2, events.size());
413 
414         Assertions.assertFalse(events.get(0).isIncreasing());
415         Assertions.assertEquals(881.897, events.get(0).getState().getDate().durationFrom(initDate), 1.0e-3);
416         Assertions.assertTrue(events.get(1).isIncreasing());
417         Assertions.assertEquals(1242.146, events.get(1).getState().getDate().durationFrom(initDate), 1.0e-3);
418 
419     }
420 
421     @BeforeEach
422     public void setUp() {
423         try {
424 
425             Utils.setDataRoot("regular-data");
426 
427             utc = TimeScalesFactory.getUTC();
428 
429             // Computation date
430             // Satellite position as circular parameters
431             mu = 3.9860047e14;
432 
433             initDate = new AbsoluteDate(new DateComponents(1969, 8, 28), TimeComponents.H00, utc);
434 
435             Vector3D position = new Vector3D(7.0e6, 1.0e6, 4.0e6);
436             Vector3D velocity = new Vector3D(-500.0, 8000.0, 1000.0);
437             initialOrbit = new EquinoctialOrbit(new PVCoordinates(position, velocity),
438                                                 FramesFactory.getEME2000(), initDate, mu);
439 
440 
441             // WGS84 Earth model
442             earth = new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
443                                          Constants.WGS84_EARTH_FLATTENING,
444                                          FramesFactory.getITRF(IERSConventions.IERS_2010, true));
445 
446             // Create earth center pointing attitude provider
447             earthCenterAttitudeLaw = new BodyCenterPointing(initialOrbit.getFrame(), earth);
448 
449         } catch (OrekitException oe) {
450             Assertions.fail(oe.getMessage());
451         }
452 
453     }
454 
455 
456     /** Handler for visibility event. */
457     private static class DihedralSunVisiHandler implements EventHandler {
458 
459         public Action eventOccurred(final SpacecraftState s, final EventDetector detector,
460                                     final boolean increasing) {
461             if (increasing) {
462                 //System.err.println(" Sun visibility starts " + s.getDate());
463                 AbsoluteDate startVisiDate = new AbsoluteDate(new DateComponents(1969, 8, 28),
464                                                               new TimeComponents(1, 19, 00.381),
465                                                               TimeScalesFactory.getUTC());
466 
467                 Assertions.assertTrue(s.getDate().durationFrom(startVisiDate) <= 1);
468                 return Action.CONTINUE;
469             } else {
470                 AbsoluteDate endVisiDate = new AbsoluteDate(new DateComponents(1969, 8, 28),
471                                                               new TimeComponents(1, 39 , 42.674),
472                                                               TimeScalesFactory.getUTC());
473                 Assertions.assertTrue(s.getDate().durationFrom(endVisiDate) <= 1);
474                 //System.err.println(" Sun visibility ends at " + s.getDate());
475                 return Action.CONTINUE;//STOP;
476             }
477         }
478 
479     }
480 
481 }