package org.geogebra.common.kernel.discrete; import java.util.ArrayList; import java.util.Comparator; import java.util.Iterator; import java.util.SortedSet; import java.util.Stack; import java.util.TreeSet; import org.geogebra.common.awt.GPoint2D; import org.geogebra.common.kernel.Kernel; import org.geogebra.common.kernel.Matrix.CoordSys; import org.geogebra.common.kernel.Matrix.Coords; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoPolygon; import org.geogebra.common.util.debug.Log; import org.geogebra.common.util.lang.Unicode; /** * Class to convert a GeoPolygon to a set of triangles * * based on monotone pieces and sweep line, as described here: * https://www.cs.ucsb.edu/~suri/cs235/Triangulation.pdf (Subhash Suri, UC Santa * Barbara) * * @author mathieu * */ public class PolygonTriangulation { private static class MyTreeSet<E> extends TreeSet<E> { /** * */ private static final long serialVersionUID = 1L; protected MyTreeSet() { super(); } /** * we have to implement that for gwt */ @Override public E higher(E e) { SortedSet<E> set = tailSet(e); Iterator<E> it = set.iterator(); if (it.hasNext()) { E first = it.next(); if (first != e) { return first; } if (it.hasNext()) { return it.next(); } } return null; } /** * we have to implement that for gwt */ @Override public E lower(E e) { SortedSet<E> set = headSet(e); if (set == null || set.isEmpty()) { return null; } return set.last(); } } static private class TriangulationException extends Exception { /** * */ private static final long serialVersionUID = 1L; public enum Type { LEFT_POINT_INTERSECTION, ZERO_SEGMENT, DEAD_END } private Type type; public TriangulationException(Type type) { this.type = type; } @Override public String getMessage() { return "Triangulation exception : " + type; } } final static private boolean DEBUG = false; public static final int CORNERS = 4; public static final int CORNERS_ALL = CORNERS * 2; public static final int EXTRA_POINTS = 12; /** * message debug * * @param s * message */ final static protected void debug(String s) { if (DEBUG) { Log.debug(s); } } /** * message error * * @param s * message */ final static protected void error(String s) { if (DEBUG) { Log.error(s); } } protected Point nextNewPointForNonSelfIntersectingPolygon = null; final private Comparator<Point> nonSelfIntersectingPolygonPointComparator = new Comparator<Point>() { @Override public int compare(Point p1, Point p2) { if (p1 == p2) { return 0; } if (p1.id == p2.id) { /* * error("same ids"); debug(p1.debugSegments()); * debug(p2.debugSegments()); */ // copy segments if (p1.toRight != null) { if (p2.toRight == null) { p2.toRight = new MyTreeSet<Segment>(); } for (Segment seg : p1.toRight) { seg.leftPoint = p2; p2.toRight.add(seg); } } if (p1.toLeft != null) { if (p2.toLeft == null) { p2.toLeft = new MyTreeSet<Segment>(); } for (Segment seg : p1.toLeft) { seg.rightPoint = p2; p2.toLeft.add(seg); } } // add diagonal need if (p1.needsDiagonal) { p2.needsDiagonal = true; } nextNewPointForNonSelfIntersectingPolygon = p2; return 0; } return p1.compareToOnly(p2); } }; final static double POINT_DELTA = Kernel.STANDARD_PRECISION; final static double ORIENTATION_DELTA = Kernel.STANDARD_PRECISION; private class Point implements Comparable<Point> { public double x, y; public int id; public String name; public double orientationToNext; Point prev, next; // previous and next point MyTreeSet<Segment> toRight, toLeft; boolean needsDiagonal = false; public Point(double x, double y, int id) { this.x = x; this.y = y; this.id = id; } public Point duplicate() { Point ret = new Point(x, y, id); ret.name = name; return ret; } public String debugSegments() { StringBuilder s = new StringBuilder(name); s.append(" "); if (toLeft != null) { s.append("/ to left : "); for (Segment segment : toLeft) { s.append(((int) (segment.orientation * 180 / Math.PI))); s.append(Unicode.DEGREE_CHAR); s.append(':'); s.append(segment.leftPoint.name); s.append('('); s.append(segment.usable); s.append("), "); } } if (toRight != null) { s.append("/ to right : "); for (Segment segment : toRight) { s.append( ((int) (segment.orientation * 180 / Math.PI))); s.append(Unicode.DEGREE_CHAR); s.append(':'); s.append(segment.rightPoint.name); s.append('('); s.append(segment.usable); s.append("), "); } } return s.toString(); } public void removeSegmentToRight(Segment segment) { toRight.remove(segment); } public boolean addSegmentToRight(Segment segment) { if (toRight == null) { toRight = new MyTreeSet<Segment>(); } return toRight.add(segment); } public void removeSegmentToLeft(Segment segment) { toLeft.remove(segment); } public boolean addSegmentToLeft(Segment segment) { if (toLeft == null) { toLeft = new MyTreeSet<Segment>(); } return toLeft.add(segment); } public boolean hasNoSegment() { return (toLeft == null || toLeft.isEmpty()) && (toRight == null || toRight.isEmpty()); } @Override final public int compareTo(Point p2) { if (id == p2.id) { return 0; } // smallest x if (Kernel.isGreater(p2.x, x, POINT_DELTA)) { return -1; } if (Kernel.isGreater(x, p2.x, POINT_DELTA)) { return 1; } // then smallest y if (Kernel.isGreater(p2.y, y, POINT_DELTA)) { return -1; } if (Kernel.isGreater(y, p2.y, POINT_DELTA)) { return 1; } // same point : add all point-to-point set to existing point error(this.name + "==" + p2.name); if (toRight != null) { if (p2.toRight == null) { p2.toRight = new MyTreeSet<Segment>(); } for (Segment seg : toRight) { seg.leftPoint = p2; p2.toRight.add(seg); try { cutAfterComparisonToRight(seg); } catch (TriangulationException e) { debug(e.getMessage()); } } } if (toLeft != null) { if (p2.toLeft == null) { p2.toLeft = new MyTreeSet<Segment>(); } for (Segment seg : toLeft) { seg.rightPoint = p2; p2.toLeft.add(seg); try { cutAfterComparisonToLeft(seg); } catch (TriangulationException e) { debug(e.getMessage()); } } } return 0; } @Override public boolean equals(Object o) { if (o instanceof Point) { return compareTo((Point) o) == 0; } return false; } @Override public int hashCode() { assert false : "hashCode() not implemented"; return 42; } final public int compareToOnly(Point p2) { // smallest x if (Kernel.isGreater(p2.x, x, POINT_DELTA)) { return -1; } if (Kernel.isGreater(x, p2.x, POINT_DELTA)) { return 1; } // then smallest y if (Kernel.isGreater(p2.y, y, POINT_DELTA)) { return -1; } if (Kernel.isGreater(y, p2.y, POINT_DELTA)) { return 1; } return 0; } /** * this method is only used for intersection * * @param x1 * x * @param y1 * y * @return -1 if this is before (x1,y1); 1 if this is after (x1,y1); 0 * otherwise */ final public int compareTo(double x1, double y1) { // smallest x if (Kernel.isGreater(x1, x, POINT_DELTA)) { return -1; } if (Kernel.isGreater(x, x1, POINT_DELTA)) { return 1; } // then smallest y if (Kernel.isGreater(y1, y, POINT_DELTA)) { return -1; } if (Kernel.isGreater(y, y1, POINT_DELTA)) { return 1; } return 0; } } protected Segment comparedSameOrientationSegment; protected int comparedSameOrientationValue; protected Segment comparedSameSegment; private class Segment implements Comparable<Segment> { double orientation; Point leftPoint, rightPoint; Segment above, below; Segment next; int usable = 1; Running running = Running.STOP; // equation vector double x, y, z; private boolean equationNeedsUpdate = true; public Segment() { // dummy constructor } public boolean isDummy() { return leftPoint == null; } public Segment(double orientation, Point leftPoint, Point rightPoint) { this(leftPoint, rightPoint); this.orientation = orientation; } public Segment duplicate() { return new Segment(orientation, leftPoint, rightPoint); } public Segment(Point leftPoint, Point rightPoint) { this.leftPoint = leftPoint; this.rightPoint = rightPoint; } public void setEquation() { if (equationNeedsUpdate) { y = rightPoint.x - leftPoint.x; x = -rightPoint.y + leftPoint.y; z = -x * rightPoint.x - y * rightPoint.y; equationNeedsUpdate = false; } } @Override public String toString() { if (leftPoint != null) { /* * if (running == Running.LEFT){ return * rightPoint.name+leftPoint.name; } */ return leftPoint.name + rightPoint.name; } return "dummy"; } /** * remove this segment from left and right points */ public void removeFromPoints() { leftPoint.removeSegmentToRight(this); rightPoint.removeSegmentToLeft(this); } /** * add this segment to left and right points * * @return true if new segment */ public boolean addToPoints() { boolean newRight = leftPoint.addSegmentToRight(this); boolean newLeft = rightPoint.addSegmentToLeft(this); return newRight && newLeft; } @Override public int compareTo(Segment seg) { if (this == seg) { return 0; } if (Kernel.isGreater(seg.orientation, orientation, ORIENTATION_DELTA)) { return -1; } if (Kernel.isGreater(orientation, seg.orientation, ORIENTATION_DELTA)) { return 1; } comparedSameOrientationSegment = seg; if (rightPoint.id != seg.rightPoint.id) { comparedSameOrientationValue = rightPoint .compareToOnly(seg.rightPoint); } else { comparedSameOrientationValue = leftPoint .compareToOnly(seg.leftPoint); } // error(this+","+seg+" : "+c); /* * if (c > 0){ seg.rightPoint.removeSegmentToLeft(seg); * rightPoint.addSegmentToLeft(seg); seg.rightPoint = rightPoint; * }else{ rightPoint.removeSegmentToLeft(this); } */ // same orientation : check next point id if (rightPoint.id < seg.rightPoint.id) { return -1; } if (rightPoint.id > seg.rightPoint.id) { return 1; } /* * // same right point : augment usability if (rightPoint.id == * seg.rightPoint.id){ seg.usable += usable/2; // usable is always * multiple of 2, and will be add twice (from left and from right) * debug(seg+": "+seg.usable); } */ // error("same points : "+this+","+seg); comparedSameSegment = seg; // same ptp return 0; } @Override public boolean equals(Object o) { if (o instanceof Segment) { return compareTo((Segment) o) == 0; } return false; } @Override public int hashCode() { assert false : "hashCode() not implemented"; return 42; } } /** * TriangleFan is composed of apex point index, and list of fan points * indices, knowing the clockwise/anti-clockwise orientation * * @author mathieu * */ @SuppressWarnings("serial") public static class TriangleFan extends ArrayList<Integer> { private boolean isClockWise; private int apex; /** * * @param apex * of the fan * @param isClockWise * orientation */ public TriangleFan(int apex, boolean isClockWise) { this.apex = apex; this.isClockWise = isClockWise; } /** * * @return apex point */ public int getApexPoint() { return apex; } /** * * @param i * i-th index * @return vertex index regarding clockwise/anti clockwise orientation */ public int getVertexIndex(int i) { if (isClockWise) { return get(size() - i - 1); } return get(i); } } private static class PolygonPoints extends TreeSet<Point> { /** * */ private static final long serialVersionUID = 1L; /** * says that at least one diagonal is needed */ public boolean needsDiagonals = false; public PolygonPoints(Comparator<Point> comparator) { super(comparator); } } private GeoPolygon polygon; private int maxPointIndex; private Point firstPoint; private ArrayList<PolygonPoints> polygonPointsList; private ArrayList<TriangleFan> fansList; private GPoint2D.Double[] pointsArray; /** * Constructor */ public PolygonTriangulation() { polygonPointsList = new ArrayList<PolygonPoints>(); fansList = new ArrayList<TriangleFan>(); pointsArray = new GPoint2D.Double[0]; } /** * set the polygon * * @param p * polygon */ public void setPolygon(GeoPolygon p) { this.polygon = p; } /** * clear lists */ public void clear() { polygonPointsList.clear(); fansList.clear(); maxPointIndex = 0; firstPoint = null; comparedSameOrientationSegment = null; } /** * set point id * * @param point * @param i */ private void setName(Point point, int i) { if (DEBUG) { point.name = ((GeoElement) polygon.getPointsND()[i]) .getLabelSimple(); } } /** * set point id * * @param point * @param s * name */ private void setName(Point point, String s, int i) { if (DEBUG) { if (s == null) { setName(point, i); } else { point.name = s + i; } } } /** * add a point to chain * * @param point * current point * @param x * x coord * @param y * y coord * @param name * name * @param nameId * id in polygon when name == null * @return null if new point equals current */ private Point addPointToChain(Point point, double x, double y, String name, int nameId) { if (!Kernel.isEqual(point.x, x, POINT_DELTA) || !Kernel.isEqual(point.y, y, POINT_DELTA)) { point.next = new Point(x, y, point.id + 1); setName(point.next, name, nameId); point.next.prev = point; return point.next; } return point; } /** * update points list: creates a chain from firstPoint to next points ; two * consecutive points can't be equal ; three consecutive points can't be * aligned. For each point orientation to the next (angle about Ox) is * stored. * * @return points left */ public int updatePoints() { maxPointIndex = polygon.getPointsLength(); // feed the list with no successively equal points Point point = new Point(polygon.getPointX(0), polygon.getPointY(0), 0); setName(point, 0); firstPoint = point; int length = polygon.getPointsLength(); for (int i = 1; i < length; i++) { point = addPointToChain(point, polygon.getPointX(i), polygon.getPointY(i), null, i); } // corners if (corners != null) { maxPointIndex += EXTRA_POINTS; // re-add first polygon point point = addPointToChain(point, polygon.getPointX(0), polygon.getPointY(0), null, 0); // add first four corners for (int i = 0; i < CORNERS; i++) { point = addPointToChain(point, corners[i].getX(), corners[i].getY(), "c", i); } // re-add first corner point = addPointToChain(point, corners[0].getX(), corners[0].getY(), "c", 0); // add last four corners for (int i = CORNERS; i < CORNERS_ALL; i++) { point = addPointToChain(point, corners[i].getX(), corners[i].getY(), "c", i); } // re-add first of last four corner point = addPointToChain(point, corners[CORNERS].getX(), corners[CORNERS].getY(), "c", 4); // re-add first corner point = addPointToChain(point, corners[0].getX(), corners[0].getY(), "c", 0); } int n = point.id + 1; // check first point <> last point if (Kernel.isEqual(point.x, firstPoint.x, POINT_DELTA) && Kernel.isEqual(point.y, firstPoint.y, POINT_DELTA)) { firstPoint = firstPoint.next; n--; } if (n < 3) { return n; } point.next = firstPoint; firstPoint.prev = point; if (DEBUG) { String s1 = "\n"; for (point = firstPoint; point.next != firstPoint; point = point.next) { s1 += "\n" + point.name + " = (" + point.x + "," + point.y + ")"; } s1 += "\n" + point.name + " = (" + point.x + "," + point.y + ")"; debug(s1); } // set orientations and remove flat points Point prevPoint = firstPoint; point = prevPoint.next; prevPoint.orientationToNext = Math.atan2(point.y - prevPoint.y, point.x - prevPoint.x); int removedPoints = 0; for (int i = 0; i < n && removedPoints < n - 1; i++) { // make it n times since at each step : // * we remove 1 point and go on // * we remove 2 points and go back // * we go on // so each point is visited at least once Point nextPoint = point.next; point.orientationToNext = Math.atan2(nextPoint.y - point.y, nextPoint.x - point.x); // delta orientation between 0 and 2pi double delta = point.orientationToNext - prevPoint.orientationToNext; if (delta < 0) { delta += 2 * Math.PI; } debug(prevPoint.name + " : " + (prevPoint.orientationToNext * 180 / Math.PI)); debug(prevPoint.name + "/" + point.name + "/" + nextPoint.name + " : " + (delta * 180 / Math.PI)); if (Kernel.isZero(delta, ORIENTATION_DELTA)) { // point aligned // remove point prevPoint.next = nextPoint; nextPoint.prev = prevPoint; removedPoints++; point = nextPoint; } else if (Kernel.isEqual(delta, Math.PI, ORIENTATION_DELTA)) { // U-turn debug("U-turn"); if (Kernel.isEqual(nextPoint.x, prevPoint.x, POINT_DELTA) && Kernel.isEqual(nextPoint.y, prevPoint.y, POINT_DELTA)) { // same point debug(prevPoint.name + "==" + nextPoint.name); // index is going back i--; // set correct orientation // error(prevPoint.orientationToNext*180/Math.PI+"/"+nextPoint.orientationToNext*180/Math.PI); prevPoint.orientationToNext = nextPoint.orientationToNext; // go back point = prevPoint; prevPoint = prevPoint.prev; // remove point and nextPoint point.next = nextPoint.next; nextPoint.next.prev = point; removedPoints += 2; } else if (Kernel.isGreater(0, (nextPoint.x - prevPoint.x) * (point.x - prevPoint.x) + (nextPoint.y - prevPoint.y) * (point.y - prevPoint.y), POINT_DELTA)) { // next point is back old point debug(" next point is back old point - " + (prevPoint.orientationToNext * 180 / Math.PI)); if (prevPoint.orientationToNext > 0) { prevPoint.orientationToNext -= Math.PI; } else { prevPoint.orientationToNext += Math.PI; } // remove point prevPoint.next = nextPoint; nextPoint.prev = prevPoint; removedPoints++; point = nextPoint; } else { // next point is in same direction as old point // remove point prevPoint.next = nextPoint; nextPoint.prev = prevPoint; point = nextPoint; } } else { prevPoint = point; point = nextPoint; } } firstPoint = point; // in case old firstPoint has been removed if (DEBUG) { String s = ""; for (point = firstPoint; point.next != firstPoint; point = point.next) { s += point.name + "(" + (point.orientationToNext * 180 / Math.PI) + Unicode.DEGREE_CHAR + "), "; } s += point.name + "(" + (point.orientationToNext * 180 / Math.PI) + Unicode.DEGREE_CHAR + ")"; debug(s); } debug(n + " - " + removedPoints); return n - removedPoints; } // //////////////////////////////////// // CONVEX POLYGON ? // //////////////////////////////////// public enum Convexity { CLOCKWISE, ANTI_CLOCKWISE, NOT } /** * * @return true if the polygon is convex after simplification */ public Convexity checkIsConvex() { Point point1 = firstPoint; Point point2 = point1.next; double delta = point1.orientationToNext + Math.PI - point2.orientationToNext; if (delta < -Math.PI) { delta += 2 * Math.PI; } else if (delta > Math.PI) { delta -= 2 * Math.PI; } boolean positive = (delta > 0); debug(point1.name + "(" + (point1.orientationToNext * 180 / Math.PI) + Unicode.DEGREE_CHAR + ")"); debug(point2.name + "(" + (point2.orientationToNext * 180 / Math.PI) + Unicode.DEGREE_CHAR + ")"); debug("delta : " + (delta * 180 / Math.PI) + Unicode.DEGREE_CHAR + ")"); debug("positive : " + positive); boolean convex = true; point1 = point2; point2 = point1.next; int pointLengthMinus2 = -1; double deltaSum = delta; while (point1 != firstPoint && convex && point2 != null) { delta = point1.orientationToNext + Math.PI - point2.orientationToNext; if (delta < -Math.PI) { delta += 2 * Math.PI; } else if (delta > Math.PI) { delta -= 2 * Math.PI; } convex = positive ^ (delta < 0); debug(point2.name + "(" + (point2.orientationToNext * 180 / Math.PI) + Unicode.DEGREE_CHAR + ") -- " + "(" + (delta * 180 / Math.PI) + Unicode.DEGREE_CHAR + ") -- " + convex); point1 = point2; point2 = point1.next; pointLengthMinus2++; deltaSum += delta; } // check if (angle sum) == (n-2)*pi debug((deltaSum * 180 / Math.PI) + " , " + (pointLengthMinus2 - 2) * 180); convex = convex && Kernel.isEqual(Math.abs(deltaSum), pointLengthMinus2 * Math.PI); if (convex) { if (positive) { return Convexity.ANTI_CLOCKWISE; } return Convexity.CLOCKWISE; } return Convexity.NOT; } // //////////////////////////////////// // INTERSECTIONS // //////////////////////////////////// /** * cut a segment in two by this point * * @param segment * segment * @param pt * cutting point * @throws TriangulationException * exception if cut after comparison failed */ private Segment cut(Segment segment, Point pt) throws TriangulationException { // cut the segment segment.removeFromPoints(); Segment segment2 = new Segment(segment.orientation, pt, segment.rightPoint); segment.rightPoint = pt; segment.addToPoints(); comparedSameOrientationSegment = null; segment2.addToPoints(); segment2.usable = segment.usable; cutAfterComparisonToRight(segment2); // debug(segment2.leftPoint.debugSegments()); return segment2; } /** * After adding a segment to the points, it may be redundant with an already * existing segment in the left point * * @param segment2 * segment * @throws TriangulationException * exception if segment2 is "zero segment" (end points equal) */ protected void cutAfterComparisonToRight(Segment segment2) throws TriangulationException { if (comparedSameOrientationSegment != null) { debug(segment2 + "," + comparedSameOrientationSegment + " : " + comparedSameOrientationValue); if (segment2.rightPoint == segment2.leftPoint) { throw new TriangulationException( TriangulationException.Type.ZERO_SEGMENT); } if (comparedSameOrientationValue < 0) { // segment2 can be used once more Segment s = comparedSameOrientationSegment; comparedSameOrientationSegment = null; s.removeFromPoints(); s.leftPoint = segment2.rightPoint; segment2.usable++; segment2.addToPoints(); // check why this is necessary comparedSameOrientationSegment = null; s.addToPoints(); cutAfterComparisonToRight(s); } else if (comparedSameOrientationValue > 0) { // comparedSameOrientationSegment can be used once more Segment s = comparedSameOrientationSegment; comparedSameOrientationSegment = null; segment2.removeFromPoints(); segment2.leftPoint = s.rightPoint; s.usable++; s.addToPoints(); // check why this is necessary comparedSameOrientationSegment = null; segment2.addToPoints(); cutAfterComparisonToRight(segment2); } else { // same segment : add usability Segment s = comparedSameOrientationSegment; debug(segment2.hashCode() + " / " + s.hashCode()); comparedSameOrientationSegment = null; // same segment : add usability segment2.usable += s.usable; segment2.removeFromPoints(); s.removeFromPoints(); comparedSameOrientationSegment = null; segment2.addToPoints(); comparedSameOrientationSegment = null; } } } /** * After adding a segment to the points, it may be redundant with an already * existing segment in the left point * * @param segment2 * segment * @throws TriangulationException * exception if segment2 is "zero segment" (end points equal) */ protected void cutAfterComparisonToLeft(Segment segment2) throws TriangulationException { if (comparedSameOrientationSegment != null) { debug(segment2 + "," + comparedSameOrientationSegment + " : " + comparedSameOrientationValue); if (segment2.rightPoint == segment2.leftPoint) { throw new TriangulationException( TriangulationException.Type.ZERO_SEGMENT); } if (comparedSameOrientationValue > 0) { // comparedSameOrientationSegment can be used once more Segment s = comparedSameOrientationSegment; comparedSameOrientationSegment = null; s.removeFromPoints(); s.rightPoint = segment2.leftPoint; segment2.usable++; segment2.addToPoints(); // check why this is necessary comparedSameOrientationSegment = null; s.addToPoints(); cutAfterComparisonToLeft(s); } else if (comparedSameOrientationValue < 0) { // segment2 can be used once more Segment s = comparedSameOrientationSegment; comparedSameOrientationSegment = null; segment2.removeFromPoints(); segment2.rightPoint = s.leftPoint; s.usable++; s.addToPoints(); // check why this is necessary comparedSameOrientationSegment = null; segment2.addToPoints(); cutAfterComparisonToLeft(segment2); } else { Segment s = comparedSameOrientationSegment; debug(segment2.hashCode() + " / " + s.hashCode()); comparedSameOrientationSegment = null; // same segment : add usability segment2.usable += s.usable; segment2.removeFromPoints(); s.removeFromPoints(); comparedSameOrientationSegment = null; segment2.addToPoints(); comparedSameOrientationSegment = null; } } } /** * set intersections. After this call, PolygonTriangulation has an array of * 2D coords (pointsArray), and a list of polygons (polygonPointsList), each * composed of its points in sweep order, connected by segments running * right or left, eventually needing a diagonal. * * @throws TriangulationException * exception if creating intersections fails */ public void setIntersections() throws TriangulationException { // create segments Point point; for (point = firstPoint; point.next != firstPoint; point = point.next) { createSegment(point); } createSegment(point); // store all points in sweep order // same points are merged // aligned segments to right are cut // aligned segments to left are ignored error("=========== store points ============"); MyTreeSet<Point> pointSet = new MyTreeSet<Point>(); for (point = firstPoint; point.next != firstPoint; point = point.next) { debug("" + point.name + "(" + point.id + ")"); pointSet.add(point); } debug("" + point.name + "(" + point.id + ")"); pointSet.add(point); // at this time, pointSet only contains different points, each points // have to-left / to-right segments with different orientations if (DEBUG) { error("================== before intersections ==================="); for (Point pt : pointSet) { debug(pt.debugSegments()); } error("================== END ==================="); } if (pointSet.size() > 3) { // now compute intersections // TODO use a better storage than linear chained segments // top and bottom (dummy) segments Segment top = new Segment(); Segment bottom = new Segment(); bottom.above = top; top.below = bottom; for (Point pt = pointSet.first(); pt != pointSet .last(); pt = pointSet.higher(pt)) { String s = pt.name + " : "; /* * for (Segment seg = bottom.above ; seg != top; seg = * seg.above){ s+=seg.toString()+"("+seg.hashCode()+")"+","; } * s+=" (before)"; debug(s); * * s=pt.name+" : "; */ Segment above = null; Segment below = null; // debug(s); // remove to-left segments if (pt.toLeft != null && !pt.toLeft.isEmpty()) { // will put // to-right // segments // in place // of // to-left // segments above = pt.toLeft.first().above; below = pt.toLeft.last().below; // debug(pt.name+" : // "+pt.toLeft.first()+"("+pt.toLeft.first().hashCode()+")"+" // -- "+pt.toLeft.last()+"("+below+")"); below.above = above; above.below = below; checkIntersection(below, above, pointSet); // check if new point is aligned with existing segment boolean go = true; for (Segment segment = bottom.above; segment != top && go; segment = segment.above) { double orientation = Math.atan2( pt.y - segment.leftPoint.y, pt.x - segment.leftPoint.x); // error(segment.leftPoint.name+pt.name+" : // "+orientation); if (Kernel.isEqual(orientation, segment.orientation, ORIENTATION_DELTA)) { error("(1)" + pt.name + " aligned with " + segment); // cut the segment cut(segment, pt); // remove new left segment above = segment.above; below = segment.below; below.above = above; above.below = below; segment = below; } else if (orientation < segment.orientation) { go = false; } } } else { // search for the correct place for to-right segments debug("search the correct place : " + pt.name); boolean go = true; for (Segment segment = bottom.above; segment != top && go; segment = segment.above) { // error(segment.leftPoint.name+segment.rightPoint.name+" // : "+segment.orientation); double orientation = Math.atan2( pt.y - segment.leftPoint.y, pt.x - segment.leftPoint.x); // error(segment.leftPoint.name+pt.name+" : // "+orientation); if (Kernel.isEqual(orientation, segment.orientation, ORIENTATION_DELTA)) { error("(2)" + pt.name + " aligned with " + segment); // cut the segment cut(segment, pt); // remove new left segment above = segment.above; below = segment.below; below.above = above; above.below = below; // go = false; } else if (orientation < segment.orientation) { // found // the // place go = false; above = segment; below = above.below; error(below + "<" + pt.name + "<" + above); } } if (go) { // when there are no segment between top and // bottom above = top; below = above.below; } } // put to-right segments if (pt.toRight != null) { Segment oldBelow = below; for (Segment seg : pt.toRight) { // debug(seg+"("+seg.hashCode()+")"); below.above = seg; seg.below = below; below = seg; } below.above = above; above.below = below; checkIntersection(oldBelow, oldBelow.above, pointSet); checkIntersection(below, below.above, pointSet); } if (DEBUG) { for (Segment seg = bottom.above; seg != top; seg = seg.above) { s += seg.toString() + ","; } debug(s); } } if (DEBUG) { error("================== after intersections ==================="); for (Point pt : pointSet) { debug(pt.debugSegments()); } error("================== END ==================="); } } setNonSelfIntersecting(pointSet); } private void setNonSelfIntersecting(TreeSet<Point> pointSet) throws TriangulationException { // prepare points as an array if (pointsArray.length < maxPointIndex) { pointsArray = new GPoint2D.Double[maxPointIndex]; } // now all intersections are computed, and points are correctly chained // by oriented segments // we can divide the polygon turning e.g. counter clock-wise polygonPointsList.clear(); error("=========== non self-intersecting polygons =============="); while (!pointSet.isEmpty()) { PolygonPoints polygonPoints = new PolygonPoints( nonSelfIntersectingPolygonPointComparator); Point start = pointSet.first(); Segment segStart = start.toRight.first(); if (segStart.usable > 1) { debug("*** " + segStart + " : " + segStart.usable); segStart.usable = segStart.usable % 2; } if (segStart.usable == 0) { // check if not set to 0 just above segStart.removeFromPoints(); if (start.hasNoSegment()) { pointSet.remove(start); pointsArray[start.id] = new GPoint2D.Double(start.x, start.y); } start = segStart.rightPoint; // check right point if (start.hasNoSegment()) { pointSet.remove(start); pointsArray[start.id] = new GPoint2D.Double(start.x, start.y); } } else { Point currentPoint; Point currentPointNew; Point nextPoint = start; Point nextPointNew = nextPoint.duplicate(); Point startPointNew = nextPointNew; // polygonPoints.add(nextPointNew); Segment segment = segStart; Segment next = null; Running running = Running.RIGHT; while (running != Running.STOP) { segment.running = running; currentPoint = nextPoint; currentPointNew = nextPointNew; boolean needsDiagonal = false; debug(nextPoint.name + ", " + segment + ""); if (running == Running.RIGHT) { nextPoint = segment.rightPoint; if (nextPoint == start) { running = Running.STOP; next = segStart; } else { next = nextPoint.toLeft.lower(segment); if (next == null) { if (nextPoint.toRight != null && !nextPoint.toRight.isEmpty()) { next = nextPoint.toRight.last(); } if (next == null) { // no to-right segment next = nextPoint.toLeft.last(); running = Running.LEFT; needsDiagonal = needsDiagonal(segment, next); } } else { running = Running.LEFT; needsDiagonal = needsDiagonal(segment, next); } } debug("next : " + next); } else { // running == Running.LEFT nextPoint = segment.leftPoint; if (nextPoint == start && (start.toRight.higher(segStart) == segment || segStart == segment)) { // check // if // there // are // no // segment // between // current // and // segStart running = Running.STOP; next = segStart; } else { next = nextPoint.toRight.lower(segment); if (next == null) { if (nextPoint.toLeft != null && !nextPoint.toLeft.isEmpty()) { next = nextPoint.toLeft.last(); } if (next == null) { // no to-left segment next = nextPoint.toRight.last(); running = Running.RIGHT; needsDiagonal = needsDiagonal(segment, next); } } else { running = Running.RIGHT; needsDiagonal = needsDiagonal(segment, next); } } } // remove this segment from left and right points switch (segment.usable) { case 1: segment.removeFromPoints(); segment.usable--; // ensure to throw an exception if // this segment is used once more break; case 0: // should not happen throw new TriangulationException( TriangulationException.Type.DEAD_END); default: segment.usable--; Segment clone = segment.duplicate(); clone.running = segment.running; segment = clone; break; } // reconfigure segment to new points if (running != Running.STOP) { nextPointNew = nextPoint.duplicate(); } else { nextPointNew = startPointNew; // error(nextPointNew.debugSegments()); } if (segment.running == Running.RIGHT) { segment.leftPoint = currentPointNew; segment.rightPoint = nextPointNew; } else { segment.leftPoint = nextPointNew; segment.rightPoint = currentPointNew; } // debug(segment+""); if (!segment.addToPoints()) { comparedSameSegment.usable++; // debug("not new : "+segment+", "+segment.usable); // debug(segment.hashCode()+" / // "+comparedSameSegment.hashCode()); } // says if the point needs a diagonal nextPointNew.needsDiagonal = needsDiagonal; polygonPoints.needsDiagonals = polygonPoints.needsDiagonals || needsDiagonal; // add current point to current polygon, check if not // already in it nextNewPointForNonSelfIntersectingPolygon = nextPointNew; polygonPoints.add(nextPointNew); nextPointNew = nextNewPointForNonSelfIntersectingPolygon; // debug(nextPointNew.debugSegments()); // remove current point if no more segment if (currentPoint.hasNoSegment()) { // debug(currentPoint.name+" : remove"); pointSet.remove(currentPoint); pointsArray[currentPoint.id] = new GPoint2D.Double( currentPoint.x, currentPoint.y); } else { // debug(currentPoint.name+" : keep"); } // go on with next segment segment = next; } if (start.hasNoSegment()) { // debug(start.name+" : remove"); pointSet.remove(start); pointsArray[start.id] = new GPoint2D.Double(start.x, start.y); } else { // debug(start.name+" : keep"); } // add current polygon to list polygonPointsList.add(polygonPoints); if (DEBUG) { debug("--------------------------------"); for (Point p : polygonPoints) { debug(p.debugSegments()); } } } } error("=========== END =============="); } private enum Running { RIGHT, LEFT, STOP } /** * * @param a * @param b * @param pointSet * @throws TriangulationException * exception if an intersection is a segment left point (should * not occur, unless due to numerical precision) */ final private void checkIntersection(Segment a, Segment b, TreeSet<Point> pointSet) throws TriangulationException { debug("check intersection : " + a + "-" + b); if (a.isDummy() || b.isDummy()) { return; } if (a.rightPoint == b.rightPoint) { return; } if (a.leftPoint == b.leftPoint) { return; } // ensure a and b have correct equation a.setEquation(); b.setEquation(); // calculate possible intersection point double x = a.y * b.z - a.z * b.y; double y = a.z * b.x - a.x * b.z; double z = a.x * b.y - a.y * b.x; debug(x + "," + y + "," + z); if (!Kernel.isZero(z, POINT_DELTA)) { // create intersection point // Point pt = new Point(x/z, y/z); double x1 = x / z; double y1 = y / z; // check intersection point is inside segments int al, ar, bl, br; if ((al = a.leftPoint.compareTo(x1, y1)) > 0 || (ar = a.rightPoint.compareTo(x1, y1)) < 0 || (bl = b.leftPoint.compareTo(x1, y1)) > 0 || (br = b.rightPoint.compareTo(x1, y1)) < 0) { // point outside the segments : no intersection } else if (al == 0) { // happen only after some aligned points and // a.leftPoint is current point in sweep // line debug("al : " + a.leftPoint.name); throw new TriangulationException( TriangulationException.Type.LEFT_POINT_INTERSECTION); /* * pt = a.leftPoint; */ } else if (ar == 0) { Point pt = a.rightPoint; error("ar : " + pt.name); cut(b, pt); } else if (bl == 0) { // happen only after some aligned points and // b.leftPoint is current point in sweep // line debug("bl : " + b.leftPoint.name + " " + a + "/" + b); throw new TriangulationException( TriangulationException.Type.LEFT_POINT_INTERSECTION); /* * pt = b.leftPoint; */ } else if (br == 0) { Point pt = b.rightPoint; error("br : " + pt.name); cut(a, pt); } else { // point strictly inside the segments /* * // check if point is strictly inside the segments if * (pt.compareToOnly(a.leftPoint) > 0 && * pt.compareToOnly(a.rightPoint) < 0 && * pt.compareToOnly(b.leftPoint) > 0 && * pt.compareToOnly(b.rightPoint) < 0){ */ Point pt = new Point(x / z, y / z, maxPointIndex); pt.name = Integer.toString(pt.id); maxPointIndex++; error(a + "-" + b); debug("inter : " + pt.name + " : " + pt.x + "," + pt.y); // remove old segments a.removeFromPoints(); b.removeFromPoints(); // create new segments Segment a2 = new Segment(a.orientation, pt, a.rightPoint); Segment b2 = new Segment(b.orientation, pt, b.rightPoint); a2.addToPoints(); b2.addToPoints(); a2.usable = a.usable; b2.usable = b.usable; // set old segments right point a.rightPoint = pt; b.rightPoint = pt; // re-add old segments (with correct right points) a.addToPoints(); b.addToPoints(); // says that old segments need an update for equation // a.equationNeedsUpdate(); // b.equationNeedsUpdate(); // add point to set pointSet.add(pt); // error(pt.debugSegments()); } } } final private void createSegment(Point point) { // debug(point.name+", "+((int) // (point.orientationToNext*180/Math.PI))+ Unicode.degreeChar + // ", "+point.next.name); Segment segment; if (Kernel.isGreater(point.orientationToNext, -Math.PI / 2) && Kernel .isGreaterEqual(Math.PI / 2, point.orientationToNext)) { // point // is // left // point segment = new Segment(point.orientationToNext, point, point.next); } else { // point is right point segment = new Segment( getReverseOrientation(point.orientationToNext), point.next, point); } segment.addToPoints(); } final static private double getReverseOrientation(double orientation) { if (orientation > 0) { return orientation - Math.PI; } return orientation + Math.PI; } // ////////////////////////////////////////////// // TRIANGULATION // ////////////////////////////////////////////// private enum Chain { BOTH, BELOW, ABOVE } /** * triangulate since polygon has been cut into non-self-intersecting pieces. * After that, fansList contains all fans that cover the polygon. */ public void triangulate() { fansList.clear(); for (PolygonPoints polygonPoints : polygonPointsList) { triangulate(polygonPoints); } } /** * triangulate a polygon : cut it into monotone pieces, then feed the fans * list * * @param polygonPoints */ private void triangulate(PolygonPoints polygonPoints) { if (polygonPoints.size() < 3) { // not a drawable polygon error("*** not a polygon ***"); return; } // //////////////////////////////////////////// // set diagonals if (polygonPoints.needsDiagonals) { if (DEBUG) { String s = "set diagonals of "; for (Point pt : polygonPoints) { s += pt.name; if (pt.needsDiagonal) { s += "(*)"; } } debug(s); } // top and bottom (dummy) segments Segment top = new Segment(); Segment bottom = new Segment(); bottom.above = top; top.below = bottom; for (Point pt : polygonPoints) { Segment above = null; Segment below = null; // remove to-left segments if (pt.toLeft != null && !pt.toLeft.isEmpty()) { // will put // to-right // segments // in place // of // to-left // segments above = pt.toLeft.first().above; below = pt.toLeft.last().below; below.above = above; above.below = below; if (pt.needsDiagonal) { // error("diagonal to right : // "+below+"<"+pt.name+"<"+above); Point pt2; if (below.rightPoint .compareToOnly(above.rightPoint) < 0) { pt2 = below.rightPoint; } else { pt2 = above.rightPoint; } Segment diagonal = new Segment( Math.atan2(pt2.y - pt.y, pt2.x - pt.x), pt, pt2); diagonal.addToPoints(); diagonal.usable++; // error("diagonal to right : "+diagonal); } } else { // search for the correct place for to-right segments // error(pt.name); boolean go = true; for (Segment segment = bottom.above; segment != top && go; segment = segment.above) { double orientation = Math.atan2( pt.y - segment.leftPoint.y, pt.x - segment.leftPoint.x); if (orientation < segment.orientation) { // found the // place go = false; above = segment; below = above.below; } } if (go) { // when there are no segment between top and // bottom above = top; below = above.below; } if (pt.needsDiagonal) { // error("diagonal to left : // "+below+"<"+pt.name+"<"+above); if (below.usable > 1) { below.removeFromPoints(); below.rightPoint = pt; below.addToPoints(); // error("below is diagonal, replace : "+below); // remove below below = below.below; below.above = above; above.below = below; } else if (above.usable > 1) { above.removeFromPoints(); above.rightPoint = pt; above.addToPoints(); // error("above is diagonal, replace : "+above); // remove above above = above.above; below.above = above; above.below = below; } else { Point pt2; if (below.leftPoint .compareToOnly(above.leftPoint) < 0) { pt2 = above.leftPoint; } else { pt2 = below.leftPoint; } Segment diagonal = new Segment( Math.atan2(pt.y - pt2.y, pt.x - pt2.x), pt2, pt); diagonal.addToPoints(); diagonal.usable++; // error("diagonal to left : "+diagonal); } } } // put to-right segments if (pt.toRight != null) { for (Segment seg : pt.toRight) { below.above = seg; seg.below = below; below = seg; } below.above = above; above.below = below; } } } // //////////////////////////////////////////// // cut in monotone pieces (it may happen even if no diagonal, when some // points are re-used while (!polygonPoints.isEmpty()) { String s = "Monotone piece : "; Point start = polygonPoints.first(); Point currentPoint = start; Point nextPoint; Segment segStart = start.toRight.first(); Segment segment = segStart; Segment next = null; Running running = Running.RIGHT; Running oldRunning; while (running != Running.STOP) { oldRunning = running; if (running == Running.RIGHT) { nextPoint = segment.rightPoint; if (nextPoint == start) { running = Running.STOP; // next = segStart; } else { next = nextPoint.toLeft.lower(segment); if (next == null) { if (nextPoint.toRight != null && !nextPoint.toRight.isEmpty()) { next = nextPoint.toRight.last(); } if (next == null) { // no to-right segment next = nextPoint.toLeft.higher(segment); running = Running.LEFT; } } else { running = Running.LEFT; } } } else { // running == Running.LEFT nextPoint = segment.leftPoint; if (nextPoint == start) { running = Running.STOP; // next = segStart; } else { next = nextPoint.toRight.lower(segment); if (next == null) { if (nextPoint.toLeft != null && !nextPoint.toLeft.isEmpty()) { next = nextPoint.toLeft.last(); } if (next == null) { // no to-left segment next = nextPoint.toRight.higher(segment); running = Running.RIGHT; } } else { running = Running.RIGHT; } } } s += currentPoint.name; segment.removeFromPoints(); if (oldRunning == Running.LEFT) { if (segment.usable > 1) { // debug("segment "+segment+" is diagonal, running left, // keep point : "+nextPoint.name); segment.usable--; // usable once less, clone it Segment clone = segment.duplicate(); clone.addToPoints(); clone.usable = segment.usable; } if (running == Running.LEFT) { next.next = segment; } } else { // oldRunning == Running.RIGHT if (segment.usable > 1) { // debug("segment "+segment+" is diagonal, running // right, keep point : "+currentPoint.name); segment.usable--; // usable once less, clone it Segment clone = segment.duplicate(); clone.addToPoints(); clone.usable = segment.usable; } if (running == Running.RIGHT) { segment.next = next; } } /* * currentPoint.usable--; if (currentPoint.usable == 0){ * polygonPoints.remove(currentPoint); } */ if (currentPoint.hasNoSegment()) { polygonPoints.remove(currentPoint); debug("remove : " + currentPoint.name); } else { debug("keep : " + currentPoint.name); } segment = next; currentPoint = nextPoint; } /* * s+="\nabove : "; for (Segment seg = segment ; seg != null ; seg = * seg.next ){ s += seg+","; } s+="\nbelow : "; for (Segment seg = * segStart ; seg != null ; seg = seg.next ){ s += seg+","; } */ if (start.hasNoSegment()) { polygonPoints.remove(start); debug("remove : " + start.name); } else { debug("keep : " + start.name); } debug(s); triangulate(segStart, segment); } } static final private boolean needsDiagonal(Segment seg1, Segment seg2) { // debug(seg1+"("+((int) // (seg1.orientation*180/Math.PI)) + Unicode.degreeChar + // ")"+","+seg2+"("+((int) // (seg2.orientation*180/Math.PI)) + Unicode.degreeChar + ")"); if (seg1.orientation < seg2.orientation) { return true; } return false; } /** * @param firstBelow0 * segment below * @param firstAbove0 * segment above */ public void triangulate(Segment firstBelow0, Segment firstAbove0) { Segment firstAbove = firstAbove0; Segment firstBelow = firstBelow0; // init stack Chain chain; Stack<Point> stack = new Stack<Point>(); stack.push(firstAbove.leftPoint); Point pAbove = firstAbove.rightPoint; Point pBelow = firstBelow.rightPoint; if (pAbove.compareToOnly(pBelow) < 0) { // debug("above : "+pAbove.name); chain = Chain.ABOVE; stack.push(pAbove); firstAbove = firstAbove.next; } else { // debug("below : "+pBelow.name); chain = Chain.BELOW; stack.push(pBelow); firstBelow = firstBelow.next; } // loop while (firstAbove != null && firstBelow != null) { // StringBuilder s = new StringBuilder("fan : "); // ArrayList<Integer> currentTriangleFan = new ArrayList<Integer>(); TriangleFan currentTriangleFan; Point top = stack.peek(); Point vi; Chain viChain; // debug(firstAbove+"/"+firstBelow); if (chain == Chain.ABOVE) { // top point is pAbove // if (firstAbove != null){ pAbove = firstAbove.rightPoint; if (pAbove.compareToOnly(pBelow) < 0) { // next point is above vi = pAbove; viChain = Chain.ABOVE; firstAbove = firstAbove.next; } else { // next point is below vi = pBelow; viChain = Chain.BELOW; firstBelow = firstBelow.next; } /* * }else{ // next point is below vi = pBelow; viChain = * Chain.BELOW; firstBelow = firstBelow.next; } */ } else { // (chain == Chain.BELOW){ // top point is pBelow // if (firstBelow != null){ pBelow = firstBelow.rightPoint; if (pBelow.compareToOnly(pAbove) < 0) { // next point is below vi = pBelow; viChain = Chain.BELOW; firstBelow = firstBelow.next; } else { // next point is above vi = pAbove; viChain = Chain.ABOVE; firstAbove = firstAbove.next; } /* * }else{ // next point is above vi = pAbove; viChain = * Chain.ABOVE; firstAbove = firstAbove.next; } */ } boolean clockWise = false; // boolean viBetween = vi > min && vi < max; // debugDiagonal("(vi > min && vi < max) , (top > min && top < max) // : "+(vi // > min && vi < max)+","+(top > min && top < max),vi,top); if (viChain != chain) { // vi and top are not on the same chain debugDiagonal("case 2 ", top, vi); // debug("case 2, "+viChain+" : "+vi.name); if (viChain == Chain.ABOVE) { clockWise = true; } currentTriangleFan = new TriangleFan(vi.id, clockWise); // s.append(vi.name); while (!stack.isEmpty()) { Point v = stack.pop(); currentTriangleFan.add(v.id); // s.append(v.name); debugDiagonal("diagonal : ", vi, v); } stack.push(top); stack.push(vi); } else { // vi and top are on the same chain debugDiagonal("case 1 ", top, vi); // debug("case 1, "+viChain+" : "+vi.name); if (viChain == Chain.BELOW) { clockWise = true; } currentTriangleFan = new TriangleFan(vi.id, clockWise); // s.append(vi.name); // first correct point Point vk = stack.pop(); currentTriangleFan.add(vk.id); // s.append(vk.name); debugDiagonal("diagonal ", vi, vk); double dx2 = vk.x - vi.x; double dy2 = vk.y - vi.y; boolean go = true; while (!stack.isEmpty() && go) { double dx1 = dx2; double dy1 = dy2; Point v = stack.pop(); dx2 = v.x - vi.x; dy2 = v.y - vi.y; if (Kernel.isGreater(dx1 * dy2, dx2 * dy1) ^ (viChain != Chain.BELOW)) { // not same // orientation stack.push(v); // re-push v in stack go = false; } else { vk = v; currentTriangleFan.add(vk.id); // s.append(vk.name); debugDiagonal("diagonal ", vi, vk); } } stack.push(vk); stack.push(vi); } if (currentTriangleFan.size() > 1) { // add fan only if at least 3 // points fansList.add(currentTriangleFan); if (DEBUG) { if (clockWise) { // error(s.toString()); } else { // debug(s.toString()); } } } chain = viChain; } /* * String s="fans: "; for (ArrayList<Point> fan : ret){ for (Point p : * fan){ s+=p.name; } s+=", "; } debug(s); */ } final static private void debugDiagonal(String s, Point p1, Point p2) { debug(s + ": " + p1.name + "," + p2.name); } /** * * @return list of list of points indices, which constitute triangle fans * covering the polygon */ public ArrayList<TriangleFan> getTriangleFans() { return fansList; } private Coords[] completeVertices = new Coords[0]; private Coords[] corners = null; /** * set complete 3D vertex array (with intersections) * * @param vertices * original points vertices * @param cs * coord sys to compute 3D points for intersections * @param length * vertices length */ public void setCompleteVertices(Coords[] vertices, CoordSys cs, int length) { if (maxPointIndex == length) { return; } if (completeVertices.length < maxPointIndex) { completeVertices = new Coords[maxPointIndex]; } for (int i = 0; i < length; i++) { completeVertices[i] = vertices[i]; } for (int i = length; i < maxPointIndex; i++) { GPoint2D.Double point = pointsArray[i]; if (point != null) { completeVertices[i] = cs.getPoint(point.x, point.y); } } } /** * * @param vertices * original points vertices * @param length * vertices length * @return complete 3D vertex array (with intersections) */ public Coords[] getCompleteVertices(Coords[] vertices, int length) { if (maxPointIndex == length) { return vertices; } return completeVertices; } /** * * @return max index for points */ public int getMaxPointIndex() { return maxPointIndex; } public void setCorners(Coords[] corners) { this.corners = corners; } }