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    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.geometry.spherical.twod;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collection;
22  import java.util.Collections;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.stream.Collectors;
26  import java.util.stream.Stream;
27  
28  import org.apache.commons.geometry.core.Transform;
29  import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBoundedRegion;
30  import org.apache.commons.geometry.core.partitioning.Hyperplane;
31  import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
32  import org.apache.commons.geometry.core.partitioning.Split;
33  import org.apache.commons.geometry.euclidean.threed.Vector3D;
34  import org.apache.commons.numbers.angle.Angle;
35  import org.apache.commons.numbers.core.Precision;
36  
37  /** Class representing a convex area in 2D spherical space. The boundaries of this
38   * area, if any, are composed of convex great circle arcs.
39   */
40  public final class ConvexArea2S extends AbstractConvexHyperplaneBoundedRegion<Point2S, GreatArc>
41      implements BoundarySource2S {
42      /** Instance representing the full spherical area. */
43      private static final ConvexArea2S FULL = new ConvexArea2S(Collections.emptyList());
44  
45      /** Constant containing the area of the full spherical space. */
46      private static final double FULL_SIZE = 4 * Math.PI;
47  
48      /** Constant containing the area of half of the spherical space. */
49      private static final double HALF_SIZE = Angle.TWO_PI;
50  
51      /** Empirically determined threshold for computing the weighted centroid vector using the
52       * triangle fan approach. Areas with boundary sizes under this value use the triangle fan
53       * method to increase centroid accuracy.
54       */
55      private static final double TRIANGLE_FAN_CENTROID_COMPUTE_THRESHOLD = 1e-2;
56  
57      /** Construct an instance from its boundaries. Callers are responsible for ensuring
58       * that the given path represents the boundary of a convex area. No validation is
59       * performed.
60       * @param boundaries the boundaries of the convex area
61       */
62      private ConvexArea2S(final List<GreatArc> boundaries) {
63          super(boundaries);
64      }
65  
66      /** {@inheritDoc} */
67      @Override
68      public Stream<GreatArc> boundaryStream() {
69          return getBoundaries().stream();
70      }
71  
72      /** Get a path instance representing the boundary of the area. The path is oriented
73       * so that the minus sides of the arcs lie on the inside of the area.
74       * @return the boundary path of the area
75       */
76      public GreatArcPath getBoundaryPath() {
77          final List<GreatArcPath> paths = InteriorAngleGreatArcConnector.connectMinimized(getBoundaries());
78          if (paths.isEmpty()) {
79              return GreatArcPath.empty();
80          }
81  
82          return paths.get(0);
83      }
84  
85      /** Get an array of interior angles for the area. An empty array is returned if there
86       * are no boundary intersections (ie, it has only one boundary or no boundaries at all).
87       *
88       * <p>The order of the angles corresponds with the order of the boundaries returned
89       * by {@link #getBoundaries()}: if {@code i} is an index into the boundaries list,
90       * then {@code angles[i]} is the angle between boundaries {@code i} and {@code (i+1) % boundariesSize}.</p>
91       * @return an array of interior angles for the area
92       */
93      public double[] getInteriorAngles() {
94          final List<GreatArc> arcs = getBoundaryPath().getArcs();
95          final int numSides = arcs.size();
96  
97          if (numSides < 2) {
98              return new double[0];
99          }
100 
101         final double[] angles = new double[numSides];
102 
103         GreatArc current;
104         GreatArc next;
105         for (int i = 0; i < numSides; ++i) {
106             current = arcs.get(i);
107             next = arcs.get((i + 1) % numSides);
108 
109             angles[i] = Math.PI - current.getCircle()
110                     .angle(next.getCircle(), current.getEndPoint());
111         }
112 
113         return angles;
114     }
115 
116     /** {@inheritDoc} */
117     @Override
118     public double getSize() {
119         final int numSides = getBoundaries().size();
120 
121         if (numSides == 0) {
122             return FULL_SIZE;
123         } else if (numSides == 1) {
124             return HALF_SIZE;
125         } else {
126             // use the extended version of Girard's theorem
127             // https://en.wikipedia.org/wiki/Spherical_trigonometry#Girard's_theorem
128             final double[] angles = getInteriorAngles();
129             final double sum = Arrays.stream(angles).sum();
130 
131             return sum - ((angles.length - 2) * Math.PI);
132         }
133     }
134 
135     /** {@inheritDoc} */
136     @Override
137     public Point2S getCentroid() {
138         final Vector3D weighted = getWeightedCentroidVector();
139         return weighted == null ? null : Point2S.from(weighted);
140     }
141 
142     /** Return the weighted centroid vector of the area. The returned vector points in the direction of the
143      * centroid point on the surface of the unit sphere with the length of the vector proportional to the
144      * effective mass of the area at the centroid. By adding the weighted centroid vectors of multiple
145      * convex areas, a single centroid can be computed for the combined area.
146      * @return weighted centroid vector.
147      * @see <a href="https://archive.org/details/centroidinertiat00broc">
148      *  <em>The Centroid and Inertia Tensor for a Spherical Triangle</em> - John E. Brock</a>
149      */
150     Vector3D getWeightedCentroidVector() {
151         final List<GreatArc> arcs = getBoundaries();
152         final int numBoundaries = arcs.size();
153 
154         switch (numBoundaries) {
155         case 0:
156             // full space; no centroid
157             return null;
158         case 1:
159             // hemisphere
160             return computeHemisphereWeightedCentroidVector(arcs.get(0));
161         case 2:
162             // lune
163             return computeLuneWeightedCentroidVector(arcs.get(0), arcs.get(1));
164         default:
165             // triangle or other convex polygon
166             if (getBoundarySize() < TRIANGLE_FAN_CENTROID_COMPUTE_THRESHOLD) {
167                 return computeTriangleFanWeightedCentroidVector(arcs);
168             }
169 
170             return computeArcPoleWeightedCentroidVector(arcs);
171         }
172     }
173 
174     /** {@inheritDoc} */
175     @Override
176     public Split<ConvexArea2S> split(final Hyperplane<Point2S> splitter) {
177         return splitInternal(splitter, this, GreatArc.class, ConvexArea2S::new);
178     }
179 
180     /** Return a BSP tree representing the same region as this instance.
181      */
182     @Override
183     public RegionBSPTree2S toTree() {
184         return RegionBSPTree2S.from(getBoundaries(), true);
185     }
186 
187     /** Return a new instance transformed by the argument.
188      * @param transform transform to apply
189      * @return a new instance transformed by the argument
190      */
191     public ConvexArea2S transform(final Transform<Point2S> transform) {
192         return transformInternal(transform, this, GreatArc.class, ConvexArea2S::new);
193     }
194 
195     /** {@inheritDoc} */
196     @Override
197     public GreatArc trim(final HyperplaneConvexSubset<Point2S> sub) {
198         return (GreatArc) super.trim(sub);
199     }
200 
201     /** Return an instance representing the full spherical 2D space.
202      * @return an instance representing the full spherical 2D space.
203      */
204     public static ConvexArea2S full() {
205         return FULL;
206     }
207 
208     /** Construct a convex area by creating great circles between adjacent vertices. The vertices must be given
209      * in a counter-clockwise around order the interior of the shape. If the area is intended to be closed, the
210      * beginning point must be repeated at the end of the path.
211      * @param vertices vertices to use to construct the area
212      * @param precision precision context used to create new great circle instances
213      * @return a convex area constructed using great circles between adjacent vertices
214      * @see #fromVertexLoop(Collection, Precision.DoubleEquivalence)
215      */
216     public static ConvexArea2S fromVertices(final Collection<Point2S> vertices,
217             final Precision.DoubleEquivalence precision) {
218         return fromVertices(vertices, false, precision);
219     }
220 
221     /** Construct a convex area by creating great circles between adjacent vertices. An implicit great circle is
222      * created between the last vertex given and the first one, if needed. The vertices must be given in a
223      * counter-clockwise around order the interior of the shape.
224      * @param vertices vertices to use to construct the area
225      * @param precision precision context used to create new great circles instances
226      * @return a convex area constructed using great circles between adjacent vertices
227      * @see #fromVertices(Collection, Precision.DoubleEquivalence)
228      */
229     public static ConvexArea2S fromVertexLoop(final Collection<Point2S> vertices,
230             final Precision.DoubleEquivalence precision) {
231         return fromVertices(vertices, true, precision);
232     }
233 
234     /** Construct a convex area from great circles between adjacent vertices.
235      * @param vertices vertices to use to construct the area
236      * @param close if true, an additional great circle will be created between the last and first vertex
237      * @param precision precision context used to create new great circle instances
238      * @return a convex area constructed using great circles between adjacent vertices
239      */
240     public static ConvexArea2S fromVertices(final Collection<Point2S> vertices, final boolean close,
241             final Precision.DoubleEquivalence precision) {
242 
243         if (vertices.isEmpty()) {
244             return full();
245         }
246 
247         final List<GreatCircle> circles = new ArrayList<>();
248 
249         Point2S first = null;
250         Point2S prev = null;
251         Point2S cur = null;
252 
253         for (final Point2S vertex : vertices) {
254             cur = vertex;
255 
256             if (first == null) {
257                 first = cur;
258             }
259 
260             if (prev != null && !cur.eq(prev, precision)) {
261                 circles.add(GreatCircles.fromPoints(prev, cur, precision));
262             }
263 
264             prev = cur;
265         }
266 
267         if (close && cur != null && !cur.eq(first, precision)) {
268             circles.add(GreatCircles.fromPoints(cur, first, precision));
269         }
270 
271         if (!vertices.isEmpty() && circles.isEmpty()) {
272             throw new IllegalStateException("Unable to create convex area: only a single unique vertex provided");
273         }
274 
275         return fromBounds(circles);
276     }
277 
278     /** Construct a convex area from an arc path. The area represents the intersection of all of the negative
279      * half-spaces of the great circles in the path. The boundaries of the returned area may therefore not match
280      * the arcs in the path.
281      * @param path path to construct the area from
282      * @return a convex area constructed from the great circles in the given path
283      */
284     public static ConvexArea2S fromPath(final GreatArcPath path) {
285         final List<GreatCircle> bounds = path.getArcs().stream()
286             .map(GreatArc::getCircle)
287             .collect(Collectors.toList());
288 
289         return fromBounds(bounds);
290     }
291 
292     /** Create a convex area formed by the intersection of the negative half-spaces of the
293      * given bounding great circles. The returned instance represents the area that is on the
294      * minus side of all of the given circles. Note that this method does not support areas
295      * of zero size (ie, infinitely thin areas or points.)
296      * @param bounds great circles used to define the convex area
297      * @return a new convex area instance representing the area on the minus side of all
298      *      of the bounding great circles or an instance representing the full area if no
299      *      circles are given
300      * @throws IllegalArgumentException if the given set of bounding great circles do not form a convex area,
301      *      meaning that there is no region that is on the minus side of all of the bounding circles.
302      */
303     public static ConvexArea2S fromBounds(final GreatCircle... bounds) {
304         return fromBounds(Arrays.asList(bounds));
305     }
306 
307     /** Create a convex area formed by the intersection of the negative half-spaces of the
308      * given bounding great circles. The returned instance represents the area that is on the
309      * minus side of all of the given circles. Note that this method does not support areas
310      * of zero size (ie, infinitely thin areas or points.)
311      * @param bounds great circles used to define the convex area
312      * @return a new convex area instance representing the area on the minus side of all
313      *      of the bounding great circles or an instance representing the full area if no
314      *      circles are given
315      * @throws IllegalArgumentException if the given set of bounding great circles do not form a convex area,
316      *      meaning that there is no region that is on the minus side of all of the bounding circles.
317      */
318     public static ConvexArea2S fromBounds(final Iterable<GreatCircle> bounds) {
319         final List<GreatArc> arcs = new ConvexRegionBoundaryBuilder<>(GreatArc.class).build(bounds);
320         return arcs.isEmpty() ?
321                 full() :
322                 new ConvexArea2S(arcs);
323     }
324 
325     /** Compute the weighted centroid vector for the hemisphere formed by the given arc.
326      * @param arc arc defining the hemisphere
327      * @return the weighted centroid vector for the hemisphere
328      * @see #getWeightedCentroidVector()
329      */
330     private static Vector3D computeHemisphereWeightedCentroidVector(final GreatArc arc) {
331         return arc.getCircle().getPole().withNorm(HALF_SIZE);
332     }
333 
334     /** Compute the weighted centroid vector for the lune formed by the given arcs.
335      * @param a first arc for the lune
336      * @param b second arc for the lune
337      * @return the weighted centroid vector for the lune
338      * @see #getWeightedCentroidVector()
339      */
340     private static Vector3D computeLuneWeightedCentroidVector(final GreatArc a, final GreatArc b) {
341         final Point2S aMid = a.getCentroid();
342         final Point2S bMid = b.getCentroid();
343 
344         // compute the centroid vector as the exact center of the lune to avoid inaccurate
345         // results with very small regions
346         final Vector3D.Unit centroid = aMid.slerp(bMid, 0.5).getVector();
347 
348         // compute the weight using the reverse of the algorithm from computeArcPoleWeightedCentroidVector()
349         final double weight =
350             (a.getSize() * centroid.dot(a.getCircle().getPole())) +
351             (b.getSize() * centroid.dot(b.getCircle().getPole()));
352 
353         return centroid.withNorm(weight);
354     }
355 
356     /** Compute the weighted centroid vector for the triangle or polygon formed by the given arcs
357      * by adding together the arc pole vectors multiplied by their respective arc lengths. This
358      * algorithm is described in the paper <a href="https://archive.org/details/centroidinertiat00broc">
359      * <em>The Centroid and Inertia Tensor for a Spherical Triangle</em></a> by John E Brock.
360      *
361      * <p>Note: This algorithm works well in general but is susceptible to floating point errors
362      * on very small areas. In these cases, the computed centroid may not be in the expected location
363      * and may even be outside of the area. The {@link #computeTriangleFanWeightedCentroidVector(List)}
364      * method can produce more accurate results in these cases.</p>
365      * @param arcs boundary arcs for the area
366      * @return the weighted centroid vector for the area
367      * @see #computeTriangleFanWeightedCentroidVector(List)
368      */
369     private static Vector3D computeArcPoleWeightedCentroidVector(final List<GreatArc> arcs) {
370         final Vector3D.Sum centroid = Vector3D.Sum.create();
371 
372         for (final GreatArc arc : arcs) {
373             centroid.addScaled(arc.getSize(), arc.getCircle().getPole());
374         }
375 
376         return centroid.get();
377     }
378 
379     /** Compute the weighted centroid vector for the triangle or polygon formed by the given arcs
380      * using a triangle fan approach. This method is specifically designed for use with areas of very small size,
381      * where use of the standard algorithm from {@link ##computeArcPoleWeightedCentroidVector(List))} can produce
382      * inaccurate results. The algorithm proceeds as follows:
383      * <ol>
384      *  <li>The polygon is divided into spherical triangles using a triangle fan.</li>
385      *  <li>For each triangle, the vectors of the 3 spherical points are added together to approximate the direction
386      *      of the spherical centroid. This ensures that the computed centroid lies within the area.</li>
387      *  <li>The length of the weighted centroid vector is determined by computing the sum of the contributions that
388      *      each arc in the triangle would make to the centroid using the algorithm from
389      *      {@link ##computeArcPoleWeightedCentroidVector(List)}. This essentially performs part of that algorithm in
390      *      reverse: given a centroid direction, compute the contribution that each arc makes.</li>
391      *  <li>The sum of the weighted centroid vectors for each triangle is computed and returned.</li>
392      * </ol>
393      * @param arcs boundary arcs for the area; must contain at least 3 arcs
394      * @return the weighted centroid vector for the area
395      * @see ##computeArcPoleWeightedCentroidVector(List)
396      */
397     private static Vector3D computeTriangleFanWeightedCentroidVector(final List<GreatArc> arcs) {
398         final Iterator<GreatArc> arcIt = arcs.iterator();
399 
400         final Point2S p0 = arcIt.next().getStartPoint();
401         final Vector3D.Unit v0 = p0.getVector();
402 
403         final Vector3D.Sum areaCentroid = Vector3D.Sum.create();
404 
405         GreatArc arc;
406         Point2S p1;
407         Point2S p2;
408         Vector3D.Unit v1;
409         Vector3D.Unit v2;
410         Vector3D.Unit triangleCentroid;
411         double triangleCentroidLen;
412         while (arcIt.hasNext()) {
413             arc = arcIt.next();
414 
415             if (!arc.contains(p0)) {
416                 p1 = arc.getStartPoint();
417                 p2 = arc.getEndPoint();
418 
419                 v1 = p1.getVector();
420                 v2 = p2.getVector();
421 
422                 triangleCentroid = Vector3D.Sum.create()
423                         .add(v0)
424                         .add(v1)
425                         .add(v2)
426                         .get().normalize();
427                 triangleCentroidLen =
428                         computeArcCentroidContribution(v0, v1, triangleCentroid) +
429                         computeArcCentroidContribution(v1, v2, triangleCentroid) +
430                         computeArcCentroidContribution(v2, v0, triangleCentroid);
431 
432                 areaCentroid.addScaled(triangleCentroidLen, triangleCentroid);
433             }
434         }
435 
436         return areaCentroid.get();
437     }
438 
439     /** Compute the contribution made by a single arc to a weighted centroid vector.
440      * @param a first point in the arc
441      * @param b second point in the arc
442      * @param triangleCentroid the centroid vector for the area
443      * @return the contribution made by the arc {@code ab} to the length of the weighted centroid vector
444      */
445     private static double computeArcCentroidContribution(final Vector3D.Unit a, final Vector3D.Unit b,
446             final Vector3D.Unit triangleCentroid) {
447         final double arcLength = a.angle(b);
448         final Vector3D.Unit planeNormal = a.cross(b).normalize();
449 
450         return arcLength * triangleCentroid.dot(planeNormal);
451     }
452 }