SphericalPolygonsSet.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF 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.  *      https://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. /*
  18.  * This is not the original file distributed by the Apache Software Foundation
  19.  * It has been modified by the Hipparchus project
  20.  */
  21. package org.hipparchus.geometry.spherical.twod;

  22. import java.util.ArrayList;
  23. import java.util.Arrays;
  24. import java.util.Collection;
  25. import java.util.Collections;
  26. import java.util.List;
  27. import java.util.function.IntPredicate;

  28. import org.hipparchus.exception.LocalizedCoreFormats;
  29. import org.hipparchus.exception.MathIllegalArgumentException;
  30. import org.hipparchus.exception.MathIllegalStateException;
  31. import org.hipparchus.geometry.LocalizedGeometryFormats;
  32. import org.hipparchus.geometry.Point;
  33. import org.hipparchus.geometry.enclosing.EnclosingBall;
  34. import org.hipparchus.geometry.enclosing.WelzlEncloser;
  35. import org.hipparchus.geometry.euclidean.threed.Euclidean3D;
  36. import org.hipparchus.geometry.euclidean.threed.Rotation;
  37. import org.hipparchus.geometry.euclidean.threed.RotationConvention;
  38. import org.hipparchus.geometry.euclidean.threed.SphereGenerator;
  39. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  40. import org.hipparchus.geometry.partitioning.AbstractRegion;
  41. import org.hipparchus.geometry.partitioning.BSPTree;
  42. import org.hipparchus.geometry.partitioning.BoundaryProjection;
  43. import org.hipparchus.geometry.partitioning.InteriorPointFinder;
  44. import org.hipparchus.geometry.partitioning.RegionFactory;
  45. import org.hipparchus.geometry.partitioning.SubHyperplane;
  46. import org.hipparchus.geometry.spherical.oned.Arc;
  47. import org.hipparchus.geometry.spherical.oned.LimitAngle;
  48. import org.hipparchus.geometry.spherical.oned.S1Point;
  49. import org.hipparchus.geometry.spherical.oned.Sphere1D;
  50. import org.hipparchus.geometry.spherical.oned.SubLimitAngle;
  51. import org.hipparchus.util.FastMath;
  52. import org.hipparchus.util.MathUtils;
  53. import org.hipparchus.util.Precision;

  54. /** This class represents a region on the 2-sphere: a set of spherical polygons.
  55.  */
  56. public class SphericalPolygonsSet
  57.     extends AbstractRegion<Sphere2D, S2Point, Circle, SubCircle,
  58.                            Sphere1D, S1Point, LimitAngle, SubLimitAngle> {

  59.     /** Boundary defined as an array of closed loops start vertices. */
  60.     private List<Vertex> loops;

  61.     /** Build a polygons set representing the whole real 2-sphere.
  62.      * @param tolerance below which points are consider to be identical
  63.      * @exception MathIllegalArgumentException if tolerance is smaller than {@link
  64.      * Sphere2D#SMALLEST_TOLERANCE}
  65.      */
  66.     public SphericalPolygonsSet(final double tolerance) throws MathIllegalArgumentException {
  67.         super(tolerance);
  68.         Sphere2D.checkTolerance(tolerance);
  69.     }

  70.     /** Build a polygons set representing a hemisphere.
  71.      * @param pole pole of the hemisphere (the pole is in the inside half)
  72.      * @param tolerance below which points are consider to be identical
  73.      * @exception MathIllegalArgumentException if tolerance is smaller than {@link Sphere2D#SMALLEST_TOLERANCE}
  74.      */
  75.     public SphericalPolygonsSet(final Vector3D pole, final double tolerance)
  76.         throws MathIllegalArgumentException {
  77.         super(new BSPTree<>(new Circle(pole, tolerance).wholeHyperplane(),
  78.                             new BSPTree<>(Boolean.FALSE),
  79.                             new BSPTree<>(Boolean.TRUE),
  80.                             null),
  81.               tolerance);
  82.         Sphere2D.checkTolerance(tolerance);
  83.     }

  84.     /** Build a polygons set representing a regular polygon.
  85.      * @param center center of the polygon (the center is in the inside half)
  86.      * @param meridian point defining the reference meridian for first polygon vertex
  87.      * @param outsideRadius distance of the vertices to the center
  88.      * @param n number of sides of the polygon
  89.      * @param tolerance below which points are consider to be identical
  90.      * @exception MathIllegalArgumentException if tolerance is smaller than {@link Sphere2D#SMALLEST_TOLERANCE}
  91.      */
  92.     public SphericalPolygonsSet(final Vector3D center, final Vector3D meridian,
  93.                                 final double outsideRadius, final int n,
  94.                                 final double tolerance)
  95.         throws MathIllegalArgumentException {
  96.         this(tolerance, createRegularPolygonVertices(center, meridian, outsideRadius, n));
  97.     }

  98.     /** Build a polygons set from a BSP tree.
  99.      * <p>The leaf nodes of the BSP tree <em>must</em> have a
  100.      * {@code Boolean} attribute representing the inside status of
  101.      * the corresponding cell (true for inside cells, false for outside
  102.      * cells). In order to avoid building too many small objects, it is
  103.      * recommended to use the predefined constants
  104.      * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p>
  105.      * @param tree inside/outside BSP tree representing the region
  106.      * @param tolerance below which points are consider to be identical
  107.      * @exception MathIllegalArgumentException if tolerance is smaller than {@link Sphere2D#SMALLEST_TOLERANCE}
  108.      */
  109.     public SphericalPolygonsSet(final BSPTree<Sphere2D, S2Point, Circle, SubCircle> tree, final double tolerance)
  110.         throws MathIllegalArgumentException {
  111.         super(tree, tolerance);
  112.         Sphere2D.checkTolerance(tolerance);
  113.     }

  114.     /** Build a polygons set from a Boundary REPresentation (B-rep).
  115.      * <p>The boundary is provided as a collection of {@link
  116.      * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
  117.      * interior part of the region on its minus side and the exterior on
  118.      * its plus side.</p>
  119.      * <p>The boundary elements can be in any order, and can form
  120.      * several non-connected sets (like for example polygons with holes
  121.      * or a set of disjoint polygons considered as a whole). In
  122.      * fact, the elements do not even need to be connected together
  123.      * (their topological connections are not used here). However, if the
  124.      * boundary does not really separate an inside open from an outside
  125.      * open (open having here its topological meaning), then subsequent
  126.      * calls to the {@link
  127.      * org.hipparchus.geometry.partitioning.Region#checkPoint(org.hipparchus.geometry.Point)
  128.      * checkPoint} method will not be meaningful anymore.</p>
  129.      * <p>If the boundary is empty, the region will represent the whole
  130.      * space.</p>
  131.      * @param boundary collection of boundary elements, as a
  132.      * collection of {@link SubHyperplane SubHyperplane} objects
  133.      * @param tolerance below which points are consider to be identical
  134.      * @exception MathIllegalArgumentException if tolerance is smaller than {@link Sphere2D#SMALLEST_TOLERANCE}
  135.      */
  136.     public SphericalPolygonsSet(final Collection<SubCircle> boundary, final double tolerance)
  137.         throws MathIllegalArgumentException {
  138.         super(boundary, tolerance);
  139.         Sphere2D.checkTolerance(tolerance);
  140.     }

  141.     /** Build a polygon from a simple list of vertices.
  142.      * <p>The boundary is provided as a list of points considering to
  143.      * represent the vertices of a simple loop. The interior part of the
  144.      * region is on the left side of this path and the exterior is on its
  145.      * right side.</p>
  146.      * <p>This constructor does not handle polygons with a boundary
  147.      * forming several disconnected paths (such as polygons with holes).</p>
  148.      * <p>For cases where this simple constructor applies, it is expected to
  149.      * be numerically more robust than the {@link #SphericalPolygonsSet(Collection,
  150.      * double) general constructor} using {@link SubHyperplane subhyperplanes}.</p>
  151.      * <p>If the list is empty, the region will represent the whole
  152.      * space.</p>
  153.      * <p>This constructor assumes that edges between {@code vertices}, including the edge
  154.      * between the last and the first vertex, are shorter than pi. If edges longer than pi
  155.      * are used it may produce unintuitive results, such as reversing the direction of the
  156.      * edge. This implies using a {@code vertices} array of length 1 or 2 in this
  157.      * constructor produces an ill-defined region. Use one of the other constructors or
  158.      * {@link RegionFactory} instead.</p>
  159.      * <p>The list of {@code vertices} is reduced by selecting a sub-set of vertices
  160.      * before creating the boundary set. Every point in {@code vertices} will be on the
  161.      * {boundary of the constructed polygon set, but not necessarily the center-line of
  162.      * the boundary.</p>
  163.      * <p>
  164.      * Polygons with thin pikes or dents are inherently difficult to handle because
  165.      * they involve circles with almost opposite directions at some vertices. Polygons
  166.      * whose vertices come from some physical measurement with noise are also
  167.      * difficult because an edge that should be straight may be broken in lots of
  168.      * different pieces with almost equal directions. In both cases, computing the
  169.      * circles intersections is not numerically robust due to the almost 0 or almost
  170.      * &pi; angle. Such cases need to carefully adjust the {@code hyperplaneThickness}
  171.      * parameter. A too small value would often lead to completely wrong polygons
  172.      * with large area wrongly identified as inside or outside. Large values are
  173.      * often much safer. As a rule of thumb, a value slightly below the size of the
  174.      * most accurate detail needed is a good value for the {@code hyperplaneThickness}
  175.      * parameter.
  176.      * </p>
  177.      * @param hyperplaneThickness tolerance below which points are considered to
  178.      * belong to the hyperplane (which is therefore more a slab). Should be greater than
  179.      * {@code FastMath.ulp(4 * FastMath.PI)} for meaningful results.
  180.      * @param vertices vertices of the simple loop boundary
  181.      * @exception MathIllegalArgumentException if tolerance is smaller than {@link Sphere1D#SMALLEST_TOLERANCE}
  182.      * @exception org.hipparchus.exception.MathRuntimeException if {@code vertices}
  183.      * contains only a single vertex or repeated vertices.
  184.      */
  185.     public SphericalPolygonsSet(final double hyperplaneThickness, final S2Point ... vertices)
  186.         throws MathIllegalArgumentException {
  187.         super(verticesToTree(hyperplaneThickness, vertices), hyperplaneThickness);
  188.         Sphere2D.checkTolerance(hyperplaneThickness);
  189.     }

  190.     /** Build the vertices representing a regular polygon.
  191.      * @param center center of the polygon (the center is in the inside half)
  192.      * @param meridian point defining the reference meridian for first polygon vertex
  193.      * @param outsideRadius distance of the vertices to the center
  194.      * @param n number of sides of the polygon
  195.      * @return vertices array
  196.      */
  197.     private static S2Point[] createRegularPolygonVertices(final Vector3D center, final Vector3D meridian,
  198.                                                           final double outsideRadius, final int n) {
  199.         final S2Point[] array = new S2Point[n];
  200.         final Rotation r0 = new Rotation(Vector3D.crossProduct(center, meridian),
  201.                                          outsideRadius, RotationConvention.VECTOR_OPERATOR);
  202.         array[0] = new S2Point(r0.applyTo(center));

  203.         final Rotation r = new Rotation(center, MathUtils.TWO_PI / n, RotationConvention.VECTOR_OPERATOR);
  204.         for (int i = 1; i < n; ++i) {
  205.             array[i] = new S2Point(r.applyTo(array[i - 1].getVector()));
  206.         }

  207.         return array;
  208.     }

  209.     /** Build the BSP tree of a polygons set from a simple list of vertices.
  210.      * <p>The boundary is provided as a list of points considering to
  211.      * represent the vertices of a simple loop. The interior part of the
  212.      * region is on the left side of this path and the exterior is on its
  213.      * right side.</p>
  214.      * <p>This constructor does not handle polygons with a boundary
  215.      * forming several disconnected paths (such as polygons with holes).</p>
  216.      * <p>This constructor handles only polygons with edges strictly shorter
  217.      * than \( \pi \). If longer edges are needed, they need to be broken up
  218.      * in smaller sub-edges so this constraint holds.</p>
  219.      * <p>For cases where this simple constructor applies, it is expected to
  220.      * be numerically more robust than the {@link #SphericalPolygonsSet(Collection, double) general
  221.      * constructor} using {@link SubHyperplane subhyperplanes}.</p>
  222.      * @param hyperplaneThickness tolerance below which points are consider to
  223.      * belong to the hyperplane (which is therefore more a slab)
  224.      * @param vertices vertices of the simple loop boundary
  225.      * @return the BSP tree of the input vertices
  226.      */
  227.     private static BSPTree<Sphere2D, S2Point, Circle, SubCircle>
  228.         verticesToTree(final double hyperplaneThickness, S2Point ... vertices) {
  229.         // thin vertices to those that define distinct circles
  230.         vertices = reduce(hyperplaneThickness, vertices).toArray(new S2Point[0]);
  231.         final int n = vertices.length;
  232.         if (n == 0) {
  233.             // the tree represents the whole space
  234.             return new BSPTree<>(Boolean.TRUE);
  235.         }

  236.         // build the vertices
  237.         final Vertex[] vArray = new Vertex[n];
  238.         for (int i = 0; i < n; ++i) {
  239.             vArray[i] = new Vertex(vertices[i]);
  240.         }

  241.         // build the edges
  242.         final List<Edge> edges = new ArrayList<>(n);
  243.         Vertex end = vArray[n - 1];
  244.         for (int i = 0; i < n; ++i) {

  245.             // get the endpoints of the edge
  246.             final Vertex start = end;
  247.             end = vArray[i];

  248.             // get the circle supporting the edge
  249.             final Circle circle = new Circle(start.getLocation(), end.getLocation(), hyperplaneThickness);

  250.             // create the edge and store it
  251.             edges.add(new Edge(start, end,
  252.                                Vector3D.angle(start.getLocation().getVector(),
  253.                                               end.getLocation().getVector()),
  254.                                circle));

  255.         }

  256.         // build the tree top-down
  257.         final BSPTree<Sphere2D, S2Point, Circle, SubCircle> tree = new BSPTree<>();
  258.         insertEdges(tree, edges);

  259.         return tree;

  260.     }

  261.     /**
  262.      * Compute a subset of vertices that define the boundary to within the given
  263.      * tolerance. This method partitions {@code vertices} into segments that all lie same
  264.      * arc to within {@code hyperplaneThickness}, and then returns the end points of the
  265.      * arcs. Combined arcs are limited to length of pi. If the input vertices has arcs
  266.      * longer than pi these will be preserved in the returned data.
  267.      *
  268.      * @param hyperplaneThickness of each circle in radians.
  269.      * @param vertices            to decimate.
  270.      * @return a subset of {@code vertices}.
  271.      */
  272.     private static List<S2Point> reduce(final double hyperplaneThickness,
  273.                                         final S2Point[] vertices) {
  274.         final int n = vertices.length;
  275.         if (n <= 3) {
  276.             // can't reduce to fewer than three points
  277.             return Arrays.asList(vertices.clone());
  278.         }
  279.         final List<S2Point> points = new ArrayList<>();
  280.         /* Use a simple greedy search to add points to a circle s.t. all intermediate
  281.          * points are within the thickness. Running time is O(n lg n) worst case.
  282.          * Since the first vertex may be the middle of a straight edge, look backward
  283.          * and forward to establish the first edge.
  284.          * Uses the property that any two points define a circle, so don't check
  285.          * circles that just span two points.
  286.          */
  287.         // first look backward
  288.         final IntPredicate onCircleBackward = j -> {
  289.             final int i = n - 2 - j;
  290.             // circle spanning considered points
  291.             final Circle circle = new Circle(vertices[0], vertices[i], hyperplaneThickness);
  292.             final Arc arc = circle.getArc(vertices[0], vertices[i]);
  293.             if (arc.getSize() >= FastMath.PI) {
  294.                 return false;
  295.             }
  296.             for (int k = i + 1; k < n; k++) {
  297.                 final S2Point vertex = vertices[k];
  298.                 if (FastMath.abs(circle.getOffset(vertex)) > hyperplaneThickness ||
  299.                         arc.getOffset(circle.toSubSpace(vertex)) > 0) {
  300.                     // point is not within the thickness or arc, start new edge
  301.                     return false;
  302.                 }
  303.             }
  304.             return true;
  305.         };
  306.         // last index in vertices of last entry added to points
  307.         int last = n - 2 - searchHelper(onCircleBackward, 0, n - 2);
  308.         if (last > 1) {
  309.             points.add(vertices[last]);
  310.         } else {
  311.             // all points lie on one semi-circle, distance from 0 to 1 is > pi
  312.             // ill-defined case, just return three points from the list
  313.             return Arrays.asList(Arrays.copyOfRange(vertices, 0, 3));
  314.         }
  315.         final int first = last;
  316.         // then build edges forward
  317.         for (int j = 1; ; j += 2) {
  318.             final int lastFinal = last;
  319.             final IntPredicate onCircle = i -> {
  320.                 // circle spanning considered points
  321.                 final Circle circle = new Circle(vertices[lastFinal], vertices[i], hyperplaneThickness);
  322.                 final Arc arc = circle.getArc(vertices[lastFinal], vertices[i]);
  323.                 if (arc.getSize() >= FastMath.PI) {
  324.                     return false;
  325.                 }
  326.                 final int end = lastFinal < i ? i : i + n;
  327.                 for (int k = lastFinal + 1; k < end; k++) {
  328.                     final S2Point vertex = vertices[k % n];
  329.                     if (FastMath.abs(circle.getOffset(vertex)) > hyperplaneThickness ||
  330.                             arc.getOffset(circle.toSubSpace(vertex)) > 0) {
  331.                         // point is not within the thickness or arc, start new edge
  332.                         return false;
  333.                     }
  334.                 }
  335.                 return true;
  336.             };
  337.             j = searchHelper(onCircle, j, first + 1);
  338.             if (j >= first) {
  339.                 break;
  340.             }
  341.             last = j;
  342.             points.add(vertices[last]);
  343.         }
  344.         // put first point last
  345.         final S2Point swap = points.remove(0);
  346.         points.add(swap);
  347.         return points;
  348.     }

  349.     /**
  350.      * Search {@code items} for the first item where {@code predicate} is false between
  351.      * {@code a} and {@code b}. Assumes that predicate switches from true to false at
  352.      * exactly one location in [a, b]. Similar to {@link Arrays#binarySearch(int[], int,
  353.      * int, int)} except that 1. it operates on indices, not elements, 2. there is not a
  354.      * shortcut for equality, and 3. it is optimized for cases where the return value is
  355.      * close to a.
  356.      *
  357.      * <p> This method achieves O(lg n) performance in the worst case, where n = b - a.
  358.      * Performance improves to O(lg(i-a)) when i is close to a, where i is the return
  359.      * value.
  360.      *
  361.      * @param predicate to apply.
  362.      * @param a         start, inclusive.
  363.      * @param b         end, exclusive.
  364.      * @return a if a==b, a-1 if predicate.test(a) == false, b - 1 if predicate.test(b-1),
  365.      * otherwise i s.t. predicate.test(i) == true && predicate.test(i + 1) == false.
  366.      * @throws MathIllegalArgumentException if a > b.
  367.      */
  368.     private static int searchHelper(final IntPredicate predicate,
  369.                                     final int a,
  370.                                     final int b) {
  371.         if (a > b) {
  372.             throw new MathIllegalArgumentException(
  373.                     LocalizedCoreFormats.LOWER_ENDPOINT_ABOVE_UPPER_ENDPOINT, a, b);
  374.         }
  375.         // Argument checks and special cases
  376.         if (a == b) {
  377.             return a;
  378.         }
  379.         if (!predicate.test(a)) {
  380.             return a - 1;
  381.         }

  382.         // start with exponential search
  383.         int start = a;
  384.         int end = b;
  385.         for (int i = 2; a + i < b; i *= 2) {
  386.             if (predicate.test(a + i)) {
  387.                 // update lower bound of search
  388.                 start = a + i;
  389.             } else {
  390.                 // found upper bound of search
  391.                 end = a + i;
  392.                 break;
  393.             }
  394.         }

  395.         // next binary search
  396.         // copied from Arrays.binarySearch() and modified to work on indices alone
  397.         int low = start;
  398.         int high = end - 1;
  399.         while (low <= high) {
  400.             final int mid = (low + high) >>> 1;
  401.             if (predicate.test(mid)) {
  402.                 low = mid + 1;
  403.             } else {
  404.                 high = mid - 1;
  405.             }
  406.         }
  407.         // low is now insertion point, according to Arrays.binarySearch()
  408.         return low - 1;
  409.     }

  410.     /**
  411.      * Recursively build a tree by inserting cut sub-hyperplanes.
  412.      * @param node  current tree node (it is a leaf node at the beginning
  413.      *              of the call)
  414.      * @param edges list of edges to insert in the cell defined by this node
  415.      *              (excluding edges not belonging to the cell defined by this node)
  416.      */
  417.     private static void insertEdges(final BSPTree<Sphere2D, S2Point, Circle, SubCircle> node, final List<Edge> edges) {

  418.         // find an edge with an hyperplane that can be inserted in the node
  419.         int index = 0;
  420.         Edge inserted = null;
  421.         while (inserted == null && index < edges.size()) {
  422.             inserted = edges.get(index++);
  423.             if (!node.insertCut(inserted.getCircle())) {
  424.                 inserted = null;
  425.             }
  426.         }

  427.         if (inserted == null) {
  428.             // no suitable edge was found, the node remains a leaf node
  429.             // we need to set its inside/outside boolean indicator
  430.             final BSPTree<Sphere2D, S2Point, Circle, SubCircle> parent = node.getParent();
  431.             if (parent == null || node == parent.getMinus()) {
  432.                 node.setAttribute(Boolean.TRUE);
  433.             } else {
  434.                 node.setAttribute(Boolean.FALSE);
  435.             }
  436.             return;
  437.         }

  438.         // we have split the node by inserting an edge as a cut sub-hyperplane
  439.         // distribute the remaining edges in the two sub-trees
  440.         final List<Edge> outsideList = new ArrayList<>();
  441.         final List<Edge> insideList  = new ArrayList<>();
  442.         for (final Edge edge : edges) {
  443.             if (edge != inserted) {
  444.                 edge.split(inserted.getCircle(), outsideList, insideList);
  445.             }
  446.         }

  447.         // recurse through lower levels
  448.         if (!outsideList.isEmpty()) {
  449.             insertEdges(node.getPlus(), outsideList);
  450.         } else {
  451.             node.getPlus().setAttribute(Boolean.FALSE);
  452.         }
  453.         if (!insideList.isEmpty()) {
  454.             insertEdges(node.getMinus(), insideList);
  455.         } else {
  456.             node.getMinus().setAttribute(Boolean.TRUE);
  457.         }

  458.     }

  459.     /** {@inheritDoc} */
  460.     @Override
  461.     public SphericalPolygonsSet buildNew(final BSPTree<Sphere2D, S2Point, Circle, SubCircle> tree) {
  462.         return new SphericalPolygonsSet(tree, getTolerance());
  463.     }

  464.     /** {@inheritDoc} */
  465.     @Override
  466.     public S2Point getInteriorPoint() {

  467.         final BSPTree<Sphere2D, S2Point, Circle, SubCircle> tree = getTree(false);

  468.         if (tree.getCut() == null) {
  469.             // full sphere or empty region
  470.             return ((Boolean) tree.getAttribute()) ? S2Point.PLUS_K : null;
  471.         }
  472.         else if (tree.getPlus().getCut() == null && tree.getMinus().getCut() == null) {
  473.             // half sphere
  474.             final Vector3D pole     = tree.getCut().getHyperplane().getPole();
  475.             final Vector3D interior = ((Boolean) tree.getMinus().getAttribute()) ? pole : pole.negate();
  476.             return new S2Point(interior);
  477.         }
  478.         else {
  479.             // regular case
  480.             final InteriorPointFinder<Sphere2D, S2Point, Circle, SubCircle> finder =
  481.                     new InteriorPointFinder<>(S2Point.PLUS_I);
  482.             tree.visit(finder);
  483.             final BSPTree.InteriorPoint<Sphere2D, S2Point> interior = finder.getPoint();
  484.             return interior == null ? null : interior.getPoint();
  485.         }

  486.     }

  487.     /** {@inheritDoc}
  488.      * @exception MathIllegalStateException if the tolerance setting does not allow to build
  489.      * a clean non-ambiguous boundary
  490.      */
  491.     @Override
  492.     protected void computeGeometricalProperties() throws MathIllegalStateException {

  493.         final BSPTree<Sphere2D, S2Point, Circle, SubCircle> tree = getTree(true);

  494.         if (tree.getCut() == null) {

  495.             // the instance has a single cell without any boundaries

  496.             if ((Boolean) tree.getAttribute()) {
  497.                 // the instance covers the whole space
  498.                 setSize(4 * FastMath.PI);
  499.                 setBarycenter(new S2Point(0, 0));
  500.             } else {
  501.                 setSize(0);
  502.                 setBarycenter(S2Point.NaN);
  503.             }

  504.         } else {

  505.             // the instance has a boundary
  506.             final PropertiesComputer pc = new PropertiesComputer(getTolerance());
  507.             tree.visit(pc);
  508.             setSize(pc.getArea());
  509.             setBarycenter(pc.getBarycenter());

  510.         }

  511.     }

  512.     /** Get the boundary loops of the polygon.
  513.      * <p>The polygon boundary can be represented as a list of closed loops,
  514.      * each loop being given by exactly one of its vertices. From each loop
  515.      * start vertex, one can follow the loop by finding the outgoing edge,
  516.      * then the end vertex, then the next outgoing edge ... until the start
  517.      * vertex of the loop (exactly the same instance) is found again once
  518.      * the full loop has been visited.</p>
  519.      * <p>If the polygon has no boundary at all, a zero length loop
  520.      * array will be returned.</p>
  521.      * <p>If the polygon is a simple one-piece polygon, then the returned
  522.      * array will contain a single vertex.
  523.      * </p>
  524.      * <p>All edges in the various loops have the inside of the region on
  525.      * their left side (i.e. toward their pole) and the outside on their
  526.      * right side (i.e. away from their pole) when moving in the underlying
  527.      * circle direction. This means that the closed loops obey the direct
  528.      * trigonometric orientation.</p>
  529.      * @return boundary of the polygon, organized as an unmodifiable list of loops start vertices.
  530.      * @exception MathIllegalStateException if the tolerance setting does not allow to build
  531.      * a clean non-ambiguous boundary
  532.      * @see Vertex
  533.      * @see Edge
  534.      */
  535.     public List<Vertex> getBoundaryLoops() throws MathIllegalStateException {

  536.         if (loops == null) {
  537.             if (getTree(false).getCut() == null) {
  538.                 loops = Collections.emptyList();
  539.             } else {

  540.                 // sort the arcs according to their start point
  541.                 final EdgesWithNodeInfoBuilder visitor = new EdgesWithNodeInfoBuilder(getTolerance());
  542.                 getTree(true).visit(visitor);
  543.                 final List<EdgeWithNodeInfo> edges = visitor.getEdges();

  544.                 // connect all edges, using topological criteria first
  545.                 // and using Euclidean distance only as a last resort
  546.                 int pending = edges.size();
  547.                 pending -= naturalFollowerConnections(edges);
  548.                 if (pending > 0) {
  549.                     pending -= splitEdgeConnections(edges);
  550.                 }
  551.                 if (pending > 0) {
  552.                     pending -= closeVerticesConnections(edges);
  553.                 }
  554.                 if (pending > 0) {
  555.                     // this should not happen
  556.                     throw new MathIllegalStateException(LocalizedGeometryFormats.OUTLINE_BOUNDARY_LOOP_OPEN);
  557.                 }


  558.                 // extract the edges loops
  559.                 loops = new ArrayList<>();
  560.                 for (EdgeWithNodeInfo s = getUnprocessed(edges); s != null; s = getUnprocessed(edges)) {
  561.                     loops.add(s.getStart());
  562.                     followLoop(s);
  563.                 }

  564.             }
  565.         }

  566.         return Collections.unmodifiableList(loops);

  567.     }

  568.     /** Connect the edges using only natural follower information.
  569.      * @param edges edges complete edges list
  570.      * @return number of connections performed
  571.      */
  572.     private int naturalFollowerConnections(final List<EdgeWithNodeInfo> edges) {
  573.         int connected = 0;
  574.         for (final EdgeWithNodeInfo edge : edges) {
  575.             if (edge.getEnd().getOutgoing() == null) {
  576.                 for (final EdgeWithNodeInfo candidateNext : edges) {
  577.                     if (EdgeWithNodeInfo.areNaturalFollowers(edge, candidateNext)) {
  578.                         // connect the two edges
  579.                         edge.setNextEdge(candidateNext);
  580.                         ++connected;
  581.                         break;
  582.                     }
  583.                 }
  584.             }
  585.         }
  586.         return connected;
  587.     }

  588.     /** Connect the edges resulting from a circle splitting a circular edge.
  589.      * @param edges edges complete edges list
  590.      * @return number of connections performed
  591.      */
  592.     private int splitEdgeConnections(final List<EdgeWithNodeInfo> edges) {
  593.         int connected = 0;
  594.         for (final EdgeWithNodeInfo edge : edges) {
  595.             if (edge.getEnd().getOutgoing() == null) {
  596.                 for (final EdgeWithNodeInfo candidateNext : edges) {
  597.                     if (EdgeWithNodeInfo.resultFromASplit(edge, candidateNext)) {
  598.                         // connect the two edges
  599.                         edge.setNextEdge(candidateNext);
  600.                         ++connected;
  601.                         break;
  602.                     }
  603.                 }
  604.             }
  605.         }
  606.         return connected;
  607.     }

  608.     /** Connect the edges using spherical distance.
  609.      * <p>
  610.      * This connection heuristic should be used last, as it relies
  611.      * only on a fuzzy distance criterion.
  612.      * </p>
  613.      * @param edges edges complete edges list
  614.      * @return number of connections performed
  615.      */
  616.     private int closeVerticesConnections(final List<EdgeWithNodeInfo> edges) {
  617.         int connected = 0;
  618.         for (final EdgeWithNodeInfo edge : edges) {
  619.             if (edge.getEnd().getOutgoing() == null && edge.getEnd() != null) {
  620.                 final Vector3D end = edge.getEnd().getLocation().getVector();
  621.                 EdgeWithNodeInfo selectedNext = null;
  622.                 double min = Double.POSITIVE_INFINITY;
  623.                 for (final EdgeWithNodeInfo candidateNext : edges) {
  624.                     if (candidateNext.getStart().getIncoming() == null) {
  625.                         final double distance = Vector3D.distance(end, candidateNext.getStart().getLocation().getVector());
  626.                         if (distance < min) {
  627.                             selectedNext = candidateNext;
  628.                             min          = distance;
  629.                         }
  630.                     }
  631.                 }
  632.                 if (min <= getTolerance()) {
  633.                     // connect the two edges
  634.                     edge.setNextEdge(selectedNext);
  635.                     ++connected;
  636.                 }
  637.             }
  638.         }
  639.         return connected;
  640.     }

  641.     /** Get first unprocessed edge from a list.
  642.      * @param edges edges list
  643.      * @return first edge that has not been processed yet
  644.      * or null if all edges have been processed
  645.      */
  646.     private EdgeWithNodeInfo getUnprocessed(final List<EdgeWithNodeInfo> edges) {
  647.         for (final EdgeWithNodeInfo edge : edges) {
  648.             if (!edge.isProcessed()) {
  649.                 return edge;
  650.             }
  651.         }
  652.         return null;
  653.     }

  654.     /** Build the loop containing a edge.
  655.      * <p>
  656.      * All edges put in the loop will be marked as processed.
  657.      * </p>
  658.      * @param defining edge used to define the loop
  659.      */
  660.     private void followLoop(final EdgeWithNodeInfo defining) {

  661.         defining.setProcessed(true);

  662.         // process edges in connection order
  663.         EdgeWithNodeInfo previous = defining;
  664.         EdgeWithNodeInfo next     = (EdgeWithNodeInfo) defining.getEnd().getOutgoing();
  665.         while (next != defining) {
  666.             next.setProcessed(true);

  667.             // filter out spurious vertices
  668.             if (Vector3D.angle(previous.getCircle().getPole(), next.getCircle().getPole()) <= Precision.EPSILON) {
  669.                 // the vertex between the two edges is a spurious one
  670.                 // replace the two edges by a single one
  671.                 previous.setNextEdge(next.getEnd().getOutgoing());
  672.                 previous.setLength(previous.getLength() + next.getLength());
  673.             }

  674.             previous = next;
  675.             next     = (EdgeWithNodeInfo) next.getEnd().getOutgoing();

  676.         }

  677.     }

  678.     /** Get a spherical cap enclosing the polygon.
  679.      * <p>
  680.      * This method is intended as a first test to quickly identify points
  681.      * that are guaranteed to be outside of the region, hence performing a full
  682.      * {@link AbstractRegion#checkPoint(Point)}  checkPoint}
  683.      * only if the point status remains undecided after the quick check. It is
  684.      * is therefore mostly useful to speed up computation for small polygons with
  685.      * complex shapes (say a country boundary on Earth), as the spherical cap will
  686.      * be small and hence will reliably identify a large part of the sphere as outside,
  687.      * whereas the full check can be more computing intensive. A typical use case is
  688.      * therefore:
  689.      * </p>
  690.      * <pre>
  691.      *   // compute region, plus an enclosing spherical cap
  692.      *   SphericalPolygonsSet complexShape = ...;
  693.      *   EnclosingBall&lt;Sphere2D, S2Point&gt; cap = complexShape.getEnclosingCap();
  694.      *
  695.      *   // check lots of points
  696.      *   for (Vector3D p : points) {
  697.      *
  698.      *     final Location l;
  699.      *     if (cap.contains(p)) {
  700.      *       // we cannot be sure where the point is
  701.      *       // we need to perform the full computation
  702.      *       l = complexShape.checkPoint(v);
  703.      *     } else {
  704.      *       // no need to do further computation,
  705.      *       // we already know the point is outside
  706.      *       l = Location.OUTSIDE;
  707.      *     }
  708.      *
  709.      *     // use l ...
  710.      *
  711.      *   }
  712.      * </pre>
  713.      * <p>
  714.      * In the special cases of empty or whole sphere polygons, special
  715.      * spherical caps are returned, with angular radius set to negative
  716.      * or positive infinity so the {@link
  717.      * EnclosingBall#contains(org.hipparchus.geometry.Point) ball.contains(point)}
  718.      * method return always false or true.
  719.      * </p>
  720.      * <p>
  721.      * This method is <em>not</em> guaranteed to return the smallest enclosing cap.
  722.      * </p>
  723.      * @return a spherical cap enclosing the polygon
  724.      */
  725.     public EnclosingBall<Sphere2D, S2Point> getEnclosingCap() {

  726.         // handle special cases first
  727.         if (isEmpty()) {
  728.             return new EnclosingBall<>(S2Point.PLUS_K, Double.NEGATIVE_INFINITY);
  729.         }
  730.         if (isFull()) {
  731.             return new EnclosingBall<>(S2Point.PLUS_K, Double.POSITIVE_INFINITY);
  732.         }

  733.         // as the polygons is neither empty nor full, it has some boundaries and cut hyperplanes
  734.         final BSPTree<Sphere2D, S2Point, Circle, SubCircle> root = getTree(false);
  735.         if (isEmpty(root.getMinus()) && isFull(root.getPlus())) {
  736.             // the polygon covers an hemisphere, and its boundary is one 2π long edge
  737.             final Circle circle = root.getCut().getHyperplane();
  738.             return new EnclosingBall<>(new S2Point(circle.getPole()).negate(), MathUtils.SEMI_PI);
  739.         }
  740.         if (isFull(root.getMinus()) && isEmpty(root.getPlus())) {
  741.             // the polygon covers an hemisphere, and its boundary is one 2π long edge
  742.             final Circle circle = root.getCut().getHyperplane();
  743.             return new EnclosingBall<>(new S2Point(circle.getPole()), MathUtils.SEMI_PI);
  744.         }

  745.         // gather some inside points, to be used by the encloser
  746.         final List<Vector3D> points = getInsidePoints();

  747.         // extract points from the boundary loops, to be used by the encloser as well
  748.         final List<Vertex> boundary = getBoundaryLoops();
  749.         for (final Vertex loopStart : boundary) {
  750.             int count = 0;
  751.             for (Vertex v = loopStart; count == 0 || v != loopStart; v = v.getOutgoing().getEnd()) {
  752.                 ++count;
  753.                 points.add(v.getLocation().getVector());
  754.             }
  755.         }

  756.         // find the smallest enclosing 3D sphere
  757.         final SphereGenerator generator = new SphereGenerator();
  758.         final WelzlEncloser<Euclidean3D, Vector3D> encloser =
  759.                 new WelzlEncloser<>(getTolerance(), generator);
  760.         EnclosingBall<Euclidean3D, Vector3D> enclosing3D = encloser.enclose(points);
  761.         final Vector3D[] support3D = enclosing3D.getSupport();

  762.         // convert to 3D sphere to spherical cap
  763.         final double r = enclosing3D.getRadius();
  764.         final double h = enclosing3D.getCenter().getNorm();
  765.         if (h < getTolerance()) {
  766.             // the 3D sphere is centered on the unit sphere and covers it
  767.             // fall back to a crude approximation, based only on outside convex cells
  768.             EnclosingBall<Sphere2D, S2Point> enclosingS2 =
  769.                     new EnclosingBall<>(S2Point.PLUS_K, Double.POSITIVE_INFINITY);
  770.             for (Vector3D outsidePoint : getOutsidePoints()) {
  771.                 final S2Point outsideS2 = new S2Point(outsidePoint);
  772.                 final BoundaryProjection<Sphere2D, S2Point> projection = projectToBoundary(outsideS2);
  773.                 if (FastMath.PI - projection.getOffset() < enclosingS2.getRadius()) {
  774.                     enclosingS2 = new EnclosingBall<>(outsideS2.negate(),
  775.                                                       FastMath.PI - projection.getOffset(),
  776.                                                       projection.getProjected());
  777.                 }
  778.             }
  779.             return enclosingS2;
  780.         }
  781.         final S2Point[] support = new S2Point[support3D.length];
  782.         for (int i = 0; i < support3D.length; ++i) {
  783.             support[i] = new S2Point(support3D[i]);
  784.         }

  785.         return new EnclosingBall<>(new S2Point(enclosing3D.getCenter()),
  786.                                    FastMath.acos((1 + h * h - r * r) / (2 * h)),
  787.                                    support);


  788.     }

  789.     /** Gather some inside points.
  790.      * @return list of points known to be strictly in all inside convex cells
  791.      */
  792.     private List<Vector3D> getInsidePoints() {
  793.         final PropertiesComputer pc = new PropertiesComputer(getTolerance());
  794.         getTree(true).visit(pc);
  795.         return pc.getConvexCellsInsidePoints();
  796.     }

  797.     /** Gather some outside points.
  798.      * @return list of points known to be strictly in all outside convex cells
  799.      */
  800.     private List<Vector3D> getOutsidePoints() {
  801.         final RegionFactory<Sphere2D, S2Point, Circle, SubCircle> factory = new RegionFactory<>();
  802.         final SphericalPolygonsSet complement = (SphericalPolygonsSet) factory.getComplement(this);
  803.         final PropertiesComputer pc = new PropertiesComputer(getTolerance());
  804.         complement.getTree(true).visit(pc);
  805.         return pc.getConvexCellsInsidePoints();
  806.     }

  807. }