1   /* Copyright 2002-2026 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 java.util.ArrayList;
20  import java.util.List;
21  
22  import org.hipparchus.geometry.euclidean.threed.Line;
23  import org.hipparchus.geometry.euclidean.threed.Vector3D;
24  import org.hipparchus.util.FastMath;
25  import org.hipparchus.util.MathUtils;
26  import org.orekit.bodies.GeodeticPoint;
27  import org.orekit.bodies.OneAxisEllipsoid;
28  import org.orekit.errors.OrekitException;
29  import org.orekit.errors.OrekitMessages;
30  import org.orekit.frames.Frame;
31  import org.orekit.frames.Transform;
32  import org.orekit.propagation.events.VisibilityTrigger;
33  
34  /** Class representing a spacecraft sensor Field Of View with shape defined by a smooth single loop.
35   * @author Luc Maisonobe
36   * @since 10.1
37   */
38  public abstract class SmoothFieldOfView extends AbstractFieldOfView {
39  
40      /** Direction of the FOV center. */
41      private final Vector3D center;
42  
43      /** X axis defining FoV boundary. */
44      private final Vector3D xAxis;
45  
46      /** Y axis defining FoV boundary. */
47      private final Vector3D yAxis;
48  
49      /** Z axis defining FoV boundary. */
50      private final Vector3D zAxis;
51  
52      /** Build a new instance.
53       * @param center direction of the FOV center (Z<sub>smooth</sub>), in spacecraft frame
54       * @param primaryMeridian vector defining the (+X<sub>smooth</sub>, Z<sub>smooth</sub>)
55       * half-plane (it is allowed to have {@code primaryMeridian} not orthogonal to
56       * {@code center} as orthogonality will be fixed internally)
57       * @param margin angular margin to apply to the zone (if positive,
58       * the Field Of View will consider points slightly outside of the
59       * zone are still visible)
60       */
61      protected SmoothFieldOfView(final Vector3D center, final Vector3D primaryMeridian,
62                                  final double margin) {
63  
64          super(margin);
65  
66          this.center = center;
67          this.zAxis  = center.normalize();
68          this.yAxis  = Vector3D.crossProduct(center, primaryMeridian).normalize();
69          this.xAxis  = Vector3D.crossProduct(yAxis, center).normalize();
70  
71      }
72  
73      /** Get the direction of the FOV center, in spacecraft frame.
74       * @return direction of the FOV center, in spacecraft frame
75       */
76      public Vector3D getCenter() {
77          return center;
78      }
79  
80      /** Get the X axis defining FoV boundary.
81       * @return X axis defining FoV boundary, in spacecraft frame
82       */
83      public Vector3D getX() {
84          return xAxis;
85      }
86  
87      /** Get the Y axis defining FoV boundary.
88       * @return Y axis defining FoV boundary, in spacecraft frame
89       */
90      public Vector3D getY() {
91          return yAxis;
92      }
93  
94      /** Get the Z axis defining FoV boundary.
95       * @return Z axis defining FoV boundary, in spacecraft frame
96       */
97      public Vector3D getZ() {
98          return zAxis;
99      }
100 
101     /** {@inheritDoc} */
102     @Override
103     public List<List<GeodeticPoint>> getFootprint(final Transform fovToBody,
104                                                   final OneAxisEllipsoid body,
105                                                   final double angularStep) {
106 
107         return getFootprint(fovToBody, body, angularStep, DEFAULT_EXTERNAL_FOOTPRINT, DEFAULT_MAX_DIST);
108 
109     }
110 
111     /** {@inheritDoc} */
112     @Override
113     public List<List<GeodeticPoint>> getFootprint(final Transform fovToBody,
114                                                   final OneAxisEllipsoid body,
115                                                   final double angularStep,
116                                                   final boolean extendedFootprint,
117                                                   final double maxDistance) {
118 
119         final Frame     bodyFrame = body.getBodyFrame();
120         final Vector3D  position  = fovToBody.transformPosition(Vector3D.ZERO);
121         final double    r         = position.getNorm();
122         if (body.isInside(position)) {
123             throw new OrekitException(OrekitMessages.POINT_INSIDE_ELLIPSOID);
124         }
125 
126         // prepare loop around FoV
127         boolean                         intersectionsFound = false;
128         final int                       nbPoints           = (int) FastMath.ceil(MathUtils.TWO_PI / angularStep);
129         final List<GeodeticPoint>       loop               = new ArrayList<>(nbPoints);
130 
131         // loop in inverse trigonometric order, so footprint is in trigonometric order
132         final double step = MathUtils.TWO_PI / nbPoints;
133         for (int i = 0; i < nbPoints; ++i) {
134             final Vector3D direction   = directionAt(-i * step);
135             final Vector3D awaySC      = new Vector3D(r, direction);
136             final Vector3D awayBody    = fovToBody.transformPosition(awaySC);
137             final Line     lineOfSight = new Line(position, awayBody, 1.0e-3);
138             GeodeticPoint  gp          = body.getIntersectionPoint(lineOfSight, position, bodyFrame, null);
139             if (gp != null &&
140                 Vector3D.dotProduct(awayBody.subtract(position), body.transform(gp).subtract(position)) < 0) {
141                 // the intersection is in fact on the half-line pointing
142                 // towards the back side, it is a spurious intersection
143                 gp = null;
144             }
145 
146             if (gp != null) {
147                 // the line of sight does intersect the body
148                 intersectionsFound = true;
149             } else {
150                 // the line of sight does not intersect body
151                 if (!extendedFootprint) {
152                     // we use a point on the limb
153                     gp = body.transform(body.pointOnLimb(position, awayBody), bodyFrame, null);
154                 } else {
155                     // we project in the proper direction (to a point in space)
156                     // such that the line's length is the requested value (maxDistance)
157                     final double temp_norm = awaySC.getNorm();
158                     Vector3D temp_vec = awaySC.scalarMultiply(1 / temp_norm).scalarMultiply(maxDistance);
159                     temp_vec = fovToBody.transformPosition(temp_vec);
160                     gp = body.transform(temp_vec, bodyFrame, null);
161                 }
162             }
163 
164             // add the point
165             loop.add(gp);
166 
167         }
168 
169         final List<List<GeodeticPoint>> footprint = new ArrayList<>();
170         if (!extendedFootprint) { //if we care about non-intersection
171             if (intersectionsFound) {
172                 // at least some of the points did intersect the body, there is a footprint
173                 footprint.add(loop);
174             } else {
175                 // the Field Of View loop does not cross the body
176                 // either the body is outside of Field Of View, or it is fully contained
177                 // we check the center
178                 final Vector3D bodyCenter = fovToBody.getStaticInverse().transformPosition(Vector3D.ZERO);
179                 if (offsetFromBoundary(bodyCenter, 0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV) < 0.0) {
180                     // the body is fully contained in the Field Of View
181                     // the previous loop did compute the full limb as the footprint
182                     footprint.add(loop);
183                 }
184             }
185         } else { // if we don't care
186             footprint.add(loop);
187         }
188 
189         return footprint;
190 
191     }
192 
193     /** Get boundary direction at angle.
194      * @param angle phase angle of the boundary direction
195      * @return boundary direction at phase angle in spacecraft frame
196      */
197     protected abstract Vector3D directionAt(double angle);
198 
199 }