PolygonFigure.java

/*
 * @(#)PolygonFigure.java
 *
 * Project:		JHotdraw - a GUI framework for technical drawings
 *				http://www.jhotdraw.org
 *				http://jhotdraw.sourceforge.net
 * Copyright:	© by the original author(s) and all contributors
 * License:		Lesser GNU Public License (LGPL)
 *				http://www.opensource.org/licenses/lgpl-license.html
 */

package CH.ifa.draw.contrib;

import CH.ifa.draw.framework.*;
import CH.ifa.draw.util.*;
import CH.ifa.draw.standard.*;
import CH.ifa.draw.figures.*;
import java.awt.*;
import java.util.*;
import java.io.IOException;

/**
 * A scalable, rotatable polygon with an arbitrary number of points
 * Based on PolyLineFigure
 *
 * @author Doug Lea  (dl at gee, Fri Feb 28 07:47:05 1997)
 * @version <$CURRENT_VERSION$>
 */
public  class PolygonFigure extends AttributeFigure {

	/**
	 * Distance threshold for smoothing away or locating points
	 **/
	static final int TOO_CLOSE = 2;

	/*
	 * Serialization support.
	 */
	private static final long serialVersionUID = 6254089689239215026L;
	private int polygonFigureSerializedDataVersion = 1;

	private Polygon fPoly;

	public PolygonFigure() {
		super();
		setInternalPolygon(new Polygon());
	}

	public PolygonFigure(int x, int y) {
		this();
		getInternalPolygon().addPoint(x, y);
	}

	public PolygonFigure(Polygon p) {
		setInternalPolygon(new Polygon(p.xpoints, p.ypoints, p.npoints));
	}

	public Rectangle displayBox() {
		return bounds(getInternalPolygon());
	}


	public boolean isEmpty() {
		return ((pointCount() < 3) ||
				((size().width < TOO_CLOSE) && (size().height < TOO_CLOSE)));
	}

	public Vector handles() {
		Vector handles = new Vector(pointCount());
		for (int i = 0; i < pointCount(); i++) {
			handles.addElement(new PolygonHandle(this, locator(i), i));
		}
		handles.addElement(new PolygonScaleHandle(this));
		return handles;
	}


	public void basicDisplayBox(Point origin, Point corner) {
		Rectangle r = displayBox();
		int dx = origin.x - r.x;
		int dy = origin.y - r.y;
		getInternalPolygon().translate(dx, dy);
		r = displayBox();
		Point oldCorner = new Point(r.x + r.width, r.y + r.height);
		scaleRotate(oldCorner, getInternalPolygon(), corner);
	}

	/**
	 * @return a copy of the internal polygon
	 **/
	public Polygon getPolygon() {
		return new Polygon(fPoly.xpoints, fPoly.ypoints, fPoly.npoints);
	}

	protected void setInternalPolygon(Polygon newPolygon) {
		fPoly = newPolygon;
	}

	public Polygon getInternalPolygon() {
		return fPoly;
	}

	public Point center() {
		return center(getInternalPolygon());
	}

	public Enumeration points() {
		Vector pts = new Vector(pointCount());
		for (int i = 0; i < pointCount(); ++i) {
			pts.addElement(new Point(getInternalPolygon().xpoints[i], 
									 getInternalPolygon().ypoints[i]));
		}
		return pts.elements();
	}

	public int pointCount() {
		return getInternalPolygon().npoints;
	}

	public void basicMoveBy(int dx, int dy) {
		getInternalPolygon().translate(dx, dy);
	}

	public void drawBackground(Graphics g) {
		g.fillPolygon(getInternalPolygon());
	}

	public void drawFrame(Graphics g) {
		g.drawPolygon(getInternalPolygon());
	}

	public boolean containsPoint(int x, int y) {
		return getInternalPolygon().contains(x, y);
	}

	public Connector connectorAt(int x, int y) {
		return new ChopPolygonConnector(this);
	}

	/**
	 * Adds a node to the list of points.
	 */
	public void addPoint(int x, int y) {
		getInternalPolygon().addPoint(x, y);
		changed();
	}


	/**
	 * Changes the position of a node.
	 */
	public void setPointAt(Point p, int i) {
		willChange();
		getInternalPolygon().xpoints[i] = p.x;
		getInternalPolygon().ypoints[i] = p.y;
		changed();
	}

