Newer
Older
piccolo2d.java / extras / edu / umd / cs / piccolox / swt / PSWTPath.java
@Jesse Grosjean Jesse Grosjean on 5 Oct 2006 15 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.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();
	}	
}