package org.osm2world.core.math; import static java.lang.Math.min; import static org.osm2world.core.math.GeometryUtil.distanceFromLineSegment; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * a non-self-intersecting polygon in the XZ plane */ public class SimplePolygonXZ extends PolygonXZ { /** stores the signed area */ private Double signedArea; /** stores the result for {@link #getArea()} */ private Double area; /** stores the result for {@link #isClockwise()} */ private Boolean clockwise; /** * @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 SimplePolygonXZ(List<VectorXZ> vertexLoop) { super(vertexLoop); assertLoopLength(vertexLoop); assertNotSelfIntersecting(vertexLoop); } private void calculateArea() { this.signedArea = calculateSignedArea(vertexLoop); this.area = Math.abs(signedArea); this.clockwise = signedArea < 0; assertNonzeroArea(); } 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's area */ public double getArea() { if (area == null) { calculateArea(); } return area; } /** returns the centroid (or "barycenter") of the polygon */ public VectorXZ getCentroid() { if (signedArea == null) { calculateArea(); } double xSum = 0, zSum = 0; int numVertices = vertexLoop.size() - 1; for (int i = 0; i < numVertices; i++) { double factor = vertexLoop.get(i).x * vertexLoop.get(i+1).z - vertexLoop.get(i+1).x * vertexLoop.get(i).z; xSum += (vertexLoop.get(i).x + vertexLoop.get(i+1).x) * factor; zSum += (vertexLoop.get(i).z + vertexLoop.get(i+1).z) * factor; } double areaFactor = 1 / (6 * signedArea); return new VectorXZ(areaFactor * xSum, areaFactor * zSum); } /** * returns the largest distance between any pair of vertices * of this polygon */ public double getDiameter() { double maxDistance = 0; for (int i = 1; i < vertexLoop.size() - 1; i++) { for (int j = 0; j < i; j++) { double distance = vertexLoop.get(i).distanceTo(vertexLoop.get(j)); if (distance > maxDistance) { maxDistance = distance; } } } return maxDistance; } /** returns true if the polygon has clockwise orientation */ public boolean isClockwise() { if (area == null) { calculateArea(); } return clockwise; } @Override public boolean isSelfIntersecting() { return false; } @Override public boolean isSimple() { return true; } @Override public SimplePolygonXZ asSimplePolygon() { return this; } /** * @return a {@link PolygonWithHolesXZ} * with this polygon as the outer polygon and no holes */ public PolygonWithHolesXZ asPolygonWithHolesXZ() { return new PolygonWithHolesXZ(this, Collections.<SimplePolygonXZ>emptyList()); } /** * returns this polygon if it is counterclockwise, * or the reversed polygon if it is clockwise. */ public SimplePolygonXZ makeClockwise() { return makeRotationSense(true); } /** * returns this polygon if it is clockwise, * or the reversed polygon if it is counterclockwise. */ public SimplePolygonXZ makeCounterclockwise() { return makeRotationSense(false); } private SimplePolygonXZ makeRotationSense(boolean clockwise) { if (this.isClockwise() ^ clockwise) { return this.reverse(); } else { return this; } } @Override public SimplePolygonXZ reverse() { List<VectorXZ> newVertexLoop = new ArrayList<VectorXZ>(vertexLoop); Collections.reverse(newVertexLoop); return new SimplePolygonXZ(newVertexLoop); } /** * creates a new polygon by adding a shift vector to each vector of this */ public SimplePolygonXZ shift(VectorXZ shiftVector) { List<VectorXZ> newVertexLoop = new ArrayList<VectorXZ>(vertexLoop.size()); newVertexLoop.add(vertexLoop.get(0).add(shiftVector)); for (VectorXZ v : vertexLoop) { if (!v.equals(vertexLoop.get(0))) { newVertexLoop.add(v.add(shiftVector)); } } newVertexLoop.add(newVertexLoop.get(0)); return new SimplePolygonXZ(newVertexLoop); } /** * returns true if the polygon defined by the polygonVertexLoop parameter * contains a given position */ public static boolean contains(List<VectorXZ> polygonVertexLoop, VectorXZ test) { assertLoopProperty(polygonVertexLoop); int i, j; boolean c = false; for (i = 0, j = polygonVertexLoop.size() - 1; i < polygonVertexLoop.size(); j = i++) { if (((polygonVertexLoop.get(i).z > test.z) != (polygonVertexLoop.get(j).z > test.z)) && (test.x < (polygonVertexLoop.get(j).x - polygonVertexLoop.get(i).x) * (test.z - polygonVertexLoop.get(i).z) / (polygonVertexLoop.get(j).z - polygonVertexLoop.get(i).z) + polygonVertexLoop.get(i).x)) c = !c; } return c; } /** * returns true if the polygon contains a given position */ public boolean contains(VectorXZ test) { return SimplePolygonXZ.contains(vertexLoop, test); } /** * returns true if this polygon contains the parameter polygon */ public boolean contains(PolygonXZ p) { //FIXME: it is possible that a polygon contains all vertices of another polygon, but still not the entire polygon for (VectorXZ v : p.getVertices()) { if (!vertexLoop.contains(v) && !this.contains(v)) { return false; } } return true; } /** * returns the distance of a point to the segments this polygon. * Note that the distance can be > 0 even if the polygon contains the point */ public double distanceToSegments(VectorXZ p) { double minDistance = Double.MAX_VALUE; for (LineSegmentXZ s : getSegments()) { minDistance = min(minDistance, distanceFromLineSegment(p, s)); } return minDistance; } /** * returns a different polygon that is constructed from this polygon * by removing all vertices where this has an angle close to 180° * (i.e. where removing the vertex does not change the polygon very much). */ public SimplePolygonXZ getSimplifiedPolygon() { boolean[] delete = new boolean[size()]; int deleteCount = 0; for (int i = 0; i < size(); i++) { VectorXZ segmentBefore = getVertex(i).subtract(getVertexBefore(i)); VectorXZ segmentAfter = getVertexAfter(i).subtract(getVertex(i)); double dot = segmentBefore.normalize().dot(segmentAfter.normalize()); if (Math.abs(dot - 1) < 0.05) { delete[i] = true; deleteCount += 1; } } if (deleteCount == 0 || deleteCount > size() - 3) { return this; } else { List<VectorXZ> newVertexList = new ArrayList<VectorXZ>(getVertices()); //iterate backwards => it doesn't matter when higher indices change for (int i = size() - 1; i >= 0; i--) { if (delete[i]) { newVertexList.remove(i); } } newVertexList.add(newVertexList.get(0)); return new SimplePolygonXZ(newVertexList); } } /** * calculates the area of a planar non-self-intersecting polygon. * The result is negative if the polygon is clockwise. */ private static double calculateSignedArea(List<VectorXZ> vertexLoop) { double sum = 0f; for (int i = 0; i + 1 < vertexLoop.size(); i++) { sum += vertexLoop.get(i).x * vertexLoop.get(i+1).z; sum -= vertexLoop.get(i+1).x * vertexLoop.get(i).z; } return sum / 2; } /** * @throws InvalidGeometryException if the vertex loop is too short */ private static void assertLoopLength(List<VectorXZ> vertexLoop) { if (vertexLoop.size() <= 3) { throw new InvalidGeometryException( "polygon needs more than 2 vertices\n" + "Polygon vertex loop: " + vertexLoop); } } /** * @throws InvalidGeometryException if the vertex loop is self-intersecting */ private static void assertNotSelfIntersecting(List<VectorXZ> vertexLoop) { if (isSelfIntersecting(vertexLoop)) { throw new InvalidGeometryException( "polygon must not be self-intersecting\n" + "Polygon vertices: " + vertexLoop); } } /** * @throws InvalidGeometryException if area is 0 */ private void assertNonzeroArea() { if (area == 0) { throw new InvalidGeometryException( "a polygon's area must be positive, but it's " + area + " for this polygon.\nThis problem can be caused " + "by broken polygon data or imprecise calculations" + "\nPolygon vertices: " + vertexLoop); } } }