	/**
	 * Insert a node at the given point.
	 */
	public void insertPointAt(Point p, int i) {
		willChange();
		int n = pointCount() + 1;
		int[] xs = new int[n];
		int[] ys = new int[n];
		for (int j = 0; j < i; ++j) {
			xs[j] = getInternalPolygon().xpoints[j];
			ys[j] = getInternalPolygon().ypoints[j];
		}
		xs[i] = p.x;
		ys[i] = p.y;
		for (int j = i; j < pointCount(); ++j) {
			xs[j + 1] = getInternalPolygon().xpoints[j];
			ys[j + 1] = getInternalPolygon().ypoints[j];
		}

		setInternalPolygon(new Polygon(xs, ys, n));
		changed();
	}

	public void removePointAt(int i) {
		willChange();
		int n = pointCount() - 1;
		int[] xs = new int[n];
		int[] ys = new int[n];
		for (int j = 0; j < i; ++j) {
			xs[j] = getInternalPolygon().xpoints[j];
			ys[j] = getInternalPolygon().ypoints[j];
		}
		for (int j = i; j < n; ++j) {
			xs[j] = getInternalPolygon().xpoints[j + 1];
			ys[j] = getInternalPolygon().ypoints[j + 1];
		}
		setInternalPolygon(new Polygon(xs, ys, n));
		changed();
	}

	/**
	 * Scale and rotate relative to anchor
	 **/
	public  void scaleRotate(Point anchor, Polygon originalPolygon, Point p) {
		willChange();

		// use center to determine relative angles and lengths
		Point ctr = center(originalPolygon);
		double anchorLen = Geom.length(ctr.x, ctr.y, anchor.x, anchor.y);

		if (anchorLen > 0.0) {
			double newLen = Geom.length(ctr.x, ctr.y, p.x, p.y);
			double ratio = newLen / anchorLen;

			double anchorAngle = Math.atan2(anchor.y - ctr.y, anchor.x - ctr.x);
			double newAngle = Math.atan2(p.y - ctr.y, p.x - ctr.x);
			double rotation = newAngle - anchorAngle;

			int n = originalPolygon.npoints;
			int[] xs = new int[n];
			int[] ys = new int[n];

			for (int i = 0; i < n; ++i) {
				int x = originalPolygon.xpoints[i];
				int y = originalPolygon.ypoints[i];
				double l = Geom.length(ctr.x, ctr.y, x, y) * ratio;
				double a = Math.atan2(y - ctr.y, x - ctr.x) + rotation;
				xs[i] = (int)(ctr.x + l * Math.cos(a) + 0.5);
				ys[i] = (int)(ctr.y + l * Math.sin(a) + 0.5);
			}
			setInternalPolygon(new Polygon(xs, ys, n));
		}
		changed();
	}


	/**
	 * Remove points that are nearly colinear with others
	 **/
	public void smoothPoints() {
		willChange();
		boolean removed = false;
		int n = pointCount();
		do {
			removed = false;
			int i = 0;
			while (i < n && n >= 3) {
				int nxt = (i + 1) % n;
				int prv = (i - 1 + n) % n;

				if ((Geom.distanceFromLine(getInternalPolygon().xpoints[prv], 
						getInternalPolygon().ypoints[prv],
						getInternalPolygon().xpoints[nxt],
						getInternalPolygon().ypoints[nxt],
						getInternalPolygon().xpoints[i],
						getInternalPolygon().ypoints[i]) < TOO_CLOSE)) {
					removed = true;
					--n;
					for (int j = i; j < n; ++j) {
						getInternalPolygon().xpoints[j] = getInternalPolygon().xpoints[j + 1];
						getInternalPolygon().ypoints[j] = getInternalPolygon().ypoints[j + 1];
					}
				}
				else {
					++i;
				}
			}
		} while(removed);
		if (n != pointCount()) {
			setInternalPolygon(new Polygon(getInternalPolygon().xpoints, 
											getInternalPolygon().ypoints, n));
		}
		changed();
	}

	/**
	 * Splits the segment at the given point if a segment was hit.
	 * @return the index of the segment or -1 if no segment was hit.
	 */
	public int splitSegment(int x, int y) {
		int i = findSegment(x, y);
		if (i != -1) {
			insertPointAt(new Point(x, y), i+1);
			return i + 1;
		}
		else {
			return -1;
		}
	}

	public Point pointAt(int i) {
		return new Point(getInternalPolygon().xpoints[i], 
						 getInternalPolygon().ypoints[i]);
	}

