Newer
Older
piccolo2d.java / src / edu / umd / cs / piccolo / nodes / PPath.java
@Jesse Grosjean Jesse Grosjean on 5 Oct 2006 14 KB piccolo java
/*
 * Copyright (c) 2002-@year@, University of Maryland
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list of conditions
 * and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
 * and the following disclaimer in the documentation and/or other materials provided with the
 * distribution.
 *
 * Neither the name of the University of Maryland nor the names of its contributors may be used to
 * endorse or promote products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Piccolo was written at the Human-Computer Interaction Laboratory www.cs.umd.edu/hcil by Jesse Grosjean
 * under the supervision of Ben Bederson. The Piccolo website is www.cs.umd.edu/hcil/piccolo.
 */
package edu.umd.cs.piccolo.nodes;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.util.PAffineTransform;
import edu.umd.cs.piccolo.util.PPaintContext;
import edu.umd.cs.piccolo.util.PUtil;

/**
 * <b>PPath</b> is a wrapper around a java.awt.geom.GeneralPath. The
 * setBounds method works by scaling the path to fit into the specified
 * bounds. This normally works well, but if the specified base bounds
 * get too small then it is impossible to expand the path shape again since
 * all its numbers have tended to zero, so application code may need to take
 * this into consideration. 
 * <P>
 * One option that applications have is to call <code>startResizeBounds</code> before
 * starting an interaction that may make the bounds very small, and calling 
 * <code>endResizeBounds</code> when this interaction is finished. When this is done
 * PPath will use a copy of the original path to do the resizing so the numbers
 * in the path wont loose resolution.
 * <P>
 * This class also provides methods for constructing common shapes using a 
 * general path.
 * <P>
 * @version 1.0
 * @author Jesse Grosjean
 */
public class PPath extends PNode {
	
	/** 
	 * The property name that identifies a change of this node's stroke paint
	 * (see {@link #getStrokePaint getStrokePaint}). Both old and new value will
	 * be set correctly to Paint objects in any property change event.
	 */
	public static final String PROPERTY_STROKE_PAINT = "strokePaint";
    public static final int PROPERTY_CODE_STROKE_PAINT = 1 << 16;

	/** 
	 * The property name that identifies a change of this node's stroke (see
	 * {@link #getStroke getStroke}). Both old and new value will be set
	 * correctly to Stroke objects in any property change event.
	 */
	public static final String PROPERTY_STROKE = "stroke";
    public static final int PROPERTY_CODE_STROKE = 1 << 17;

	/** 
	 * The property name that identifies a change of this node's path (see
	 * {@link #getPathReference getPathReference}).  In any property change
	 * event the new value will be a reference to this node's path,  but old
	 * value will always be null.
	 */
	public static final String PROPERTY_PATH = "path";
    public static final int PROPERTY_CODE_PATH = 1 << 18;
	
	private static final Rectangle2D.Float TEMP_RECTANGLE = new Rectangle2D.Float();
	private static final RoundRectangle2D.Float TEMP_ROUNDRECTANGLE = new RoundRectangle2D.Float();
	private static final Ellipse2D.Float TEMP_ELLIPSE = new Ellipse2D.Float();
	private static final PAffineTransform TEMP_TRANSFORM = new PAffineTransform();
	private static final BasicStroke DEFAULT_STROKE = new BasicStroke(1.0f);
	private static final Color DEFAULT_STROKE_PAINT = Color.black;
	
	private transient GeneralPath path;
	private transient GeneralPath resizePath;
	private transient Stroke stroke;
	private transient boolean updatingBoundsFromPath;
	private Paint strokePaint;

	public static PPath createRectangle(float x, float y, float width, float height) {
		TEMP_RECTANGLE.setFrame(x, y, width, height);
		PPath result = new PPath(TEMP_RECTANGLE);
		result.setPaint(Color.white);
		return result;
	}

	public static PPath createRoundRectangle(float x, float y, float width, float height, float arcWidth, float arcHeight) {
		TEMP_ROUNDRECTANGLE.setRoundRect(x, y, width, height, arcWidth, arcHeight);
		PPath result = new PPath(TEMP_ROUNDRECTANGLE);
		result.setPaint(Color.white);
		return result;
	}

	public static PPath createEllipse(float x, float y, float width, float height) {
		TEMP_ELLIPSE.setFrame(x, y, width, height);
		PPath result = new PPath(TEMP_ELLIPSE);
		result.setPaint(Color.white);
		return result;
	}
	
