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.geometry.fov;
18  
19  import org.hipparchus.geometry.euclidean.threed.RotationOrder;
20  import org.hipparchus.geometry.euclidean.threed.Vector3D;
21  import org.hipparchus.geometry.euclidean.twod.Vector2D;
22  import org.hipparchus.random.UnitSphereRandomVectorGenerator;
23  import org.hipparchus.random.Well19937a;
24  import org.hipparchus.util.FastMath;
25  import org.hipparchus.util.MathUtils;
26  import org.junit.jupiter.api.Assertions;
27  import org.junit.jupiter.api.Test;
28  import org.orekit.attitudes.LofOffset;
29  import org.orekit.attitudes.NadirPointing;
30  import org.orekit.bodies.Ellipse;
31  import org.orekit.frames.FramesFactory;
32  import org.orekit.frames.LOFType;
33  import org.orekit.frames.Transform;
34  import org.orekit.propagation.events.VisibilityTrigger;
35  import org.orekit.time.AbsoluteDate;
36  
37  import java.lang.reflect.InvocationTargetException;
38  import java.lang.reflect.Method;
39  
40  public class EllipticalFieldOfViewTest extends AbstractSmoothFieldOfViewTest {
41  
42      @Test
43      public void testPlanarProjection() {
44  
45          EllipticalFieldOfView fov = new EllipticalFieldOfView(Vector3D.PLUS_K, Vector3D.PLUS_I,
46                                                                FastMath.toRadians(40.0), FastMath.toRadians(10.0),
47                                                                0.0);
48  
49  
50          // test direction
51          final Vector3D d         = new Vector3D(0.4, 0.8, 0.2).normalize();
52  
53          // plane ellipse
54          final Ellipse  ellipse   = new Ellipse(fov.getZ(), fov.getX(), fov.getY(),
55                                                 FastMath.tan(fov.getHalfApertureAlongX()),
56                                                 FastMath.tan(fov.getHalfApertureAlongY()),
57                                                 FramesFactory.getGCRF());
58          final Vector3D projected   = new Vector3D(1.0 / d.getZ(), d);
59          final Vector3D closestProj = ellipse.toSpace(ellipse.projectToEllipse(ellipse.toPlane(projected)));
60  
61          // the closest point to the planar project ellipse belongs to the ellipse on the sphere
62          Assertions.assertEquals(0.0,
63                              fov.offsetFromBoundary(closestProj, 0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV),
64                              2.0e-15);
65  
66          // approximate computation of closest point on the ellipse on the sphere
67          Vector3D closestSphere = null;
68          for (double eta = 0; eta < MathUtils.TWO_PI; eta += 0.0001) {
69              Vector3D p = fov.directionAt(eta);
70              if (closestSphere == null || Vector3D.angle(p, d) < Vector3D.angle(closestSphere, d)) {
71                  closestSphere = p;
72              }
73          }
74          Assertions.assertEquals(0.0,
75                              fov.offsetFromBoundary(closestSphere, 0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV),
76                              2.0e-15);
77  
78          // computing the closest point to the planar project ellipse
79          // does NOT give the closest point on the ellipse on the sphere
80          Assertions.assertEquals(Vector3D.angle(closestProj, d) - 0.0056958,
81                              Vector3D.angle(closestSphere, d),
82                              1.0e-7);
83  
84      }
85  
86      @Test
87      public void testFocalPoints() {
88  
89          EllipticalFieldOfView fov = new EllipticalFieldOfView(Vector3D.PLUS_K, Vector3D.PLUS_I,
90                                                                FastMath.toRadians(40.0), FastMath.toRadians(10.0),
91                                                                0.0);
92  
93          // find the angular foci of the ellipse
94          final Vector3D f1Sphere  = fov.getFocus1();
95          final Vector3D f2Sphere  = fov.getFocus2();
96  
97          // find the planar foci in the (XY) plane
98          Vector2D f1Plane = new Vector2D(f1Sphere.getX() * FastMath.cos(fov.getHalfApertureAlongY()), 0.0);
99          Vector2D f2Plane = new Vector2D(-f1Plane.getX(), f1Plane.getY());
100 
101         // the focal points on plane are NOT the projection of the focal points on sphere
102         Assertions.assertTrue(f1Sphere.getX() - f1Plane.getX() > +0.0095);
103         Assertions.assertTrue(f2Sphere.getX() - f2Plane.getX() < -0.0095);
104 
105         // find the constant sum of the distances to foci
106         final double angularDist = 2 * fov.getHalfApertureAlongX();
107         final double d = 2 * FastMath.sin(fov.getHalfApertureAlongX());
108 
109         for (double angle = 0; angle < MathUtils.TWO_PI; angle += 0.001) {
110 
111             // sum of angular distances on sphere is constant
112             final Vector3D pSphere = fov.directionAt(angle);
113             Assertions.assertEquals(angularDist, Vector3D.angle(pSphere, f1Sphere) + Vector3D.angle(pSphere, f2Sphere), 1.0e-14);
114 
115             // sum of Cartesian distances projected on plane is constant
116             final Vector2D pPlane = new Vector2D(pSphere.getX(), pSphere.getY());
117             Assertions.assertEquals(d, Vector2D.distance(pPlane, f1Plane) + Vector2D.distance(pPlane, f2Plane), 5.0e-16);
118 
119         }
120 
121     }
122 
123     @Test
124     public void testDirectionFromDistances()
125         throws NoSuchMethodException, SecurityException, IllegalAccessException,
126                IllegalArgumentException, InvocationTargetException {
127 
128         final EllipticalFieldOfView fov = new EllipticalFieldOfView(Vector3D.PLUS_K, Vector3D.PLUS_I,
129                                                                     FastMath.toRadians(40.0), FastMath.toRadians(10.0),
130                                                                     0.0);
131         final Vector3D f1Sphere  = fov.getFocus1();
132         final Vector3D f2Sphere  = fov.getFocus2();
133         final double   a         = FastMath.max(fov.getHalfApertureAlongX(), fov.getHalfApertureAlongY());
134         final double   delta     = Vector3D.angle(f1Sphere, f2Sphere);
135         final double   dMin      = a - delta / 2;
136         final double   dMax      = a + delta / 2;
137 
138         Method directionAt = EllipticalFieldOfView.class.getDeclaredMethod("directionAt",
139                                                                            Double.TYPE, Double.TYPE, Double.TYPE);
140         directionAt.setAccessible(true);
141         for (double d1 = dMin; d1 <= dMax; d1 += 0.001) {
142             final double d2 = 2 * a - d1;
143             final Vector3D dPlus = (Vector3D) directionAt.invoke(fov, d1, d2, +1.0);
144             Assertions.assertEquals(d1, Vector3D.angle(dPlus, f1Sphere), 2.0e-14);
145             Assertions.assertEquals(d2, Vector3D.angle(dPlus, f2Sphere), 2.0e-14);
146             Assertions.assertEquals(0.0,
147                                 fov.offsetFromBoundary(dPlus, 0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV),
148                                 1.0e-13);
149             final Vector3D dMinus = (Vector3D) directionAt.invoke(fov, d1, d2, -1.0);
150             Assertions.assertEquals(d1, Vector3D.angle(dMinus, f1Sphere), 2.0e-14);
151             Assertions.assertEquals(d2, Vector3D.angle(dMinus, f2Sphere), 2.0e-14);
152             Assertions.assertEquals(0.0,
153                                 fov.offsetFromBoundary(dPlus, 0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV),
154                                 1.0e-13);
155 
156         }
157 
158     }
159 
160     @Test
161     public void testNadirNoMargin() {
162         doTestFootprint(new EllipticalFieldOfView(Vector3D.PLUS_K, Vector3D.PLUS_I,
163                                                   FastMath.toRadians(4.0), FastMath.toRadians(2.0),
164                                                   0.0),
165                         new NadirPointing(orbit.getFrame(), earth),
166                         2.0, 4.0, 83.8280, 86.9120, 120567.3, 241701.8);
167     }
168 
169     @Test
170     public void testNadirMargin() {
171         doTestFootprint(new EllipticalFieldOfView(Vector3D.PLUS_K, Vector3D.PLUS_I,
172                                                   FastMath.toRadians(4.0), FastMath.toRadians(2.0),
173                                                   0.01),
174                         new NadirPointing(orbit.getFrame(), earth),
175                         2.0, 4.0, 83.8280, 86.9120, 120567.3, 241701.8);
176     }
177 
178     @Test
179     public void testRollPitchYaw() {
180         doTestFootprint(new EllipticalFieldOfView(Vector3D.PLUS_K, Vector3D.PLUS_I,
181                                                   FastMath.toRadians(4.0), FastMath.toRadians(2.0),
182                                                   0.0),
183                         new LofOffset(orbit.getFrame(), LOFType.LVLH_CCSDS, RotationOrder.XYZ,
184                                       FastMath.toRadians(10),
185                                       FastMath.toRadians(20),
186                                       FastMath.toRadians(5)),
187                         2.0, 4.0, 47.7675, 60.2403, 1219597.1, 1817011.0);
188     }
189 
190     @Test
191     public void testFOVPartiallyTruncatedAtLimb() {
192         doTestFootprint(new EllipticalFieldOfView(Vector3D.PLUS_K, Vector3D.PLUS_I,
193                                                   FastMath.toRadians(4.0), FastMath.toRadians(2.0),
194                                                   0.0),
195                         new LofOffset(orbit.getFrame(), LOFType.LVLH_CCSDS, RotationOrder.XYZ,
196                                       FastMath.toRadians(-10),
197                                       FastMath.toRadians(-39),
198                                       FastMath.toRadians(-5)),
199                         0.3899, 4.0, 0.0, 24.7014, 3213727.9, 5346638.0);
200     }
201 
202     @Test
203     public void testFOVLargerThanEarth() {
204         doTestFootprint(new EllipticalFieldOfView(Vector3D.PLUS_K, Vector3D.PLUS_I,
205                                                   FastMath.toRadians(50.0), FastMath.toRadians(45.0),
206                                                   0.0),
207                         new NadirPointing(orbit.getFrame(), earth),
208                         40.3505, 40.4655, 0.0, 0.0, 5323032.8, 5347029.8);
209     }
210 
211     @Test
212     public void testFOVAwayFromEarth() {
213         doTestFOVAwayFromEarth(new EllipticalFieldOfView(Vector3D.MINUS_K, Vector3D.PLUS_I,
214                                                          FastMath.toRadians(4.0), FastMath.toRadians(2.0),
215                                                          0.0),
216                                new LofOffset(orbit.getFrame(), LOFType.LVLH_CCSDS, RotationOrder.XYZ,
217                                              FastMath.toRadians(-10),
218                                              FastMath.toRadians(-39),
219                                              FastMath.toRadians(-5)),
220                                Vector3D.MINUS_K);
221     }
222 
223     @Test
224     public void testNoFootprintInside() {
225         doTestNoFootprintInside(new EllipticalFieldOfView(Vector3D.PLUS_K, Vector3D.PLUS_I,
226                                                           FastMath.toRadians(4.0), FastMath.toRadians(2.0),
227                                                           0.0),
228                                 new Transform(AbsoluteDate.J2000_EPOCH, new Vector3D(5e6, 3e6, 2e6)));
229     }
230 
231     @Test
232     public void testConventionsTangentPoints()
233         throws NoSuchMethodException, SecurityException, IllegalAccessException,
234                IllegalArgumentException, InvocationTargetException {
235         Method directionAt = EllipticalFieldOfView.class.getDeclaredMethod("directionAt", Double.TYPE);
236         directionAt.setAccessible(true);
237         final EllipticalFieldOfView ang  = new EllipticalFieldOfView(Vector3D.PLUS_I, Vector3D.PLUS_J,
238                                                                      FastMath.toRadians(10.0), FastMath.toRadians(40.0),
239                                                                      0.0);
240         final EllipticalFieldOfView cart = new EllipticalFieldOfView(ang.getCenter(), ang.getX(),
241                                                                      ang.getHalfApertureAlongX(), ang.getHalfApertureAlongY(),
242                                                                      ang.getMargin());
243         for (int i = 0; i < 4; ++i) {
244             final double theta = i * 0.5 * FastMath.PI;
245             final Vector3D pAng  = (Vector3D) directionAt.invoke(ang, theta);
246             final Vector3D pCart = (Vector3D) directionAt.invoke(cart, theta);
247             Assertions.assertEquals(0.0, Vector3D.angle(pAng, pCart), 1.0e-15);
248         }
249     }
250 
251     @Test
252     public void testPointsOnBoundary() {
253         doTestPointsOnBoundary(new EllipticalFieldOfView(Vector3D.PLUS_I, Vector3D.PLUS_J,
254                                                          FastMath.toRadians(10.0), FastMath.toRadians(40.0),
255                                                          0.0),
256                                2.0e-12);
257     }
258 
259     @Test
260     public void testPointsOutsideBoundary() {
261         doTestPointsNearBoundary(new EllipticalFieldOfView(Vector3D.PLUS_I, Vector3D.PLUS_J,
262                                                            FastMath.toRadians(10.0), FastMath.toRadians(40.0),
263                                                            0.0),
264                                  0.1, 0.0101573, 0.1, 1.0e-7);
265     }
266 
267     @Test
268     public void testPointsInsideBoundary() {
269         doTestPointsNearBoundary(new EllipticalFieldOfView(Vector3D.PLUS_I, Vector3D.PLUS_J,
270                                                            FastMath.toRadians(10.0), FastMath.toRadians(40.0),
271                                                            0.0),
272                                  -0.1, -0.1, -0.0693260, 1.0e-7);
273     }
274 
275     @Test
276     public void testPointsAlongPrincipalAxes() {
277 
278         final EllipticalFieldOfView fov  = new EllipticalFieldOfView(Vector3D.PLUS_I, Vector3D.PLUS_J,
279                                                                      FastMath.toRadians(10.0), FastMath.toRadians(40.0),
280                                                                      0.0);
281 
282         // test points in the primary meridian
283         Assertions.assertTrue(fov.offsetFromBoundary(new Vector3D(FastMath.cos(FastMath.toRadians(11)),
284                                                               FastMath.sin(-FastMath.toRadians(11)),
285                                                               0.0),
286                                                  0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV) > 0.0);
287         Assertions.assertTrue(fov.offsetFromBoundary(new Vector3D(FastMath.cos(FastMath.toRadians(9)),
288                                                               FastMath.sin(-FastMath.toRadians(9)),
289                                                               0.0),
290                                                  0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV) < 0.0);
291         Assertions.assertTrue(fov.offsetFromBoundary(new Vector3D(FastMath.cos(FastMath.toRadians(9)),
292                                                               FastMath.sin(FastMath.toRadians(9)),
293                                                               0.0),
294                                                  0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV) < 0.0);
295         Assertions.assertTrue(fov.offsetFromBoundary(new Vector3D(FastMath.cos(FastMath.toRadians(11)),
296                                                               FastMath.sin(FastMath.toRadians(11)),
297                                                               0.0),
298                                                  0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV) > 0.0);
299 
300         // test points in the secondary meridian
301         Assertions.assertTrue(fov.offsetFromBoundary(new Vector3D(FastMath.cos(FastMath.toRadians(41)),
302                                                               0.0,
303                                                               FastMath.sin(-FastMath.toRadians(41))),
304                                                  0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV) > 0.0);
305         Assertions.assertTrue(fov.offsetFromBoundary(new Vector3D(FastMath.cos(FastMath.toRadians(39)),
306                                                               0.0,
307                                                               FastMath.sin(-FastMath.toRadians(39))),
308                                                  0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV) < 0.0);
309         Assertions.assertTrue(fov.offsetFromBoundary(new Vector3D(FastMath.cos(FastMath.toRadians(39)),
310                                                               0.0,
311                                                               FastMath.sin(FastMath.toRadians(39))),
312                                                  0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV) < 0.0);
313         Assertions.assertTrue(fov.offsetFromBoundary(new Vector3D(FastMath.cos(FastMath.toRadians(41)),
314                                                               0.0,
315                                                               FastMath.sin(FastMath.toRadians(41))),
316                                                  0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV) > 0.0);
317     }
318 
319     @Test
320     public void testOffsetAngularAccuracy() {
321 
322         final EllipticalFieldOfView fov  = new EllipticalFieldOfView(Vector3D.PLUS_I, Vector3D.PLUS_J,
323                                                                      FastMath.toRadians(10.0), FastMath.toRadians(40.0),
324                                                                      0.0);
325         final Vector3D f1 = fov.getFocus1();
326         final Vector3D f2 = fov.getFocus2();
327         final double   a  = FastMath.max(fov.getHalfApertureAlongX(), fov.getHalfApertureAlongY());
328 
329         UnitSphereRandomVectorGenerator random =
330                         new UnitSphereRandomVectorGenerator(3, new Well19937a(0xc9383d990d45a111l));
331         for (int i = 0; i < 300; ++i) {
332             final Vector3D los = new Vector3D(random.nextVector());
333             final double   d1  = Vector3D.angle(los, f1);
334             final double   d2  = Vector3D.angle(los, f2);
335             double etaMin;
336             double etaMax;
337             if (Vector3D.dotProduct(los, fov.getY()) > 0) {
338                 if (Vector3D.dotProduct(los, fov.getX()) > 0) {
339                     etaMin = 0;
340                     etaMax = 0.5 * FastMath.PI;
341                 } else {
342                     etaMin = 0.5 * FastMath.PI;
343                     etaMax = FastMath.PI;
344                 }
345             } else {
346                 if (Vector3D.dotProduct(los, fov.getX()) < 0) {
347                     etaMin = FastMath.PI;
348                     etaMax = 1.5 * FastMath.PI;
349                 } else {
350                     etaMin = 1.5 * FastMath.PI;
351                     etaMax = 2.0 * FastMath.PI;
352                 }
353             }
354             double minDist = Double.POSITIVE_INFINITY;
355             for (double eta = etaMin; eta < etaMax; eta += 0.0001) {
356                 minDist = FastMath.min(minDist, Vector3D.angle(los, fov.directionAt(eta)));
357             }
358             minDist = FastMath.copySign(minDist, d1 + d2 - 2 * a);
359 
360             // here, we intentionally use an impossibly large radius to ensure we don't use the speed-up
361             // and compute offset as an exact angle
362             double hugeRadius = FastMath.PI;
363             double realOffset = fov.offsetFromBoundary(los, hugeRadius, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV);
364             Assertions.assertEquals(minDist + hugeRadius, realOffset, 3.0e-7);
365 
366             // here, we intentionally use a zero radius, so we may use the speed-up
367             // and may underestimate the offset
368             double approximateOffset = fov.offsetFromBoundary(los, 0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV);
369             Assertions.assertTrue(approximateOffset < minDist + 3.0e-7);
370 
371         }
372 
373     }
374 
375     @Test
376     public void testBoundary() {
377         doTestBoundary(new EllipticalFieldOfView(Vector3D.PLUS_I, Vector3D.PLUS_J,
378                                                  FastMath.toRadians(10.0), FastMath.toRadians(40.0),
379                                                  0.01),
380                        new Well19937a(0x5148b24d1bdf90cel),
381                        2.0e-9);
382     }
383 
384     private void doTestPointsOnBoundary(final EllipticalFieldOfView fov, double tol) {
385         try {
386             Method directionAt = EllipticalFieldOfView.class.getDeclaredMethod("directionAt", Double.TYPE);
387             directionAt.setAccessible(true);
388             for (double theta = 0; theta < MathUtils.TWO_PI; theta += 0.01) {
389                 final Vector3D direction = (Vector3D) directionAt.invoke(fov, theta);
390                 Assertions.assertEquals(0.0,
391                                     fov.offsetFromBoundary(direction, 0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV),
392                                     tol);
393             }
394         } catch (NoSuchMethodException | SecurityException | IllegalAccessException |
395                         IllegalArgumentException | InvocationTargetException e) {
396             Assertions.fail(e.getLocalizedMessage());
397         }
398     }
399 
400     private void doTestPointsNearBoundary(final EllipticalFieldOfView fov, final double delta,
401                                           final double expectedMin, final double expectedMax, final double tol) {
402         try {
403             final EllipticalFieldOfView near = new EllipticalFieldOfView(fov.getCenter(), fov.getX(),
404                                                                          fov.getHalfApertureAlongX() + delta,
405                                                                          fov.getHalfApertureAlongY() + delta,
406                                                                          fov.getMargin());
407             Method directionAt = EllipticalFieldOfView.class.getDeclaredMethod("directionAt", Double.TYPE);
408             directionAt.setAccessible(true);
409             double minOffset = Double.POSITIVE_INFINITY;
410             double maxOffset = Double.NEGATIVE_INFINITY;
411             for (double theta = 0; theta < MathUtils.TWO_PI; theta += 0.01) {
412                 final Vector3D direction = (Vector3D) directionAt.invoke(near, theta);
413                 final double offset = fov.offsetFromBoundary(direction, 0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV);
414                 minOffset = FastMath.min(minOffset, offset);
415                 maxOffset = FastMath.max(maxOffset, offset);
416             }
417             Assertions.assertEquals(expectedMin, minOffset, tol);
418             Assertions.assertEquals(expectedMax, maxOffset, tol);
419         } catch (NoSuchMethodException | SecurityException | IllegalAccessException |
420                         IllegalArgumentException | InvocationTargetException e) {
421             Assertions.fail(e.getLocalizedMessage());
422         }
423     }
424 
425 }