ConvexArea2S.java

  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. import java.util.ArrayList;
  19. import java.util.Arrays;
  20. import java.util.Collection;
  21. import java.util.Collections;
  22. import java.util.Iterator;
  23. import java.util.List;
  24. import java.util.stream.Collectors;
  25. import java.util.stream.Stream;

  26. import org.apache.commons.geometry.core.Transform;
  27. import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBoundedRegion;
  28. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  29. import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
  30. import org.apache.commons.geometry.core.partitioning.Split;
  31. import org.apache.commons.geometry.euclidean.threed.Vector3D;
  32. import org.apache.commons.numbers.angle.Angle;
  33. import org.apache.commons.numbers.core.Precision;

  34. /** Class representing a convex area in 2D spherical space. The boundaries of this
  35.  * area, if any, are composed of convex great circle arcs.
  36.  */
  37. public final class ConvexArea2S extends AbstractConvexHyperplaneBoundedRegion<Point2S, GreatArc>
  38.     implements BoundarySource2S {
  39.     /** Instance representing the full spherical area. */
  40.     private static final ConvexArea2S FULL = new ConvexArea2S(Collections.emptyList());

  41.     /** Constant containing the area of the full spherical space. */
  42.     private static final double FULL_SIZE = 4 * Math.PI;

  43.     /** Constant containing the area of half of the spherical space. */
  44.     private static final double HALF_SIZE = Angle.TWO_PI;

  45.     /** Empirically determined threshold for computing the weighted centroid vector using the
  46.      * triangle fan approach. Areas with boundary sizes under this value use the triangle fan
  47.      * method to increase centroid accuracy.
  48.      */
  49.     private static final double TRIANGLE_FAN_CENTROID_COMPUTE_THRESHOLD = 1e-2;

  50.     /** Construct an instance from its boundaries. Callers are responsible for ensuring
  51.      * that the given path represents the boundary of a convex area. No validation is
  52.      * performed.
  53.      * @param boundaries the boundaries of the convex area
  54.      */
  55.     private ConvexArea2S(final List<GreatArc> boundaries) {
  56.         super(boundaries);
  57.     }

  58.     /** {@inheritDoc} */
  59.     @Override
  60.     public Stream<GreatArc> boundaryStream() {
  61.         return getBoundaries().stream();
  62.     }

  63.     /** Get a path instance representing the boundary of the area. The path is oriented
  64.      * so that the minus sides of the arcs lie on the inside of the area.
  65.      * @return the boundary path of the area
  66.      */
  67.     public GreatArcPath getBoundaryPath() {
  68.         final List<GreatArcPath> paths = InteriorAngleGreatArcConnector.connectMinimized(getBoundaries());
  69.         if (paths.isEmpty()) {
  70.             return GreatArcPath.empty();
  71.         }

  72.         return paths.get(0);
  73.     }

  74.     /** Get an array of interior angles for the area. An empty array is returned if there
  75.      * are no boundary intersections (ie, it has only one boundary or no boundaries at all).
  76.      *
  77.      * <p>The order of the angles corresponds with the order of the boundaries returned
  78.      * by {@link #getBoundaries()}: if {@code i} is an index into the boundaries list,
  79.      * then {@code angles[i]} is the angle between boundaries {@code i} and {@code (i+1) % boundariesSize}.</p>
  80.      * @return an array of interior angles for the area
  81.      */
  82.     public double[] getInteriorAngles() {
  83.         final List<GreatArc> arcs = getBoundaryPath().getArcs();
  84.         final int numSides = arcs.size();

  85.         if (numSides < 2) {
  86.             return new double[0];
  87.         }

  88.         final double[] angles = new double[numSides];

  89.         GreatArc current;
  90.         GreatArc next;
  91.         for (int i = 0; i < numSides; ++i) {
  92.             current = arcs.get(i);
  93.             next = arcs.get((i + 1) % numSides);

  94.             angles[i] = Math.PI - current.getCircle()
  95.                     .angle(next.getCircle(), current.getEndPoint());
  96.         }

  97.         return angles;
  98.     }

  99.     /** {@inheritDoc} */
  100.     @Override
  101.     public double getSize() {
  102.         final int numSides = getBoundaries().size();

  103.         if (numSides == 0) {
  104.             return FULL_SIZE;
  105.         } else if (numSides == 1) {
  106.             return HALF_SIZE;
  107.         } else {
  108.             // use the extended version of Girard's theorem
  109.             // https://en.wikipedia.org/wiki/Spherical_trigonometry#Girard's_theorem
  110.             final double[] angles = getInteriorAngles();
  111.             final double sum = Arrays.stream(angles).sum();

  112.             return sum - ((angles.length - 2) * Math.PI);
  113.         }
  114.     }

  115.     /** {@inheritDoc} */
  116.     @Override
  117.     public Point2S getCentroid() {
  118.         final Vector3D weighted = getWeightedCentroidVector();
  119.         return weighted == null ? null : Point2S.from(weighted);
  120.     }

  121.     /** Return the weighted centroid vector of the area. The returned vector points in the direction of the
  122.      * centroid point on the surface of the unit sphere with the length of the vector proportional to the
  123.      * effective mass of the area at the centroid. By adding the weighted centroid vectors of multiple
  124.      * convex areas, a single centroid can be computed for the combined area.
  125.      * @return weighted centroid vector.
  126.      * @see <a href="https://archive.org/details/centroidinertiat00broc">
  127.      *  <em>The Centroid and Inertia Tensor for a Spherical Triangle</em> - John E. Brock</a>
  128.      */
  129.     Vector3D getWeightedCentroidVector() {
  130.         final List<GreatArc> arcs = getBoundaries();
  131.         final int numBoundaries = arcs.size();

  132.         switch (numBoundaries) {
  133.         case 0:
  134.             // full space; no centroid
  135.             return null;
  136.         case 1:
  137.             // hemisphere
  138.             return computeHemisphereWeightedCentroidVector(arcs.get(0));
  139.         case 2:
  140.             // lune
  141.             return computeLuneWeightedCentroidVector(arcs.get(0), arcs.get(1));
  142.         default:
  143.             // triangle or other convex polygon
  144.             if (getBoundarySize() < TRIANGLE_FAN_CENTROID_COMPUTE_THRESHOLD) {
  145.                 return computeTriangleFanWeightedCentroidVector(arcs);
  146.             }

  147.             return computeArcPoleWeightedCentroidVector(arcs);
  148.         }
  149.     }

  150.     /** {@inheritDoc} */
  151.     @Override
  152.     public Split<ConvexArea2S> split(final Hyperplane<Point2S> splitter) {
  153.         return splitInternal(splitter, this, GreatArc.class, ConvexArea2S::new);
  154.     }

  155.     /** Return a BSP tree representing the same region as this instance.
  156.      */
  157.     @Override
  158.     public RegionBSPTree2S toTree() {
  159.         return RegionBSPTree2S.from(getBoundaries(), true);
  160.     }

  161.     /** Return a new instance transformed by the argument.
  162.      * @param transform transform to apply
  163.      * @return a new instance transformed by the argument
  164.      */
  165.     public ConvexArea2S transform(final Transform<Point2S> transform) {
  166.         return transformInternal(transform, this, GreatArc.class, ConvexArea2S::new);
  167.     }

  168.     /** {@inheritDoc} */
  169.     @Override
  170.     public GreatArc trim(final HyperplaneConvexSubset<Point2S> sub) {
  171.         return (GreatArc) super.trim(sub);
  172.     }

  173.     /** Return an instance representing the full spherical 2D space.
  174.      * @return an instance representing the full spherical 2D space.
  175.      */
  176.     public static ConvexArea2S full() {
  177.         return FULL;
  178.     }

  179.     /** Construct a convex area by creating great circles between adjacent vertices. The vertices must be given
  180.      * in a counter-clockwise around order the interior of the shape. If the area is intended to be closed, the
  181.      * beginning point must be repeated at the end of the path.
  182.      * @param vertices vertices to use to construct the area
  183.      * @param precision precision context used to create new great circle instances
  184.      * @return a convex area constructed using great circles between adjacent vertices
  185.      * @see #fromVertexLoop(Collection, Precision.DoubleEquivalence)
  186.      */
  187.     public static ConvexArea2S fromVertices(final Collection<Point2S> vertices,
  188.             final Precision.DoubleEquivalence precision) {
  189.         return fromVertices(vertices, false, precision);
  190.     }

  191.     /** Construct a convex area by creating great circles between adjacent vertices. An implicit great circle is
  192.      * created between the last vertex given and the first one, if needed. The vertices must be given in a
  193.      * counter-clockwise around order the interior of the shape.
  194.      * @param vertices vertices to use to construct the area
  195.      * @param precision precision context used to create new great circles instances
  196.      * @return a convex area constructed using great circles between adjacent vertices
  197.      * @see #fromVertices(Collection, Precision.DoubleEquivalence)
  198.      */
  199.     public static ConvexArea2S fromVertexLoop(final Collection<Point2S> vertices,
  200.             final Precision.DoubleEquivalence precision) {
  201.         return fromVertices(vertices, true, precision);
  202.     }

  203.     /** Construct a convex area from great circles between adjacent vertices.
  204.      * @param vertices vertices to use to construct the area
  205.      * @param close if true, an additional great circle will be created between the last and first vertex
  206.      * @param precision precision context used to create new great circle instances
  207.      * @return a convex area constructed using great circles between adjacent vertices
  208.      */
  209.     public static ConvexArea2S fromVertices(final Collection<Point2S> vertices, final boolean close,
  210.             final Precision.DoubleEquivalence precision) {

  211.         if (vertices.isEmpty()) {
  212.             return full();
  213.         }

  214.         final List<GreatCircle> circles = new ArrayList<>();

  215.         Point2S first = null;
  216.         Point2S prev = null;
  217.         Point2S cur = null;

  218.         for (final Point2S vertex : vertices) {
  219.             cur = vertex;

  220.             if (first == null) {
  221.                 first = cur;
  222.             }

  223.             if (prev != null && !cur.eq(prev, precision)) {
  224.                 circles.add(GreatCircles.fromPoints(prev, cur, precision));
  225.             }

  226.             prev = cur;
  227.         }

  228.         if (close && cur != null && !cur.eq(first, precision)) {
  229.             circles.add(GreatCircles.fromPoints(cur, first, precision));
  230.         }

  231.         if (!vertices.isEmpty() && circles.isEmpty()) {
  232.             throw new IllegalStateException("Unable to create convex area: only a single unique vertex provided");
  233.         }

  234.         return fromBounds(circles);
  235.     }

  236.     /** Construct a convex area from an arc path. The area represents the intersection of all of the negative
  237.      * half-spaces of the great circles in the path. The boundaries of the returned area may therefore not match
  238.      * the arcs in the path.
  239.      * @param path path to construct the area from
  240.      * @return a convex area constructed from the great circles in the given path
  241.      */
  242.     public static ConvexArea2S fromPath(final GreatArcPath path) {
  243.         final List<GreatCircle> bounds = path.getArcs().stream()
  244.             .map(GreatArc::getCircle)
  245.             .collect(Collectors.toList());

  246.         return fromBounds(bounds);
  247.     }

  248.     /** Create a convex area formed by the intersection of the negative half-spaces of the
  249.      * given bounding great circles. The returned instance represents the area that is on the
  250.      * minus side of all of the given circles. Note that this method does not support areas
  251.      * of zero size (ie, infinitely thin areas or points.)
  252.      * @param bounds great circles used to define the convex area
  253.      * @return a new convex area instance representing the area on the minus side of all
  254.      *      of the bounding great circles or an instance representing the full area if no
  255.      *      circles are given
  256.      * @throws IllegalArgumentException if the given set of bounding great circles do not form a convex area,
  257.      *      meaning that there is no region that is on the minus side of all of the bounding circles.
  258.      */
  259.     public static ConvexArea2S fromBounds(final GreatCircle... bounds) {
  260.         return fromBounds(Arrays.asList(bounds));
  261.     }

  262.     /** Create a convex area formed by the intersection of the negative half-spaces of the
  263.      * given bounding great circles. The returned instance represents the area that is on the
  264.      * minus side of all of the given circles. Note that this method does not support areas
  265.      * of zero size (ie, infinitely thin areas or points.)
  266.      * @param bounds great circles used to define the convex area
  267.      * @return a new convex area instance representing the area on the minus side of all
  268.      *      of the bounding great circles or an instance representing the full area if no
  269.      *      circles are given
  270.      * @throws IllegalArgumentException if the given set of bounding great circles do not form a convex area,
  271.      *      meaning that there is no region that is on the minus side of all of the bounding circles.
  272.      */
  273.     public static ConvexArea2S fromBounds(final Iterable<GreatCircle> bounds) {
  274.         final List<GreatArc> arcs = new ConvexRegionBoundaryBuilder<>(GreatArc.class).build(bounds);
  275.         return arcs.isEmpty() ?
  276.                 full() :
  277.                 new ConvexArea2S(arcs);
  278.     }

  279.     /** Compute the weighted centroid vector for the hemisphere formed by the given arc.
  280.      * @param arc arc defining the hemisphere
  281.      * @return the weighted centroid vector for the hemisphere
  282.      * @see #getWeightedCentroidVector()
  283.      */
  284.     private static Vector3D computeHemisphereWeightedCentroidVector(final GreatArc arc) {
  285.         return arc.getCircle().getPole().withNorm(HALF_SIZE);
  286.     }

  287.     /** Compute the weighted centroid vector for the lune formed by the given arcs.
  288.      * @param a first arc for the lune
  289.      * @param b second arc for the lune
  290.      * @return the weighted centroid vector for the lune
  291.      * @see #getWeightedCentroidVector()
  292.      */
  293.     private static Vector3D computeLuneWeightedCentroidVector(final GreatArc a, final GreatArc b) {
  294.         final Point2S aMid = a.getCentroid();
  295.         final Point2S bMid = b.getCentroid();

  296.         // compute the centroid vector as the exact center of the lune to avoid inaccurate
  297.         // results with very small regions
  298.         final Vector3D.Unit centroid = aMid.slerp(bMid, 0.5).getVector();

  299.         // compute the weight using the reverse of the algorithm from computeArcPoleWeightedCentroidVector()
  300.         final double weight =
  301.             (a.getSize() * centroid.dot(a.getCircle().getPole())) +
  302.             (b.getSize() * centroid.dot(b.getCircle().getPole()));

  303.         return centroid.withNorm(weight);
  304.     }

  305.     /** Compute the weighted centroid vector for the triangle or polygon formed by the given arcs
  306.      * by adding together the arc pole vectors multiplied by their respective arc lengths. This
  307.      * algorithm is described in the paper <a href="https://archive.org/details/centroidinertiat00broc">
  308.      * <em>The Centroid and Inertia Tensor for a Spherical Triangle</em></a> by John E Brock.
  309.      *
  310.      * <p>Note: This algorithm works well in general but is susceptible to floating point errors
  311.      * on very small areas. In these cases, the computed centroid may not be in the expected location
  312.      * and may even be outside of the area. The {@link #computeTriangleFanWeightedCentroidVector(List)}
  313.      * method can produce more accurate results in these cases.</p>
  314.      * @param arcs boundary arcs for the area
  315.      * @return the weighted centroid vector for the area
  316.      * @see #computeTriangleFanWeightedCentroidVector(List)
  317.      */
  318.     private static Vector3D computeArcPoleWeightedCentroidVector(final List<GreatArc> arcs) {
  319.         final Vector3D.Sum centroid = Vector3D.Sum.create();

  320.         for (final GreatArc arc : arcs) {
  321.             centroid.addScaled(arc.getSize(), arc.getCircle().getPole());
  322.         }

  323.         return centroid.get();
  324.     }

  325.     /** Compute the weighted centroid vector for the triangle or polygon formed by the given arcs
  326.      * using a triangle fan approach. This method is specifically designed for use with areas of very small size,
  327.      * where use of the standard algorithm from {@link ##computeArcPoleWeightedCentroidVector(List))} can produce
  328.      * inaccurate results. The algorithm proceeds as follows:
  329.      * <ol>
  330.      *  <li>The polygon is divided into spherical triangles using a triangle fan.</li>
  331.      *  <li>For each triangle, the vectors of the 3 spherical points are added together to approximate the direction
  332.      *      of the spherical centroid. This ensures that the computed centroid lies within the area.</li>
  333.      *  <li>The length of the weighted centroid vector is determined by computing the sum of the contributions that
  334.      *      each arc in the triangle would make to the centroid using the algorithm from
  335.      *      {@link ##computeArcPoleWeightedCentroidVector(List)}. This essentially performs part of that algorithm in
  336.      *      reverse: given a centroid direction, compute the contribution that each arc makes.</li>
  337.      *  <li>The sum of the weighted centroid vectors for each triangle is computed and returned.</li>
  338.      * </ol>
  339.      * @param arcs boundary arcs for the area; must contain at least 3 arcs
  340.      * @return the weighted centroid vector for the area
  341.      * @see ##computeArcPoleWeightedCentroidVector(List)
  342.      */
  343.     private static Vector3D computeTriangleFanWeightedCentroidVector(final List<GreatArc> arcs) {
  344.         final Iterator<GreatArc> arcIt = arcs.iterator();

  345.         final Point2S p0 = arcIt.next().getStartPoint();
  346.         final Vector3D.Unit v0 = p0.getVector();

  347.         final Vector3D.Sum areaCentroid = Vector3D.Sum.create();

  348.         GreatArc arc;
  349.         Point2S p1;
  350.         Point2S p2;
  351.         Vector3D.Unit v1;
  352.         Vector3D.Unit v2;
  353.         Vector3D.Unit triangleCentroid;
  354.         double triangleCentroidLen;
  355.         while (arcIt.hasNext()) {
  356.             arc = arcIt.next();

  357.             if (!arc.contains(p0)) {
  358.                 p1 = arc.getStartPoint();
  359.                 p2 = arc.getEndPoint();

  360.                 v1 = p1.getVector();
  361.                 v2 = p2.getVector();

  362.                 triangleCentroid = Vector3D.Sum.create()
  363.                         .add(v0)
  364.                         .add(v1)
  365.                         .add(v2)
  366.                         .get().normalize();
  367.                 triangleCentroidLen =
  368.                         computeArcCentroidContribution(v0, v1, triangleCentroid) +
  369.                         computeArcCentroidContribution(v1, v2, triangleCentroid) +
  370.                         computeArcCentroidContribution(v2, v0, triangleCentroid);

  371.                 areaCentroid.addScaled(triangleCentroidLen, triangleCentroid);
  372.             }
  373.         }

  374.         return areaCentroid.get();
  375.     }

  376.     /** Compute the contribution made by a single arc to a weighted centroid vector.
  377.      * @param a first point in the arc
  378.      * @param b second point in the arc
  379.      * @param triangleCentroid the centroid vector for the area
  380.      * @return the contribution made by the arc {@code ab} to the length of the weighted centroid vector
  381.      */
  382.     private static double computeArcCentroidContribution(final Vector3D.Unit a, final Vector3D.Unit b,
  383.             final Vector3D.Unit triangleCentroid) {
  384.         final double arcLength = a.angle(b);
  385.         final Vector3D.Unit planeNormal = a.cross(b).normalize();

  386.         return arcLength * triangleCentroid.dot(planeNormal);
  387.     }
  388. }