Newer
Older
piccolo2d.java / extras / src / main / java / edu / umd / cs / piccolox / swt / PSWTPath.java
/*
 * Copyright (c) 2008, 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.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 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();
    }
}