package org.djutils.draw.line; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Locale; import org.djutils.draw.DrawRuntimeException; import org.djutils.draw.point.Point2d; import org.djutils.exceptions.Throw; /** * Polygon2d.java. Closed PolyLine2d. The actual closing point (which is the same as the starting point) is NOT included in the * super PolyLine2d. The constructors automatically remove the last point if it is a at the same location as the first point. *

* Copyright (c) 2020-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See DJUTILS License. *

* @author Alexander Verbraeck * @author Peter Knoppers */ public class Polygon2d extends PolyLine2d { /** */ private static final long serialVersionUID = 20209999L; /** * Construct a new Polygon2d. * @param x double[]; the x coordinates of the points * @param y double[]; the y coordinates of the points * @throws DrawRuntimeException when any two successive points are equal, or when there are too few points */ public Polygon2d(final double[] x, final double[] y) throws DrawRuntimeException { super(fixClosingPointX(Throw.whenNull(x, "x may not be null"), Throw.whenNull(y, "y may not be null")), fixClosingPointY(x, y)); } /** * Ensure that the last point is not equal to the first. Remove the last point if necessary. * @param x double[]; the x coordinates of the points * @param y double[]; the y coordinates of the points * @return double[]; the x coordinates of the points (possibly a copy with the last element removed) */ private static double[] fixClosingPointX(final double[] x, final double[] y) { if (x.length > 1 && y.length == x.length && x[0] == x[x.length - 1] && y[0] == y[x.length - 1]) { return Arrays.copyOf(x, x.length - 1); } return x; } /** * Ensure that the last point is not equal to the first. Remove the last point if necessary. * @param x double[]; the x coordinates of the points * @param y double[]; the y coordinates of the points * @return double[]; the y coordinates of the points (possibly a copy with the last element removed) */ private static double[] fixClosingPointY(final double[] x, final double[] y) { if (x.length > 1 && y.length == x.length && x[0] == x[x.length - 1] && y[0] == y[x.length - 1]) { return Arrays.copyOf(y, x.length - 1); } return y; } /** * Construct a new Polygon2d. * @param points Point2d[]; array of Point2d objects. * @throws NullPointerException when points is null * @throws DrawRuntimeException when points is too short, or contains successive duplicate points */ public Polygon2d(final Point2d[] points) throws NullPointerException, DrawRuntimeException { this(PolyLine2d.makeArray(points, p -> p.x), PolyLine2d.makeArray(points, p -> p.y)); } /** * Construct a new Polygon2d. * @param point1 Point2d; the first point of the new Polygon2d * @param point2 Point2d; the second point of the new Polygon2d * @param otherPoints Point2d[]; all remaining points of the new Polygon2d (may be null) * @throws NullPointerException when point1 or point2 is null * @throws DrawRuntimeException when point1 is equal to the last point of otherPoints, or any two successive points are * equal */ public Polygon2d(final Point2d point1, final Point2d point2, final Point2d... otherPoints) throws NullPointerException, DrawRuntimeException { super(Throw.whenNull(point1, "point1 may not be null"), Throw.whenNull(point2, "point2 may not be null"), fixClosingPoint(point1, otherPoints)); } /** * Ensure that the last point of otherPoints is not equal to point1. Remove the last point if necessary. * @param point1 Point2d; the first point of a new Polygon2d * @param otherPoints Point2d[]; the remaining points of a new Polygon2d (may be null) * @return Point2d[]; otherPoints (possibly a copy thereof with the last entry removed) */ private static Point2d[] fixClosingPoint(final Point2d point1, final Point2d[] otherPoints) { if (otherPoints == null || otherPoints.length == 0) { return otherPoints; } Point2d[] result = otherPoints; Point2d lastPoint = result[result.length - 1]; if (point1.x == lastPoint.x && point1.y == lastPoint.y) { result = Arrays.copyOf(otherPoints, result.length - 1); lastPoint = result[result.length - 1]; } Throw.when(point1.x == lastPoint.x && point1.y == lastPoint.y, DrawRuntimeException.class, "Before last point and last point are at same location"); return result; } /** * Construct a new Polygon2d from a list of Point2d objects. * @param points List<Point2d>; the list of points * @throws NullPointerException when points is null * @throws DrawRuntimeException when points is too short, or the last two points are at the same location */ public Polygon2d(final List points) throws NullPointerException, DrawRuntimeException { super(fixClosingPoint(true, Throw.whenNull(points, "points may not be null"))); } /** * Ensure that the last point in the list is different from the first point by possibly removing the last point. * @param doNotModifyList boolean; if true; the list of points will not be modified (if the last point is to be removed; the * entire list up to the last point is duplicated) * @param points List<Point2d>; the list of points * @return List<Point2d>; the fixed list * @throws DrawRuntimeException when the (resulting) list is too short, or the before last and last point of points have the * same coordinates */ private static List fixClosingPoint(final boolean doNotModifyList, final List points) throws DrawRuntimeException { Throw.when(points.size() < 2, DrawRuntimeException.class, "Need at least two points"); Point2d firstPoint = points.get(0); Point2d lastPoint = points.get(points.size() - 1); List result = points; if (firstPoint.x == lastPoint.x && firstPoint.y == lastPoint.y) { if (doNotModifyList) { result = new ArrayList<>(points.size() - 1); for (int i = 0; i < points.size() - 1; i++) { result.add(points.get(i)); } } else { result.remove(points.size() - 1); } lastPoint = result.get(result.size() - 1); } Throw.when(firstPoint.x == lastPoint.x && firstPoint.y == lastPoint.y, DrawRuntimeException.class, "Before last point and last point are at same location"); return result; } /** * Construct a new Polygon2d from an iterator that yields Point2d. * @param iterator Iterator<Point2d>; the iterator */ public Polygon2d(final Iterator iterator) { this(fixClosingPoint(false, iteratorToList(Throw.whenNull(iterator, "iterator cannot be null")))); } /** * Create a new Polygon2d, optionally filtering out repeating successive points. * @param filterDuplicates boolean; if true; filter out successive repeated points; otherwise do not filter * @param points Point2d...; the coordinates of the polygon as Point2d * @throws DrawRuntimeException when number of points < 2 */ public Polygon2d(final boolean filterDuplicates, final Point2d... points) throws DrawRuntimeException { this(PolyLine2d.cleanPoints(filterDuplicates, Arrays.stream(points).iterator())); } /** * Create a new Polygon2d, optionally filtering out repeating successive points. * @param filterDuplicates boolean; if true; filter out successive repeated points; otherwise do not filter * @param pointList List<Point2d>; list of the coordinates of the line as Point3d; any duplicate points in this list * are removed (this method may modify the provided list) * @throws DrawRuntimeException when number of non-equal points < 2 */ public Polygon2d(final boolean filterDuplicates, final List pointList) throws DrawRuntimeException { this(PolyLine2d.cleanPoints(filterDuplicates, pointList.iterator())); } /** * Determine if this Polygon2d is convex. Returns bogus result for self-intersecting polygons. Derived from * http://paulbourke.net/geometry/polygonmesh/source2.c * @return boolean; true if this Polygon2d is convex; false if this Polygon2d is concave */ public final boolean isConvex() { int flag = 0; for (int i = 0; i < size(); i++) { int j = (i + 1) % size(); int k = (j + 1) % size(); double z = (getX(j) - getX(i)) * (getY(k) - getY(j)) - (getY(j) - getY(i)) * (getX(k) - getX(j)); if (z < 0) { flag |= 1; } else if (z > 0) { flag |= 2; } if (flag == 3) { return false; } } return flag != 0; } /** * Determine if a point is inside this Polygon. Returns bogus results for self-intersecting polygons. * @param point Point2d; the point * @return boolean; true if the point is inside this polygon, false if the point is outside this polygon. Results are * ill-defined for points on the edges of this Polygon. */ public boolean contains(final Point2d point) { return contains(point.x, point.y); } /** * Determine if a point is inside this Polygon. Returns bogus results for self-intersecting polygons. Derived from * http://paulbourke.net/geometry/polygonmesh/ * @param x double; the x-coordinate of the point * @param y double; the y-coordinate of the point * @return boolean; true if the point is inside this polygon, false if the point is outside this polygon. Results are * ill-defined for points on the edges of this Polygon. */ public boolean contains(final double x, final double y) { if (!getBounds().contains(x, y)) { return false; } int counter = 0; // Unlike Paul Bourke, we initialize prevPoint to the last point of the polygon (so we never have to wrap around) double prevPointX = getX(size() - 1); double prevPointY = getY(size() - 1); for (int i = 0; i < size(); i++) { double curPointX = getX(i); double curPointY = getY(i); // Combined 4 if statements into one; I trust that the java compiler will short-circuit this nicely if (y > Math.min(prevPointY, curPointY) && y <= Math.max(prevPointY, curPointY) && x <= Math.max(prevPointX, curPointX) && prevPointY != curPointY) { double xIntersection = (y - prevPointY) * (curPointX - prevPointX) / (curPointY - prevPointY) + prevPointX; if (prevPointX == curPointX || x <= xIntersection) { counter++; } } prevPointX = curPointX; prevPointY = curPointY; } return counter % 2 != 0; } /** * Compute the surface of this Polygon2d. Sign of the result reflects the winding-ness of this this Polygon2d. If this * Polygon2d self-intersects, the result is bogus. * @return double; the surface of this Polygon2d */ public double surface() { // TODO scale all coordinates back to something local to reduce rounding errors double result = 0; // We initialize previous point to the last point of this Polygon2d to avoid wrapping problems double prevX = getX(size() - 1); double prevY = getY(size() - 1); for (int i = 0; i < size(); i++) { double thisX = getX(i); double thisY = getY(i); result += prevX * thisY - thisX * prevY; prevX = thisX; prevY = thisY; } return result / 2; } /** {@inheritDoc} */ @Override public double getLength() { // Length a polygon is computed by taking the length of the PolyLine and adding the length of the closing segment return super.getLength() + Math.hypot(getX(size() - 1) - getX(0), getY(size() - 1) - getY(0)); } /** {@inheritDoc} */ @Override public LineSegment2d getSegment(final int index) { if (index < size() - 1) { return super.getSegment(index); } Throw.when(index != size() - 1, DrawRuntimeException.class, "index must be in range 0..size() - 1"); return new LineSegment2d(getX(index), getY(index), getX(0), getY(0)); } /** {@inheritDoc} */ @Override public Polygon2d reverse() { return new Polygon2d(super.reverse().getPoints()); } /** {@inheritDoc} */ @Override public final String toExcel() { StringBuffer s = new StringBuffer(); for (int i = 0; i < size(); i++) { s.append(getX(i) + "\t" + getY(i) + "\n"); } s.append(getX(0) + "\t" + getY(0) + "\n"); return s.toString(); } /** {@inheritDoc} */ @Override public final String toPlot() { StringBuffer result = new StringBuffer(); for (int i = 0; i < size(); i++) { result.append(String.format(Locale.US, "%s%.3f,%.3f", 0 == result.length() ? "M" : " L", getX(i), getY(i))); } result.append(String.format(Locale.US, " L%.3f,%.3f", getX(0), getY(0))); result.append("\n"); return result.toString(); } /** {@inheritDoc} */ @Override public final String toString() { return toString("%f", false); } /** {@inheritDoc} */ @Override public String toString(final String doubleFormat, final boolean doNotIncludeClassName) { StringBuilder result = new StringBuilder(); if (!doNotIncludeClassName) { result.append("Polygon2d "); } result.append("[super="); result.append(super.toString(doubleFormat, false)); result.append("]"); return result.toString(); } }