FootprintOverlapDetector.java

  1. /* Copyright 2002-2024 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. import java.util.ArrayList;
  19. import java.util.List;

  20. import org.hipparchus.geometry.enclosing.EnclosingBall;
  21. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  22. import org.hipparchus.geometry.spherical.twod.Edge;
  23. import org.hipparchus.geometry.spherical.twod.S2Point;
  24. import org.hipparchus.geometry.spherical.twod.Sphere2D;
  25. import org.hipparchus.geometry.spherical.twod.SphericalPolygonsSet;
  26. import org.hipparchus.geometry.spherical.twod.Vertex;
  27. import org.hipparchus.ode.events.Action;
  28. import org.hipparchus.util.FastMath;
  29. import org.hipparchus.util.SinCos;
  30. import org.orekit.bodies.BodyShape;
  31. import org.orekit.bodies.GeodeticPoint;
  32. import org.orekit.bodies.OneAxisEllipsoid;
  33. import org.orekit.frames.StaticTransform;
  34. import org.orekit.geometry.fov.FieldOfView;
  35. import org.orekit.models.earth.tessellation.DivertedSingularityAiming;
  36. import org.orekit.models.earth.tessellation.EllipsoidTessellator;
  37. import org.orekit.propagation.SpacecraftState;
  38. import org.orekit.propagation.events.handlers.EventHandler;
  39. import org.orekit.propagation.events.handlers.StopOnIncreasing;

  40. /** Detector triggered by geographical region entering/leaving a spacecraft sensor
  41.  * {@link FieldOfView Field Of View}.
  42.  * <p>
  43.  * This detector is a mix between to {@link FieldOfViewDetector} and {@link
  44.  * GeographicZoneDetector}. Similar to the first detector above, it triggers events
  45.  * related to entry/exit of targets in a Field Of View, taking attitude into account.
  46.  * Similar to the second detector above, its target is an entire geographic region
  47.  * (which can even be split in several non-connected patches and can have holes).
  48.  * </p>
  49.  * <p>
  50.  * This detector is typically used for ground observation missions with agile
  51.  * satellites than can look away from nadir.
  52.  * </p>
  53.  * <p>The default implementation behavior is to {@link Action#CONTINUE continue}
  54.  * propagation at FOV entry and to {@link Action#STOP stop} propagation
  55.  * at FOV exit. This can be changed by calling
  56.  * {@link #withHandler(EventHandler)} after construction.</p>
  57.  * @see org.orekit.propagation.Propagator#addEventDetector(EventDetector)
  58.  * @see FieldOfViewDetector
  59.  * @see GeographicZoneDetector
  60.  * @author Luc Maisonobe
  61.  * @since 7.1
  62.  */
  63. public class FootprintOverlapDetector extends AbstractDetector<FootprintOverlapDetector> {

  64.     /** Field of view. */
  65.     private final FieldOfView fov;

  66.     /** Body on which the geographic zone is defined. */
  67.     private final OneAxisEllipsoid body;

  68.     /** Geographic zone to consider. */
  69.     private final SphericalPolygonsSet zone;

  70.     /** Linear step used for sampling the geographic zone. */
  71.     private final double samplingStep;

  72.     /** Sampling of the geographic zone. */
  73.     private final List<SamplingPoint> sampledZone;

  74.     /** Center of the spherical cap surrounding the zone. */
  75.     private final Vector3D capCenter;

  76.     /** Cosine of the radius of the spherical cap surrounding the zone. */
  77.     private final double capCos;

  78.     /** Sine of the radius of the spherical cap surrounding the zone. */
  79.     private final double capSin;

  80.     /** Build a new instance.
  81.      * <p>The maximal interval between distance to FOV boundary checks should
  82.      * be smaller than the half duration of the minimal pass to handle,
  83.      * otherwise some short passes could be missed.</p>
  84.      * @param fov sensor field of view
  85.      * @param body body on which the geographic zone is defined
  86.      * @param zone geographic zone to consider
  87.      * @param samplingStep linear step used for sampling the geographic zone (in meters)
  88.      * @since 10.1
  89.      */
  90.     public FootprintOverlapDetector(final FieldOfView fov,
  91.                                     final OneAxisEllipsoid body,
  92.                                     final SphericalPolygonsSet zone,
  93.                                     final double samplingStep) {
  94.         this(s -> DEFAULT_MAXCHECK, DEFAULT_THRESHOLD, DEFAULT_MAX_ITER,
  95.              new StopOnIncreasing(),
  96.              fov, body, zone, samplingStep, sample(body, zone, samplingStep));
  97.     }

  98.     /** Protected constructor with full parameters.
  99.      * <p>
  100.      * This constructor is not public as users are expected to use the builder
  101.      * API with the various {@code withXxx()} methods to set up the instance
  102.      * in a readable manner without using a huge amount of parameters.
  103.      * </p>
  104.      * @param maxCheck maximum checking interval
  105.      * @param threshold convergence threshold (s)
  106.      * @param maxIter maximum number of iterations in the event time search
  107.      * @param handler event handler to call at event occurrences
  108.      * @param body body on which the geographic zone is defined
  109.      * @param zone geographic zone to consider
  110.      * @param fov sensor field of view
  111.      * @param sampledZone sampling of the geographic zone
  112.      * @param samplingStep linear step used for sampling the geographic zone (in meters)
  113.      */
  114.     protected FootprintOverlapDetector(final AdaptableInterval maxCheck, final double threshold,
  115.                                        final int maxIter, final EventHandler handler,
  116.                                        final FieldOfView fov,
  117.                                        final OneAxisEllipsoid body,
  118.                                        final SphericalPolygonsSet zone,
  119.                                        final double samplingStep,
  120.                                        final List<SamplingPoint> sampledZone) {

  121.         super(maxCheck, threshold, maxIter, handler);
  122.         this.fov          = fov;
  123.         this.body         = body;
  124.         this.samplingStep = samplingStep;
  125.         this.zone         = zone;
  126.         this.sampledZone  = sampledZone;

  127.         final EnclosingBall<Sphere2D, S2Point> cap = zone.getEnclosingCap();
  128.         final SinCos sc = FastMath.sinCos(cap.getRadius());
  129.         this.capCenter    = cap.getCenter().getVector();
  130.         this.capCos       = sc.cos();
  131.         this.capSin       = sc.sin();

  132.     }

  133.     /** Sample the region.
  134.      * @param body body on which the geographic zone is defined
  135.      * @param zone geographic zone to consider
  136.      * @param samplingStep  linear step used for sampling the geographic zone (in meters)
  137.      * @return sampling points
  138.      */
  139.     private static List<SamplingPoint> sample(final OneAxisEllipsoid body,
  140.                                               final SphericalPolygonsSet zone,
  141.                                               final double samplingStep) {

  142.         final List<SamplingPoint> sampledZone = new ArrayList<SamplingPoint>();

  143.         // sample the zone boundary
  144.         final List<Vertex> boundary = zone.getBoundaryLoops();
  145.         for (final Vertex loopStart : boundary) {
  146.             int count = 0;
  147.             for (Vertex v = loopStart; count == 0 || v != loopStart; v = v.getOutgoing().getEnd()) {
  148.                 ++count;
  149.                 final Edge edge = v.getOutgoing();
  150.                 final int n = (int) FastMath.ceil(edge.getLength() * body.getEquatorialRadius() / samplingStep);
  151.                 for (int i = 0; i < n; ++i) {
  152.                     final S2Point intermediate = new S2Point(edge.getPointAt(i * edge.getLength() / n));
  153.                     final GeodeticPoint gp = new GeodeticPoint(0.5 * FastMath.PI - intermediate.getPhi(),
  154.                                                                intermediate.getTheta(), 0.0);
  155.                     sampledZone.add(new SamplingPoint(body.transform(gp), gp.getZenith()));
  156.                 }
  157.             }
  158.         }

  159.         // sample the zone interior
  160.         final EllipsoidTessellator tessellator =
  161.                         new EllipsoidTessellator(body, new DivertedSingularityAiming(zone), 1);
  162.         final List<List<GeodeticPoint>> gpSample = tessellator.sample(zone, samplingStep, samplingStep);
  163.         for (final List<GeodeticPoint> list : gpSample) {
  164.             for (final GeodeticPoint gp : list) {
  165.                 sampledZone.add(new SamplingPoint(body.transform(gp), gp.getZenith()));
  166.             }
  167.         }

  168.         return sampledZone;

  169.     }

  170.     /** {@inheritDoc} */
  171.     @Override
  172.     protected FootprintOverlapDetector create(final AdaptableInterval newMaxCheck, final double newThreshold,
  173.                                               final int newMaxIter,
  174.                                               final EventHandler newHandler) {
  175.         return new FootprintOverlapDetector(newMaxCheck, newThreshold, newMaxIter, newHandler,
  176.                                             fov, body, zone, samplingStep, sampledZone);
  177.     }

  178.     /** Get the geographic zone triggering the events.
  179.      * <p>
  180.      * The zone is mapped on the unit sphere
  181.      * </p>
  182.      * @return geographic zone triggering the events
  183.      */
  184.     public SphericalPolygonsSet getZone() {
  185.         return zone;
  186.     }

  187.     /** Get the Field Of View.
  188.      * @return Field Of View
  189.      * @since 10.1
  190.      */
  191.     public FieldOfView getFOV() {
  192.         return fov;
  193.     }

  194.     /** Get the body on which the geographic zone is defined.
  195.      * @return body on which the geographic zone is defined
  196.      */
  197.     public BodyShape getBody() {
  198.         return body;
  199.     }

  200.     /** {@inheritDoc}
  201.      * <p>
  202.      * The g function value is the minimum offset among the region points
  203.      * with respect to the Field Of View boundary. It is positive if all region
  204.      * points are outside of the Field Of View, and negative if at least some
  205.      * of the region points are inside of the Field Of View. The minimum is
  206.      * computed by sampling the region, considering only the points for which
  207.      * the spacecraft is above the horizon. The accuracy of the detection
  208.      * depends on the linear sampling step set at detector construction. If
  209.      * the spacecraft is below horizon for all region points, an arbitrary
  210.      * positive value is returned.
  211.      * </p>
  212.      * <p>
  213.      * As per the previous definition, when the region enters the Field Of
  214.      * View, a decreasing event is generated, and when the region leaves
  215.      * the Field Of View, an increasing event is generated.
  216.      * </p>
  217.      */
  218.     public double g(final SpacecraftState s) {

  219.         // initial arbitrary positive value
  220.         double value = FastMath.PI;

  221.         // get spacecraft position in body frame
  222.         final Vector3D      scBody      = s.getPosition(body.getBodyFrame());

  223.         // map the point to a sphere
  224.         final GeodeticPoint gp          = body.transform(scBody, body.getBodyFrame(), s.getDate());
  225.         final S2Point       s2p         = new S2Point(gp.getLongitude(), 0.5 * FastMath.PI - gp.getLatitude());

  226.         // for faster computation, we start using only the surrounding cap, to filter out
  227.         // far away points (which correspond to most of the points if the zone is small)
  228.         final Vector3D p   = s2p.getVector();
  229.         final double   dot = Vector3D.dotProduct(p, capCenter);
  230.         if (dot < capCos) {
  231.             // the spacecraft is outside of the cap, look for the closest cap point
  232.             final Vector3D t     = p.subtract(dot, capCenter).normalize();
  233.             final Vector3D close = new Vector3D(capCos, capCenter, capSin, t);
  234.             if (Vector3D.dotProduct(p, close) < -0.01) {
  235.                 // the spacecraft is not visible from the cap edge,
  236.                 // even taking some margin into account for sphere/ellipsoid different shapes
  237.                 // this induces no points in the zone can see the spacecraft,
  238.                 // we can return the arbitrary initial positive value without performing further computation
  239.                 return value;
  240.             }
  241.         }

  242.         // the spacecraft may be visible from some points in the zone, check them all
  243.         final StaticTransform bodyToSc = StaticTransform.compose(
  244.                 s.getDate(),
  245.                 body.getBodyFrame().getStaticTransformTo(s.getFrame(), s.getDate()),
  246.                 s.toTransform());
  247.         for (final SamplingPoint point : sampledZone) {
  248.             final Vector3D lineOfSightBody = point.getPosition().subtract(scBody);
  249.             if (Vector3D.dotProduct(lineOfSightBody, point.getZenith()) <= 0) {
  250.                 // spacecraft is above this sample point local horizon
  251.                 // get line of sight in spacecraft frame
  252.                 final double offset = fov.offsetFromBoundary(bodyToSc.transformVector(lineOfSightBody),
  253.                                                              0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV);
  254.                 value = FastMath.min(value, offset);
  255.             }
  256.         }

  257.         return value;

  258.     }

  259.     /** Container for sampling points. */
  260.     private static class SamplingPoint {

  261.         /** Position of the point. */
  262.         private final Vector3D position;

  263.         /** Zenith vector of the point. */
  264.         private final Vector3D zenith;

  265.         /** Simple constructor.
  266.          * @param position position of the point
  267.          * @param zenith zenith vector of the point
  268.          */
  269.         SamplingPoint(final Vector3D position, final Vector3D zenith) {
  270.             this.position = position;
  271.             this.zenith   = zenith;
  272.         }

  273.         /** Get the point position.
  274.          * @return point position
  275.          */
  276.         public Vector3D getPosition() {
  277.             return position;
  278.         }

  279.         /** Get the point zenith vector.
  280.          * @return point zenith vector
  281.          */
  282.         public Vector3D getZenith() {
  283.             return zenith;
  284.         }

  285.     }

  286. }