ConvexArea.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.euclidean.twod;

  18. import java.util.Arrays;
  19. import java.util.Collection;
  20. import java.util.Collections;
  21. import java.util.List;
  22. import java.util.stream.Stream;

  23. import org.apache.commons.geometry.core.Transform;
  24. import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBoundedRegion;
  25. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  26. import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
  27. import org.apache.commons.geometry.core.partitioning.Split;
  28. import org.apache.commons.geometry.euclidean.twod.path.InteriorAngleLinePathConnector;
  29. import org.apache.commons.geometry.euclidean.twod.path.LinePath;
  30. import org.apache.commons.numbers.core.Precision;

  31. /** Class representing a finite or infinite convex area in Euclidean 2D space.
  32.  * The boundaries of this area, if any, are composed of convex line subsets.
  33.  */
  34. public class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D, LineConvexSubset>
  35.     implements BoundarySource2D {

  36.     /** Error message used when attempting to construct a convex polygon from a non-convex line path. */
  37.     private static final String NON_CONVEX_PATH_ERROR = "Cannot construct convex polygon from non-convex path: ";

  38.     /** Instance representing the full 2D plane. */
  39.     private static final ConvexArea FULL = new ConvexArea(Collections.emptyList());

  40.     /** Simple constructor. Callers are responsible for ensuring that the given path
  41.      * represents the boundary of a convex area. No validation is performed.
  42.      * @param boundaries the boundaries of the convex area
  43.      */
  44.     protected ConvexArea(final List<LineConvexSubset> boundaries) {
  45.         super(boundaries);
  46.     }

  47.     /** {@inheritDoc} */
  48.     @Override
  49.     public Stream<LineConvexSubset> boundaryStream() {
  50.         return getBoundaries().stream();
  51.     }

  52.     /** Get the connected line subset paths comprising the boundary of the area. The
  53.      * line subsets are oriented so that their minus sides point toward the interior of the
  54.      * region. The size of the returned list is
  55.      * <ul>
  56.      *      <li><strong>0</strong> if the convex area is full,</li>
  57.      *      <li><strong>1</strong> if at least one boundary is present and
  58.      *          a single path can connect all line subsets (this will be the case
  59.      *          for most instances), and</li>
  60.      *      <li><strong>2</strong> if only two boundaries exist and they are
  61.      *          parallel to each other (in which case they cannot be connected
  62.      *          as a single path).</li>
  63.      * </ul>
  64.      * @return the line subset paths comprising the boundary of the area.
  65.      */
  66.     public List<LinePath> getBoundaryPaths() {
  67.         // use connectMaximized() here since that will prevent us from skipping vertices
  68.         // when there are multiple equivalent vertices to choose from for a given endpoint
  69.         return InteriorAngleLinePathConnector.connectMaximized(getBoundaries());
  70.     }

  71.     /** Get the vertices for the area in a counter-clockwise order. Each vertex in the
  72.      * returned list is unique. If the boundary of the area is closed, the start vertex is
  73.      * <em>not</em> repeated at the end of the list.
  74.      *
  75.      * <p>It is important to note that, in general, the list of vertices returned by this method
  76.      * is not sufficient to completely characterize the area. For example, a simple triangle
  77.      * has 3 vertices, but an infinite area constructed from two parallel lines and two lines that
  78.      * intersect between them will also have 3 vertices. It is also possible for non-empty areas to
  79.      * contain no vertices at all. For example, an area with no boundaries (representing the full
  80.      * space), an area with a single boundary, or an area with two parallel boundaries will not
  81.      * contain any vertices.</p>
  82.      * @return the list of vertices for the area in a counter-clockwise order
  83.      */
  84.     public List<Vector2D> getVertices() {
  85.         final List<LinePath> paths = getBoundaryPaths();

  86.         // we will only have vertices if we have a single path; otherwise, we have a full
  87.         // area or two non-intersecting infinite line subsets
  88.         if (paths.size() == 1) {
  89.             final LinePath path = paths.get(0);
  90.             final List<Vector2D> vertices = path.getVertexSequence();

  91.             if (path.isClosed()) {
  92.                 // do not include the repeated start point
  93.                 return vertices.subList(0, vertices.size() - 1);
  94.             }
  95.             return vertices;
  96.         }

  97.         return Collections.emptyList();
  98.     }

  99.     /** Return a new instance transformed by the argument.
  100.      * @param transform transform to apply
  101.      * @return a new instance transformed by the argument
  102.      */
  103.     public ConvexArea transform(final Transform<Vector2D> transform) {
  104.         return transformInternal(transform, this, LineConvexSubset.class, ConvexArea::new);
  105.     }

  106.     /** {@inheritDoc} */
  107.     @Override
  108.     public LineConvexSubset trim(final HyperplaneConvexSubset<Vector2D> convexSubset) {
  109.         return (LineConvexSubset) super.trim(convexSubset);
  110.     }

  111.     /** {@inheritDoc} */
  112.     @Override
  113.     public double getSize() {
  114.         if (isFull()) {
  115.             return Double.POSITIVE_INFINITY;
  116.         }

  117.         double quadrilateralAreaSum = 0.0;

  118.         for (final LineConvexSubset boundary : getBoundaries()) {
  119.             if (boundary.isInfinite()) {
  120.                 return Double.POSITIVE_INFINITY;
  121.             }

  122.             quadrilateralAreaSum += boundary.getStartPoint().signedArea(boundary.getEndPoint());
  123.         }

  124.         return 0.5 * quadrilateralAreaSum;
  125.     }

  126.     /** {@inheritDoc} */
  127.     @Override
  128.     public Vector2D getCentroid() {
  129.         final List<LineConvexSubset> boundaries = getBoundaries();

  130.         double quadrilateralAreaSum = 0.0;
  131.         double scaledSumX = 0.0;
  132.         double scaledSumY = 0.0;

  133.         double signedArea;
  134.         Vector2D startPoint;
  135.         Vector2D endPoint;

  136.         for (final LineConvexSubset seg : boundaries) {
  137.             if (seg.isInfinite()) {
  138.                 // infinite => no centroid
  139.                 return null;
  140.             }

  141.             startPoint = seg.getStartPoint();
  142.             endPoint = seg.getEndPoint();

  143.             signedArea = startPoint.signedArea(endPoint);

  144.             quadrilateralAreaSum += signedArea;

  145.             scaledSumX += signedArea * (startPoint.getX() + endPoint.getX());
  146.             scaledSumY += signedArea * (startPoint.getY() + endPoint.getY());
  147.         }

  148.         if (quadrilateralAreaSum > 0) {
  149.             return Vector2D.of(scaledSumX, scaledSumY).multiply(1.0 / (3.0 * quadrilateralAreaSum));
  150.         }

  151.         return null;
  152.     }

  153.     /** {@inheritDoc} */
  154.     @Override
  155.     public Split<ConvexArea> split(final Hyperplane<Vector2D> splitter) {
  156.         return splitInternal(splitter, this, LineConvexSubset.class, ConvexArea::new);
  157.     }

  158.     /** Return a BSP tree representing the same region as this instance.
  159.      */
  160.     @Override
  161.     public RegionBSPTree2D toTree() {
  162.         return RegionBSPTree2D.from(getBoundaries(), true);
  163.     }

  164.     /** Return an instance representing the full 2D area.
  165.      * @return an instance representing the full 2D area.
  166.      */
  167.     public static ConvexArea full() {
  168.         return FULL;
  169.     }

  170.     /** Construct a convex polygon from the given vertices.
  171.      * @param vertices vertices to use to construct the polygon
  172.      * @param precision precision context used for floating point comparisons
  173.      * @return a convex polygon constructed using the given vertices
  174.      * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
  175.      * @throws IllegalArgumentException if the constructed path does not define a closed, convex polygon
  176.      * @see LinePath#fromVertexLoop(Collection, Precision.DoubleEquivalence)
  177.      */
  178.     public static ConvexArea convexPolygonFromVertices(final Collection<Vector2D> vertices,
  179.             final Precision.DoubleEquivalence precision) {
  180.         return convexPolygonFromPath(LinePath.fromVertexLoop(vertices, precision));
  181.     }

  182.     /** Construct a convex polygon from a line path.
  183.      * @param path path to construct the polygon from
  184.      * @return a convex polygon constructed from the given line path
  185.      * @throws IllegalArgumentException if the path does not define a closed, convex polygon
  186.      */
  187.     public static ConvexArea convexPolygonFromPath(final LinePath path) {
  188.         // ensure that the path is closed; this also ensures that we do not have any infinite elements
  189.         if (!path.isClosed()) {
  190.             throw new IllegalArgumentException("Cannot construct convex polygon from unclosed path: " + path);
  191.         }

  192.         final List<LineConvexSubset> elements = path.getElements();
  193.         if (elements.size() < 3) {
  194.             throw new IllegalArgumentException(
  195.                     "Cannot construct convex polygon from path with less than 3 elements: " + path);
  196.         }

  197.         // go through the elements and validate that the produced area is convex and finite
  198.         // using the precision context from the first path element
  199.         final LineConvexSubset startElement = elements.get(0);
  200.         final Vector2D startVertex = startElement.getStartPoint();
  201.         final Precision.DoubleEquivalence precision = startElement.getPrecision();

  202.         Vector2D curVector;
  203.         Vector2D prevVector = null;

  204.         double signedArea;
  205.         double totalSignedArea = 0.0;

  206.         LineConvexSubset element;

  207.         // we can skip the last element since the we know that the path is closed, meaning that the
  208.         // last element's end point is equal to our start point
  209.         for (int i = 0; i < elements.size() - 1; ++i) {
  210.             element = elements.get(i);

  211.             curVector = startVertex.vectorTo(element.getEndPoint());

  212.             if (prevVector != null) {
  213.                 signedArea = prevVector.signedArea(curVector);
  214.                 if (precision.lt(signedArea, 0.0)) {
  215.                     throw new IllegalArgumentException(NON_CONVEX_PATH_ERROR + path);
  216.                 }

  217.                 totalSignedArea += signedArea;
  218.             }

  219.             prevVector = curVector;
  220.         }

  221.         if (precision.lte(totalSignedArea, 0.0)) {
  222.             throw new IllegalArgumentException(NON_CONVEX_PATH_ERROR + path);
  223.         }

  224.         return new ConvexArea(elements);
  225.     }

  226.     /** Create a convex area formed by the intersection of the negative half-spaces of the
  227.      * given bounding lines. The returned instance represents the area that is on the
  228.      * minus side of all of the given lines. Note that this method does not support areas
  229.      * of zero size (ie, infinitely thin areas or points.)
  230.      * @param bounds lines used to define the convex area
  231.      * @return a new convex area instance representing the area on the minus side of all
  232.      *      of the bounding lines or an instance representing the full area if no lines are
  233.      *      given
  234.      * @throws IllegalArgumentException if the given set of bounding lines do not form a convex area,
  235.      *      meaning that there is no region that is on the minus side of all of the bounding lines.
  236.      */
  237.     public static ConvexArea fromBounds(final Line... bounds) {
  238.         return fromBounds(Arrays.asList(bounds));
  239.     }

  240.     /** Create a convex area formed by the intersection of the negative half-spaces of the
  241.      * given bounding lines. The returned instance represents the area that is on the
  242.      * minus side of all of the given lines. Note that this method does not support areas
  243.      * of zero size (ie, infinitely thin areas or points.)
  244.      * @param bounds lines used to define the convex area
  245.      * @return a new convex area instance representing the area on the minus side of all
  246.      *      of the bounding lines or an instance representing the full area if the collection
  247.      *      is empty
  248.      * @throws IllegalArgumentException if the given set of bounding lines do not form a convex area,
  249.      *      meaning that there is no region that is on the minus side of all of the bounding lines.
  250.      */
  251.     public static ConvexArea fromBounds(final Iterable<Line> bounds) {
  252.         final List<LineConvexSubset> subsets =
  253.                 new ConvexRegionBoundaryBuilder<>(LineConvexSubset.class).build(bounds);
  254.         return subsets.isEmpty() ? full() : new ConvexArea(subsets);
  255.     }
  256. }