/******************************************************************************* * Copyright (c) 2011, 2015 itemis AG and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Alexander Nyßen (itemis AG) - initial API and implementation * Matthias Wienand (itemis AG) - contribution for Bugzilla #355997 * *******************************************************************************/ package org.eclipse.gef.geometry.planar; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.Set; import org.eclipse.gef.geometry.internal.utils.PointListUtils; import org.eclipse.gef.geometry.internal.utils.PrecisionUtils; /** * Represents the geometric shape of a convex polygon. * * Note that while all manipulations (e.g. within shrink, expand) within this * class are based on double precision, all comparisons (e.g. within contains, * intersects, equals, etc.) are based on a limited precision (with an accuracy * defined within {@link PrecisionUtils}) to compensate for rounding effects. * * @author anyssen * @author mwienand * */ public class Polygon extends AbstractPointListBasedGeometry<Polygon> implements IShape { /** * Pair of {@link Line} segment and integer counter to count segments of * {@link Polygon}s. */ private class SegmentCounter { public Line segment; public int count; public SegmentCounter(Line segment, int count) { this.segment = segment; this.count = count; } } /** * List of {@link SegmentCounter}s to count segments of {@link Polygon}s. */ private class SegmentList { public ArrayList<SegmentCounter> segmentCounterList; public SegmentList() { segmentCounterList = new ArrayList<>(); } public SegmentCounter find(Line segment) { for (SegmentCounter i : segmentCounterList) { if (segment.equals(i.segment)) { return i; } } // segment not in list, create new segment counter for it SegmentCounter newSegCounter = new SegmentCounter(segment, 0); segmentCounterList.add(newSegCounter); return newSegCounter; } } private static Polygon clipEar(Polygon p, int[] ear, ArrayList<Polygon> ears) { Point[] points = p.getPoints(); ears.add(new Polygon(points[ear[0]], points[ear[1]], points[ear[2]])); return new Polygon(getPointsWithout(points, ear[1])); } /** * Searches the given list of {@link Point}s for a vertex that starts an * ear. An ear is a list of 3 vertices which build up a triangle that lies * inside the {@link Polygon} respective to the list of {@link Point}s and * can be clipped out of it so that the remaining {@link Polygon} remains * simple. * * @param points * @return */ private static int[] findEarVertex(Polygon p) { Point[] points = p.getPoints(); for (int start = 0; start < points.length; start++) { int mid = start == points.length - 1 ? 0 : start + 1; int end = start == points.length - 2 ? 0 : start == points.length - 1 ? 1 : start + 2; if (p.contains(new Line(points[start], points[end]))) { return new int[] { start, mid, end }; } } // this should never happen (for simple polygons) return null; } private static Point[] getPointsWithout(Point[] points, int... indicesToRemove) { Point[] rest = new Point[points.length - indicesToRemove.length]; Arrays.sort(indicesToRemove); for (int i = 0, j = 0; i < indicesToRemove.length; i++) { for (int r = j; r < indicesToRemove[i]; r++) { rest[r - i] = points[r]; } j = indicesToRemove[i] + 1; } for (int i = indicesToRemove[indicesToRemove.length - 1] + 1; i < points.length; i++) { rest[i - indicesToRemove.length] = points[i]; } return rest; } /** * Clips exactly one ear off of the given {@link Polygon} and adds it to the * list of ears. If the resulting {@link Polygon} is a triangle, this is * added to the list of ears, too. Otherwise, the method recurses. * * @param p * @param ears */ private static void triangulate(Polygon p, ArrayList<Polygon> ears) { if (p == null) { throw new IllegalArgumentException( "The given Polygon may not be null."); } if (ears == null) { throw new IllegalArgumentException( "The given ear-list may not be null."); } if (p.points.length < 3) { throw new IllegalArgumentException( "The given Polygon may not have less than three vertices."); } if (p.points.length == 3) { ears.add(p.getCopy()); return; } int[] ear = findEarVertex(p); Polygon rest = clipEar(p, ear, ears); // recurse triangulate(rest, ears); } private static final long serialVersionUID = 1L; /** * Constructs a new {@link Polygon} from a even-numbered sequence of * coordinates. * * @param coordinates * an alternating, even-numbered sequence of x and y coordinates, * representing the {@link Point}s from which the {@link Polygon} * is to be created * @see #Polygon(Point...) */ public Polygon(double... coordinates) { super(coordinates); } /** * Constructs a new {@link Polygon} from the given sequence of {@link Point} * s. The {@link Polygon} that is created will be automatically closed, i.e. * it will not only contain a segment between succeeding points of the * sequence but as well back from the last to the first point. * * @param points * a sequence of points, from which the {@link Polygon} is to be * created. */ public Polygon(Point... points) { super(points); } /** * Assures that this {@link Polygon} is simple, i.e. it does not have any * self-intersections. We do not need to test for voids as they are not * considered in the interpretation of the {@link Polygon}'s {@link Point}s. * * If the {@link Polygon} does not have at least three vertices, a * {@link IllegalStateException} is thrown. * * The edges are added to the {@link Polygon} one after the other. If a * self-intersection is found an {@link IllegalStateException} is thrown. */ private void assureSimplicity() { if (points.length < 3) { throw new IllegalStateException( "A polygon can only be constructed of at least 3 vertices."); } for (Line e1 : getOutlineSegments()) { for (Line e2 : getOutlineSegments()) { if (!e1.getP1().equals(e2.getP1()) && !e1.getP2().equals(e2.getP1()) && !e1.getP1().equals(e2.getP2()) && !e1.getP2().equals(e2.getP2())) { if (e1.touches(e2)) { throw new IllegalStateException( "Only simple polygons allowed. A polygon without any self-intersections is considered to be simple. This polygon is not simple."); } } } } } /** * Checks whether the point that is represented by its x- and y-coordinates * is contained within this {@link Polygon}. * * @param x * the x-coordinate of the point to test * @param y * the y-coordinate of the point to test * @return <code>true</code> if the point represented by its coordinates if * contained within this {@link Polygon}, <code>false</code> * otherwise */ public boolean contains(double x, double y) { return contains(new Point(x, y)); } @Override public boolean contains(IGeometry g) { if (g instanceof Line) { return contains((Line) g); } else if (g instanceof Polygon) { return contains((Polygon) g); } else if (g instanceof Polyline) { return contains((Polyline) g); } else if (g instanceof Rectangle) { return contains((Rectangle) g); } return ShapeUtils.contains(this, g); } /** * Checks whether the given {@link Line} is fully contained within this * {@link Polygon}. * * @param line * The {@link Line} to test for containment * @return <code>true</code> if the given {@link Line} is fully contained, * <code>false</code> otherwise */ public boolean contains(Line line) { // quick rejection test: if the end points are not contained, the line // may not be contained if (!contains(line.getP1()) || !contains(line.getP2())) { return false; } Set<Double> intersectionParams = new HashSet<>(); for (Line seg : getOutlineSegments()) { Point poi = seg.getIntersection(line); if (poi != null) { intersectionParams.add(line.getParameterAt(poi)); } } if (intersectionParams.size() <= 1) { return true; } Double[] poiParams = intersectionParams.toArray(new Double[] {}); Arrays.sort(poiParams, new Comparator<Double>() { @Override public int compare(Double t, Double u) { double d = t - u; return d < 0 ? -1 : d > 0 ? 1 : 0; } }); // check the points between the intersections for containment if (!contains(line.get(poiParams[0] / 2))) { return false; } for (int i = 0; i < poiParams.length - 1; i++) { if (!contains(line.get((poiParams[i] + poiParams[i + 1]) / 2))) { return false; } } return contains(line.get((poiParams[poiParams.length - 1] + 1) / 2)); } /** * @see IGeometry#contains(Point) */ @Override public boolean contains(Point p) { if (points.length == 0) { return false; } else if (points.length == 1) { return points[0].equals(p); } else if (points.length == 2) { return new Line(points[0], points[1]).contains(p); } else { // perform a quick rejection test via the bounds Rectangle bounds = getBounds(); if (!bounds.contains(p)) { return false; } /* * choose a point p' outside the polygon: */ Point pp = new Point(p.x + bounds.getWidth() + 1, p.y); /* * construct a line from p to p': */ Line testLine = new Line(p, pp); /* * compute if there is an even or odd number of intersection of the * test line with all sides of the polygon; handle the special case * the point is located on one of the sides */ boolean odd = false; for (int i = 0; i < points.length; i++) { Point p1 = points[i]; Point p2 = points[i + 1 < points.length ? i + 1 : 0]; // check whether the point is located on the current side if (p1.equals(p2)) { if (p1.equals(p)) { return true; } continue; } Line segment = new Line(p1, p2); if (segment.contains(p)) { return true; } /* * check if one of the two vertices of the link line is * contained by the test line. the containment test is done to * handle special cases where the intersection has to be counted * appropriately. * * 1) if the vertex is above (greater y-component) the other * point of the line, it is counted once. * * 2) if the vertex is below (lower y-component) or on the same * height as the other point of the line, it is omitted. */ boolean p1contained = testLine.contains(p1); boolean p2contained = testLine.contains(p2); // TODO: is imprecision needed for this test? if (p1contained || p2contained) { if (p1contained) { if (p1.y > p2.y) { odd = !odd; } } if (p2contained) { if (p2.y > p1.y) { odd = !odd; } } continue; } /* * check the current link for an intersection with the test * line. if there is an intersection, change state. * * Special case error prevention: If the point in question (p) * is very near to an edge of and inside the polygon, it can * happen, that the edge.contains(p) is false, but an * intersection can be found, although p is right to the edge. * To prevent a wrong state change, the intersection has to be * right-of p. */ Point poi = testLine.getIntersection(segment); if (poi != null && poi.x >= p.x) { odd = !odd; } } return odd; } } /** * Checks whether the given {@link Polygon} is fully contained within this * {@link Polygon}. * * @param p * The {@link Polygon} to test for containment * @return <code>true</code> if the given {@link Polygon} is fully * contained, <code>false</code> otherwise. */ public boolean contains(Polygon p) { // all segments of the given polygon have to be contained Line[] otherSegments = p.getOutlineSegments(); for (int i = 0; i < otherSegments.length; i++) { if (!contains(otherSegments[i])) { return false; } } return true; } /** * Tests if the given {@link Polyline} p is contained in this * {@link Polygon}. * * @param p * The {@link Polyline} to test for containment. * @return true if it is contained, false otherwise */ public boolean contains(Polyline p) { // all segments of the given polygon have to be contained Line[] otherSegments = p.getCurves(); for (int i = 0; i < otherSegments.length; i++) { if (!contains(otherSegments[i])) { return false; } } return true; } /** * Checks whether the given {@link Rectangle} is fully contained within this * {@link Polygon}. * * @param r * the {@link Rectangle} to test for containment * @return <code>true</code> if the given {@link Rectangle} is fully * contained, <code>false</code> otherwise. */ public boolean contains(Rectangle r) { return contains(r.toPolygon()); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o instanceof Polygon) { Polygon p = (Polygon) o; return equals(p.getPoints()); } return false; } /** * Checks whether this {@link Polygon} and the one that is indirectly given * via the given array of points are regarded to be equal. The * {@link Polygon}s will be regarded equal, if they are characterized by the * same segments. As a {@link Polygon} is always regarded to be closed, the * list of points may not have to correspond in each index value, they may * also be shifted by a certain offset. Moreover, the vertices of two * equally {@link Polygon}s may be reverted in order. * * @param points * an array of {@link Point} characterizing a {@link Polygon} to * be checked for equality * @return <code>true</code> if the sequence of points that characterize * this {@link Polygon} and the {@link Polygon} indirectly given via * the array of points are regarded to form the same segments. */ public boolean equals(Point[] points) { if (points.length != this.points.length) { return false; } // walk through the segments of this polygon and count them SegmentList segments = new SegmentList(); for (Line seg : getOutlineSegments()) { SegmentCounter sc = segments.find(seg); sc.count++; } // walk through the segments of the other polygon and decrement their // counter for (Line seg : new Polygon(points).getOutlineSegments()) { SegmentCounter sc = segments.find(seg); if (sc.count == 0) { // if we add a new one or we delete one too often, these // polygons are not equal return false; } sc.count--; } return true; } /** * Computes the area of this {@link Polygon}. * * @return the area of this {@link Polygon} */ public double getArea() { return Math.abs(getSignedArea()); } /** * Returns a copy of this {@link Polygon}, which is made up by the same * points. * * @return a new {@link Polygon} with an identical set of points. */ @Override public Polygon getCopy() { return new Polygon(getPoints()); } @Override public Polyline getOutline() { return new Polyline(PointListUtils.toSegmentsArray(points, true)); } /** * Returns a sequence of {@link Line}s, representing the segments that are * obtained by linking each two successive point of this {@link Polygon} * (including the last and the first one). * * @return an array of {@link Line}s, representing the segments that make up * this {@link Polygon} */ @Override public Line[] getOutlineSegments() { return PointListUtils.toSegmentsArray(points, true); } /** * Computes the signed area of this {@link Polygon}. The sign of the area is * negative for counter clockwise ordered vertices. It is positive for * clockwise ordered vertices. * * @return the signed area of this {@link Polygon} */ public double getSignedArea() { if (points.length < 3) { return 0; } double area = 0; for (int i = 0; i < points.length - 1; i++) { area += points[i].x * points[i + 1].y - points[i].y * points[i + 1].x; } // closing segment area += points[points.length - 1].x * points[0].y - points[points.length - 1].y * points[0].x; return area * 0.5; } /** * @see IGeometry#getTransformed(AffineTransform) */ @Override public Polygon getTransformed(AffineTransform t) { // shape type should remain polygon (not path) return new Polygon(t.getTransformed(points)); } /** * Naive, recursive ear-clipping algorithm to triangulate this simple, * planar {@link Polygon}. * * @return triangulation {@link Polygon}s (triangles) */ public Polygon[] getTriangulation() { assureSimplicity(); ArrayList<Polygon> ears = new ArrayList<>(points.length - 2); triangulate(this, ears); return ears.toArray(new Polygon[] {}); } /** * @see IGeometry#toPath() */ @Override public Path toPath() { Path path = new Path(); if (points.length > 0) { path.moveTo(points[0].x, points[0].y); for (int i = 1; i < points.length; i++) { path.lineTo(points[i].x, points[i].y); } path.close(); } return path; } @Override public String toString() { StringBuffer stringBuffer = new StringBuffer("Polygon: "); if (points.length > 0) { for (int i = 0; i < points.length; i++) { stringBuffer .append("(" + points[i].x + ", " + points[i].y + ")"); stringBuffer.append(" -> "); } stringBuffer.append("(" + points[0].x + ", " + points[0].y + ")"); } else { stringBuffer.append("<no points>"); } return stringBuffer.toString(); } }