View Javadoc
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  /*
19   * This is not the original file distributed by the Apache Software Foundation
20   * It has been modified by the Hipparchus project
21   */
22  package org.hipparchus.geometry.spherical.twod;
23  
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.List;
29  import java.util.function.IntPredicate;
30  
31  import org.hipparchus.exception.LocalizedCoreFormats;
32  import org.hipparchus.exception.MathIllegalArgumentException;
33  import org.hipparchus.exception.MathIllegalStateException;
34  import org.hipparchus.geometry.LocalizedGeometryFormats;
35  import org.hipparchus.geometry.Point;
36  import org.hipparchus.geometry.enclosing.EnclosingBall;
37  import org.hipparchus.geometry.enclosing.WelzlEncloser;
38  import org.hipparchus.geometry.euclidean.threed.Euclidean3D;
39  import org.hipparchus.geometry.euclidean.threed.Rotation;
40  import org.hipparchus.geometry.euclidean.threed.RotationConvention;
41  import org.hipparchus.geometry.euclidean.threed.SphereGenerator;
42  import org.hipparchus.geometry.euclidean.threed.Vector3D;
43  import org.hipparchus.geometry.partitioning.AbstractRegion;
44  import org.hipparchus.geometry.partitioning.BSPTree;
45  import org.hipparchus.geometry.partitioning.BoundaryProjection;
46  import org.hipparchus.geometry.partitioning.InteriorPointFinder;
47  import org.hipparchus.geometry.partitioning.RegionFactory;
48  import org.hipparchus.geometry.partitioning.SubHyperplane;
49  import org.hipparchus.geometry.spherical.oned.Arc;
50  import org.hipparchus.geometry.spherical.oned.LimitAngle;
51  import org.hipparchus.geometry.spherical.oned.S1Point;
52  import org.hipparchus.geometry.spherical.oned.Sphere1D;
53  import org.hipparchus.geometry.spherical.oned.SubLimitAngle;
54  import org.hipparchus.util.FastMath;
55  import org.hipparchus.util.MathUtils;
56  import org.hipparchus.util.Precision;
57  
58  /** This class represents a region on the 2-sphere: a set of spherical polygons.
59   */
60  public class SphericalPolygonsSet
61      extends AbstractRegion<Sphere2D, S2Point, Circle, SubCircle,
62                             Sphere1D, S1Point, LimitAngle, SubLimitAngle> {
63  
64      /** Boundary defined as an array of closed loops start vertices. */
65      private List<Vertex> loops;
66  
67      /** Build a polygons set representing the whole real 2-sphere.
68       * @param tolerance below which points are consider to be identical
69       * @exception MathIllegalArgumentException if tolerance is smaller than {@link
70       * Sphere2D#SMALLEST_TOLERANCE}
71       */
72      public SphericalPolygonsSet(final double tolerance) throws MathIllegalArgumentException {
73          super(tolerance);
74          Sphere2D.checkTolerance(tolerance);
75      }
76  
77      /** Build a polygons set representing a hemisphere.
78       * @param pole pole of the hemisphere (the pole is in the inside half)
79       * @param tolerance below which points are consider to be identical
80       * @exception MathIllegalArgumentException if tolerance is smaller than {@link Sphere2D#SMALLEST_TOLERANCE}
81       */
82      public SphericalPolygonsSet(final Vector3D pole, final double tolerance)
83          throws MathIllegalArgumentException {
84          super(new BSPTree<>(new Circle(pole, tolerance).wholeHyperplane(),
85                              new BSPTree<>(Boolean.FALSE),
86                              new BSPTree<>(Boolean.TRUE),
87                              null),
88                tolerance);
89          Sphere2D.checkTolerance(tolerance);
90      }
91  
92      /** Build a polygons set representing a regular polygon.
93       * @param center center of the polygon (the center is in the inside half)
94       * @param meridian point defining the reference meridian for first polygon vertex
95       * @param outsideRadius distance of the vertices to the center
96       * @param n number of sides of the polygon
97       * @param tolerance below which points are consider to be identical
98       * @exception MathIllegalArgumentException if tolerance is smaller than {@link Sphere2D#SMALLEST_TOLERANCE}
99       */
100     public SphericalPolygonsSet(final Vector3D center, final Vector3D meridian,
101                                 final double outsideRadius, final int n,
102                                 final double tolerance)
103         throws MathIllegalArgumentException {
104         this(tolerance, createRegularPolygonVertices(center, meridian, outsideRadius, n));
105     }
106 
107     /** Build a polygons set from a BSP tree.
108      * <p>The leaf nodes of the BSP tree <em>must</em> have a
109      * {@code Boolean} attribute representing the inside status of
110      * the corresponding cell (true for inside cells, false for outside
111      * cells). In order to avoid building too many small objects, it is
112      * recommended to use the predefined constants
113      * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p>
114      * @param tree inside/outside BSP tree representing the region
115      * @param tolerance below which points are consider to be identical
116      * @exception MathIllegalArgumentException if tolerance is smaller than {@link Sphere2D#SMALLEST_TOLERANCE}
117      */
118     public SphericalPolygonsSet(final BSPTree<Sphere2D, S2Point, Circle, SubCircle> tree, final double tolerance)
119         throws MathIllegalArgumentException {
120         super(tree, tolerance);
121         Sphere2D.checkTolerance(tolerance);
122     }
123 
124     /** Build a polygons set from a Boundary REPresentation (B-rep).
125      * <p>The boundary is provided as a collection of {@link
126      * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
127      * interior part of the region on its minus side and the exterior on
128      * its plus side.</p>
129      * <p>The boundary elements can be in any order, and can form
130      * several non-connected sets (like for example polygons with holes
131      * or a set of disjoint polygons considered as a whole). In
132      * fact, the elements do not even need to be connected together
133      * (their topological connections are not used here). However, if the
134      * boundary does not really separate an inside open from an outside
135      * open (open having here its topological meaning), then subsequent
136      * calls to the {@link
137      * org.hipparchus.geometry.partitioning.Region#checkPoint(org.hipparchus.geometry.Point)
138      * checkPoint} method will not be meaningful anymore.</p>
139      * <p>If the boundary is empty, the region will represent the whole
140      * space.</p>
141      * @param boundary collection of boundary elements, as a
142      * collection of {@link SubHyperplane SubHyperplane} objects
143      * @param tolerance below which points are consider to be identical
144      * @exception MathIllegalArgumentException if tolerance is smaller than {@link Sphere2D#SMALLEST_TOLERANCE}
145      */
146     public SphericalPolygonsSet(final Collection<SubCircle> boundary, final double tolerance)
147         throws MathIllegalArgumentException {
148         super(boundary, tolerance);
149         Sphere2D.checkTolerance(tolerance);
150     }
151 
152     /** Build a polygon from a simple list of vertices.
153      * <p>The boundary is provided as a list of points considering to
154      * represent the vertices of a simple loop. The interior part of the
155      * region is on the left side of this path and the exterior is on its
156      * right side.</p>
157      * <p>This constructor does not handle polygons with a boundary
158      * forming several disconnected paths (such as polygons with holes).</p>
159      * <p>For cases where this simple constructor applies, it is expected to
160      * be numerically more robust than the {@link #SphericalPolygonsSet(Collection,
161      * double) general constructor} using {@link SubHyperplane subhyperplanes}.</p>
162      * <p>If the list is empty, the region will represent the whole
163      * space.</p>
164      * <p>This constructor assumes that edges between {@code vertices}, including the edge
165      * between the last and the first vertex, are shorter than pi. If edges longer than pi
166      * are used it may produce unintuitive results, such as reversing the direction of the
167      * edge. This implies using a {@code vertices} array of length 1 or 2 in this
168      * constructor produces an ill-defined region. Use one of the other constructors or
169      * {@link RegionFactory} instead.</p>
170      * <p>The list of {@code vertices} is reduced by selecting a sub-set of vertices
171      * before creating the boundary set. Every point in {@code vertices} will be on the
172      * {boundary of the constructed polygon set, but not necessarily the center-line of
173      * the boundary.</p>
174      * <p>
175      * Polygons with thin pikes or dents are inherently difficult to handle because
176      * they involve circles with almost opposite directions at some vertices. Polygons
177      * whose vertices come from some physical measurement with noise are also
178      * difficult because an edge that should be straight may be broken in lots of
179      * different pieces with almost equal directions. In both cases, computing the
180      * circles intersections is not numerically robust due to the almost 0 or almost
181      * &pi; angle. Such cases need to carefully adjust the {@code hyperplaneThickness}
182      * parameter. A too small value would often lead to completely wrong polygons
183      * with large area wrongly identified as inside or outside. Large values are
184      * often much safer. As a rule of thumb, a value slightly below the size of the
185      * most accurate detail needed is a good value for the {@code hyperplaneThickness}
186      * parameter.
187      * </p>
188      * @param hyperplaneThickness tolerance below which points are considered to
189      * belong to the hyperplane (which is therefore more a slab). Should be greater than
190      * {@code FastMath.ulp(4 * FastMath.PI)} for meaningful results.
191      * @param vertices vertices of the simple loop boundary
192      * @exception MathIllegalArgumentException if tolerance is smaller than {@link Sphere1D#SMALLEST_TOLERANCE}
193      * @exception org.hipparchus.exception.MathRuntimeException if {@code vertices}
194      * contains only a single vertex or repeated vertices.
195      */
196     public SphericalPolygonsSet(final double hyperplaneThickness, final S2Point ... vertices)
197         throws MathIllegalArgumentException {
198         super(verticesToTree(hyperplaneThickness, vertices), hyperplaneThickness);
199         Sphere2D.checkTolerance(hyperplaneThickness);
200     }
201 
202     /** Build the vertices representing a regular polygon.
203      * @param center center of the polygon (the center is in the inside half)
204      * @param meridian point defining the reference meridian for first polygon vertex
205      * @param outsideRadius distance of the vertices to the center
206      * @param n number of sides of the polygon
207      * @return vertices array
208      */
209     private static S2Point[] createRegularPolygonVertices(final Vector3D center, final Vector3D meridian,
210                                                           final double outsideRadius, final int n) {
211         final S2Point[] array = new S2Point[n];
212         final Rotation r0 = new Rotation(Vector3D.crossProduct(center, meridian),
213                                          outsideRadius, RotationConvention.VECTOR_OPERATOR);
214         array[0] = new S2Point(r0.applyTo(center));
215 
216         final Rotation r = new Rotation(center, MathUtils.TWO_PI / n, RotationConvention.VECTOR_OPERATOR);
217         for (int i = 1; i < n; ++i) {
218             array[i] = new S2Point(r.applyTo(array[i - 1].getVector()));
219         }
220 
221         return array;
222     }
223 
224     /** Build the BSP tree of a polygons set from a simple list of vertices.
225      * <p>The boundary is provided as a list of points considering to
226      * represent the vertices of a simple loop. The interior part of the
227      * region is on the left side of this path and the exterior is on its
228      * right side.</p>
229      * <p>This constructor does not handle polygons with a boundary
230      * forming several disconnected paths (such as polygons with holes).</p>
231      * <p>This constructor handles only polygons with edges strictly shorter
232      * than \( \pi \). If longer edges are needed, they need to be broken up
233      * in smaller sub-edges so this constraint holds.</p>
234      * <p>For cases where this simple constructor applies, it is expected to
235      * be numerically more robust than the {@link #SphericalPolygonsSet(Collection, double) general
236      * constructor} using {@link SubHyperplane subhyperplanes}.</p>
237      * @param hyperplaneThickness tolerance below which points are consider to
238      * belong to the hyperplane (which is therefore more a slab)
239      * @param vertices vertices of the simple loop boundary
240      * @return the BSP tree of the input vertices
241      */
242     private static BSPTree<Sphere2D, S2Point, Circle, SubCircle>
243         verticesToTree(final double hyperplaneThickness, S2Point ... vertices) {
244         // thin vertices to those that define distinct circles
245         vertices = reduce(hyperplaneThickness, vertices).toArray(new S2Point[0]);
246         final int n = vertices.length;
247         if (n == 0) {
248             // the tree represents the whole space
249             return new BSPTree<>(Boolean.TRUE);
250         }
251 
252         // build the vertices
253         final Vertex[] vArray = new Vertex[n];
254         for (int i = 0; i < n; ++i) {
255             vArray[i] = new Vertex(vertices[i]);
256         }
257 
258         // build the edges
259         final List<Edge> edges = new ArrayList<>(n);
260         Vertex end = vArray[n - 1];
261         for (int i = 0; i < n; ++i) {
262 
263             // get the endpoints of the edge
264             final Vertex start = end;
265             end = vArray[i];
266 
267             // get the circle supporting the edge
268             final Circle circle = new Circle(start.getLocation(), end.getLocation(), hyperplaneThickness);
269 
270             // create the edge and store it
271             edges.add(new Edge(start, end,
272                                Vector3D.angle(start.getLocation().getVector(),
273                                               end.getLocation().getVector()),
274                                circle));
275 
276         }
277 
278         // build the tree top-down
279         final BSPTree<Sphere2D, S2Point, Circle, SubCircle> tree = new BSPTree<>();
280         insertEdges(tree, edges);
281 
282         return tree;
283 
284     }
285 
286     /**
287      * Compute a subset of vertices that define the boundary to within the given
288      * tolerance. This method partitions {@code vertices} into segments that all lie same
289      * arc to within {@code hyperplaneThickness}, and then returns the end points of the
290      * arcs. Combined arcs are limited to length of pi. If the input vertices has arcs
291      * longer than pi these will be preserved in the returned data.
292      *
293      * @param hyperplaneThickness of each circle in radians.
294      * @param vertices            to decimate.
295      * @return a subset of {@code vertices}.
296      */
297     private static List<S2Point> reduce(final double hyperplaneThickness,
298                                         final S2Point[] vertices) {
299         final int n = vertices.length;
300         if (n <= 3) {
301             // can't reduce to fewer than three points
302             return Arrays.asList(vertices.clone());
303         }
304         final List<S2Point> points = new ArrayList<>();
305         /* Use a simple greedy search to add points to a circle s.t. all intermediate
306          * points are within the thickness. Running time is O(n lg n) worst case.
307          * Since the first vertex may be the middle of a straight edge, look backward
308          * and forward to establish the first edge.
309          * Uses the property that any two points define a circle, so don't check
310          * circles that just span two points.
311          */
312         // first look backward
313         final IntPredicate onCircleBackward = j -> {
314             final int i = n - 2 - j;
315             // circle spanning considered points
316             final Circle circle = new Circle(vertices[0], vertices[i], hyperplaneThickness);
317             final Arc arc = circle.getArc(vertices[0], vertices[i]);
318             if (arc.getSize() >= FastMath.PI) {
319                 return false;
320             }
321             for (int k = i + 1; k < n; k++) {
322                 final S2Point vertex = vertices[k];
323                 if (FastMath.abs(circle.getOffset(vertex)) > hyperplaneThickness ||
324                         arc.getOffset(circle.toSubSpace(vertex)) > 0) {
325                     // point is not within the thickness or arc, start new edge
326                     return false;
327                 }
328             }
329             return true;
330         };
331         // last index in vertices of last entry added to points
332         int last = n - 2 - searchHelper(onCircleBackward, 0, n - 2);
333         if (last > 1) {
334             points.add(vertices[last]);
335         } else {
336             // all points lie on one semi-circle, distance from 0 to 1 is > pi
337             // ill-defined case, just return three points from the list
338             return Arrays.asList(Arrays.copyOfRange(vertices, 0, 3));
339         }
340         final int first = last;
341         // then build edges forward
342         for (int j = 1; ; j += 2) {
343             final int lastFinal = last;
344             final IntPredicate onCircle = i -> {
345                 // circle spanning considered points
346                 final Circle circle = new Circle(vertices[lastFinal], vertices[i], hyperplaneThickness);
347                 final Arc arc = circle.getArc(vertices[lastFinal], vertices[i]);
348                 if (arc.getSize() >= FastMath.PI) {
349                     return false;
350                 }
351                 final int end = lastFinal < i ? i : i + n;
352                 for (int k = lastFinal + 1; k < end; k++) {
353                     final S2Point vertex = vertices[k % n];
354                     if (FastMath.abs(circle.getOffset(vertex)) > hyperplaneThickness ||
355                             arc.getOffset(circle.toSubSpace(vertex)) > 0) {
356                         // point is not within the thickness or arc, start new edge
357                         return false;
358                     }
359                 }
360                 return true;
361             };
362             j = searchHelper(onCircle, j, first + 1);
363             if (j >= first) {
364                 break;
365             }
366             last = j;
367             points.add(vertices[last]);
368         }
369         // put first point last
370         final S2Point swap = points.remove(0);
371         points.add(swap);
372         return points;
373     }
374 
375     /**
376      * Search {@code items} for the first item where {@code predicate} is false between
377      * {@code a} and {@code b}. Assumes that predicate switches from true to false at
378      * exactly one location in [a, b]. Similar to {@link Arrays#binarySearch(int[], int,
379      * int, int)} except that 1. it operates on indices, not elements, 2. there is not a
380      * shortcut for equality, and 3. it is optimized for cases where the return value is
381      * close to a.
382      *
383      * <p> This method achieves O(lg n) performance in the worst case, where n = b - a.
384      * Performance improves to O(lg(i-a)) when i is close to a, where i is the return
385      * value.
386      *
387      * @param predicate to apply.
388      * @param a         start, inclusive.
389      * @param b         end, exclusive.
390      * @return a if a==b, a-1 if predicate.test(a) == false, b - 1 if predicate.test(b-1),
391      * otherwise i s.t. predicate.test(i) == true && predicate.test(i + 1) == false.
392      * @throws MathIllegalArgumentException if a > b.
393      */
394     private static int searchHelper(final IntPredicate predicate,
395                                     final int a,
396                                     final int b) {
397         if (a > b) {
398             throw new MathIllegalArgumentException(
399                     LocalizedCoreFormats.LOWER_ENDPOINT_ABOVE_UPPER_ENDPOINT, a, b);
400         }
401         // Argument checks and special cases
402         if (a == b) {
403             return a;
404         }
405         if (!predicate.test(a)) {
406             return a - 1;
407         }
408 
409         // start with exponential search
410         int start = a;
411         int end = b;
412         for (int i = 2; a + i < b; i *= 2) {
413             if (predicate.test(a + i)) {
414                 // update lower bound of search
415                 start = a + i;
416             } else {
417                 // found upper bound of search
418                 end = a + i;
419                 break;
420             }
421         }
422 
423         // next binary search
424         // copied from Arrays.binarySearch() and modified to work on indices alone
425         int low = start;
426         int high = end - 1;
427         while (low <= high) {
428             final int mid = (low + high) >>> 1;
429             if (predicate.test(mid)) {
430                 low = mid + 1;
431             } else {
432                 high = mid - 1;
433             }
434         }
435         // low is now insertion point, according to Arrays.binarySearch()
436         return low - 1;
437     }
438 
439     /**
440      * Recursively build a tree by inserting cut sub-hyperplanes.
441      * @param node  current tree node (it is a leaf node at the beginning
442      *              of the call)
443      * @param edges list of edges to insert in the cell defined by this node
444      *              (excluding edges not belonging to the cell defined by this node)
445      */
446     private static void insertEdges(final BSPTree<Sphere2D, S2Point, Circle, SubCircle> node, final List<Edge> edges) {
447 
448         // find an edge with an hyperplane that can be inserted in the node
449         int index = 0;
450         Edge inserted = null;
451         while (inserted == null && index < edges.size()) {
452             inserted = edges.get(index++);
453             if (!node.insertCut(inserted.getCircle())) {
454                 inserted = null;
455             }
456         }
457 
458         if (inserted == null) {
459             // no suitable edge was found, the node remains a leaf node
460             // we need to set its inside/outside boolean indicator
461             final BSPTree<Sphere2D, S2Point, Circle, SubCircle> parent = node.getParent();
462             if (parent == null || node == parent.getMinus()) {
463                 node.setAttribute(Boolean.TRUE);
464             } else {
465                 node.setAttribute(Boolean.FALSE);
466             }
467             return;
468         }
469 
470         // we have split the node by inserting an edge as a cut sub-hyperplane
471         // distribute the remaining edges in the two sub-trees
472         final List<Edge> outsideList = new ArrayList<>();
473         final List<Edge> insideList  = new ArrayList<>();
474         for (final Edge edge : edges) {
475             if (edge != inserted) {
476                 edge.split(inserted.getCircle(), outsideList, insideList);
477             }
478         }
479 
480         // recurse through lower levels
481         if (!outsideList.isEmpty()) {
482             insertEdges(node.getPlus(), outsideList);
483         } else {
484             node.getPlus().setAttribute(Boolean.FALSE);
485         }
486         if (!insideList.isEmpty()) {
487             insertEdges(node.getMinus(), insideList);
488         } else {
489             node.getMinus().setAttribute(Boolean.TRUE);
490         }
491 
492     }
493 
494     /** {@inheritDoc} */
495     @Override
496     public SphericalPolygonsSet buildNew(final BSPTree<Sphere2D, S2Point, Circle, SubCircle> tree) {
497         return new SphericalPolygonsSet(tree, getTolerance());
498     }
499 
500     /** {@inheritDoc} */
501     @Override
502     public S2Point getInteriorPoint() {
503 
504         final BSPTree<Sphere2D, S2Point, Circle, SubCircle> tree = getTree(false);
505 
506         if (tree.getCut() == null) {
507             // full sphere or empty region
508             return ((Boolean) tree.getAttribute()) ? S2Point.PLUS_K : null;
509         }
510         else if (tree.getPlus().getCut() == null && tree.getMinus().getCut() == null) {
511             // half sphere
512             final Vector3D pole     = tree.getCut().getHyperplane().getPole();
513             final Vector3D interior = ((Boolean) tree.getMinus().getAttribute()) ? pole : pole.negate();
514             return new S2Point(interior);
515         }
516         else {
517             // regular case
518             final InteriorPointFinder<Sphere2D, S2Point, Circle, SubCircle> finder =
519                     new InteriorPointFinder<>(S2Point.PLUS_I);
520             tree.visit(finder);
521             final BSPTree.InteriorPoint<Sphere2D, S2Point> interior = finder.getPoint();
522             return interior == null ? null : interior.getPoint();
523         }
524 
525     }
526 
527     /** {@inheritDoc}
528      * @exception MathIllegalStateException if the tolerance setting does not allow to build
529      * a clean non-ambiguous boundary
530      */
531     @Override
532     protected void computeGeometricalProperties() throws MathIllegalStateException {
533 
534         final BSPTree<Sphere2D, S2Point, Circle, SubCircle> tree = getTree(true);
535 
536         if (tree.getCut() == null) {
537 
538             // the instance has a single cell without any boundaries
539 
540             if ((Boolean) tree.getAttribute()) {
541                 // the instance covers the whole space
542                 setSize(4 * FastMath.PI);
543                 setBarycenter(new S2Point(0, 0));
544             } else {
545                 setSize(0);
546                 setBarycenter(S2Point.NaN);
547             }
548 
549         } else {
550 
551             // the instance has a boundary
552             final PropertiesComputer pc = new PropertiesComputer(getTolerance());
553             tree.visit(pc);
554             setSize(pc.getArea());
555             setBarycenter(pc.getBarycenter());
556 
557         }
558 
559     }
560 
561     /** Get the boundary loops of the polygon.
562      * <p>The polygon boundary can be represented as a list of closed loops,
563      * each loop being given by exactly one of its vertices. From each loop
564      * start vertex, one can follow the loop by finding the outgoing edge,
565      * then the end vertex, then the next outgoing edge ... until the start
566      * vertex of the loop (exactly the same instance) is found again once
567      * the full loop has been visited.</p>
568      * <p>If the polygon has no boundary at all, a zero length loop
569      * array will be returned.</p>
570      * <p>If the polygon is a simple one-piece polygon, then the returned
571      * array will contain a single vertex.
572      * </p>
573      * <p>All edges in the various loops have the inside of the region on
574      * their left side (i.e. toward their pole) and the outside on their
575      * right side (i.e. away from their pole) when moving in the underlying
576      * circle direction. This means that the closed loops obey the direct
577      * trigonometric orientation.</p>
578      * @return boundary of the polygon, organized as an unmodifiable list of loops start vertices.
579      * @exception MathIllegalStateException if the tolerance setting does not allow to build
580      * a clean non-ambiguous boundary
581      * @see Vertex
582      * @see Edge
583      */
584     public List<Vertex> getBoundaryLoops() throws MathIllegalStateException {
585 
586         if (loops == null) {
587             if (getTree(false).getCut() == null) {
588                 loops = Collections.emptyList();
589             } else {
590 
591                 // sort the arcs according to their start point
592                 final EdgesWithNodeInfoBuilder visitor = new EdgesWithNodeInfoBuilder(getTolerance());
593                 getTree(true).visit(visitor);
594                 final List<EdgeWithNodeInfo> edges = visitor.getEdges();
595 
596                 // connect all edges, using topological criteria first
597                 // and using Euclidean distance only as a last resort
598                 int pending = edges.size();
599                 pending -= naturalFollowerConnections(edges);
600                 if (pending > 0) {
601                     pending -= splitEdgeConnections(edges);
602                 }
603                 if (pending > 0) {
604                     pending -= closeVerticesConnections(edges);
605                 }
606                 if (pending > 0) {
607                     // this should not happen
608                     throw new MathIllegalStateException(LocalizedGeometryFormats.OUTLINE_BOUNDARY_LOOP_OPEN);
609                 }
610 
611 
612                 // extract the edges loops
613                 loops = new ArrayList<>();
614                 for (EdgeWithNodeInfo s = getUnprocessed(edges); s != null; s = getUnprocessed(edges)) {
615                     loops.add(s.getStart());
616                     followLoop(s);
617                 }
618 
619             }
620         }
621 
622         return Collections.unmodifiableList(loops);
623 
624     }
625 
626     /** Connect the edges using only natural follower information.
627      * @param edges edges complete edges list
628      * @return number of connections performed
629      */
630     private int naturalFollowerConnections(final List<EdgeWithNodeInfo> edges) {
631         int connected = 0;
632         for (final EdgeWithNodeInfo edge : edges) {
633             if (edge.getEnd().getOutgoing() == null) {
634                 for (final EdgeWithNodeInfo candidateNext : edges) {
635                     if (EdgeWithNodeInfo.areNaturalFollowers(edge, candidateNext)) {
636                         // connect the two edges
637                         edge.setNextEdge(candidateNext);
638                         ++connected;
639                         break;
640                     }
641                 }
642             }
643         }
644         return connected;
645     }
646 
647     /** Connect the edges resulting from a circle splitting a circular edge.
648      * @param edges edges complete edges list
649      * @return number of connections performed
650      */
651     private int splitEdgeConnections(final List<EdgeWithNodeInfo> edges) {
652         int connected = 0;
653         for (final EdgeWithNodeInfo edge : edges) {
654             if (edge.getEnd().getOutgoing() == null) {
655                 for (final EdgeWithNodeInfo candidateNext : edges) {
656                     if (EdgeWithNodeInfo.resultFromASplit(edge, candidateNext)) {
657                         // connect the two edges
658                         edge.setNextEdge(candidateNext);
659                         ++connected;
660                         break;
661                     }
662                 }
663             }
664         }
665         return connected;
666     }
667 
668     /** Connect the edges using spherical distance.
669      * <p>
670      * This connection heuristic should be used last, as it relies
671      * only on a fuzzy distance criterion.
672      * </p>
673      * @param edges edges complete edges list
674      * @return number of connections performed
675      */
676     private int closeVerticesConnections(final List<EdgeWithNodeInfo> edges) {
677         int connected = 0;
678         for (final EdgeWithNodeInfo edge : edges) {
679             if (edge.getEnd().getOutgoing() == null && edge.getEnd() != null) {
680                 final Vector3D end = edge.getEnd().getLocation().getVector();
681                 EdgeWithNodeInfo selectedNext = null;
682                 double min = Double.POSITIVE_INFINITY;
683                 for (final EdgeWithNodeInfo candidateNext : edges) {
684                     if (candidateNext.getStart().getIncoming() == null) {
685                         final double distance = Vector3D.distance(end, candidateNext.getStart().getLocation().getVector());
686                         if (distance < min) {
687                             selectedNext = candidateNext;
688                             min          = distance;
689                         }
690                     }
691                 }
692                 if (min <= getTolerance()) {
693                     // connect the two edges
694                     edge.setNextEdge(selectedNext);
695                     ++connected;
696                 }
697             }
698         }
699         return connected;
700     }
701 
702     /** Get first unprocessed edge from a list.
703      * @param edges edges list
704      * @return first edge that has not been processed yet
705      * or null if all edges have been processed
706      */
707     private EdgeWithNodeInfo getUnprocessed(final List<EdgeWithNodeInfo> edges) {
708         for (final EdgeWithNodeInfo edge : edges) {
709             if (!edge.isProcessed()) {
710                 return edge;
711             }
712         }
713         return null;
714     }
715 
716     /** Build the loop containing a edge.
717      * <p>
718      * All edges put in the loop will be marked as processed.
719      * </p>
720      * @param defining edge used to define the loop
721      */
722     private void followLoop(final EdgeWithNodeInfo defining) {
723 
724         defining.setProcessed(true);
725 
726         // process edges in connection order
727         EdgeWithNodeInfo previous = defining;
728         EdgeWithNodeInfo next     = (EdgeWithNodeInfo) defining.getEnd().getOutgoing();
729         while (next != defining) {
730             next.setProcessed(true);
731 
732             // filter out spurious vertices
733             if (Vector3D.angle(previous.getCircle().getPole(), next.getCircle().getPole()) <= Precision.EPSILON) {
734                 // the vertex between the two edges is a spurious one
735                 // replace the two edges by a single one
736                 previous.setNextEdge(next.getEnd().getOutgoing());
737                 previous.setLength(previous.getLength() + next.getLength());
738             }
739 
740             previous = next;
741             next     = (EdgeWithNodeInfo) next.getEnd().getOutgoing();
742 
743         }
744 
745     }
746 
747     /** Get a spherical cap enclosing the polygon.
748      * <p>
749      * This method is intended as a first test to quickly identify points
750      * that are guaranteed to be outside of the region, hence performing a full
751      * {@link AbstractRegion#checkPoint(Point)}  checkPoint}
752      * only if the point status remains undecided after the quick check. It is
753      * is therefore mostly useful to speed up computation for small polygons with
754      * complex shapes (say a country boundary on Earth), as the spherical cap will
755      * be small and hence will reliably identify a large part of the sphere as outside,
756      * whereas the full check can be more computing intensive. A typical use case is
757      * therefore:
758      * </p>
759      * <pre>
760      *   // compute region, plus an enclosing spherical cap
761      *   SphericalPolygonsSet complexShape = ...;
762      *   EnclosingBall&lt;Sphere2D, S2Point&gt; cap = complexShape.getEnclosingCap();
763      *
764      *   // check lots of points
765      *   for (Vector3D p : points) {
766      *
767      *     final Location l;
768      *     if (cap.contains(p)) {
769      *       // we cannot be sure where the point is
770      *       // we need to perform the full computation
771      *       l = complexShape.checkPoint(v);
772      *     } else {
773      *       // no need to do further computation,
774      *       // we already know the point is outside
775      *       l = Location.OUTSIDE;
776      *     }
777      *
778      *     // use l ...
779      *
780      *   }
781      * </pre>
782      * <p>
783      * In the special cases of empty or whole sphere polygons, special
784      * spherical caps are returned, with angular radius set to negative
785      * or positive infinity so the {@link
786      * EnclosingBall#contains(org.hipparchus.geometry.Point) ball.contains(point)}
787      * method return always false or true.
788      * </p>
789      * <p>
790      * This method is <em>not</em> guaranteed to return the smallest enclosing cap.
791      * </p>
792      * @return a spherical cap enclosing the polygon
793      */
794     public EnclosingBall<Sphere2D, S2Point> getEnclosingCap() {
795 
796         // handle special cases first
797         if (isEmpty()) {
798             return new EnclosingBall<>(S2Point.PLUS_K, Double.NEGATIVE_INFINITY);
799         }
800         if (isFull()) {
801             return new EnclosingBall<>(S2Point.PLUS_K, Double.POSITIVE_INFINITY);
802         }
803 
804         // as the polygons is neither empty nor full, it has some boundaries and cut hyperplanes
805         final BSPTree<Sphere2D, S2Point, Circle, SubCircle> root = getTree(false);
806         if (isEmpty(root.getMinus()) && isFull(root.getPlus())) {
807             // the polygon covers an hemisphere, and its boundary is one 2π long edge
808             final Circle circle = root.getCut().getHyperplane();
809             return new EnclosingBall<>(new S2Point(circle.getPole()).negate(), MathUtils.SEMI_PI);
810         }
811         if (isFull(root.getMinus()) && isEmpty(root.getPlus())) {
812             // the polygon covers an hemisphere, and its boundary is one 2π long edge
813             final Circle circle = root.getCut().getHyperplane();
814             return new EnclosingBall<>(new S2Point(circle.getPole()), MathUtils.SEMI_PI);
815         }
816 
817         // gather some inside points, to be used by the encloser
818         final List<Vector3D> points = getInsidePoints();
819 
820         // extract points from the boundary loops, to be used by the encloser as well
821         final List<Vertex> boundary = getBoundaryLoops();
822         for (final Vertex loopStart : boundary) {
823             int count = 0;
824             for (Vertex v = loopStart; count == 0 || v != loopStart; v = v.getOutgoing().getEnd()) {
825                 ++count;
826                 points.add(v.getLocation().getVector());
827             }
828         }
829 
830         // find the smallest enclosing 3D sphere
831         final SphereGenerator generator = new SphereGenerator();
832         final WelzlEncloser<Euclidean3D, Vector3D> encloser =
833                 new WelzlEncloser<>(getTolerance(), generator);
834         EnclosingBall<Euclidean3D, Vector3D> enclosing3D = encloser.enclose(points);
835         final Vector3D[] support3D = enclosing3D.getSupport();
836 
837         // convert to 3D sphere to spherical cap
838         final double r = enclosing3D.getRadius();
839         final double h = enclosing3D.getCenter().getNorm();
840         if (h < getTolerance()) {
841             // the 3D sphere is centered on the unit sphere and covers it
842             // fall back to a crude approximation, based only on outside convex cells
843             EnclosingBall<Sphere2D, S2Point> enclosingS2 =
844                     new EnclosingBall<>(S2Point.PLUS_K, Double.POSITIVE_INFINITY);
845             for (Vector3D outsidePoint : getOutsidePoints()) {
846                 final S2Point outsideS2 = new S2Point(outsidePoint);
847                 final BoundaryProjection<Sphere2D, S2Point> projection = projectToBoundary(outsideS2);
848                 if (FastMath.PI - projection.getOffset() < enclosingS2.getRadius()) {
849                     enclosingS2 = new EnclosingBall<>(outsideS2.negate(),
850                                                       FastMath.PI - projection.getOffset(),
851                                                       projection.getProjected());
852                 }
853             }
854             return enclosingS2;
855         }
856         final S2Point[] support = new S2Point[support3D.length];
857         for (int i = 0; i < support3D.length; ++i) {
858             support[i] = new S2Point(support3D[i]);
859         }
860 
861         return new EnclosingBall<>(new S2Point(enclosing3D.getCenter()),
862                                    FastMath.acos((1 + h * h - r * r) / (2 * h)),
863                                    support);
864 
865 
866     }
867 
868     /** Gather some inside points.
869      * @return list of points known to be strictly in all inside convex cells
870      */
871     private List<Vector3D> getInsidePoints() {
872         final PropertiesComputer pc = new PropertiesComputer(getTolerance());
873         getTree(true).visit(pc);
874         return pc.getConvexCellsInsidePoints();
875     }
876 
877     /** Gather some outside points.
878      * @return list of points known to be strictly in all outside convex cells
879      */
880     private List<Vector3D> getOutsidePoints() {
881         final RegionFactory<Sphere2D, S2Point, Circle, SubCircle> factory = new RegionFactory<>();
882         final SphericalPolygonsSet complement = (SphericalPolygonsSet) factory.getComplement(this);
883         final PropertiesComputer pc = new PropertiesComputer(getTolerance());
884         complement.getTree(true).visit(pc);
885         return pc.getConvexCellsInsidePoints();
886     }
887 
888 }