	/**
	 * Return the point on the polygon that is furthest from the center
	 **/
	public Point outermostPoint() {
		Point ctr = center();
		int outer = 0;
		long dist = 0;

		for (int i = 0; i < pointCount(); ++i) {
			long d = Geom.length2(ctr.x, ctr.y, 
								getInternalPolygon().xpoints[i], 
								getInternalPolygon().ypoints[i]);
			if (d > dist) {
				dist = d;
				outer = i;
			}
		}

		return new Point(getInternalPolygon().xpoints[outer], getInternalPolygon().ypoints[outer]);
	}


	/**
	 * Gets the segment that is hit by the given point.
	 * @return the index of the segment or -1 if no segment was hit.
	 */
	public int findSegment(int x, int y) {
		double dist = TOO_CLOSE;
		int best = -1;

		for (int i = 0; i < pointCount(); i++) {
			int n = (i + 1) % pointCount();
			double d =  Geom.distanceFromLine(getInternalPolygon().xpoints[i], 
							getInternalPolygon().ypoints[i],
							getInternalPolygon().xpoints[n],
							getInternalPolygon().ypoints[n], x, y);
			if (d < dist) {
				dist = d;
				best = i;
			}
		}
		return best;
	}

	public Point chop(Point p) {
		return chop(getInternalPolygon(), p);
	}

	public void write(StorableOutput dw) {
		super.write(dw);
		dw.writeInt(pointCount());
		for (int i = 0; i < pointCount(); ++i) {
			dw.writeInt(getInternalPolygon().xpoints[i]);
			dw.writeInt(getInternalPolygon().ypoints[i]);
		}
	}

	public void read(StorableInput dr) throws IOException {
		super.read(dr);
		int size = dr.readInt();
		int[] xs = new int[size];
		int[] ys = new int[size];
		for (int i = 0; i < size; i++) {
			xs[i] = dr.readInt();
			ys[i] = dr.readInt();
		}
		setInternalPolygon(new Polygon(xs, ys, size));
	}

	/**
	 * Creates a locator for the point with the given index.
	 */
	public static Locator locator(final int pointIndex) {
		return new AbstractLocator() {
			public Point locate(Figure owner) {
				PolygonFigure plf = (PolygonFigure)owner;
				// guard against changing PolygonFigures -> temporary hack
				if (pointIndex < plf.pointCount()) {
					return ((PolygonFigure)owner).pointAt(pointIndex);
				}
				return new Point(-1, -1);
			}
		};
	}

	/**
	 * replacement for builtin Polygon.getBounds that doesn't always update?
	 */
	public static Rectangle bounds(Polygon p) {
		int minx = Integer.MAX_VALUE;
		int miny = Integer.MAX_VALUE;
		int maxx = Integer.MIN_VALUE;
		int maxy = Integer.MIN_VALUE;
		int n = p.npoints;
		for (int i = 0; i < n; i++) {
			int x = p.xpoints[i];
			int y = p.ypoints[i];
			if (x > maxx) {
				maxx = x;
			}
			if (x < minx) {
				minx = x;
			}
			if (y > maxy) {
				maxy = y;
			}
			if (y < miny) {
				miny = y;
			}
		}

		return new Rectangle(minx, miny, maxx - minx, maxy - miny);
	}

	public static Point center(Polygon p) {
		long sx = 0;
		long sy = 0;
		int n = p.npoints;
		for (int i = 0; i < n; i++) {
			sx += p.xpoints[i];
			sy += p.ypoints[i];
		}

		return new Point((int)(sx / n), (int)(sy / n));
	}

	public static Point chop(Polygon poly, Point p) {
		Point ctr = center(poly);
		int cx = -1;
		int cy = -1;
		long len = Long.MAX_VALUE;

		// Try for points along edge

		for (int i = 0; i < poly.npoints; ++i) {
			int nxt = (i + 1) % poly.npoints;
			Point chop = Geom.intersect(poly.xpoints[i],
										 poly.ypoints[i],
										 poly.xpoints[nxt],
										 poly.ypoints[nxt],
										 p.x,
										 p.y,
										 ctr.x,
										 ctr.y);
			if (chop != null) {
				long cl = Geom.length2(chop.x, chop.y, p.x, p.y);
				if (cl < len) {
					len = cl;
					cx = chop.x;
					cy = chop.y;
				}
			}
		}
		// if none found, pick closest vertex
		//if (len ==  Long.MAX_VALUE) {
		{ // try anyway
			for (int i = 0; i < poly.npoints; ++i) {
				long l = Geom.length2(poly.xpoints[i], poly.ypoints[i], p.x, p.y);
				if (l < len) {
					len = l;
					cx = poly.xpoints[i];
					cy = poly.ypoints[i];
				}
			}
		}
		return new Point(cx, cy);
	}
}