001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.spherical.twod;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.Iterator;
024import java.util.List;
025import java.util.stream.Collectors;
026import java.util.stream.Stream;
027
028import org.apache.commons.geometry.core.Transform;
029import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBoundedRegion;
030import org.apache.commons.geometry.core.partitioning.Hyperplane;
031import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
032import org.apache.commons.geometry.core.partitioning.Split;
033import org.apache.commons.geometry.euclidean.threed.Vector3D;
034import org.apache.commons.numbers.angle.Angle;
035import org.apache.commons.numbers.core.Precision;
036
037/** Class representing a convex area in 2D spherical space. The boundaries of this
038 * area, if any, are composed of convex great circle arcs.
039 */
040public final class ConvexArea2S extends AbstractConvexHyperplaneBoundedRegion<Point2S, GreatArc>
041    implements BoundarySource2S {
042    /** Instance representing the full spherical area. */
043    private static final ConvexArea2S FULL = new ConvexArea2S(Collections.emptyList());
044
045    /** Constant containing the area of the full spherical space. */
046    private static final double FULL_SIZE = 4 * Math.PI;
047
048    /** Constant containing the area of half of the spherical space. */
049    private static final double HALF_SIZE = Angle.TWO_PI;
050
051    /** Empirically determined threshold for computing the weighted centroid vector using the
052     * triangle fan approach. Areas with boundary sizes under this value use the triangle fan
053     * method to increase centroid accuracy.
054     */
055    private static final double TRIANGLE_FAN_CENTROID_COMPUTE_THRESHOLD = 1e-2;
056
057    /** Construct an instance from its boundaries. Callers are responsible for ensuring
058     * that the given path represents the boundary of a convex area. No validation is
059     * performed.
060     * @param boundaries the boundaries of the convex area
061     */
062    private ConvexArea2S(final List<GreatArc> boundaries) {
063        super(boundaries);
064    }
065
066    /** {@inheritDoc} */
067    @Override
068    public Stream<GreatArc> boundaryStream() {
069        return getBoundaries().stream();
070    }
071
072    /** Get a path instance representing the boundary of the area. The path is oriented
073     * so that the minus sides of the arcs lie on the inside of the area.
074     * @return the boundary path of the area
075     */
076    public GreatArcPath getBoundaryPath() {
077        final List<GreatArcPath> paths = InteriorAngleGreatArcConnector.connectMinimized(getBoundaries());
078        if (paths.isEmpty()) {
079            return GreatArcPath.empty();
080        }
081
082        return paths.get(0);
083    }
084
085    /** Get an array of interior angles for the area. An empty array is returned if there
086     * are no boundary intersections (ie, it has only one boundary or no boundaries at all).
087     *
088     * <p>The order of the angles corresponds with the order of the boundaries returned
089     * by {@link #getBoundaries()}: if {@code i} is an index into the boundaries list,
090     * then {@code angles[i]} is the angle between boundaries {@code i} and {@code (i+1) % boundariesSize}.</p>
091     * @return an array of interior angles for the area
092     */
093    public double[] getInteriorAngles() {
094        final List<GreatArc> arcs = getBoundaryPath().getArcs();
095        final int numSides = arcs.size();
096
097        if (numSides < 2) {
098            return new double[0];
099        }
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}