package org.osm2world.core.math; import static org.osm2world.core.math.GeometryUtil.distanceFromLineSegment; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.TreeSet; public class PolygonXZ { /** polygon vertices; first and last vertex are equal */ protected final List<VectorXZ> vertexLoop; /** * @param vertexLoop vertices defining the polygon; * first and last vertex must be equal * @throws InvalidGeometryException if the polygon is self-intersecting * or produces invalid area calculation results */ public PolygonXZ(List<VectorXZ> vertexLoop) { assertLoopProperty(vertexLoop); this.vertexLoop = vertexLoop; } /** * returns the number of vertices in this polygon. * The duplicated first/last vertex is <em>not</em> counted twice, * so the result is equivalent to {@link #getVertices()}.size(). */ public int size() { return vertexLoop.size()-1; } /** * returns the polygon's vertices. * Unlike {@link #getVertexLoop()}, there is no duplication * of the first/last vertex. */ public List<VectorXZ> getVertices() { return vertexLoop.subList(0, vertexLoop.size()-1); } /** * returns the polygon's vertices. First and last vertex are equal. */ public List<VectorXZ> getVertexLoop() { return vertexLoop; } /** * returns a collection that contains all vertices of this polygon * at least once. Can be used if you don't care about whether the first/last * vector is duplicated. */ public List<VectorXZ> getVertexCollection() { return vertexLoop; } /** * returns the vertex at a position in the vertex sequence */ public VectorXZ getVertex(int index) { assert 0 <= index && index < vertexLoop.size()-1; return vertexLoop.get(index); } /** * returns the successor of the vertex at a position in the vertex sequence. * This wraps around the vertex loop, so the successor of the last vertex * is the first vertex. */ public VectorXZ getVertexAfter(int index) { assert 0 <= index && index < vertexLoop.size()-1; return getVertex((index + 1) % size()); } /** * returns the predecessor of the vertex at a position in the vertex sequence. * This wraps around the vertex loop, so the predecessor of the first vertex * is the last vertex. */ public VectorXZ getVertexBefore(int index) { assert 0 <= index && index < vertexLoop.size()-1; return getVertex((index + size() - 1) % size()); } public List<LineSegmentXZ> getSegments() { List<LineSegmentXZ> segments = new ArrayList<LineSegmentXZ>(vertexLoop.size()); for (int i=0; i+1 < vertexLoop.size(); i++) { segments.add(new LineSegmentXZ(vertexLoop.get(i), vertexLoop.get(i+1))); } return segments; } /** * returns the polygon segment with minimum distance to a given point */ public LineSegmentXZ getClosestSegment(VectorXZ point) { LineSegmentXZ closestSegment = null; double closestDistance = Double.MAX_VALUE; for (LineSegmentXZ segment : getSegments()) { double distance = distanceFromLineSegment(point, segment); if (distance < closestDistance) { closestSegment = segment; closestDistance = distance; } } return closestSegment; } /** * returns true if there is an intersection between this polygon * and the line segment defined by the parameter */ public boolean intersects(VectorXZ segmentP1, VectorXZ segmentP2) { //TODO: (performance): passing "vector TO second point", rather than point2, would avoid having to calc it here - and that information could be reused for all comparisons involving the segment for (int i=0; i+1<vertexLoop.size(); i++) { VectorXZ intersection = GeometryUtil.getTrueLineSegmentIntersection( segmentP1, segmentP2, vertexLoop.get(i), vertexLoop.get(i+1) ); if (intersection != null) { return true; } } return false; } public boolean intersects(LineSegmentXZ lineSegment) { return intersects(lineSegment.p1, lineSegment.p2); } /** * returns true if there is an intersection between this polygon's * and the parameter polygon's sides */ public boolean intersects(PolygonXZ outlinePolygonXZ) { //TODO (performance): currently performs pairwise intersection checks for sides of this and other - this might not be the fastest method for (int i=0; i+1<vertexLoop.size(); i++) { if (outlinePolygonXZ.intersects(vertexLoop.get(i), vertexLoop.get(i+1))) { return true; } } return false; } public Collection<LineSegmentXZ> intersectionSegments( LineSegmentXZ lineSegment) { List<LineSegmentXZ> intersectionSegments = new ArrayList<LineSegmentXZ>(); for (LineSegmentXZ polygonSegment : getSegments()) { VectorXZ intersection = GeometryUtil.getTrueLineSegmentIntersection( lineSegment.p1, lineSegment.p2, polygonSegment.p1, polygonSegment.p2 ); if (intersection != null) { intersectionSegments.add(polygonSegment); } } return intersectionSegments; } public List<VectorXZ> intersectionPositions( LineSegmentXZ lineSegment) { List<VectorXZ> intersectionPositions = new ArrayList<VectorXZ>(); for (int i=0; i+1<vertexLoop.size(); i++) { VectorXZ intersection = GeometryUtil.getTrueLineSegmentIntersection( lineSegment.p1, lineSegment.p2, vertexLoop.get(i), vertexLoop.get(i+1) ); if (intersection != null) { intersectionPositions.add(intersection); } } return intersectionPositions; } /** * returns whether this polygon is self-intersecting */ public boolean isSelfIntersecting() { return isSelfIntersecting(vertexLoop); } /** * returns true if the polygon defined by the polygonVertexLoop parameter * is self-intersecting.<br/> * The Code is based on Shamos-Hoey's algorithm * * TODO: if the end vertex of two line segments are the same the * polygon is never considered as self intersecting on purpose. * This behavior should probably be reconsidered, but currently * left as is due to frequent cases of such polygons. */ public static boolean isSelfIntersecting(List<VectorXZ> polygonVertexLoop) { final class Event { boolean start; LineSegmentXZ line; Event(LineSegmentXZ l, boolean s) { this.line = l; this.start = s; } } // we have n-1 vertices as the first and last vertex are the same final int segments = polygonVertexLoop.size()-1; // generate an array of input events associated with their line segments Event[] events = new Event[segments*2]; for (int i = 0; i < segments; i++) { VectorXZ v1 = polygonVertexLoop.get(i); VectorXZ v2 = polygonVertexLoop.get(i+1); // Create a line where the first vertex is left (or above) the second vertex LineSegmentXZ line; if ((v1.x < v2.x) || ((v1.x == v2.x) && (v1.z < v2.z))) { line = new LineSegmentXZ(v1, v2); } else { line = new LineSegmentXZ(v2, v1); } events[2*i] = new Event(line, true); events[2*i+1] = new Event(line, false); } // sort the input events according to the x-coordinate, then z-coordinate Arrays.sort(events, new Comparator<Event>() { public int compare(Event e1, Event e2) { VectorXZ v1 = e1.start? e1.line.p1 : e1.line.p2; VectorXZ v2 = e2.start? e2.line.p1 : e2.line.p2; if (v1.x < v2.x) return -1; else if (v1.x == v2.x) { if (v1.z < v2.z) return -1; else if (v1.z == v2.z) return 0; } return 1; }}); // A TreeSet, used for the sweepline algorithm TreeSet<LineSegmentXZ> sweepLine = new TreeSet<LineSegmentXZ>(new Comparator<LineSegmentXZ>() { public int compare(LineSegmentXZ l1, LineSegmentXZ l2) { VectorXZ v1 = l1.p1; VectorXZ v2 = l2.p1; if (v1.z < v2.z) return -1; else if (v1.z == v2.z) { if (v1.x < v2.x) return -1; else if (v1.x == v2.x) { if (l1.p2.z < l2.p2.z) return -1; else if (l1.p2.z == l2.p2.z) { if (l1.p2.x < l2.p2.x) return -1; else if (l1.p2.x == l2.p2.x) return 0; } } } return 1; }}); // start the algorithm by visiting every event for (Event event : events) { LineSegmentXZ line = event.line; if (event.start) { // if it is a startpoint LineSegmentXZ lower = sweepLine.lower(line); LineSegmentXZ higher = sweepLine.higher(line); sweepLine.add(line); if (lower != null) { if (lower.intersects(line.p1, line.p2)) { return true; } } if (higher != null) { if (higher.intersects(line.p1, line.p2)) { return true; } } } else { // if it is an endpoint LineSegmentXZ lower = sweepLine.lower(line); LineSegmentXZ higher = sweepLine.higher(line); sweepLine.remove(line); if ((lower == null) || (higher == null)) { continue; } if (lower.intersects(higher.p1, higher.p2)) { return true; } } } return false; } /** * checks whether this polygon is simple */ public boolean isSimple() { try { this.asSimplePolygon(); return true; } catch (InvalidGeometryException e) { return false; } } /** * returns a polygon with the coordinates of this polygon * that is an instance of {@link SimplePolygonXZ}. * Only works if it actually {@link #isSimple()}! */ public SimplePolygonXZ asSimplePolygon() { return new SimplePolygonXZ(vertexLoop); } /** * returns a triangle with the same vertices as this polygon. * Requires that the polygon is triangular! */ public TriangleXZ asTriangleXZ() { if (vertexLoop.size() != 4) { throw new InvalidGeometryException("attempted creation of triangle " + "from polygon with vertex loop of size " + vertexLoop.size() + ": " + vertexLoop); } else { return new TriangleXZ( vertexLoop.get(0), vertexLoop.get(1), vertexLoop.get(2)); } } public PolygonXYZ xyz(final double y) { return new PolygonXYZ(VectorXZ.listXYZ(vertexLoop, y)); } public PolygonXZ reverse() { List<VectorXZ> newVertexLoop = new ArrayList<VectorXZ>(vertexLoop); Collections.reverse(newVertexLoop); return new PolygonXZ(newVertexLoop); } /** * returns the average of all vertex coordinates. * The result is not necessarily contained by this polygon. */ public VectorXZ getCenter() { double x=0, z=0; int numberVertices = vertexLoop.size()-1; for (VectorXZ vertex : getVertices()) { x += vertex.x / numberVertices; z += vertex.z / numberVertices; /* single division per coordinate after loop would be faster, * but might cause numbers to get too large */ } return new VectorXZ(x, z); } /** * returns the length of the polygon's outline. * (This does <em>not</em> return the number of vertices, * but the sum of distances between subsequent nodes.) */ public double getOutlineLength() { double length = 0; for (int i = 0; i+1 < vertexLoop.size(); i++) { length += VectorXZ.distance(vertexLoop.get(i), vertexLoop.get(i+1)); } return length; } /** * returns true if the other polygon has the same vertices in the same order, * possibly with a different start vertex */ public boolean isEquivalentTo(PolygonXZ other) { if (vertexLoop.size() != other.vertexLoop.size()) { return false; } List<VectorXZ> ownVertices = getVertices(); List<VectorXZ> otherVertices = other.getVertices(); for (int offset = 0; offset < ownVertices.size(); offset ++) { boolean matches = true; for (int i = 0; i < ownVertices.size(); i++) { int iWithOffset = (i + offset) % ownVertices.size(); if (!otherVertices.get(i).equals(ownVertices.get(iWithOffset))) { matches = false; break; } } if (matches) { return true; } } return false; } /** * checks that the first and last vertex of the vertex list are equal. * @throws IllegalArgumentException if first and last vertex aren't equal * (this is usually a programming error, * therefore InvalidGeometryException is not used) */ protected static void assertLoopProperty(List<VectorXZ> vertexLoop) { if (!vertexLoop.get(0).equals(vertexLoop.get(vertexLoop.size() - 1))) { throw new IllegalArgumentException("first and last vertex must be equal\n" + "Polygon vertices: " + vertexLoop); } } @Override public String toString() { return vertexLoop.toString(); } }