/*
* Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
* Copyright (c) 1998-2008, 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.
*
* None of the name of the University of Maryland, the name of the Piccolo2D project, or 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.
*/
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.activities.PColorActivity;
import edu.umd.cs.piccolo.activities.PInterpolatingActivity;
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 {
/**
* Allows for future serialization code to understand versioned binary
* formats.
*/
private static final long serialVersionUID = 1L;
/**
* 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";
/**
* The property code 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 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";
/**
* The property code 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 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";
/**
* The property code 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 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;
/**
* Creates a PPath object in the shape of a rectangle.
*
* @param x left of the rectangle
* @param y top of the rectangle
* @param width width of the rectangle
* @param height height of the rectangle
*
* @return created rectangle
*/
public static PPath createRectangle(final float x, final float y, final float width, final float height) {
TEMP_RECTANGLE.setFrame(x, y, width, height);
final PPath result = new PPath(TEMP_RECTANGLE);
result.setPaint(Color.white);
return result;
}
/**
* Creates a PPath object in the shape of a rounded rectangle.
*
* @param x left of the rectangle
* @param y top of the rectangle
* @param width width of the rectangle
* @param height height of the rectangle
* @param arcWidth the arc width at the corners of the rectangle
* @param arcHeight the arc height at the corners of the rectangle
*
* @return created rounded rectangle
*/
public static PPath createRoundRectangle(final float x, final float y, final float width, final float height,
final float arcWidth, final float arcHeight) {
TEMP_ROUNDRECTANGLE.setRoundRect(x, y, width, height, arcWidth, arcHeight);
final PPath result = new PPath(TEMP_ROUNDRECTANGLE);
result.setPaint(Color.white);
return result;
}
/**
* Creates a PPath object in the shape of an ellipse.
*
* @param x left of the ellipse
* @param y top of the ellipse
* @param width width of the ellipse
* @param height height of the ellipse
*
* @return created ellipse
*/
public static PPath createEllipse(final float x, final float y, final float width, final float height) {
TEMP_ELLIPSE.setFrame(x, y, width, height);
final PPath result = new PPath(TEMP_ELLIPSE);
result.setPaint(Color.white);
return result;
}
/**
* Creates a PPath in the shape of a line.
*
* @param x1 x component of the first point
* @param y1 y component of the first point
* @param x2 x component of the second point
* @param y2 y component of the second point
*
* @return created line
*/
public static PPath createLine(final float x1, final float y1, final float x2, final float y2) {
final PPath result = new PPath();
result.moveTo(x1, y1);
result.lineTo(x2, y2);
result.setPaint(Color.white);
return result;
}
/**
* Creates a PPath for the poly-line for the given points.
*
* @param points array of points for the point lines
*
* @return created poly-line for the given points
*/
public static PPath createPolyline(final Point2D[] points) {
final PPath result = new PPath();
result.setPathToPolyline(points);
result.setPaint(Color.white);
return result;
}
/**
* Creates a PPath for the poly-line for the given points.
*
* @param xp array of x components of the points of the poly-lines
* @param yp array of y components of the points of the poly-lines
*
* @return created poly-line for the given points
*/
public static PPath createPolyline(final float[] xp, final float[] yp) {
final PPath result = new PPath();
result.setPathToPolyline(xp, yp);
result.setPaint(Color.white);
return result;
}
/**
* Creates an empty PPath with the default paint and stroke.
*/
public PPath() {
strokePaint = DEFAULT_STROKE_PAINT;
stroke = DEFAULT_STROKE;
path = new GeneralPath();
}
/**
* Creates an PPath in the given shape with the default paint and stroke.
*
* @param aShape the desired shape
*/
public PPath(final 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.
*
* @param aShape desired shape or null if you desire an empty path
* @param aStroke desired stroke
*/
public PPath(final Shape aShape, final Stroke aStroke) {
this();
stroke = aStroke;
if (aShape != null) {
append(aShape, false);
}
}
/**
* Returns the stroke paint of the PPath.
*
* @return stroke paint of the PPath
*/
public Paint getStrokePaint() {
return strokePaint;
}
/**
* Sets the stroke paint of the path.
*
* @param newStrokePaint the paint to use as this path's stroke paint
*/
public void setStrokePaint(final Paint newStrokePaint) {
final Paint oldStrokePaint = strokePaint;
strokePaint = newStrokePaint;
invalidatePaint();
firePropertyChange(PROPERTY_CODE_STROKE_PAINT, PROPERTY_STROKE_PAINT, oldStrokePaint, strokePaint);
}
/**
* Returns the stroke to use when drawing the path.
*
* @return current stroke of path
*/
public Stroke getStroke() {
return stroke;
}
/**
* Sets the stroke to use when drawing the path.
*
* @param aStroke stroke to use when drawing the path
*/
public void setStroke(final Stroke aStroke) {
final Stroke old = stroke;
stroke = aStroke;
updateBoundsFromPath();
invalidatePaint();
firePropertyChange(PROPERTY_CODE_STROKE, PROPERTY_STROKE, old, stroke);
}
/** Stores the original size of the path before resizing started. */
public void startResizeBounds() {
resizePath = new GeneralPath(path);
}
/** Clears the size of the path before resizing. */
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.
*
* @param x new left position of bounds
* @param y new top position of bounds
* @param width the new width of the bounds
* @param height the new height of the bounds
*/
protected void internalUpdateBounds(final double x, final double y, final double width, final double height) {
if (updatingBoundsFromPath || path == null) {
return;
}
if (resizePath != null) {
path.reset();
path.append(resizePath, false);
}
final Rectangle2D pathBounds = path.getBounds2D();
final Rectangle2D pathStrokeBounds = getPathBoundsWithStroke();
final double strokeOutset = Math.max(pathStrokeBounds.getWidth() - pathBounds.getWidth(), pathStrokeBounds
.getHeight()
- pathBounds.getHeight());
double adjustedX = x + strokeOutset / 2;
double adjustedY = y + strokeOutset / 2;
double adjustedWidth = width - strokeOutset;
double adjustedHeight = height - strokeOutset;
final double scaleX;
if (adjustedWidth == 0 || pathBounds.getWidth() == 0) {
scaleX = 1;
}
else {
scaleX = adjustedWidth / pathBounds.getWidth();
}
final double scaleY;
if (adjustedHeight == 0 || pathBounds.getHeight() == 0) {
scaleY = 1;
}
else {
scaleY = adjustedHeight / pathBounds.getHeight();
}
TEMP_TRANSFORM.setToIdentity();
TEMP_TRANSFORM.translate(adjustedX, adjustedY);
TEMP_TRANSFORM.scale(scaleX, scaleY);
TEMP_TRANSFORM.translate(-pathBounds.getX(), -pathBounds.getY());
path.transform(TEMP_TRANSFORM);
}
/**
* Returns true if path crosses the provided bounds. Takes visibility of
* path into account.
*
* @param aBounds bounds being tested for intersection
* @return true if path visibly crosses bounds
*/
public boolean intersects(final 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;
}
/**
* Calculates the path's bounds taking stroke into account.
*
* @return bounds of the path taking stroke width into account
*/
public Rectangle2D getPathBoundsWithStroke() {
if (stroke != null) {
return stroke.createStrokedShape(path).getBounds2D();
}
else {
return path.getBounds2D();
}
}
/**
* Recomputes the bounds taking stroke into account.
*/
public void updateBoundsFromPath() {
updatingBoundsFromPath = true;
if (path == null) {
resetBounds();
}
else {
final Rectangle2D b = getPathBoundsWithStroke();
setBounds(b.getX(), b.getY(), b.getWidth(), b.getHeight());
}
updatingBoundsFromPath = false;
}
/**
* Paints the path in the provided paintContext. Can perform very
* differently depending on whether the path is being drawn using its stroke
* or its paint.
*
* It both are provided to the path, fun ensues.
*
* @param paintContext context in which painting is occurring
*/
protected void paint(final PPaintContext paintContext) {
final Paint p = getPaint();
final 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);
}
}
/**
* Provides direct access to the underlying GeneralPath object.
*
* @return underlying GeneralPath
*/
public GeneralPath getPathReference() {
return path;
}
/**
* Appends a "move" operation to the end of the path.
*
* @param x the x component of the point to move to
* @param y the y component of the point to move to
*/
public void moveTo(final float x, final float y) {
path.moveTo(x, y);
firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
updateBoundsFromPath();
invalidatePaint();
}
/**
* Draws a line from the last point in the path to point provided.
*
* @param x the x component of the point
* @param y the y component of the point
*/
public void lineTo(final float x, final float y) {
path.lineTo(x, y);
firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
updateBoundsFromPath();
invalidatePaint();
}
/**
* Adds a curved segment, defined by two new points, to the path by drawing
* a Quadratic curve that intersects both the current coordinates and the
* coordinates (x2, y2), using the specified point (x1, y1) as a quadratic
* parametric control point.
*
* @param x1 x component of quadratic parametric control point
* @param y1 y component of quadratic parametric control point
* @param x2 x component of point through which quad curve will pass
* @param y2 y component of point through which quad curve will pass
*/
public void quadTo(final float x1, final float y1, final float x2, final float y2) {
path.quadTo(x1, y1, x2, y2);
firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
updateBoundsFromPath();
invalidatePaint();
}
/**
* Adds a curved segment, defined by three new points, to the path by
* drawing a Bézier curve that intersects both the current coordinates and
* the coordinates (x3, y3), using the specified points (x1, y1) and (x2,
* y2) as Bézier control points.
*
* @param x1 x component of first Bézier control point
* @param y1 y component of first Bézier control point
* @param x2 x component of second Bézier control point
* @param y2 y component of second Bézier control point
* @param x3 x component of point through which curve must pass
* @param y3 y component of point through which curve must pass
*/
public void curveTo(final float x1, final float y1, final float x2, final float y2,
final float x3, final float y3) {
path.curveTo(x1, y1, x2, y2, x3, y3);
firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
updateBoundsFromPath();
invalidatePaint();
}
/**
* Appends the provided shape to the end of this path, it may conditionally
* connect them together if they are disjoint.
*
* @param aShape shape to append
* @param connect whether to perform a lineTo operation to the beginning of
* the shape before appending
*/
public void append(final Shape aShape, final boolean connect) {
path.append(aShape, connect);
firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
updateBoundsFromPath();
invalidatePaint();
}
/**
* Replaces this PPath's path with the one provided.
*
* @param aShape shape to replace the current one with
*/
public void setPathTo(final Shape aShape) {
path.reset();
append(aShape, false);
}
/**
* Resets the path to a rectangle with the dimensions and position provided.
*
* @param x left of the rectangle
* @param y top of te rectangle
* @param width width of the rectangle
* @param height height of the rectangle
*/
public void setPathToRectangle(final float x, final float y, final float width, final float height) {
TEMP_RECTANGLE.setFrame(x, y, width, height);
setPathTo(TEMP_RECTANGLE);
}
/**
* Resets the path to an ellipse positioned at the coordinate provided with
* the dimensions provided.
*
* @param x left of the ellipse
* @param y top of the ellipse
* @param width width of the ellipse
* @param height height of the ellipse
*/
public void setPathToEllipse(final float x, final float y, final float width, final float height) {
TEMP_ELLIPSE.setFrame(x, y, width, height);
setPathTo(TEMP_ELLIPSE);
}
/**
* Sets the path to a sequence of segments described by the points.
*
* @param points points to that lie along the generated path
*/
public void setPathToPolyline(final 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();
}
/**
* Sets the path to a sequence of segments described by the point components
* provided.
*
* @param xp the x components of the points along the path
* @param yp the y components of the points along the path
*/
public void setPathToPolyline(final float[] xp, final 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();
}
/**
* Marks the path as closed. Making changes to it impossible.
*/
public void closePath() {
path.closePath();
firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
updateBoundsFromPath();
invalidatePaint();
}
/**
* Empties the path.
*/
public void reset() {
path.reset();
firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
updateBoundsFromPath();
invalidatePaint();
}
/**
* Writes this PPath object to the output stream provided. Necessary since
* stroke and path are not serializable by default.
*
* @param out output stream into which objects are to be serialized
* @throws IOException if serialiazing to output stream fails
*/
private void writeObject(final ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
PUtil.writeStroke(stroke, out);
PUtil.writePath(path, out);
}
/**
* Deserializes a PPath object from the provided input stream. This method
* is required since Strokes and GeneralPaths are not serializable by
* default.
*
* @param in stream from which to read this PPath's state
* @throws IOException when exception occurs reading from input stream
* @throws ClassNotFoundException
*/
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
stroke = PUtil.readStroke(in);
path = PUtil.readPath(in);
}
/**
* added by miura 2012/11/29
* @param destColor
* @param duration
* @return
*/
public PInterpolatingActivity animateToStrokeColor(final Color destColor, final long duration) {
if (duration == 0) {
setPaint(destColor);
return null;
}
else {
final PColorActivity.Target t = new PColorActivity.Target() {
public Color getColor() {
return (Color) getStrokePaint();
}
public void setColor(final Color color) {
setStrokePaint(color);
}
};
final PColorActivity ca = new PColorActivity(duration, PUtil.DEFAULT_ACTIVITY_STEP_RATE, t, destColor);
addActivity(ca);
return ca;
}
}
}