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.euclidean.twod; 018 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.List; 023import java.util.stream.Stream; 024 025import org.apache.commons.geometry.core.Transform; 026import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBoundedRegion; 027import org.apache.commons.geometry.core.partitioning.Hyperplane; 028import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset; 029import org.apache.commons.geometry.core.partitioning.Split; 030import org.apache.commons.geometry.euclidean.twod.path.InteriorAngleLinePathConnector; 031import org.apache.commons.geometry.euclidean.twod.path.LinePath; 032import org.apache.commons.numbers.core.Precision; 033 034/** Class representing a finite or infinite convex area in Euclidean 2D space. 035 * The boundaries of this area, if any, are composed of convex line subsets. 036 */ 037public class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D, LineConvexSubset> 038 implements BoundarySource2D { 039 040 /** Error message used when attempting to construct a convex polygon from a non-convex line path. */ 041 private static final String NON_CONVEX_PATH_ERROR = "Cannot construct convex polygon from non-convex path: "; 042 043 /** Instance representing the full 2D plane. */ 044 private static final ConvexArea FULL = new ConvexArea(Collections.emptyList()); 045 046 /** Simple constructor. Callers are responsible for ensuring that the given path 047 * represents the boundary of a convex area. No validation is performed. 048 * @param boundaries the boundaries of the convex area 049 */ 050 protected ConvexArea(final List<LineConvexSubset> boundaries) { 051 super(boundaries); 052 } 053 054 /** {@inheritDoc} */ 055 @Override 056 public Stream<LineConvexSubset> boundaryStream() { 057 return getBoundaries().stream(); 058 } 059 060 /** Get the connected line subset paths comprising the boundary of the area. The 061 * line subsets are oriented so that their minus sides point toward the interior of the 062 * region. The size of the returned list is 063 * <ul> 064 * <li><strong>0</strong> if the convex area is full,</li> 065 * <li><strong>1</strong> if at least one boundary is present and 066 * a single path can connect all line subsets (this will be the case 067 * for most instances), and</li> 068 * <li><strong>2</strong> if only two boundaries exist and they are 069 * parallel to each other (in which case they cannot be connected 070 * as a single path).</li> 071 * </ul> 072 * @return the line subset paths comprising the boundary of the area. 073 */ 074 public List<LinePath> getBoundaryPaths() { 075 // use connectMaximized() here since that will prevent us from skipping vertices 076 // when there are multiple equivalent vertices to choose from for a given endpoint 077 return InteriorAngleLinePathConnector.connectMaximized(getBoundaries()); 078 } 079 080 /** Get the vertices for the area in a counter-clockwise order. Each vertex in the 081 * returned list is unique. If the boundary of the area is closed, the start vertex is 082 * <em>not</em> repeated at the end of the list. 083 * 084 * <p>It is important to note that, in general, the list of vertices returned by this method 085 * is not sufficient to completely characterize the area. For example, a simple triangle 086 * has 3 vertices, but an infinite area constructed from two parallel lines and two lines that 087 * intersect between them will also have 3 vertices. It is also possible for non-empty areas to 088 * contain no vertices at all. For example, an area with no boundaries (representing the full 089 * space), an area with a single boundary, or an area with two parallel boundaries will not 090 * contain any vertices.</p> 091 * @return the list of vertices for the area in a counter-clockwise order 092 */ 093 public List<Vector2D> getVertices() { 094 final List<LinePath> paths = getBoundaryPaths(); 095 096 // we will only have vertices if we have a single path; otherwise, we have a full 097 // area or two non-intersecting infinite line subsets 098 if (paths.size() == 1) { 099 final LinePath path = paths.get(0); 100 final List<Vector2D> vertices = path.getVertexSequence(); 101 102 if (path.isClosed()) { 103 // do not include the repeated start point 104 return vertices.subList(0, vertices.size() - 1); 105 } 106 return vertices; 107 } 108 109 return Collections.emptyList(); 110 } 111 112 /** Return a new instance transformed by the argument. 113 * @param transform transform to apply 114 * @return a new instance transformed by the argument 115 */ 116 public ConvexArea transform(final Transform<Vector2D> transform) { 117 return transformInternal(transform, this, LineConvexSubset.class, ConvexArea::new); 118 } 119 120 /** {@inheritDoc} */ 121 @Override 122 public LineConvexSubset trim(final HyperplaneConvexSubset<Vector2D> convexSubset) { 123 return (LineConvexSubset) super.trim(convexSubset); 124 } 125 126 /** {@inheritDoc} */ 127 @Override 128 public double getSize() { 129 if (isFull()) { 130 return Double.POSITIVE_INFINITY; 131 } 132 133 double quadrilateralAreaSum = 0.0; 134 135 for (final LineConvexSubset boundary : getBoundaries()) { 136 if (boundary.isInfinite()) { 137 return Double.POSITIVE_INFINITY; 138 } 139 140 quadrilateralAreaSum += boundary.getStartPoint().signedArea(boundary.getEndPoint()); 141 } 142 143 return 0.5 * quadrilateralAreaSum; 144 } 145 146 /** {@inheritDoc} */ 147 @Override 148 public Vector2D getCentroid() { 149 final List<LineConvexSubset> boundaries = getBoundaries(); 150 151 double quadrilateralAreaSum = 0.0; 152 double scaledSumX = 0.0; 153 double scaledSumY = 0.0; 154 155 double signedArea; 156 Vector2D startPoint; 157 Vector2D endPoint; 158 159 for (final LineConvexSubset seg : boundaries) { 160 if (seg.isInfinite()) { 161 // infinite => no centroid 162 return null; 163 } 164 165 startPoint = seg.getStartPoint(); 166 endPoint = seg.getEndPoint(); 167 168 signedArea = startPoint.signedArea(endPoint); 169 170 quadrilateralAreaSum += signedArea; 171 172 scaledSumX += signedArea * (startPoint.getX() + endPoint.getX()); 173 scaledSumY += signedArea * (startPoint.getY() + endPoint.getY()); 174 } 175 176 if (quadrilateralAreaSum > 0) { 177 return Vector2D.of(scaledSumX, scaledSumY).multiply(1.0 / (3.0 * quadrilateralAreaSum)); 178 } 179 180 return null; 181 } 182 183 /** {@inheritDoc} */ 184 @Override 185 public Split<ConvexArea> split(final Hyperplane<Vector2D> splitter) { 186 return splitInternal(splitter, this, LineConvexSubset.class, ConvexArea::new); 187 } 188 189 /** Return a BSP tree representing the same region as this instance. 190 */ 191 @Override 192 public RegionBSPTree2D toTree() { 193 return RegionBSPTree2D.from(getBoundaries(), true); 194 } 195 196 /** Return an instance representing the full 2D area. 197 * @return an instance representing the full 2D area. 198 */ 199 public static ConvexArea full() { 200 return FULL; 201 } 202 203 /** Construct a convex polygon from the given vertices. 204 * @param vertices vertices to use to construct the polygon 205 * @param precision precision context used for floating point comparisons 206 * @return a convex polygon constructed using the given vertices 207 * @throws IllegalStateException if {@code vertices} contains only a single unique vertex 208 * @throws IllegalArgumentException if the constructed path does not define a closed, convex polygon 209 * @see LinePath#fromVertexLoop(Collection, Precision.DoubleEquivalence) 210 */ 211 public static ConvexArea convexPolygonFromVertices(final Collection<Vector2D> vertices, 212 final Precision.DoubleEquivalence precision) { 213 return convexPolygonFromPath(LinePath.fromVertexLoop(vertices, precision)); 214 } 215 216 /** Construct a convex polygon from a line path. 217 * @param path path to construct the polygon from 218 * @return a convex polygon constructed from the given line path 219 * @throws IllegalArgumentException if the path does not define a closed, convex polygon 220 */ 221 public static ConvexArea convexPolygonFromPath(final LinePath path) { 222 // ensure that the path is closed; this also ensures that we do not have any infinite elements 223 if (!path.isClosed()) { 224 throw new IllegalArgumentException("Cannot construct convex polygon from unclosed path: " + path); 225 } 226 227 final List<LineConvexSubset> elements = path.getElements(); 228 if (elements.size() < 3) { 229 throw new IllegalArgumentException( 230 "Cannot construct convex polygon from path with less than 3 elements: " + path); 231 } 232 233 // go through the elements and validate that the produced area is convex and finite 234 // using the precision context from the first path element 235 final LineConvexSubset startElement = elements.get(0); 236 final Vector2D startVertex = startElement.getStartPoint(); 237 final Precision.DoubleEquivalence precision = startElement.getPrecision(); 238 239 Vector2D curVector; 240 Vector2D prevVector = null; 241 242 double signedArea; 243 double totalSignedArea = 0.0; 244 245 LineConvexSubset element; 246 247 // we can skip the last element since the we know that the path is closed, meaning that the 248 // last element's end point is equal to our start point 249 for (int i = 0; i < elements.size() - 1; ++i) { 250 element = elements.get(i); 251 252 curVector = startVertex.vectorTo(element.getEndPoint()); 253 254 if (prevVector != null) { 255 signedArea = prevVector.signedArea(curVector); 256 if (precision.lt(signedArea, 0.0)) { 257 throw new IllegalArgumentException(NON_CONVEX_PATH_ERROR + path); 258 } 259 260 totalSignedArea += signedArea; 261 } 262 263 prevVector = curVector; 264 } 265 266 if (precision.lte(totalSignedArea, 0.0)) { 267 throw new IllegalArgumentException(NON_CONVEX_PATH_ERROR + path); 268 } 269 270 return new ConvexArea(elements); 271 } 272 273 /** Create a convex area formed by the intersection of the negative half-spaces of the 274 * given bounding lines. The returned instance represents the area that is on the 275 * minus side of all of the given lines. Note that this method does not support areas 276 * of zero size (ie, infinitely thin areas or points.) 277 * @param bounds lines used to define the convex area 278 * @return a new convex area instance representing the area on the minus side of all 279 * of the bounding lines or an instance representing the full area if no lines are 280 * given 281 * @throws IllegalArgumentException if the given set of bounding lines do not form a convex area, 282 * meaning that there is no region that is on the minus side of all of the bounding lines. 283 */ 284 public static ConvexArea fromBounds(final Line... bounds) { 285 return fromBounds(Arrays.asList(bounds)); 286 } 287 288 /** Create a convex area formed by the intersection of the negative half-spaces of the 289 * given bounding lines. The returned instance represents the area that is on the 290 * minus side of all of the given lines. Note that this method does not support areas 291 * of zero size (ie, infinitely thin areas or points.) 292 * @param bounds lines used to define the convex area 293 * @return a new convex area instance representing the area on the minus side of all 294 * of the bounding lines or an instance representing the full area if the collection 295 * is empty 296 * @throws IllegalArgumentException if the given set of bounding lines do not form a convex area, 297 * meaning that there is no region that is on the minus side of all of the bounding lines. 298 */ 299 public static ConvexArea fromBounds(final Iterable<Line> bounds) { 300 final List<LineConvexSubset> subsets = 301 new ConvexRegionBoundaryBuilder<>(LineConvexSubset.class).build(bounds); 302 return subsets.isEmpty() ? full() : new ConvexArea(subsets); 303 } 304}