/*
* 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.piccolox.swt;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.nodes.PPath;
import edu.umd.cs.piccolo.util.PAffineTransform;
import edu.umd.cs.piccolo.util.PBounds;
import edu.umd.cs.piccolo.util.PPaintContext;
/**
* <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 PSWTPath extends PNode {
/**
* The property name that identifies a change of this node's path.
* 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_SHAPE = "shape";
private static final double BOUNDS_TOLERANCE = 0.01;
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 Color DEFAULT_STROKE_PAINT = Color.black;
private static final BasicStroke BASIC_STROKE = new BasicStroke();
private static final float PEN_WIDTH = 1f;
private Paint strokePaint;
boolean updatingBoundsFromPath;
Shape origShape;
Shape shape;
PAffineTransform internalXForm;
AffineTransform inverseXForm;
double[] shapePts;
public static PSWTPath createRectangle(float x, float y, float width, float height) {
TEMP_RECTANGLE.setFrame(x, y, width, height);
PSWTPath result = new PSWTPath(TEMP_RECTANGLE);
result.setPaint(Color.white);
return result;
}
public static PSWTPath createRoundRectangle(float x, float y, float width, float height, float arcWidth, float arcHeight) {
TEMP_ROUNDRECTANGLE.setRoundRect(x, y, width, height, arcWidth, arcHeight);
PSWTPath result = new PSWTPath(TEMP_ROUNDRECTANGLE);
result.setPaint(Color.white);
return result;
}
public static PSWTPath createEllipse(float x, float y, float width, float height) {
TEMP_ELLIPSE.setFrame(x, y, width, height);
PSWTPath result = new PSWTPath(TEMP_ELLIPSE);
result.setPaint(Color.white);
return result;
}
public static PSWTPath createPolyline(Point2D[] points) {
PSWTPath result = new PSWTPath();
result.setPathToPolyline(points);
result.setPaint(Color.white);
return result;
}
public static PSWTPath createPolyline(float[] xp, float[] yp) {
PSWTPath result = new PSWTPath();
result.setPathToPolyline(xp, yp);
result.setPaint(Color.white);
return result;
}
public PSWTPath() {
strokePaint = DEFAULT_STROKE_PAINT;
}
public PSWTPath(Shape aShape) {
this();
setShape(aShape);
}
//****************************************************************
// Stroke
//****************************************************************
public Paint getStrokePaint() {
return strokePaint;
}
public void setStrokeColor(Paint aPaint) {
Paint old = strokePaint;
strokePaint = aPaint;
invalidatePaint();
firePropertyChange(PPath.PROPERTY_CODE_STROKE_PAINT, PPath.PROPERTY_STROKE_PAINT, old, strokePaint);
}
/**
* 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 (origShape == null) return;
Rectangle2D pathBounds = origShape.getBounds2D();
if (Math.abs(x-pathBounds.getX())/x < BOUNDS_TOLERANCE &&
Math.abs(y-pathBounds.getY())/y < BOUNDS_TOLERANCE &&
Math.abs(width-pathBounds.getWidth())/width < BOUNDS_TOLERANCE &&
Math.abs(height-pathBounds.getHeight())/height < BOUNDS_TOLERANCE) {
return;
}
if (internalXForm == null) {
internalXForm = new PAffineTransform();
}
internalXForm.setToIdentity();
internalXForm.translate(x, y);
internalXForm.scale(width / pathBounds.getWidth(), height / pathBounds.getHeight());
internalXForm.translate(-pathBounds.getX(), -pathBounds.getY());
try {
inverseXForm = internalXForm.createInverse();
}
catch (Exception e) {
}
}
public boolean intersects(Rectangle2D aBounds) {
if (super.intersects(aBounds)) {
if (internalXForm != null) {
aBounds = new PBounds(aBounds);
internalXForm.inverseTransform(aBounds,aBounds);
}
if (getPaint() != null && shape.intersects(aBounds)) {
return true;
} else if (strokePaint != null) {
return BASIC_STROKE.createStrokedShape(shape).intersects(aBounds);
}
}
return false;
}
public void updateBoundsFromPath() {
updatingBoundsFromPath = true;
if (origShape == null) {
resetBounds();
} else {
Rectangle2D b = origShape.getBounds2D();
// Note that this pen width code does not really work for SWT since it assumes
// that the pen width scales - in actuality it does not. However, the fix would
// be to have volatile bounds for all shapes which isn't a nice alternative
super.setBounds(b.getX()-PEN_WIDTH,
b.getY()-PEN_WIDTH,
b.getWidth()+2*PEN_WIDTH,
b.getHeight()+2*PEN_WIDTH);
}
updatingBoundsFromPath = false;
}
//****************************************************************
// Painting
//****************************************************************
protected void paint(PPaintContext paintContext) {
Paint p = getPaint();
SWTGraphics2D g2 = (SWTGraphics2D)paintContext.getGraphics();
if (internalXForm != null) {
g2.transform(internalXForm);
}
if (p != null) {
g2.setBackground((Color)p);
double lineWidth = g2.getTransformedLineWidth();
if (shape instanceof Rectangle2D) {
g2.fillRect(shapePts[0]+lineWidth/2,shapePts[1]+lineWidth/2,shapePts[2]-lineWidth,shapePts[3]-lineWidth);
}
else if (shape instanceof Ellipse2D) {
g2.fillOval(shapePts[0]+lineWidth/2,shapePts[1]+lineWidth/2,shapePts[2]-lineWidth,shapePts[3]-lineWidth);
}
else if (shape instanceof Arc2D) {
g2.fillArc(shapePts[0]+lineWidth/2,shapePts[1]+lineWidth/2,shapePts[2]-lineWidth,shapePts[3]-lineWidth,shapePts[4],shapePts[5]);
}
else if (shape instanceof RoundRectangle2D) {
g2.fillRoundRect(shapePts[0]+lineWidth/2,shapePts[1]+lineWidth/2,shapePts[2]-lineWidth,shapePts[3]-lineWidth,shapePts[4],shapePts[5]);
}
else {
g2.fillPolygon(shapePts);
}
}
if (strokePaint != null) {
g2.setColor((Color)strokePaint);
double lineWidth = g2.getTransformedLineWidth();
if (shape instanceof Rectangle2D) {
g2.drawRect(shapePts[0]+lineWidth/2,shapePts[1]+lineWidth/2,shapePts[2]-lineWidth,shapePts[3]-lineWidth);
}
else if (shape instanceof Ellipse2D) {
g2.drawOval(shapePts[0]+lineWidth/2,shapePts[1]+lineWidth/2,shapePts[2]-lineWidth,shapePts[3]-lineWidth);
}
else if (shape instanceof Arc2D) {
g2.drawArc(shapePts[0]+lineWidth/2,shapePts[1]+lineWidth/2,shapePts[2]-lineWidth,shapePts[3]-lineWidth,shapePts[4],shapePts[5]);
}
else if (shape instanceof RoundRectangle2D) {
g2.drawRoundRect(shapePts[0]+lineWidth/2,shapePts[1]+lineWidth/2,shapePts[2]-lineWidth,shapePts[3]-lineWidth,shapePts[4],shapePts[5]);
}
else {
// TODO The bounds may be incorrect for polylines at the moment - resulting in graphics turds at some scales
g2.drawPolyline(shapePts);
}
}
if (inverseXForm != null) {
g2.transform(inverseXForm);
}
}
public void setShape(Shape aShape) {
this.shape = cloneShape(aShape);
this.origShape = shape;
updateShapePoints(aShape);
firePropertyChange(PPath.PROPERTY_CODE_PATH, PPath.PROPERTY_PATH, null, shape);
updateBoundsFromPath();
invalidatePaint();
}
public void updateShapePoints(Shape aShape) {
if (aShape instanceof Rectangle2D) {
if (shapePts == null || shapePts.length < 4) {
shapePts = new double[4];
}
shapePts[0] = ((Rectangle2D)shape).getX();
shapePts[1] = ((Rectangle2D)shape).getY();
shapePts[2] = ((Rectangle2D)shape).getWidth();
shapePts[3] = ((Rectangle2D)shape).getHeight();
}
else if (aShape instanceof Ellipse2D) {
if (shapePts == null || shapePts.length < 4) {
shapePts = new double[4];
}
shapePts[0] = ((Ellipse2D)shape).getX();
shapePts[1] = ((Ellipse2D)shape).getY();
shapePts[2] = ((Ellipse2D)shape).getWidth();
shapePts[3] = ((Ellipse2D)shape).getHeight();
}
else if (aShape instanceof Arc2D) {
if (shapePts == null || shapePts.length < 6) {
shapePts = new double[6];
}
shapePts[0] = ((Arc2D)shape).getX();
shapePts[1] = ((Arc2D)shape).getY();
shapePts[2] = ((Arc2D)shape).getWidth();
shapePts[3] = ((Arc2D)shape).getHeight();
shapePts[4] = ((Arc2D)shape).getAngleStart();
shapePts[5] = ((Arc2D)shape).getAngleExtent();
}
else if (aShape instanceof RoundRectangle2D) {
if (shapePts == null || shapePts.length < 6) {
shapePts = new double[6];
}
shapePts[0] = ((RoundRectangle2D)shape).getX();
shapePts[1] = ((RoundRectangle2D)shape).getY();
shapePts[2] = ((RoundRectangle2D)shape).getWidth();
shapePts[3] = ((RoundRectangle2D)shape).getHeight();
shapePts[4] = ((RoundRectangle2D)shape).getArcWidth();
shapePts[5] = ((RoundRectangle2D)shape).getArcHeight();
}
else {
shapePts = SWTShapeManager.shapeToPolyline(shape);
}
}
public Shape cloneShape(Shape aShape) {
if (aShape instanceof Rectangle2D) {
return new PBounds((Rectangle2D)aShape);
}
else if (aShape instanceof Ellipse2D) {
Ellipse2D e2 = (Ellipse2D)aShape;
return new Ellipse2D.Double(e2.getX(),e2.getY(),e2.getWidth(),e2.getHeight());
}
else if (aShape instanceof Arc2D) {
Arc2D a2 = (Arc2D)aShape;
return new Arc2D.Double(a2.getX(),a2.getY(),a2.getWidth(),a2.getHeight(),a2.getAngleStart(),a2.getAngleExtent(),a2.getArcType());
}
else if (aShape instanceof RoundRectangle2D) {
RoundRectangle2D r2 = (RoundRectangle2D)aShape;
return new RoundRectangle2D.Double(r2.getX(),r2.getY(),r2.getWidth(),r2.getHeight(),r2.getArcWidth(),r2.getArcHeight());
}
else if (aShape instanceof Line2D) {
Line2D l2 = (Line2D)aShape;
return new Line2D.Double(l2.getP1(),l2.getP2());
}
else {
new Exception().printStackTrace();
GeneralPath aPath = new GeneralPath();
aPath.append(aShape,false);
return aPath;
}
}
public void setPathToRectangle(float x, float y, float width, float height) {
TEMP_RECTANGLE.setFrame(x, y, width, height);
setShape(TEMP_RECTANGLE);
}
public void setPathToRoundRectangle(float x, float y, float width, float height, float arcWidth, float arcHeight){
TEMP_ROUNDRECTANGLE.setRoundRect(x, y, width, height, arcWidth, arcHeight);
setShape(TEMP_ROUNDRECTANGLE);
}
public void setPathToEllipse(float x, float y, float width, float height) {
TEMP_ELLIPSE.setFrame(x, y, width, height);
setShape(TEMP_ELLIPSE);
}
public void setPathToPolyline(Point2D[] points) {
GeneralPath path = new GeneralPath();
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());
}
setShape(path);
}
public void setPathToPolyline(float[] xp, float[] yp) {
GeneralPath path = new GeneralPath();
path.reset();
path.moveTo(xp[0], yp[0]);
for (int i = 1; i < xp.length; i++) {
path.lineTo(xp[i], yp[i]);
}
setShape(path);
}
//****************************************************************
// 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=" + (shape == null ? "null" : shape.toString()));
result.append(",strokePaint=" + (strokePaint == null ? "null" : strokePaint.toString()));
result.append(',');
result.append(super.paramString());
return result.toString();
}
}