Mesh.java

  1. /* Copyright 2002-2023 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.models.earth.tessellation;

  18. import java.util.ArrayList;
  19. import java.util.HashMap;
  20. import java.util.List;
  21. import java.util.Map;

  22. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  23. import org.hipparchus.geometry.euclidean.twod.Vector2D;
  24. import org.hipparchus.geometry.partitioning.Region.Location;
  25. import org.hipparchus.geometry.spherical.twod.S2Point;
  26. import org.hipparchus.geometry.spherical.twod.SphericalPolygonsSet;
  27. import org.hipparchus.util.FastMath;
  28. import org.hipparchus.util.Precision;
  29. import org.hipparchus.util.SinCos;
  30. import org.orekit.bodies.Ellipse;
  31. import org.orekit.bodies.GeodeticPoint;
  32. import org.orekit.bodies.OneAxisEllipsoid;
  33. import org.orekit.errors.OrekitException;
  34. import org.orekit.errors.OrekitMessages;

  35. /** Class creating a mesh for spherical zones tessellation.
  36.  * @author Luc Maisonobe
  37.  */
  38. class Mesh {

  39.     /** Underlying ellipsoid. */
  40.     private final OneAxisEllipsoid ellipsoid;

  41.     /** Zone of interest to tessellate. */
  42.     private final SphericalPolygonsSet zone;

  43.     /** Zone covered by the mesh. */
  44.     private SphericalPolygonsSet coverage;

  45.     /** Aiming used for orienting tiles. */
  46.     private final TileAiming aiming;

  47.     /** Distance between nodes in the along direction. */
  48.     private final double alongGap;

  49.     /** Distance between nodes in the across direction. */
  50.     private final double acrossGap;

  51.     /** Map containing nodes. */
  52.     private final Map<Long, Node> nodes;

  53.     /** Minimum along tile index. */
  54.     private int minAlongIndex;

  55.     /** Maximum along tile index. */
  56.     private int maxAlongIndex;

  57.     /** Minimum across tile index. */
  58.     private int minAcrossIndex;

  59.     /** Maximum across tile index. */
  60.     private int maxAcrossIndex;

  61.     /** Simple constructor.
  62.      * @param ellipsoid underlying ellipsoid
  63.      * @param zone zone of interest to tessellate
  64.      * @param aiming aiming used for orienting tiles
  65.      * @param alongGap distance between nodes in the along direction
  66.      * @param acrossGap distance between nodes in the across direction
  67.      * @param start location of the first node.
  68.      */
  69.     Mesh(final OneAxisEllipsoid ellipsoid, final SphericalPolygonsSet zone,
  70.          final TileAiming aiming, final double alongGap, final double acrossGap,
  71.          final S2Point start) {
  72.         this.ellipsoid      = ellipsoid;
  73.         this.zone           = zone;
  74.         this.coverage       = null;
  75.         this.aiming         = aiming;
  76.         this.alongGap       = alongGap;
  77.         this.acrossGap      = acrossGap;
  78.         this.nodes          = new HashMap<Long, Node>();
  79.         this.minAlongIndex  = 0;
  80.         this.maxAlongIndex  = 0;
  81.         this.minAcrossIndex = 0;
  82.         this.maxAcrossIndex = 0;

  83.         // safety check for singular points (typically poles)
  84.         for (final GeodeticPoint singular : aiming.getSingularPoints()) {
  85.             final S2Point s2p = new S2Point(singular.getLongitude(), 0.5 * FastMath.PI - singular.getLatitude());
  86.             if (zone.checkPoint(s2p) != Location.OUTSIDE) {
  87.                 throw new OrekitException(OrekitMessages.CANNOT_COMPUTE_AIMING_AT_SINGULAR_POINT,
  88.                                           FastMath.toDegrees(singular.getLatitude()),
  89.                                           FastMath.toDegrees(singular.getLongitude()));
  90.             }
  91.         }

  92.         // create an enabled first node at origin
  93.         final Node origin = new Node(start, 0, 0);
  94.         origin.setEnabled();

  95.         // force the first node to be considered inside
  96.         // It may appear outside if the zone is very thin and
  97.         // BSPTree.checkPoint selects a very close but wrong
  98.         // tree leaf tree for the point. Even in this case,
  99.         // we want the mesh to be properly defined and surround
  100.         // the area
  101.         origin.forceInside();

  102.         store(origin);

  103.     }

  104.     /** Get the minimum along tile index for enabled nodes.
  105.      * @return minimum along tile index for enabled nodes
  106.      */
  107.     public int getMinAlongIndex() {
  108.         return minAlongIndex;
  109.     }

  110.     /** Get the maximum along tile index for enabled nodes.
  111.      * @return maximum along tile index for enabled nodes
  112.      */
  113.     public int getMaxAlongIndex() {
  114.         return maxAlongIndex;
  115.     }

  116.     /** Get the minimum along tile index for enabled nodes for a specific across index.
  117.      * @param acrossIndex across index to use
  118.      * @return minimum along tile index for enabled nodes for a specific across index
  119.      * or {@link #getMaxAlongIndex() getMaxAlongIndex() + 1} if there
  120.      * are no nodes with the specified acrossIndex.
  121.      */
  122.     public int getMinAlongIndex(final int acrossIndex) {
  123.         for (int alongIndex = minAlongIndex; alongIndex <= maxAlongIndex; ++alongIndex) {
  124.             final Node node = getNode(alongIndex, acrossIndex);
  125.             if (node != null && node.isEnabled()) {
  126.                 return alongIndex;
  127.             }
  128.         }
  129.         return maxAlongIndex + 1;
  130.     }

  131.     /** Get the maximum along tile index for enabled nodes for a specific across index.
  132.      * @param acrossIndex across index to use
  133.      * @return maximum along tile index for enabled nodes for a specific across index
  134.      * or {@link #getMinAlongIndex() getMinAlongIndex() - 1} if there
  135.      * are no nodes with the specified acrossIndex.
  136.      */
  137.     public int getMaxAlongIndex(final int acrossIndex) {
  138.         for (int alongIndex = maxAlongIndex; alongIndex >= minAlongIndex; --alongIndex) {
  139.             final Node node = getNode(alongIndex, acrossIndex);
  140.             if (node != null && node.isEnabled()) {
  141.                 return alongIndex;
  142.             }
  143.         }
  144.         return minAlongIndex - 1;
  145.     }

  146.     /** Get the minimum across tile index.
  147.      * @return minimum across tile index
  148.      */
  149.     public int getMinAcrossIndex() {
  150.         return minAcrossIndex;
  151.     }

  152.     /** Get the maximum across tile index.
  153.      * @return maximum across tile index
  154.      */
  155.     public int getMaxAcrossIndex() {
  156.         return maxAcrossIndex;
  157.     }

  158.     /** Get the number of nodes.
  159.      * @return number of nodes
  160.      */
  161.     public int getNumberOfNodes() {
  162.         return nodes.size();
  163.     }

  164.     /** Get the distance between nodes in the along direction.
  165.      * @return distance between nodes in the along direction
  166.      */
  167.     public double getAlongGap() {
  168.         return alongGap;
  169.     }

  170.     /** Get the distance between nodes in the across direction.
  171.      * @return distance between nodes in the across direction
  172.      */
  173.     public double getAcrossGap() {
  174.         return acrossGap;
  175.     }

  176.     /** Retrieve a node from its indices.
  177.      * @param alongIndex index in the along direction
  178.      * @param acrossIndex index in the across direction
  179.      * @return node or null if no nodes are available at these indices
  180.      * @see #addNode(int, int)
  181.      */
  182.     public Node getNode(final int alongIndex, final int acrossIndex) {
  183.         return nodes.get(key(alongIndex, acrossIndex));
  184.     }

  185.     /** Add a node.
  186.      * <p>
  187.      * This method is similar to {@link #getNode(int, int) getNode} except
  188.      * it creates the node if it doesn't alreay exists. All created nodes
  189.      * have a status set to {@code disabled}.
  190.      * </p>
  191.      * @param alongIndex index in the along direction
  192.      * @param acrossIndex index in the across direction
  193.      * @return node at specified indices, guaranteed to be non-null
  194.           * @see #getNode(int, int)
  195.      */
  196.     public Node addNode(final int alongIndex, final int acrossIndex) {

  197.         // create intermediate (disabled) nodes, up to specified indices
  198.         Node node = getExistingAncestor(alongIndex, acrossIndex);
  199.         while (node.getAlongIndex() != alongIndex || node.getAcrossIndex() != acrossIndex) {
  200.             final Direction direction;
  201.             if (node.getAlongIndex() < alongIndex) {
  202.                 direction = Direction.PLUS_ALONG;
  203.             } else if (node.getAlongIndex() > alongIndex) {
  204.                 direction = Direction.MINUS_ALONG;
  205.             } else if (node.getAcrossIndex() < acrossIndex) {
  206.                 direction = Direction.PLUS_ACROSS;
  207.             } else {
  208.                 direction = Direction.MINUS_ACROSS;
  209.             }
  210.             final S2Point s2p = node.move(direction.motion(node, alongGap, acrossGap));
  211.             node = new Node(s2p, direction.neighborAlongIndex(node), direction.neighborAcrossIndex(node));
  212.             store(node);
  213.         }

  214.         return node;

  215.     }

  216.     /** Find the closest existing ancestor of a node.
  217.      * <p>
  218.      * The path from origin to any node is first in the along direction,
  219.      * and then in the across direction. Using always the same path pattern
  220.      * ensures consistent distortion of the mesh.
  221.      * </p>
  222.      * @param alongIndex index in the along direction
  223.      * @param acrossIndex index in the across direction
  224.      * @return an existing node in the path from origin to specified indices
  225.      */
  226.     private Node getExistingAncestor(final int alongIndex, final int acrossIndex) {

  227.         // start from the desired node indices
  228.         int l = alongIndex;
  229.         int c = acrossIndex;
  230.         Node node = getNode(l, c);

  231.         // rewind the path backward, up to origin,
  232.         // that is first in the across direction, then in the along direction
  233.         // the loop WILL end as there is at least one node at (0, 0)
  234.         while (node == null) {
  235.             if (c != 0) {
  236.                 c += c > 0 ? -1 : +1;
  237.             } else {
  238.                 l += l > 0 ? -1 : +1;
  239.             }
  240.             node = getNode(l, c);
  241.         }

  242.         // we have found an existing ancestor
  243.         return node;

  244.     }

  245.     /** Get the nodes that lie inside the interest zone.
  246.      * @return nodes that lie inside the interest zone
  247.      */
  248.     public List<Node> getInsideNodes() {
  249.         final List<Node> insideNodes = new ArrayList<Node>();
  250.         for (final Map.Entry<Long, Node> entry : nodes.entrySet()) {
  251.             if (entry.getValue().isInside()) {
  252.                 insideNodes.add(entry.getValue());
  253.             }
  254.         }
  255.         return insideNodes;
  256.     }

  257.     /** Find the existing node closest to a location.
  258.      * @param alongIndex index in the along direction
  259.      * @param acrossIndex index in the across direction
  260.      * @return node or null if no node is available at these indices
  261.      */
  262.     public Node getClosestExistingNode(final int alongIndex, final int acrossIndex) {

  263.         // we know at least the first node (at 0, 0) exists, we use its
  264.         // taxicab distance to the desired location as a search limit
  265.         final int maxD = FastMath.max(FastMath.abs(alongIndex), FastMath.abs(acrossIndex));

  266.         // search for an existing point in increasing taxicab distances
  267.         for (int d = 0; d < maxD; ++d) {
  268.             for (int deltaAcross = 0; deltaAcross <= d; ++deltaAcross) {
  269.                 final int deltaAlong = d - deltaAcross;
  270.                 Node node = getNode(alongIndex - deltaAlong, acrossIndex - deltaAcross);
  271.                 if (node != null) {
  272.                     return node;
  273.                 }
  274.                 if (deltaAcross != 0) {
  275.                     node = getNode(alongIndex - deltaAlong, acrossIndex + deltaAcross);
  276.                     if (node != null) {
  277.                         return node;
  278.                     }
  279.                 }
  280.                 if (deltaAlong != 0) {
  281.                     node = getNode(alongIndex + deltaAlong, acrossIndex - deltaAcross);
  282.                     if (node != null) {
  283.                         return node;
  284.                     }
  285.                     if (deltaAcross != 0) {
  286.                         node = getNode(alongIndex + deltaAlong, acrossIndex + deltaAcross);
  287.                         if (node != null) {
  288.                             return node;
  289.                         }
  290.                     }
  291.                 }
  292.             }
  293.         }

  294.         // at least the first node always exists
  295.         return getNode(0, 0);

  296.     }

  297.     /** Find the existing node closest to a location.
  298.      * @param location reference location in Cartesian coordinates
  299.      * @return node or null if no node is available at these indices
  300.      */
  301.     public Node getClosestExistingNode(final Vector3D location) {
  302.         Node selected = null;
  303.         double min = Double.POSITIVE_INFINITY;
  304.         for (final Map.Entry<Long, Node> entry : nodes.entrySet()) {
  305.             final double distance = Vector3D.distance(location, entry.getValue().getV());
  306.             if (distance < min) {
  307.                 selected = entry.getValue();
  308.                 min      = distance;
  309.             }
  310.         }
  311.         return selected;
  312.     }

  313.     /** Get the oriented list of <em>enabled</em> nodes at mesh boundary, in taxicab geometry.
  314.      * @param simplified if true, don't include intermediate points along almost straight lines
  315.      * @return list of nodes
  316.      */
  317.     public List<Node> getTaxicabBoundary(final boolean simplified) {

  318.         final List<Node> boundary = new ArrayList<Node>();
  319.         if (nodes.size() < 2) {
  320.             boundary.add(getNode(0, 0));
  321.         } else {

  322.             // search for the lower left corner
  323.             Node lowerLeft = null;
  324.             for (int i = minAlongIndex; lowerLeft == null && i <= maxAlongIndex; ++i) {
  325.                 for (int j = minAcrossIndex; lowerLeft == null && j <= maxAcrossIndex; ++j) {
  326.                     lowerLeft = getNode(i, j);
  327.                     if (lowerLeft != null && !lowerLeft.isEnabled()) {
  328.                         lowerLeft = null;
  329.                     }
  330.                 }
  331.             }

  332.             // loop counterclockwise around the mesh
  333.             Direction direction = Direction.MINUS_ACROSS;
  334.             Node node = lowerLeft;
  335.             do {
  336.                 boundary.add(node);
  337.                 Node neighbor = null;
  338.                 do {
  339.                     direction = direction.next();
  340.                     neighbor = getNode(direction.neighborAlongIndex(node),
  341.                                        direction.neighborAcrossIndex(node));
  342.                 } while (neighbor == null || !neighbor.isEnabled());
  343.                 direction = direction.next().next();
  344.                 node = neighbor;
  345.             } while (node != lowerLeft);
  346.         }

  347.         // filter out infinitely thin parts corresponding to spikes
  348.         // joining outliers points to the main mesh
  349.         boolean changed = true;
  350.         while (changed && boundary.size() > 1) {
  351.             changed = false;
  352.             final int n = boundary.size();
  353.             for (int i = 0; i < n; ++i) {
  354.                 final int previousIndex = (i + n - 1) % n;
  355.                 final int nextIndex     = (i + 1)     % n;
  356.                 if (boundary.get(previousIndex) == boundary.get(nextIndex)) {
  357.                     // the current point is an infinitely thin spike, remove it
  358.                     boundary.remove(FastMath.max(i, nextIndex));
  359.                     boundary.remove(FastMath.min(i, nextIndex));
  360.                     changed = true;
  361.                     break;
  362.                 }
  363.             }
  364.         }

  365.         if (simplified) {
  366.             for (int i = 0; i < boundary.size(); ++i) {
  367.                 final int  n        = boundary.size();
  368.                 final Node previous = boundary.get((i + n - 1) % n);
  369.                 final int  pl       = previous.getAlongIndex();
  370.                 final int  pc       = previous.getAcrossIndex();
  371.                 final Node current  = boundary.get(i);
  372.                 final int  cl       = current.getAlongIndex();
  373.                 final int  cc       = current.getAcrossIndex();
  374.                 final Node next     = boundary.get((i + 1)     % n);
  375.                 final int  nl       = next.getAlongIndex();
  376.                 final int  nc       = next.getAcrossIndex();
  377.                 if (pl == cl && cl == nl || pc == cc && cc == nc) {
  378.                     // the current point is a spurious intermediate in a straight line, remove it
  379.                     boundary.remove(i--);
  380.                 }
  381.             }
  382.         }

  383.         return boundary;

  384.     }

  385.     /** Get the zone covered by the mesh.
  386.      * @return mesh coverage
  387.      */
  388.     public SphericalPolygonsSet getCoverage() {

  389.         if (coverage == null) {

  390.             // lazy build of mesh coverage
  391.             final List<Mesh.Node> boundary = getTaxicabBoundary(true);
  392.             final S2Point[] vertices = new S2Point[boundary.size()];
  393.             for (int i = 0; i < vertices.length; ++i) {
  394.                 vertices[i] = boundary.get(i).getS2P();
  395.             }
  396.             coverage = new SphericalPolygonsSet(zone.getTolerance(), vertices);
  397.         }

  398.         // as caller may modify the BSP tree, we must provide a copy of our safe instance
  399.         return (SphericalPolygonsSet) coverage.copySelf();

  400.     }

  401.     /** Store a node.
  402.      * @param node to add
  403.      */
  404.     private void store(final Node node) {

  405.         // the new node invalidates current estimation of the coverage
  406.         coverage = null;

  407.         nodes.put(key(node.alongIndex, node.acrossIndex), node);

  408.     }

  409.     /** Convert along and across indices to map key.
  410.      * @param alongIndex index in the along direction
  411.      * @param acrossIndex index in the across direction
  412.      * @return key map key
  413.      */
  414.     private long key(final int alongIndex, final int acrossIndex) {
  415.         return ((long) alongIndex) << 32 | (((long) acrossIndex) & 0xFFFFl);
  416.     }

  417.     /** Container for mesh nodes. */
  418.     public class Node {

  419.         /** Node position in spherical coordinates. */
  420.         private final S2Point s2p;

  421.         /** Node position in Cartesian coordinates. */
  422.         private final Vector3D v;

  423.         /** Normalized along tile direction. */
  424.         private final Vector3D along;

  425.         /** Normalized across tile direction. */
  426.         private final Vector3D across;

  427.         /** Indicator for node location with respect to interest zone. */
  428.         private boolean insideZone;

  429.         /** Index in the along direction. */
  430.         private final int alongIndex;

  431.         /** Index in the across direction. */
  432.         private final int acrossIndex;

  433.         /** Indicator for construction nodes used only as intermediate points (disabled) vs. real nodes (enabled). */
  434.         private boolean enabled;

  435.         /** Create a node.
  436.          * @param s2p position in spherical coordinates (my be null)
  437.          * @param alongIndex index in the along direction
  438.          * @param acrossIndex index in the across direction
  439.          */
  440.         private Node(final S2Point s2p, final int alongIndex, final int acrossIndex) {
  441.             final GeodeticPoint gp = new GeodeticPoint(0.5 * FastMath.PI - s2p.getPhi(), s2p.getTheta(), 0.0);
  442.             this.v           = ellipsoid.transform(gp);
  443.             this.s2p         = s2p;
  444.             this.along       = aiming.alongTileDirection(v, gp);
  445.             this.across      = Vector3D.crossProduct(v, along).normalize();
  446.             this.insideZone  = zone.checkPoint(s2p) != Location.OUTSIDE;
  447.             this.alongIndex  = alongIndex;
  448.             this.acrossIndex = acrossIndex;
  449.             this.enabled     = false;
  450.         }

  451.         /** Set the enabled property.
  452.          */
  453.         public void setEnabled() {

  454.             // store status
  455.             this.enabled = true;

  456.             // update min/max indices
  457.             minAlongIndex  = FastMath.min(minAlongIndex,  alongIndex);
  458.             maxAlongIndex  = FastMath.max(maxAlongIndex,  alongIndex);
  459.             minAcrossIndex = FastMath.min(minAcrossIndex, acrossIndex);
  460.             maxAcrossIndex = FastMath.max(maxAcrossIndex, acrossIndex);

  461.         }

  462.         /** Check if a node is enabled.
  463.          * @return true if the node is enabled
  464.          */
  465.         public boolean isEnabled() {
  466.             return enabled;
  467.         }

  468.         /** Get the node position in spherical coordinates.
  469.          * @return vode position in spherical coordinates
  470.          */
  471.         public S2Point getS2P() {
  472.             return s2p;
  473.         }

  474.         /** Get the node position in Cartesian coordinates.
  475.          * @return vode position in Cartesian coordinates
  476.          */
  477.         public Vector3D getV() {
  478.             return v;
  479.         }

  480.         /** Get the normalized along tile direction.
  481.          * @return normalized along tile direction
  482.          */
  483.         public Vector3D getAlong() {
  484.             return along;
  485.         }

  486.         /** Get the normalized across tile direction.
  487.          * @return normalized across tile direction
  488.          */
  489.         public Vector3D getAcross() {
  490.             return across;
  491.         }

  492.         /** Force the node location to be considered inside interest zone.
  493.          */
  494.         private void forceInside() {
  495.             insideZone = true;
  496.         }

  497.         /** Check if the node location is inside interest zone.
  498.          * @return true if the node location is inside interest zone
  499.          */
  500.         public boolean isInside() {
  501.             return insideZone;
  502.         }

  503.         /** Get the index in the along direction.
  504.          * @return index in the along direction
  505.          */
  506.         public int getAlongIndex() {
  507.             return alongIndex;
  508.         }

  509.         /** Get the index in the across direction.
  510.          * @return index in the across direction
  511.          */
  512.         public int getAcrossIndex() {
  513.             return acrossIndex;
  514.         }

  515.         /** Move to a nearby point along surface.
  516.          * <p>
  517.          * The motion will be approximated, assuming the body surface has constant
  518.          * curvature along the motion direction. The approximation will be accurate
  519.          * for short distances, and error will increase as distance increases.
  520.          * </p>
  521.          * @param motion straight motion, which must be curved back to surface
  522.          * @return arrival point, approximately at specified distance from node
  523.          */
  524.         public S2Point move(final Vector3D motion) {

  525.             // safety check for too small motion
  526.             if (motion.getNorm() < Precision.EPSILON * v.getNorm()) {
  527.                 return s2p;
  528.             }

  529.             // find elliptic plane section
  530.             final Vector3D normal      = Vector3D.crossProduct(v, motion);
  531.             final Ellipse planeSection = ellipsoid.getPlaneSection(v, normal);

  532.             // find the center of curvature (point on the evolute) below start point
  533.             final Vector2D omega2D = planeSection.getCenterOfCurvature(planeSection.toPlane(v));
  534.             final Vector3D omega3D = planeSection.toSpace(omega2D);

  535.             // compute approximated arrival point, assuming constant radius of curvature
  536.             final Vector3D delta = v.subtract(omega3D);
  537.             final double   theta = motion.getNorm() / delta.getNorm();
  538.             final SinCos   sc    = FastMath.sinCos(theta);
  539.             final Vector3D approximated = new Vector3D(1, omega3D,
  540.                                                        sc.cos(), delta,
  541.                                                        sc.sin() / theta, motion);

  542.             // convert to spherical coordinates
  543.             final GeodeticPoint approximatedGP = ellipsoid.transform(approximated, ellipsoid.getBodyFrame(), null);
  544.             return new S2Point(approximatedGP.getLongitude(), 0.5 * FastMath.PI - approximatedGP.getLatitude());

  545.         }

  546.     }

  547. }