/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.commons.math.geometry.euclidean.twod; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.apache.commons.math.exception.MathInternalError; import org.apache.commons.math.geometry.euclidean.oned.Euclidean1D; import org.apache.commons.math.geometry.euclidean.oned.Vector1D; import org.apache.commons.math.geometry.partitioning.BSPTree; import org.apache.commons.math.geometry.partitioning.SubHyperplane; import org.apache.commons.math.geometry.partitioning.AbstractRegion; import org.apache.commons.math.geometry.partitioning.utilities.AVLTree; import org.apache.commons.math.util.FastMath; /** This class represents a 2D region: a set of polygons. * @version $Id: PolygonsSet.java 1131153 2011-06-03 19:23:56Z luc $ * @since 3.0 */ public class PolygonsSet extends AbstractRegion<Euclidean2D, Euclidean1D> { /** Vertices organized as boundary loops. */ private Vector2D[][] vertices; /** Build a polygons set representing the whole real line. */ public PolygonsSet() { super(); } /** Build a polygons set from a BSP tree. * <p>The leaf nodes of the BSP tree <em>must</em> have a * {@code Boolean} attribute representing the inside status of * the corresponding cell (true for inside cells, false for outside * cells). In order to avoid building too many small objects, it is * recommended to use the predefined constants * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p> * @param tree inside/outside BSP tree representing the region */ public PolygonsSet(final BSPTree<Euclidean2D> tree) { super(tree); } /** Build a polygons set from a Boundary REPresentation (B-rep). * <p>The boundary is provided as a collection of {@link * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the * interior part of the region on its minus side and the exterior on * its plus side.</p> * <p>The boundary elements can be in any order, and can form * several non-connected sets (like for example polygons with holes * or a set of disjoint polyhedrons considered as a whole). In * fact, the elements do not even need to be connected together * (their topological connections are not used here). However, if the * boundary does not really separate an inside open from an outside * open (open having here its topological meaning), then subsequent * calls to the {@link * org.apache.commons.math.geometry.partitioning.Region#checkPoint(org.apache.commons.math.geometry.Vector) * checkPoint} method will not be meaningful anymore.</p> * <p>If the boundary is empty, the region will represent the whole * space.</p> * @param boundary collection of boundary elements, as a * collection of {@link SubHyperplane SubHyperplane} objects */ public PolygonsSet(final Collection<SubHyperplane<Euclidean2D>> boundary) { super(boundary); } /** Build a parallellepipedic box. * @param xMin low bound along the x direction * @param xMax high bound along the x direction * @param yMin low bound along the y direction * @param yMax high bound along the y direction */ public PolygonsSet(final double xMin, final double xMax, final double yMin, final double yMax) { super(boxBoundary(xMin, xMax, yMin, yMax)); } /** Create a list of hyperplanes representing the boundary of a box. * @param xMin low bound along the x direction * @param xMax high bound along the x direction * @param yMin low bound along the y direction * @param yMax high bound along the y direction * @return boundary of the box */ private static Line[] boxBoundary(final double xMin, final double xMax, final double yMin, final double yMax) { final Vector2D minMin = new Vector2D(xMin, yMin); final Vector2D minMax = new Vector2D(xMin, yMax); final Vector2D maxMin = new Vector2D(xMax, yMin); final Vector2D maxMax = new Vector2D(xMax, yMax); return new Line[] { new Line(minMin, maxMin), new Line(maxMin, maxMax), new Line(maxMax, minMax), new Line(minMax, minMin) }; } /** {@inheritDoc} */ public PolygonsSet buildNew(final BSPTree<Euclidean2D> tree) { return new PolygonsSet(tree); } /** {@inheritDoc} */ protected void computeGeometricalProperties() { final Vector2D[][] v = getVertices(); if (v.length == 0) { if ((Boolean) getTree(false).getAttribute()) { setSize(Double.POSITIVE_INFINITY); setBarycenter(Vector2D.NaN); } else { setSize(0); setBarycenter(new Vector2D(0, 0)); } } else if (v[0][0] == null) { // there is at least one open-loop: the polygon is infinite setSize(Double.POSITIVE_INFINITY); setBarycenter(Vector2D.NaN); } else { // all loops are closed, we compute some integrals around the shape double sum = 0; double sumX = 0; double sumY = 0; for (Vector2D[] loop : v) { double x1 = loop[loop.length - 1].getX(); double y1 = loop[loop.length - 1].getY(); for (final Vector2D point : loop) { final double x0 = x1; final double y0 = y1; x1 = point.getX(); y1 = point.getY(); final double factor = x0 * y1 - y0 * x1; sum += factor; sumX += factor * (x0 + x1); sumY += factor * (y0 + y1); } } if (sum < 0) { // the polygon as a finite outside surrounded by an infinite inside setSize(Double.POSITIVE_INFINITY); setBarycenter(Vector2D.NaN); } else { setSize(sum / 2); setBarycenter(new Vector2D(sumX / (3 * sum), sumY / (3 * sum))); } } } /** Get the vertices of the polygon. * <p>The polygon boundary can be represented as an array of loops, * each loop being itself an array of vertices.</p> * <p>In order to identify open loops which start and end by * infinite edges, the open loops arrays start with a null point. In * this case, the first non null point and the last point of the * array do not represent real vertices, they are dummy points * intended only to get the direction of the first and last edge. An * open loop consisting of a single infinite line will therefore be * represented by a three elements array with one null point * followed by two dummy points. The open loops are always the first * ones in the loops array.</p> * <p>If the polygon has no boundary at all, a zero length loop * array will be returned.</p> * <p>All line segments in the various loops have the inside of the * region on their left side and the outside on their right side * when moving in the underlying line direction. This means that * closed loops surrounding finite areas obey the direct * trigonometric orientation.</p> * @return vertices of the polygon, organized as oriented boundary * loops with the open loops first (the returned value is guaranteed * to be non-null) */ public Vector2D[][] getVertices() { if (vertices == null) { if (getTree(false).getCut() == null) { vertices = new Vector2D[0][]; } else { // sort the segments according to their start point final SegmentsBuilder visitor = new SegmentsBuilder(); getTree(true).visit(visitor); final AVLTree<Segment> sorted = visitor.getSorted(); // identify the loops, starting from the open ones // (their start segments are naturally at the sorted set beginning) final ArrayList<List<Segment>> loops = new ArrayList<List<Segment>>(); while (!sorted.isEmpty()) { final AVLTree<Segment>.Node node = sorted.getSmallest(); final List<Segment> loop = followLoop(node, sorted); if (loop != null) { loops.add(loop); } } // tranform the loops in an array of arrays of points vertices = new Vector2D[loops.size()][]; int i = 0; for (final List<Segment> loop : loops) { if (loop.size() < 2) { // single infinite line final Line line = loop.get(0).getLine(); vertices[i++] = new Vector2D[] { null, line.toSpace(new Vector1D(-Float.MAX_VALUE)), line.toSpace(new Vector1D(+Float.MAX_VALUE)) }; } else if (loop.get(0).getStart() == null) { // open loop with at least one real point final Vector2D[] array = new Vector2D[loop.size() + 2]; int j = 0; for (Segment segment : loop) { if (j == 0) { // null point and first dummy point double x = segment.getLine().toSubSpace(segment.getEnd()).getX(); x -= FastMath.max(1.0, FastMath.abs(x / 2)); array[j++] = null; array[j++] = segment.getLine().toSpace(new Vector1D(x)); } if (j < (array.length - 1)) { // current point array[j++] = segment.getEnd(); } if (j == (array.length - 1)) { // last dummy point double x = segment.getLine().toSubSpace(segment.getStart()).getX(); x += FastMath.max(1.0, FastMath.abs(x / 2)); array[j++] = segment.getLine().toSpace(new Vector1D(x)); } } vertices[i++] = array; } else { final Vector2D[] array = new Vector2D[loop.size()]; int j = 0; for (Segment segment : loop) { array[j++] = segment.getStart(); } vertices[i++] = array; } } } } return vertices.clone(); } /** Follow a boundary loop. * @param node node containing the segment starting the loop * @param sorted set of segments belonging to the boundary, sorted by * start points (contains {@code node}) * @return a list of connected sub-hyperplanes starting at * {@code node} */ private List<Segment> followLoop(final AVLTree<Segment>.Node node, final AVLTree<Segment> sorted) { final ArrayList<Segment> loop = new ArrayList<Segment>(); Segment segment = node.getElement(); loop.add(segment); final Vector2D globalStart = segment.getStart(); Vector2D end = segment.getEnd(); node.delete(); // is this an open or a closed loop ? final boolean open = segment.getStart() == null; while ((end != null) && (open || (globalStart.distance(end) > 1.0e-10))) { // search the sub-hyperplane starting where the previous one ended AVLTree<Segment>.Node selectedNode = null; Segment selectedSegment = null; double selectedDistance = Double.POSITIVE_INFINITY; final Segment lowerLeft = new Segment(end, -1.0e-10, -1.0e-10); final Segment upperRight = new Segment(end, +1.0e-10, +1.0e-10); for (AVLTree<Segment>.Node n = sorted.getNotSmaller(lowerLeft); (n != null) && (n.getElement().compareTo(upperRight) <= 0); n = n.getNext()) { segment = (Segment) n.getElement(); final double distance = end.distance(segment.getStart()); if (distance < selectedDistance) { selectedNode = n; selectedSegment = segment; selectedDistance = distance; } } if (selectedDistance > 1.0e-10) { // this is a degenerated loop, it probably comes from a very // tiny region with some segments smaller than the threshold, we // simply ignore it return null; } end = selectedSegment.getEnd(); loop.add(selectedSegment); selectedNode.delete(); } if ((loop.size() == 2) && !open) { // this is a degenerated infinitely thin loop, we simply ignore it return null; } if ((end == null) && !open) { throw new MathInternalError(); } return loop; } }