/* * Copyright (c) 2016 Fraunhofer IGD * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * Fraunhofer IGD <http://www.igd.fraunhofer.de/> */ package de.fhg.igd.geom.shape; import java.util.ArrayList; import java.util.List; import de.fhg.igd.geom.BoundingBox; import de.fhg.igd.geom.Extent; import de.fhg.igd.geom.Point2D; import de.fhg.igd.geom.algorithm.sweepline.Point2DEvent; import de.fhg.igd.geom.algorithm.sweepline.Point2DEventQueue; import de.fhg.igd.geom.algorithm.sweepline.SweepLine; import de.fhg.igd.geom.algorithm.sweepline.SweepLineSegment; /** * This class describes closed 2D polylines with straight segments. The * underlying Point2D sequence may be explicitly closed or not. This shouldn't * make a difference for spatial computations, but may affect some operations. * * @author Thorsten Reitz */ public class Polygon extends Shape { /** * The class' serial version UID */ private static final long serialVersionUID = 3575023575724612793L; /** * The BoundingBox of this Polygon */ private BoundingBox boundingBox; /** * Default Constructor. */ public Polygon() { super(); } /** * Constructor with zero ID and a given Point2D[]. * * @param points An Array of Point2D objects. */ public Polygon(Point2D[] points) { this(); this.setPoints(points); } // functional methods ...................................................... /** * @return true if the given Point2D is inside this Polygon. * @see #contains(double, double) * @param p2d the Point that may be inside of this Polygon */ private boolean contains(Point2D p2d) { return contains(p2d.getX(), p2d.getY()); } /** * @return true if the given point is inside this Polygon. Uses the standard * contains algorithm that checks how often a ray projected form the * point parallel to the y axis cuts a segment of the polygon. If * the number is even, it's outide, if it is odd, it's inside. * @param x the x ordinate of the point that may be inside of this Polygon * @param y the y ordinate of the point that may be inside of this Polygon */ private boolean contains(double x, double y) { // First, make sure that this Polygon is at least a triangle, otherwise // it has no area. Also, test if the point is within this Polygon's // Extent. if (this.getPoints().length <= 2 || this.inExtent(x, y) == false) { return false; } int hits = 0; int points_length = this.getPoints().length; // save the coordinates of the last point. double lastx = this.getPoints()[points_length - 1].getX(); double lasty = this.getPoints()[points_length - 1].getY(); // variables for the current point. double curx; double cury; // Now, test all edges of the polygon. for (int i = 0; i < points_length; lastx = curx, lasty = cury, i++) { curx = this.getPoints()[i].getX(); cury = this.getPoints()[i].getY(); if (cury == lasty) { continue; } double leftx; if (curx < lastx) { if (x >= lastx) { continue; } leftx = curx; } else { if (x >= curx) { continue; } leftx = lastx; } double test1, test2; if (cury < lasty) { if (y < cury || y >= lasty) { continue; } if (x < leftx) { hits++; continue; } test1 = x - curx; test2 = y - cury; } else { if (y < lasty || y >= cury) { continue; } if (x < leftx) { hits++; continue; } test1 = x - lastx; test2 = y - lasty; } if (test1 < (test2 / (lasty - cury) * (lastx - curx))) { hits++; } } return ((hits & 1) != 0); } /** * Checks if the extent lies within the polygon * * @param e the extent * @return true if e lies within the polygon */ public boolean contains(Extent e) { return this.contains(e.getMinX(), e.getMinY()) && this.contains(e.getMaxX(), e.getMinY()) && this.contains(e.getMinX(), e.getMaxY()) && this.contains(e.getMaxX(), e.getMaxY()); } /** * Adds a Polygon to a Point2DEventQueue queue. * * @param p the Polygon to add * @param q the queue to add to * @return a list of line2d objects which are vertical and which could not * be added. */ private static List<Line2D> addToPoint2DEventQueue(Polygon p, Point2DEventQueue q) { List<Line2D> verticalLines = new ArrayList<Line2D>(); // add line segments for (int i = 0; i < p.getPoints().length - 1; ++i) { Point2D p1 = p.getPoints()[i]; Point2D p2 = p.getPoints()[i + 1]; if (p1.equals(p2)) { // don't add degenerated lines continue; } Line2D l = new Line2D(new Point2D[] { p1, p2 }); if (p1.getX() == p2.getX()) { // save this vertical line, but don't add it verticalLines.add(l); continue; } q.add(l, p); } return verticalLines; } /** * Tests if this Polygon intersects another one * * @param p the other Polygon * @return true if this Polygon intersects the other one, false otherwise */ private boolean intersects(Polygon p) { // initialize EventQueue and add this Polygon and p Point2DEventQueue eq = new Point2DEventQueue(); List<Line2D> v1 = addToPoint2DEventQueue(this, eq); List<Line2D> v2 = addToPoint2DEventQueue(p, eq); // check vertical lines separately for (Line2D vl : v1) { for (int i = 0; i < p.getPoints().length - 1; ++i) { Point2D p1 = p.getPoints()[i]; Point2D p2 = p.getPoints()[i + 1]; if (doLinesIntersect(vl.getPoints()[0], vl.getPoints()[1], p1, p2)) { return true; } } } for (Line2D vl : v2) { for (int i = 0; i < this.getPoints().length - 1; ++i) { Point2D p1 = this.getPoints()[i]; Point2D p2 = this.getPoints()[i + 1]; if (doLinesIntersect(vl.getPoints()[0], vl.getPoints()[1], p1, p2)) { return true; } } } // initialize Sweep-line SweepLine sl = new SweepLine(); while (eq.size() > 0) { Point2DEvent e = eq.remove(); if (e.isLeft()) { Line2D sege = e.getLineSegment(); SweepLineSegment sls = sl.add(e); // get elements above and below sege SweepLineSegment above = sl.getAboveSegment(sls); SweepLineSegment below = sl.getBelowSegment(sls); if (above != null) { if (sls.getPolygon() != above.getPolygon()) { // check for intersection between sege and the above // segment Line2D sega = above.getLine(); if (doLinesIntersect(sege.getPoints()[0], sege.getPoints()[1], sega.getPoints()[0], sega.getPoints()[1])) { return true; } } } if (below != null) { if (sls.getPolygon() != below.getPolygon()) { // check for intersection between sege and the above // segment Line2D segb = below.getLine(); if (doLinesIntersect(sege.getPoints()[0], sege.getPoints()[1], segb.getPoints()[0], segb.getPoints()[1])) { return true; } } } } else { SweepLineSegment sls = sl.get(e); // get elements above and below sege SweepLineSegment above = sl.getAboveSegment(sls); SweepLineSegment below = sl.getBelowSegment(sls); // remove e from sweep-line sl.remove(sls); // check for intersection between the segments above and below if (above != null && below != null) { if (above.getPolygon() != below.getPolygon()) { Line2D sega = above.getLine(); Line2D segb = below.getLine(); if (doLinesIntersect(sega.getPoints()[0], sega.getPoints()[1], segb.getPoints()[0], segb.getPoints()[1])) { return true; } } } } } return false; } /** * Tests if this Polygon completely contains another one. First it checks * for intersection (because the two Polygons must not intersect). Then it * checks if this Polygon contains at least one point (actually the first * one) of the other Polygon. * * @param p the other Polygon * @return true if this Polygon completely contains p, false otherwise */ public boolean contains(Polygon p) { if (this.intersects(p)) { return false; } return this.contains(p.points[0]); } /** * <p> * This method will test if there is an intersection point between the * vectors defined by (a1,a2) and (b1,b2). If there is none, it will return * null, otherwise it will return the parameter (usually called "lambda") * where the intersection point can be found on the first line. * </p> * <p> * <b>Attention</b>: This method may also return lambda values lower than * 0.0 or greater than 1.0. In this case the intersection point lies outside * the first line! * </p> * <p> * <b>Attention</b>: If the result value is * <code>0.0 <= lambda <= 1.0</code> this does not mean that the * intersection point lies on both lines in all cases. This method * intersects vectors and so the intersection point may lie on the first * line but not on the second one. If you want to make sure the intersection * point lies on both lines, always call this method as follows: * </p> * * <pre> * Double lambda1 = findIntersectionParameter(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y); * if (lambda1 == null || lambda1 < 0.0 || lambda1 > 1.0) { * // there is no intersection point! * } * Double lambda2 = findIntersectionParameter(b1x, b1y, b2x, b2y, a1x, a1y, a2x, a2y); * if (lambda1 == null || lambda1 < 0.0 || lambda1 > 1.0) { * // there is no intersection point! * } * //there is an intersection point: * double dx = a2x - a1x; * double dy = a2y - a1y; * Point2D intersectionPoint = new Point2D(a1x + lambda * dx, a1y + lambda * dy); * </pre> * * @param a1x the x ordinate of the first point of the first line * @param a1y the y ordinate of the first point of the first line * @param a2x the x ordinate of the second point of the first line * @param a2y the y ordinate of the second point of the first line * @param b1x the x ordinate first point of the second line * @param b1y the y ordinate first point of the second line * @param b2x the x ordinate second point of the second line * @param b2y the y ordinate second point of the second line * @return lambda */ private static Double findIntersectionParameter(double a1x, double a1y, double a2x, double a2y, double b1x, double b1y, double b2x, double b2y) { double abx = a2x - a1x; double aby = a2y - a1y; double bbx = b2x - b1x; double bby = b2y - b1y; // calculate first Determinant value: double D1 = abx * bby - aby * bbx; // if the determinant is not 0, there is an intersection point and thus, // a solution to the LGS. We do not check againt exaclty 0 since // this could lead to false negatives with double values. if (Math.abs(D1) > 0.0000001) { return ((b1x - a1x) * bby - (b1y - a1y) * bbx) / D1; } return null; } /** * <p> * This method will test if there is an intersection point between the lines * defined by (a1,a2) and (b1,b2). If there is none, it will return null, * otherwise it will return the coordinate of the intersection point. * </p> * <p> * Other that * {@link #findIntersectionParameter(double, double, double, double, double, double, double, double)} * this method makes sure the intersection point lies on the given lines. * </p> * * @param a1 the first point of the first line * @param a2 the second point of the first line * @param b1 the first point of the second line * @param b2 the second point of the second line * @return true if there is an intersection */ private static boolean doLinesIntersect(Point2D a1, Point2D a2, Point2D b1, Point2D b2) { double a1x = a1.getX(); double a1y = a1.getY(); double a2x = a2.getX(); double a2y = a2.getY(); double b1x = b1.getX(); double b1y = b1.getY(); double b2x = b2.getX(); double b2y = b2.getY(); Double s = findIntersectionParameter(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y); if (s == null || s < 0.0 || s > 1.0) { return false; } // double check the other line Double s2 = findIntersectionParameter(b1x, b1y, b2x, b2y, a1x, a1y, a2x, a2y); if (s2 == null || s2 < 0.0 || s2 > 1.0) { return false; } return true; } /** * This method tests if a given point falls within this Polygon's Extent. * * @param x the x ordinate of the point * @param y the y ordinate * @return true if the point falls within this Polygon's Extent */ private boolean inExtent(double x, double y) { if (x >= this.getBoundingBox().getMinX() && x <= this.getBoundingBox().getMaxX() && y >= this.getBoundingBox().getMinY() && y <= this.getBoundingBox().getMaxY()) { return true; } return false; } /** * Converts this Polygon to an AWT Polygon. Only suitable for output, is not * null-safe! * * @param scale_x the scale factor in x direction * @param scale_y the scale factor in y direction * @param offset the offset to be added to the converted Polygon * @return the converted Polygon */ public java.awt.Polygon toAWTPolygon(double scale_x, double scale_y, Point2D offset) { double offset_x = 0; double offset_y = 0; if (offset != null) { offset_x = offset.getX(); offset_y = offset.getY(); } java.awt.Polygon poly = new java.awt.Polygon(); int[] xpoints = new int[this.getPoints().length]; int[] ypoints = new int[this.getPoints().length]; for (int i = 0; i < this.getPoints().length; i++) { xpoints[i] = (int) ((this.getPoints()[i].getX() - offset_x) * scale_x); ypoints[i] = (int) ((this.getPoints()[i].getY() - offset_y) * scale_y); } poly.xpoints = xpoints; poly.ypoints = ypoints; poly.npoints = this.getPoints().length; return poly; } /** * @return The BoundingBox for this Polygon. Takes into account possible * Transformations. */ @Override public BoundingBox getBoundingBox() { if (this.boundingBox == null) { this.boundingBox = BoundingBox.compute(this.points); } return this.boundingBox; } @Override public void setPoints(Point2D[] points) { super.setPoints(points); boundingBox = null; } /** * @see Object#equals(Object) */ @Override public boolean equals(Object o) { if (!super.equals(o)) { return false; } if (this == o) { return true; } if (!(o instanceof Polygon)) { return false; } return true; } /** * @see Object#hashCode() */ @Override public int hashCode() { return super.hashCode(); } /** * standard toString() method. Contains call to super.toString(). * * @see Object#toString() */ @Override public String toString() { StringBuilder buffer = new StringBuilder(); buffer.append("Polygon["); buffer.append(super.toString()); buffer.append("]"); return buffer.toString(); } }