PolygonalFieldOfView.java

  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. import java.util.ArrayList;
  19. import java.util.List;

  20. import org.hipparchus.geometry.enclosing.EnclosingBall;
  21. import org.hipparchus.geometry.euclidean.threed.Line;
  22. import org.hipparchus.geometry.euclidean.threed.Rotation;
  23. import org.hipparchus.geometry.euclidean.threed.RotationConvention;
  24. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  25. import org.hipparchus.geometry.partitioning.Region;
  26. import org.hipparchus.geometry.spherical.twod.Edge;
  27. import org.hipparchus.geometry.spherical.twod.S2Point;
  28. import org.hipparchus.geometry.spherical.twod.Sphere2D;
  29. import org.hipparchus.geometry.spherical.twod.SphericalPolygonsSet;
  30. import org.hipparchus.geometry.spherical.twod.Vertex;
  31. import org.hipparchus.util.FastMath;
  32. import org.hipparchus.util.MathUtils;
  33. import org.hipparchus.util.SinCos;
  34. import org.orekit.bodies.GeodeticPoint;
  35. import org.orekit.bodies.OneAxisEllipsoid;
  36. import org.orekit.errors.OrekitException;
  37. import org.orekit.errors.OrekitMessages;
  38. import org.orekit.frames.Frame;
  39. import org.orekit.frames.Transform;
  40. import org.orekit.propagation.events.VisibilityTrigger;

  41. /** Class representing a spacecraft sensor Field Of View with polygonal shape.
  42.  * <p>Fields Of View are zones defined on the unit sphere centered on the
  43.  * spacecraft. They can have any shape, they can be split in several
  44.  * non-connected patches and can have holes.</p>
  45.  * @author Luc Maisonobe
  46.  * @since 10.1
  47.  */
  48. public class PolygonalFieldOfView extends AbstractFieldOfView {

  49.     /** Spherical zone. */
  50.     private final SphericalPolygonsSet zone;

  51.     /** Spherical cap surrounding the zone. */
  52.     private final EnclosingBall<Sphere2D, S2Point> cap;

  53.     /** Build a new instance.
  54.      * @param zone interior of the Field Of View, in spacecraft frame
  55.      * @param margin angular margin to apply to the zone (if positive,
  56.      * points outside of the raw FoV but close enough to the boundary are
  57.      * considered visible; if negative, points inside of the raw FoV
  58.      * but close enough to the boundary are considered not visible)
  59.      */
  60.     public PolygonalFieldOfView(final SphericalPolygonsSet zone, final double margin) {
  61.         super(margin);
  62.         this.zone = zone;
  63.         this.cap  = zone.getEnclosingCap();
  64.     }

  65.     /** Build Field Of View with a regular polygon shape.
  66.      * @param center center of the polygon (the center is in the inside part)
  67.      * @param coneType type of defining cone
  68.      * @param meridian point defining the reference meridian for one contact
  69.      * point between defining cone and polygon (i.e. either a polygon edge
  70.      * middle point or a polygon vertex)
  71.      * @param radius defining cone angular radius
  72.      * @param n number of sides of the polygon
  73.      * @param margin angular margin to apply to the zone (if positive,
  74.      * points outside of the raw FoV but close enough to the boundary are
  75.      * considered visible; if negative, points inside of the raw FoV
  76.      * but close enough to the boundary are considered not visible)
  77.      * @since 10.1
  78.      */
  79.     public PolygonalFieldOfView(final Vector3D center, final DefiningConeType coneType,
  80.                                 final Vector3D meridian, final double radius,
  81.                                 final int n, final double margin) {

  82.         super(margin);

  83.         final double   verticesRadius = coneType.verticesRadius(radius, n);
  84.         final Vector3D vertex         = coneType.createVertex(center, meridian, verticesRadius, n);
  85.         this.zone                     = new SphericalPolygonsSet(center, vertex, verticesRadius,
  86.                                                                  n, 1.0e-12 * verticesRadius);

  87.         final Rotation r = new Rotation(center, MathUtils.TWO_PI / n, RotationConvention.VECTOR_OPERATOR);
  88.         final S2Point[] support = new S2Point[n];
  89.         support[0] = new S2Point(vertex);
  90.         for (int i = 1; i < n; ++i) {
  91.             support[i] = new S2Point(r.applyTo(support[i - 1].getVector()));
  92.         }
  93.         this.cap = new EnclosingBall<>(new S2Point(center), Vector3D.angle(center, vertex), support);

  94.     }

  95.     /** Get the interior zone.
  96.      * @return the interior zone
  97.      */
  98.     public SphericalPolygonsSet getZone() {
  99.         return zone;
  100.     }

  101.     /** {@inheritDoc} */
  102.     @Override
  103.     public double offsetFromBoundary(final Vector3D lineOfSight, final double angularRadius,
  104.                                      final VisibilityTrigger trigger) {

  105.         final S2Point los             = new S2Point(lineOfSight);
  106.         final double  margin          = getMargin();
  107.         final double  correctedRadius = trigger.radiusCorrection(angularRadius);
  108.         final double  deadBand        = margin + angularRadius;

  109.         // for faster computation, we start using only the surrounding cap, to filter out
  110.         // far away points (which correspond to most of the points if the Field Of View is small)
  111.         final double crudeDistance = cap.getCenter().distance(los) - cap.getRadius();
  112.         if (crudeDistance > deadBand + 0.01) {
  113.             // we know we are strictly outside of the zone,
  114.             // use the crude distance to compute the (positive) return value
  115.             return crudeDistance + correctedRadius - margin;
  116.         }

  117.         // we are close, we need to compute carefully the exact offset;
  118.         // we project the point to the closest zone boundary
  119.         return zone.projectToBoundary(los).getOffset() + correctedRadius - margin;

  120.     }

  121.     /** {@inheritDoc} */
  122.     @Override
  123.     public Vector3D projectToBoundary(final Vector3D lineOfSight) {
  124.         return ((S2Point) zone.projectToBoundary(new S2Point(lineOfSight)).getProjected()).getVector();
  125.     }

  126.     /** {@inheritDoc} */
  127.     @Override
  128.     public List<List<GeodeticPoint>> getFootprint(final Transform fovToBody,
  129.                                                   final OneAxisEllipsoid body,
  130.                                                   final double angularStep) {

  131.         final Frame     bodyFrame = body.getBodyFrame();
  132.         final Vector3D  position  = fovToBody.transformPosition(Vector3D.ZERO);
  133.         final double    r         = position.getNorm();
  134.         if (body.isInside(position)) {
  135.             throw new OrekitException(OrekitMessages.POINT_INSIDE_ELLIPSOID);
  136.         }

  137.         final List<List<GeodeticPoint>> footprint = new ArrayList<>();

  138.         final List<Vertex> boundary = zone.getBoundaryLoops();
  139.         for (final Vertex loopStart : boundary) {
  140.             int count = 0;
  141.             final List<GeodeticPoint> loop  = new ArrayList<>();
  142.             boolean intersectionsFound      = false;
  143.             for (Edge edge = loopStart.getOutgoing();
  144.                  count == 0 || edge.getStart() != loopStart;
  145.                  edge = edge.getEnd().getOutgoing()) {
  146.                 ++count;
  147.                 final int    n     = (int) FastMath.ceil(edge.getLength() / angularStep);
  148.                 final double delta =  edge.getLength() / n;
  149.                 for (int i = 0; i < n; ++i) {
  150.                     final Vector3D awaySC      = new Vector3D(r, edge.getPointAt(i * delta));
  151.                     final Vector3D awayBody    = fovToBody.transformPosition(awaySC);
  152.                     final Line     lineOfSight = new Line(position, awayBody, 1.0e-3);
  153.                     GeodeticPoint  gp          = body.getIntersectionPoint(lineOfSight, position,
  154.                                                                            bodyFrame, null);
  155.                     if (gp != null &&
  156.                         Vector3D.dotProduct(awayBody.subtract(position),
  157.                                             body.transform(gp).subtract(position)) < 0) {
  158.                         // the intersection is in fact on the half-line pointing
  159.                         // towards the back side, it is a spurious intersection
  160.                         gp = null;
  161.                     }

  162.                     if (gp != null) {
  163.                         // the line of sight does intersect the body
  164.                         intersectionsFound = true;
  165.                     } else {
  166.                         // the line of sight does not intersect body
  167.                         // we use a point on the limb
  168.                         gp = body.transform(body.pointOnLimb(position, awayBody), bodyFrame, null);
  169.                     }

  170.                     // add the point in front of the list
  171.                     // (to ensure the loop will be in trigonometric orientation)
  172.                     loop.add(0, gp);

  173.                 }
  174.             }

  175.             if (intersectionsFound) {
  176.                 // at least some of the points did intersect the body,
  177.                 // this loop contributes to the footprint
  178.                 footprint.add(loop);
  179.             }

  180.         }

  181.         if (footprint.isEmpty()) {
  182.             // none of the Field Of View loops cross the body
  183.             // either the body is outside of Field Of View, or it is fully contained
  184.             // we check the center
  185.             final Vector3D bodyCenter = fovToBody.getStaticInverse().transformPosition(Vector3D.ZERO);
  186.             if (zone.checkPoint(new S2Point(bodyCenter)) != Region.Location.OUTSIDE) {
  187.                 // the body is fully contained in the Field Of View
  188.                 // we use the full limb as the footprint
  189.                 final Vector3D x        = bodyCenter.orthogonal();
  190.                 final Vector3D y        = Vector3D.crossProduct(bodyCenter, x).normalize();
  191.                 final double   sinEta   = body.getEquatorialRadius() / r;
  192.                 final double   sinEta2  = sinEta * sinEta;
  193.                 final double   cosAlpha = (FastMath.cos(angularStep) + sinEta2 - 1) / sinEta2;
  194.                 final int      n        = (int) FastMath.ceil(MathUtils.TWO_PI / FastMath.acos(cosAlpha));
  195.                 final double   delta    = MathUtils.TWO_PI / n;
  196.                 final List<GeodeticPoint> loop = new ArrayList<>(n);
  197.                 for (int i = 0; i < n; ++i) {
  198.                     final SinCos sc = FastMath.sinCos(i * delta);
  199.                     final Vector3D outside = new Vector3D(r * sc.cos(), x,
  200.                                                           r * sc.sin(), y);
  201.                     loop.add(body.transform(body.pointOnLimb(position, outside), bodyFrame, null));
  202.                 }
  203.                 footprint.add(loop);
  204.             }
  205.         }

  206.         return footprint;

  207.     }

  208.     /** Enumerate for cone/polygon relative position.
  209.      * @since 10.1
  210.      */
  211.     public enum DefiningConeType {

  212.         /** Constant for cones inside polygons and touching it at polygon edges middle points. */
  213.         INSIDE_CONE_TOUCHING_POLYGON_AT_EDGES_MIDDLE() {

  214.             /** {@inheritDoc}*/
  215.             @Override
  216.             protected double verticesRadius(final double radius, final int n) {
  217.                 // convert the inside (edges middle points) radius to outside (vertices) radius
  218.                 return FastMath.atan(FastMath.tan(radius) / FastMath.cos(FastMath.PI / n));
  219.             }

  220.             /** {@inheritDoc}*/
  221.             @Override
  222.             protected Vector3D createVertex(final Vector3D center, final Vector3D meridian,
  223.                                             final double verticesRadius, final int n) {
  224.                 // convert the edge middle meridian to a vertex
  225.                 final SinCos scA = FastMath.sinCos(FastMath.PI / n);
  226.                 final SinCos scR = FastMath.sinCos(verticesRadius);
  227.                 final Vector3D z = center.normalize();
  228.                 final Vector3D y = Vector3D.crossProduct(center, meridian).normalize();
  229.                 final Vector3D x = Vector3D.crossProduct(y, z);
  230.                 return new Vector3D(scR.sin() * scA.cos(), x, scR.sin() * scA.sin(), y, scR.cos(), z);
  231.             }

  232.         },

  233.         /** Constant for cones outside polygons and touching it at polygon vertices. */
  234.         OUTSIDE_CONE_TOUCHING_POLYGON_AT_VERTICES() {

  235.             /** {@inheritDoc}*/
  236.             @Override
  237.             protected double verticesRadius(final double radius, final int n) {
  238.                 return radius;
  239.             }

  240.             /** {@inheritDoc}*/
  241.             @Override
  242.             protected Vector3D createVertex(final Vector3D center, final Vector3D meridian,
  243.                                             final double verticesRadius, final int n) {
  244.                 // convert the vertex meridian to a vertex
  245.                 final SinCos scR = FastMath.sinCos(verticesRadius);
  246.                 final Vector3D z = center.normalize();
  247.                 final Vector3D y = Vector3D.crossProduct(center, meridian).normalize();
  248.                 final Vector3D x = Vector3D.crossProduct(y, z);
  249.                 return new Vector3D(scR.sin(), x, scR.cos(), z);
  250.             }

  251.         };

  252.         /** Compute radius of cone going through vertices.
  253.          * @param radius defining cone angular radius
  254.          * @param n number of sides of the polygon
  255.          * @return radius of cone going through vertices
  256.          */
  257.         protected abstract double verticesRadius(double radius, int n);

  258.         /** Create a vertex.
  259.          * @param center center of the polygon (the center is in the inside part)
  260.          * @param meridian point defining the reference meridian for one contact
  261.          * point between defining cone and polygon (i.e. either a polygon edge
  262.          * middle point or a polygon vertex)
  263.          * @param verticesRadius defining radius of cone passing through vertices
  264.          * @param n number of sides of the polygon
  265.          * @return created vertex
  266.          */
  267.         protected abstract Vector3D createVertex(Vector3D center, Vector3D meridian,
  268.                                                  double verticesRadius, int n);

  269.     }

  270. }