Mesh.java

  1. /* Copyright 2002-2018 CS Systèmes d'Information
  2.  * Licensed to CS Systèmes d'Information (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.orekit.bodies.Ellipse;
  30. import org.orekit.bodies.GeodeticPoint;
  31. import org.orekit.bodies.OneAxisEllipsoid;
  32. import org.orekit.errors.OrekitException;

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

  37.     /** Underlying ellipsoid. */
  38.     private final OneAxisEllipsoid ellipsoid;

  39.     /** Zone of interest to tessellate. */
  40.     private final SphericalPolygonsSet zone;

  41.     /** Zone covered by the mesh. */
  42.     private SphericalPolygonsSet coverage;

  43.     /** Aiming used for orienting tiles. */
  44.     private final TileAiming aiming;

  45.     /** Distance between nodes in the along direction. */
  46.     private final double alongGap;

  47.     /** Distance between nodes in the across direction. */
  48.     private final double acrossGap;

  49.     /** Map containing nodes. */
  50.     private final Map<Long, Node> nodes;

  51.     /** Minimum along tile index. */
  52.     private int minAlongIndex;

  53.     /** Maximum along tile index. */
  54.     private int maxAlongIndex;

  55.     /** Minimum across tile index. */
  56.     private int minAcrossIndex;

  57.     /** Maximum across tile index. */
  58.     private int maxAcrossIndex;

  59.     /** Simple constructor.
  60.      * @param ellipsoid underlying ellipsoid
  61.      * @param zone zone of interest to tessellate
  62.      * @param aiming aiming used for orienting tiles
  63.      * @param alongGap distance between nodes in the along direction
  64.      * @param acrossGap distance between nodes in the across direction
  65.      * @param start location of the first node.
  66.      * @exception OrekitException if along direction of first tile cannot be computed
  67.      */
  68.     Mesh(final OneAxisEllipsoid ellipsoid, final SphericalPolygonsSet zone,
  69.                 final TileAiming aiming, final double alongGap, final double acrossGap,
  70.                 final S2Point start)
  71.         throws OrekitException {
  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.         // create an enabled first node at origin
  84.         final Node origin = new Node(start, 0, 0);
  85.         origin.setEnabled();

  86.         // force the first node to be considered inside
  87.         // It may appear outside if the zone is very thin and
  88.         // BSPTree.checkPoint selects a very close but wrong
  89.         // tree leaf tree for the point. Even in this case,
  90.         // we want the mesh to be properly defined and surround
  91.         // the area
  92.         origin.forceInside();

  93.         store(origin);

  94.     }

  95.     /** Get the minimum along tile index for enabled nodes.
  96.      * @return minimum along tile index for enabled nodes
  97.      */
  98.     public int getMinAlongIndex() {
  99.         return minAlongIndex;
  100.     }

  101.     /** Get the maximum along tile index for enabled nodes.
  102.      * @return maximum along tile index for enabled nodes
  103.      */
  104.     public int getMaxAlongIndex() {
  105.         return maxAlongIndex;
  106.     }

  107.     /** Get the minimum along tile index for enabled nodes for a specific across index.
  108.      * @param acrossIndex across index to use
  109.      * @return minimum along tile index for enabled nodes for a specific across index
  110.      * or {@link #getMaxAlongIndex() getMaxAlongIndex() + 1} if there
  111.      * are no nodes with the specified acrossIndex.
  112.      */
  113.     public int getMinAlongIndex(final int acrossIndex) {
  114.         for (int alongIndex = minAlongIndex; alongIndex <= maxAlongIndex; ++alongIndex) {
  115.             final Node node = getNode(alongIndex, acrossIndex);
  116.             if (node != null && node.isEnabled()) {
  117.                 return alongIndex;
  118.             }
  119.         }
  120.         return maxAlongIndex + 1;
  121.     }

  122.     /** Get the maximum along tile index for enabled nodes for a specific across index.
  123.      * @param acrossIndex across index to use
  124.      * @return maximum along tile index for enabled nodes for a specific across index
  125.      * or {@link #getMinAlongIndex() getMinAlongIndex() - 1} if there
  126.      * are no nodes with the specified acrossIndex.
  127.      */
  128.     public int getMaxAlongIndex(final int acrossIndex) {
  129.         for (int alongIndex = maxAlongIndex; alongIndex >= minAlongIndex; --alongIndex) {
  130.             final Node node = getNode(alongIndex, acrossIndex);
  131.             if (node != null && node.isEnabled()) {
  132.                 return alongIndex;
  133.             }
  134.         }
  135.         return minAlongIndex - 1;
  136.     }

  137.     /** Get the minimum across tile index.
  138.      * @return minimum across tile index
  139.      */
  140.     public int getMinAcrossIndex() {
  141.         return minAcrossIndex;
  142.     }

  143.     /** Get the maximum across tile index.
  144.      * @return maximum across tile index
  145.      */
  146.     public int getMaxAcrossIndex() {
  147.         return maxAcrossIndex;
  148.     }

  149.     /** Get the number of nodes.
  150.      * @return number of nodes
  151.      */
  152.     public int getNumberOfNodes() {
  153.         return nodes.size();
  154.     }

  155.     /** Get the distance between nodes in the along direction.
  156.      * @return distance between nodes in the along direction
  157.      */
  158.     public double getAlongGap() {
  159.         return alongGap;
  160.     }

  161.     /** Get the distance between nodes in the across direction.
  162.      * @return distance between nodes in the across direction
  163.      */
  164.     public double getAcrossGap() {
  165.         return acrossGap;
  166.     }

  167.     /** Retrieve a node from its indices.
  168.      * @param alongIndex index in the along direction
  169.      * @param acrossIndex index in the across direction
  170.      * @return node or null if no nodes are available at these indices
  171.      * @see #addNode(int, int)
  172.      */
  173.     public Node getNode(final int alongIndex, final int acrossIndex) {
  174.         return nodes.get(key(alongIndex, acrossIndex));
  175.     }

  176.     /** Add a node.
  177.      * <p>
  178.      * This method is similar to {@link #getNode(int, int) getNode} except
  179.      * it creates the node if it doesn't alreay exists. All created nodes
  180.      * have a status set to {@code disabled}.
  181.      * </p>
  182.      * @param alongIndex index in the along direction
  183.      * @param acrossIndex index in the across direction
  184.      * @return node at specified indices, guaranteed to be non-null
  185.      * @exception OrekitException if tile direction cannot be computed
  186.      * @see #getNode(int, int)
  187.      */
  188.     public Node addNode(final int alongIndex, final int acrossIndex)
  189.         throws OrekitException {

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

  207.         return node;

  208.     }

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

  220.         // start from the desired node indices
  221.         int l = alongIndex;
  222.         int c = acrossIndex;
  223.         Node node = getNode(l, c);

  224.         // rewind the path backward, up to origin,
  225.         // that is first in the across direction, then in the along direction
  226.         // the loop WILL end as there is at least one node at (0, 0)
  227.         while (node == null) {
  228.             if (c != 0) {
  229.                 c += c > 0 ? -1 : +1;
  230.             } else {
  231.                 l += l > 0 ? -1 : +1;
  232.             }
  233.             node = getNode(l, c);
  234.         }

  235.         // we have found an existing ancestor
  236.         return node;

  237.     }

  238.     /** Get the nodes that lie inside the interest zone.
  239.      * @return nodes that lie inside the interest zone
  240.      */
  241.     public List<Node> getInsideNodes() {
  242.         final List<Node> insideNodes = new ArrayList<Node>();
  243.         for (final Map.Entry<Long, Node> entry : nodes.entrySet()) {
  244.             if (entry.getValue().isInside()) {
  245.                 insideNodes.add(entry.getValue());
  246.             }
  247.         }
  248.         return insideNodes;
  249.     }

  250.     /** Find the existing node closest to a location.
  251.      * @param alongIndex index in the along direction
  252.      * @param acrossIndex index in the across direction
  253.      * @return node or null if no node is available at these indices
  254.      */
  255.     public Node getClosestExistingNode(final int alongIndex, final int acrossIndex) {

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

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

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

  289.     }

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

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

  311.         final List<Node> boundary = new ArrayList<Node>();
  312.         if (nodes.size() < 2) {
  313.             boundary.add(getNode(0, 0));
  314.         } else {

  315.             // search for the lower left corner
  316.             Node lowerLeft = null;
  317.             for (int i = minAlongIndex; lowerLeft == null && i <= maxAlongIndex; ++i) {
  318.                 for (int j = minAcrossIndex; lowerLeft == null && j <= maxAcrossIndex; ++j) {
  319.                     lowerLeft = getNode(i, j);
  320.                     if (lowerLeft != null && !lowerLeft.isEnabled()) {
  321.                         lowerLeft = null;
  322.                     }
  323.                 }
  324.             }

  325.             // loop counterclockwise around the mesh
  326.             Direction direction = Direction.MINUS_ACROSS;
  327.             Node node = lowerLeft;
  328.             do {
  329.                 boundary.add(node);
  330.                 Node neighbor = null;
  331.                 do {
  332.                     direction = direction.next();
  333.                     neighbor = getNode(direction.neighborAlongIndex(node),
  334.                                        direction.neighborAcrossIndex(node));
  335.                 } while (neighbor == null || !neighbor.isEnabled());
  336.                 direction = direction.next().next();
  337.                 node = neighbor;
  338.             } while (node != lowerLeft);
  339.         }

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

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

  376.         return boundary;

  377.     }

  378.     /** Get the zone covered by the mesh.
  379.      * @return mesh coverage
  380.      */
  381.     public SphericalPolygonsSet getCoverage() {

  382.         if (coverage == null) {

  383.             // lazy build of mesh coverage
  384.             final List<Mesh.Node> boundary = getTaxicabBoundary(true);
  385.             final S2Point[] vertices = new S2Point[boundary.size()];
  386.             for (int i = 0; i < vertices.length; ++i) {
  387.                 vertices[i] = boundary.get(i).getS2P();
  388.             }
  389.             coverage = new SphericalPolygonsSet(zone.getTolerance(), vertices);
  390.         }

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

  393.     }

  394.     /** Store a node.
  395.      * @param node to add
  396.      */
  397.     private void store(final Node node) {

  398.         // the new node invalidates current estimation of the coverage
  399.         coverage = null;

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

  401.     }

  402.     /** Convert along and across indices to map key.
  403.      * @param alongIndex index in the along direction
  404.      * @param acrossIndex index in the across direction
  405.      * @return key map key
  406.      */
  407.     private long key(final int alongIndex, final int acrossIndex) {
  408.         return ((long) alongIndex) << 32 | (((long) acrossIndex) & 0xFFFFl);
  409.     }

  410.     /** Container for mesh nodes. */
  411.     public class Node {

  412.         /** Node position in spherical coordinates. */
  413.         private final S2Point s2p;

  414.         /** Node position in Cartesian coordinates. */
  415.         private final Vector3D v;

  416.         /** Normalized along tile direction. */
  417.         private final Vector3D along;

  418.         /** Normalized across tile direction. */
  419.         private final Vector3D across;

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

  422.         /** Index in the along direction. */
  423.         private final int alongIndex;

  424.         /** Index in the across direction. */
  425.         private final int acrossIndex;

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

  428.         /** Create a node.
  429.          * @param s2p position in spherical coordinates (my be null)
  430.          * @param alongIndex index in the along direction
  431.          * @param acrossIndex index in the across direction
  432.          * @exception OrekitException if tile direction cannot be computed
  433.          */
  434.         private Node(final S2Point s2p, final int alongIndex, final int acrossIndex)
  435.             throws OrekitException {
  436.             final GeodeticPoint gp = new GeodeticPoint(0.5 * FastMath.PI - s2p.getPhi(), s2p.getTheta(), 0.0);
  437.             this.v           = ellipsoid.transform(gp);
  438.             this.s2p         = s2p;
  439.             this.along       = aiming.alongTileDirection(v, gp);
  440.             this.across      = Vector3D.crossProduct(v, along).normalize();
  441.             this.insideZone  = zone.checkPoint(s2p) != Location.OUTSIDE;
  442.             this.alongIndex  = alongIndex;
  443.             this.acrossIndex = acrossIndex;
  444.             this.enabled     = false;
  445.         }

  446.         /** Set the enabled property.
  447.          */
  448.         public void setEnabled() {

  449.             // store status
  450.             this.enabled = true;

  451.             // update min/max indices
  452.             minAlongIndex  = FastMath.min(minAlongIndex,  alongIndex);
  453.             maxAlongIndex  = FastMath.max(maxAlongIndex,  alongIndex);
  454.             minAcrossIndex = FastMath.min(minAcrossIndex, acrossIndex);
  455.             maxAcrossIndex = FastMath.max(maxAcrossIndex, acrossIndex);

  456.         }

  457.         /** Check if a node is enabled.
  458.          * @return true if the node is enabled
  459.          */
  460.         public boolean isEnabled() {
  461.             return enabled;
  462.         }

  463.         /** Get the node position in spherical coordinates.
  464.          * @return vode position in spherical coordinates
  465.          */
  466.         public S2Point getS2P() {
  467.             return s2p;
  468.         }

  469.         /** Get the node position in Cartesian coordinates.
  470.          * @return vode position in Cartesian coordinates
  471.          */
  472.         public Vector3D getV() {
  473.             return v;
  474.         }

  475.         /** Get the normalized along tile direction.
  476.          * @return normalized along tile direction
  477.          */
  478.         public Vector3D getAlong() {
  479.             return along;
  480.         }

  481.         /** Get the normalized across tile direction.
  482.          * @return normalized across tile direction
  483.          */
  484.         public Vector3D getAcross() {
  485.             return across;
  486.         }

  487.         /** Force the node location to be considered inside interest zone.
  488.          */
  489.         private void forceInside() {
  490.             insideZone = true;
  491.         }

  492.         /** Check if the node location is inside interest zone.
  493.          * @return true if the node location is inside interest zone
  494.          */
  495.         public boolean isInside() {
  496.             return insideZone;
  497.         }

  498.         /** Get the index in the along direction.
  499.          * @return index in the along direction
  500.          */
  501.         public int getAlongIndex() {
  502.             return alongIndex;
  503.         }

  504.         /** Get the index in the across direction.
  505.          * @return index in the across direction
  506.          */
  507.         public int getAcrossIndex() {
  508.             return acrossIndex;
  509.         }

  510.         /** Move to a nearby point along surface.
  511.          * <p>
  512.          * The motion will be approximated, assuming the body surface has constant
  513.          * curvature along the motion direction. The approximation will be accurate
  514.          * for short distances, and error will increase as distance increases.
  515.          * </p>
  516.          * @param motion straight motion, which must be curved back to surface
  517.          * @return arrival point, approximately at specified distance from node
  518.          * @exception OrekitException if points cannot be converted to geodetic coordinates
  519.          */
  520.         public S2Point move(final Vector3D motion)
  521.             throws OrekitException {

  522.             // safety check for too small motion
  523.             if (motion.getNorm() < Precision.EPSILON * v.getNorm()) {
  524.                 return s2p;
  525.             }

  526.             // find elliptic plane section
  527.             final Vector3D normal      = Vector3D.crossProduct(v, motion);
  528.             final Ellipse planeSection = ellipsoid.getPlaneSection(v, normal);

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

  532.             // compute approximated arrival point, assuming constant radius of curvature
  533.             final Vector3D delta = v.subtract(omega3D);
  534.             final double   theta = motion.getNorm() / delta.getNorm();
  535.             final Vector3D approximated = new Vector3D(1, omega3D,
  536.                                                        FastMath.cos(theta), delta,
  537.                                                        FastMath.sin(theta) / theta, motion);

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

  541.         }

  542.     }

  543. }