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