	public static PPath createLine(float x1, float y1, float x2, float y2) {
		PPath result = new PPath();
		result.moveTo(x1, y1);
		result.lineTo(x2, y2);
		result.setPaint(Color.white);
		return result;
	}
	
	public static PPath createPolyline(Point2D[] points) {
		PPath result = new PPath();
		result.setPathToPolyline(points);
		result.setPaint(Color.white);
		return result;
	}

	public static PPath createPolyline(float[] xp, float[] yp) {
		PPath result = new PPath();
		result.setPathToPolyline(xp, yp);
		result.setPaint(Color.white);
		return result;
	}
		
	public PPath() {
		strokePaint = DEFAULT_STROKE_PAINT;
		stroke = DEFAULT_STROKE;
		path = new GeneralPath();
	}

	public PPath(Shape aShape) {
		this(aShape, DEFAULT_STROKE);
	}

	/**
	 * Construct this path with the given shape and stroke.
	 * This method may be used to optimize the creation of a large number of
	 * PPaths. Normally PPaths have a default stroke of width one, but when a
	 * path has a non null stroke it takes significantly longer to compute its
	 * bounds. This method allows you to override that default stroke before the
	 * bounds are ever calculated, so if you pass in a null stroke here you
	 * won't ever have to pay that bounds calculation price if you don't need
	 * to.
	 */
	public PPath(Shape aShape, Stroke aStroke) {
		this();
		stroke = aStroke;
		if (aShape != null) append(aShape, false);
	}
	
	//****************************************************************
	// Stroke
	//****************************************************************
	
	public Paint getStrokePaint() {
		return strokePaint;
	}

	public void setStrokePaint(Paint aPaint) {
		Paint old = strokePaint;
		strokePaint = aPaint;
		invalidatePaint();
		firePropertyChange(PROPERTY_CODE_STROKE_PAINT ,PROPERTY_STROKE_PAINT, old, strokePaint);
	}
	
	public Stroke getStroke() {
		return stroke;
	}

	public void setStroke(Stroke aStroke) {
		Stroke old = stroke;
		stroke = aStroke;
		updateBoundsFromPath();
		invalidatePaint();
		firePropertyChange(PROPERTY_CODE_STROKE ,PROPERTY_STROKE, old, stroke);
	}
		
	//****************************************************************
	// Bounds
	//****************************************************************
		
	public void startResizeBounds() {
		resizePath = new GeneralPath(path);
	}

	public void endResizeBounds() {
		resizePath = null;
	}
				
	/**
	 * Set the bounds of this path. This method works by scaling the path 
	 * to fit into the specified bounds. This normally works well, but if 
	 * the specified base bounds get too small then it is impossible to 
	 * expand the path shape again since all its numbers have tended to zero, 
	 * so application code may need to take this into consideration.
	 */
	protected void internalUpdateBounds(double x, double y, double width, double height) {
		if (updatingBoundsFromPath) return;
		if (path == null) return;
		
		if (resizePath != null) {
			path.reset();
			path.append(resizePath, false);
		}

		Rectangle2D pathBounds = path.getBounds2D();
		Rectangle2D pathStrokeBounds = getPathBoundsWithStroke();
		double strokeOutset = Math.max(pathStrokeBounds.getWidth() - pathBounds.getWidth(), 
										pathStrokeBounds.getHeight() - pathBounds.getHeight());
		
		x += strokeOutset / 2;
		y += strokeOutset / 2;
		width -= strokeOutset;
		height -= strokeOutset;
		
		double scaleX = (width == 0 || pathBounds.getWidth() == 0) ? 1 : width / pathBounds.getWidth();
		double scaleY = (height == 0 || pathBounds.getHeight() == 0) ? 1 : height / pathBounds.getHeight();
		
		TEMP_TRANSFORM.setToIdentity();
		TEMP_TRANSFORM.translate(x, y);
		TEMP_TRANSFORM.scale(scaleX, scaleY);
		TEMP_TRANSFORM.translate(-pathBounds.getX(), -pathBounds.getY());
		
		path.transform(TEMP_TRANSFORM);
	}
	
	public boolean intersects(Rectangle2D aBounds) {
		if (super.intersects(aBounds)) {
			if (getPaint() != null && path.intersects(aBounds)) {
				return true;
			} else if (stroke != null && strokePaint != null) {
				return stroke.createStrokedShape(path).intersects(aBounds);
			}
		}
		return false;
	}
			
	public Rectangle2D getPathBoundsWithStroke() {
		if (stroke != null) {
			return stroke.createStrokedShape(path).getBounds2D();
		} else {
			return path.getBounds2D();
		}
	}
			
