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 * π 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<Sphere2D, S2Point> 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 }