	public void updateBoundsFromPath() {
		updatingBoundsFromPath = true;
		if (path == null) {
			resetBounds();
		} else {
			Rectangle2D b = getPathBoundsWithStroke();
			setBounds(b.getX(), b.getY(), b.getWidth(), b.getHeight());
		}
		updatingBoundsFromPath = false;
	}
	
	//****************************************************************
	// Painting
	//****************************************************************
	
	protected void paint(PPaintContext paintContext) {
		Paint p = getPaint();
		Graphics2D g2 = paintContext.getGraphics();
		
		if (p != null) {
			g2.setPaint(p);
			g2.fill(path);
		}

		if (stroke != null && strokePaint != null) {
			g2.setPaint(strokePaint);
			g2.setStroke(stroke);
			g2.draw(path);
		}		
	}	
		
	//****************************************************************
	// Path Support set java.awt.GeneralPath documentation for more
	// information on using these methods.
	//****************************************************************

	public GeneralPath getPathReference() {
		return path;
	}
	
	public void moveTo(float x, float y) {
		path.moveTo(x, y);
		firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
		updateBoundsFromPath();
		invalidatePaint();
	}
	
	public void lineTo(float x, float y) {
		path.lineTo(x, y);
		firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
		updateBoundsFromPath();
		invalidatePaint();
	}

	public void quadTo(float x1, float y1, float x2, float y2) {
		path.quadTo(x1, y1, x2, y2);
		firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
		updateBoundsFromPath();
		invalidatePaint();
	}

	public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) {
		path.curveTo(x1, y1, x2, y2, x3, y3);
		firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
		updateBoundsFromPath();
		invalidatePaint();
	}
	
	public void append(Shape aShape, boolean connect) {
		path.append(aShape, connect);
		firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
		updateBoundsFromPath();
		invalidatePaint();
	}
	
	public void setPathTo(Shape aShape) {
		path.reset();
		append(aShape, false);
	}

	public void setPathToRectangle(float x, float y, float width, float height) {
		TEMP_RECTANGLE.setFrame(x, y, width, height);
		setPathTo(TEMP_RECTANGLE);
	}

	public void setPathToEllipse(float x, float y, float width, float height) {
		TEMP_ELLIPSE.setFrame(x, y, width, height);
		setPathTo(TEMP_ELLIPSE);
	}

	public void setPathToPolyline(Point2D[] points) {
		path.reset();
		path.moveTo((float)points[0].getX(), (float)points[0].getY());
		for (int i = 1; i < points.length; i++) {
			path.lineTo((float)points[i].getX(), (float)points[i].getY());
		}
		firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
		updateBoundsFromPath();
		invalidatePaint();		
	}

	public void setPathToPolyline(float[] xp, float[] yp) {
		path.reset();
		path.moveTo(xp[0], yp[0]);
		for (int i = 1; i < xp.length; i++) {
			path.lineTo(xp[i], yp[i]);
		}
		firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
		updateBoundsFromPath();
		invalidatePaint();
	}
	
	public void closePath() {
		path.closePath();
		firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
		updateBoundsFromPath();
		invalidatePaint();
	}
	
	public void reset() {
		path.reset();
		firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
		updateBoundsFromPath();
		invalidatePaint();
	}
	
	//****************************************************************
	// Serialization
	//****************************************************************
	
	private void writeObject(ObjectOutputStream out) throws IOException {
		out.defaultWriteObject();
		PUtil.writeStroke(stroke, out); 	   
		PUtil.writePath(path, out); 	   
	}

	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
		in.defaultReadObject();		
		stroke = PUtil.readStroke(in);
		path = PUtil.readPath(in);		
	}
	
	//****************************************************************
	// Debugging - methods for debugging
	//****************************************************************
	
	/**
	 * Returns a string representing the state of this node. This method is
	 * intended to be used only for debugging purposes, and the content and
	 * format of the returned string may vary between implementations. The
	 * returned string may be empty but may not be <code>null</code>.
	 *
	 * @return  a string representation of this node's state
	 */
	protected String paramString() {
		StringBuffer result = new StringBuffer();

		result.append("path=" + (path == null ? "null" : path.toString()));
		result.append(",stroke=" + (stroke == null ? "null" : stroke.toString()));
		result.append(",strokePaint=" + (strokePaint == null ? "null" : strokePaint.toString()));
		result.append(',');
		result.append(super.paramString());

		return result.toString();
	}	
}