/* * org.openmicroscopy.shoola.util.ui.drawingtools.figures.RotateEllipseFigure * *------------------------------------------------------------------------------ * Copyright (C) 2006-2007 University of Dundee. All rights reserved. * * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * *------------------------------------------------------------------------------ */ package org.openmicroscopy.shoola.util.ui.drawingtools.figures; //Java imports import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Collection; import java.util.LinkedList; //Third-party libraries import org.jhotdraw.draw.AbstractAttributedFigure; import org.jhotdraw.draw.AttributeKey; import org.jhotdraw.draw.AttributeKeys; import org.jhotdraw.draw.ConnectionFigure; import org.jhotdraw.draw.Connector; import org.jhotdraw.draw.Handle; import org.jhotdraw.draw.ResizeHandleKit; import org.jhotdraw.draw.TransformHandleKit; import org.jhotdraw.geom.Geom; import org.jhotdraw.samples.svg.Gradient; import org.jhotdraw.samples.svg.SVGAttributeKeys; //Application-internal dependencies /** * Version of an EllipseFigure that the user can rotate. * * @author Jean-Marie Burel      * <a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a> * @author Donald MacDonald      * <a href="mailto:donald@lifesci.dundee.ac.uk">donald@lifesci.dundee.ac.uk</a> * @version 3.0 * <small> * (<b>Internal version:</b> $Revision: $Date: $) * </small> * @since OME3.0 */ public class RotateEllipseFigure extends AbstractAttributedFigure { /** The geometry of the ellipse figure. */ protected Ellipse2D.Double ellipse; /** * This is used to perform faster drawing and hit testing. */ protected Shape cachedTransformedShape; /** Creates a new instance. */ public RotateEllipseFigure() { this(0, 0, 0, 0); } /** * Created a new rotated Ellipse with position x, y and width and height. * * @param x The x-coordinate of the top-left corner. * @param y The y-coordinate of the top-left corner. * @param width The width of the ellipse. * @param height The height of the ellipse. */ public RotateEllipseFigure(double x, double y, double width, double height) { ellipse = new Ellipse2D.Double(x, y, width, height); AffineTransform transform = new AffineTransform(); transform.setToIdentity(); AttributeKeys.TRANSFORM.set(this, transform); } /** * Sets the attribute key to the value newValue on the figure. If the * attribute is an AffineTransform then we need to redraw the image. * * @param key The attribute key. * @param newValue The new value of the attribute. */ public void setAttribute(AttributeKey key, Object newValue) { if (key == AttributeKeys.TRANSFORM) { invalidate(); } super.setAttribute(key, newValue); } /** * Returns the bounds of the ellipse before applying the affine transform. * * @return See above. */ public Rectangle2D.Double getBounds() { return new Rectangle2D.Double(ellipse.getBounds2D().getX(), ellipse.getBounds2D().getY(), ellipse.getBounds2D().getWidth(), ellipse.getBounds2D().getHeight()); } /** * Overridden to return the drawing area, taking into account the * affine transformation. * @see AbstractAttributedFigure#getDrawingArea() */ public Rectangle2D.Double getDrawingArea() { Rectangle2D rx = getTransformedShape().getBounds2D(); Rectangle2D.Double r = (rx instanceof Rectangle2D.Double) ? (Rectangle2D.Double) rx : new Rectangle2D.Double(rx.getX(), rx.getY(), rx.getWidth(), rx.getHeight()); AffineTransform object = AttributeKeys.TRANSFORM.get(this); if (object == null) { double g = SVGAttributeKeys.getPerpendicularHitGrowth(this)*2; Geom.grow(r, g, g); } else { double strokeTotalWidth = AttributeKeys.getStrokeTotalWidth(this); double width = strokeTotalWidth/2d; width *= Math.max(object.getScaleX(), object.getScaleY()); Geom.grow(r, width, width); } return r; } public boolean containsMapped(double oX, double oY) { AffineTransform t = AttributeKeys.TRANSFORM.get(this); double x = oX-ellipse.getCenterX(); double y = oY-ellipse.getCenterY(); double[] matrix = new double[6]; t.getMatrix(matrix); matrix[4] = 0; matrix[5] = 0; // Create a new transform with rotation only, AffineTransform aT = new AffineTransform(matrix); Point2D.Double startPt = new Point2D.Double(1,0); Point2D.Double a = (Point2D.Double)aT.transform(startPt, null); // Calculate the starting rotation point. double thetaStart = Math.acos(startPt.y/Math.sqrt (startPt.x*startPt.x+startPt.y*startPt.y)); // Calculate the current rotation the ellipse has undergone. double rotation = Math.acos(a.y/Math.sqrt(a.x*a.x+a.y*a.y)); // Normalise to compensate for rotations > 180' double theta = rotation-Math.floor(rotation/Math.PI)*Math.PI-thetaStart; AffineTransform newT = new AffineTransform(); newT.setToRotation(theta); Point2D rotatedPoint = new Point2D.Double(); newT.transform(new Point2D.Double(x, y), rotatedPoint); return containsEllipseAlgorithm(rotatedPoint.getX(), rotatedPoint.getY(), 0,0, ellipse.getWidth(), ellipse.getHeight()); } private boolean containsEllipseAlgorithm(double x, double y, double cx, double cy, double w, double h) { double wr = w/2; double hr = h/2; double xx = x-cx; double yy = y-cy; double dist = (xx*xx)/(wr*wr)+(yy*yy)/(hr*hr); return dist< 1; } /** * Checks if a Point2D.Double is inside the figure. * @see AbstractAttributedFigure#contains(Point2D.Double) */ public boolean contains(Point2D.Double p) { // XXX - This does not take the stroke width into account! return getTransformedShape().contains(p); } /** * Returns the ellipse after the affine transform has been applied to it. * This will return the ellipse with the correct width and height, but the * x, y coords will be the lead.x, lead.y of the transformed shape. * * @return see above. */ public Ellipse2D.Double getTransformedEllipse() { AffineTransform t = AttributeKeys.TRANSFORM.get(this); if (t == null) return ellipse; Ellipse2D.Double e = new Ellipse2D.Double(0,0,0,0); Point2D.Double startW = (Point2D.Double) t.transform( new Point2D.Double(0, ellipse.getCenterY()), null); Point2D.Double endW = (Point2D.Double) t.transform( new Point2D.Double(ellipse.getWidth(), ellipse.getCenterY()), null); Point2D.Double startH = (Point2D.Double) t.transform( new Point2D.Double(ellipse.getCenterX(), 0), null); Point2D.Double endH = (Point2D.Double) t.transform( new Point2D.Double(ellipse.getCenterX(), ellipse.getHeight()), null); Point2D.Double lead = (Point2D.Double) t.transform( new Point2D.Double(0,0), null); e.width = Math.round(startW.distance(endW)); e.height = Math.round(startH.distance(endH)); e.x = Math.round(lead.getX()); e.y = Math.round(lead.getY()); return e; } /** * Returns the Transformed ellipse as a transformedShape, of type Shape. * * @return See above. */ protected Shape getTransformedShape() { AffineTransform t = AttributeKeys.TRANSFORM.get(this); if (t == null) cachedTransformedShape = ellipse; else cachedTransformedShape = t.createTransformedShape(ellipse); return cachedTransformedShape; } /** * Returns the Transformed ellipse as a transformedShape, of type Shape. * * @return See above. */ protected Shape getTransformedShape(double i, double j) { AffineTransform t = AttributeKeys.TRANSFORM.get(this); if (t == null) cachedTransformedShape = ellipse; else { Ellipse2D.Double newEllipse = new Ellipse2D.Double( ellipse.x-i, ellipse.y-j, ellipse.width+i, ellipse.height+j); cachedTransformedShape = t.createTransformedShape(newEllipse); } return cachedTransformedShape; } /** * Returns the ellipse. * * @return See above. */ public Ellipse2D.Double getEllipse() { return ellipse; } /** * Overridden to return the correct handles. * @see AbstractAttributedFigure#createHandles(int) */ public Collection<Handle> createHandles(int detailLevel) { LinkedList<Handle> handles = new LinkedList<Handle>(); switch (detailLevel%2) { case 0: ResizeHandleKit.addResizeHandles(this, handles); break; case 1: TransformHandleKit.addTransformHandles(this, handles); break; default: break; } return handles; } /** * Sets the ellipse geometry. * @param x see above. * @param y see above. * @param width see above. * @param height see above. */ public void setEllipse(double x, double y, double width, double height) { ellipse.x = x; ellipse.y = y; ellipse.width = width; ellipse.height = height; invalidate(); } /** * Sets the bounds of the ellipse from the anchor to lead. * * @param anchor The start point of the drawing action. * @param lead The end point the drawing action. * */ public void setBounds(Point2D.Double anchor, Point2D.Double lead) { ellipse.x = Math.min(anchor.x, lead.x); ellipse.y = Math.min(anchor.y, lead.y); ellipse.width = Math.max(0.1, Math.abs(lead.x-anchor.x)); ellipse.height = Math.max(0.1, Math.abs(lead.y-anchor.y)); invalidate(); } /** * Returns the height of the transformed ellipse. * * @return See above. */ public double getHeight() { return getTransformedEllipse().getHeight(); } /** * Returns the width of the transformed ellipse. * * @return See above. */ public double getWidth() { return getTransformedEllipse().getWidth(); } /** * Set the width of the ellipse to the newWidth, the new ellipse will * still be centered around the same point as the original ellipse. * * @param newWidth see above. */ public void setWidth(double newWidth) { AffineTransform t = AttributeKeys.TRANSFORM.get(this); if (t == null) return; double centreX = getCentreX(); double centreY = getCentreY(); double height = getHeight(); double[] matrix = new double[6]; t.getMatrix(matrix); matrix[4] = 0; matrix[5] = 0; AffineTransform aT = new AffineTransform(matrix); Point2D.Double a = (Point2D.Double) aT.transform( new Point2D.Double(1,0), null); double theta = Math.asin(a.y/Math.sqrt(a.x*a.x+a.y*a.y)); AffineTransform newT = new AffineTransform(); newT.setToRotation(theta); Ellipse2D.Double newEllipse = new Ellipse2D.Double(0, 0, newWidth, height); Shape rotatedShape = newT.createTransformedShape(newEllipse); double rotatedShapeCentreX = rotatedShape.getBounds2D().getCenterX(); double rotatedShapeCentreY = rotatedShape.getBounds2D().getCenterY(); double diffX = centreX-rotatedShapeCentreX; double diffY = centreY-rotatedShapeCentreY; Point2D.Double lead = new Point2D.Double(diffX, diffY); AffineTransform rT = AffineTransform.getTranslateInstance(lead.x, lead.y); rT.concatenate(newT); ellipse = newEllipse; AttributeKeys.TRANSFORM.set(this, rT); invalidate(); } /** * Returns the height of the ellipse to the newHieght, the new ellipse will * still be centered around the same point as the original ellipse. * * @param newHeight see above. */ public void setHeight(double newHeight) { AffineTransform t = AttributeKeys.TRANSFORM.get(this); if (t == null) return; double centreX = getCentreX(); double centreY = getCentreY(); double width = getWidth(); double[] matrix = new double[6]; t.getMatrix(matrix); matrix[4] = 0; matrix[5] = 0; AffineTransform aT = new AffineTransform(matrix); Point2D.Double a = (Point2D.Double) aT.transform( new Point2D.Double(1, 0), null); double theta = Math.asin(a.y/Math.sqrt(a.x*a.x+a.y*a.y)); AffineTransform newT = new AffineTransform(); newT.setToRotation(theta); Ellipse2D.Double newEllipse = new Ellipse2D.Double(0, 0, width, newHeight); Shape rotatedShape = newT.createTransformedShape(newEllipse); double rotatedShapeCentreX = rotatedShape.getBounds2D().getCenterX(); double rotatedShapeCentreY = rotatedShape.getBounds2D().getCenterY(); double diffX = centreX-rotatedShapeCentreX; double diffY = centreY-rotatedShapeCentreY; Point2D.Double lead = new Point2D.Double(diffX, diffY); AffineTransform rT = AffineTransform.getTranslateInstance(lead.x, lead.y); rT.concatenate(newT); ellipse = newEllipse; AttributeKeys.TRANSFORM.set(this, rT); invalidate(); } /** * Returns the x coordinate of the figure. * * @return See above. */ public double getCentreX() { AffineTransform t = AttributeKeys.TRANSFORM.get(this); if (t == null) return ellipse.getCenterX(); Point2D src = new Point2D.Double(ellipse.getCenterX(), ellipse.getCenterY()); Point2D dest = new Point2D.Double(); t.transform(src, dest); return dest.getX(); } /** * Returns the y coordinate of the figure. * * @return See above. */ public double getCentreY() { AffineTransform t = AttributeKeys.TRANSFORM.get(this); if (t == null) return ellipse.getCenterY(); Point2D src = new Point2D.Double(ellipse.getCenterX(), ellipse.getCenterY()); Point2D dest = new Point2D.Double(); t.transform(src, dest); return dest.getY(); } /** * Transforms the figure. * * @param tx the transformation. */ public void transform(AffineTransform tx) { if (AttributeKeys.TRANSFORM.get(this) == null) { AttributeKeys.TRANSFORM.basicSetClone(this, tx); } else { AffineTransform t = AttributeKeys.TRANSFORM.getClone(this); t.preConcatenate(tx); AttributeKeys.TRANSFORM.basicSet(this, t); } } /* * UNDO/REDO methods. * @see org.jhotdraw.draw.Figure#restoreTransformTo(java.lang.Object) */ public void restoreTransformTo(Object geometry) { Object[] restoreData = (Object[]) geometry; ellipse=(Ellipse2D.Double) ((Ellipse2D.Double) restoreData[0]).clone(); SVGAttributeKeys.TRANSFORM.basicSetClone(this, (AffineTransform) restoreData[1]); SVGAttributeKeys.FILL_GRADIENT.basicSetClone(this, (Gradient) restoreData[2]); SVGAttributeKeys.STROKE_GRADIENT.basicSetClone(this, (Gradient) restoreData[3]); invalidate(); } /* * UNDO/REDO methods. * @see org.jhotdraw.draw.Figure#getTransformRestoreData(java.lang.Object) */ public Object getTransformRestoreData() { return new Object[] { ellipse.clone(), SVGAttributeKeys.TRANSFORM.getClone(this), SVGAttributeKeys.FILL_GRADIENT.getClone(this), SVGAttributeKeys.STROKE_GRADIENT.getClone(this), }; } /** * Clones the figure. */ public RotateEllipseFigure clone() { RotateEllipseFigure that = (RotateEllipseFigure) super.clone(); that.ellipse = (Ellipse2D.Double) this.ellipse.clone(); that.cachedTransformedShape = null; return that; } // EVENT HANDLING public boolean isEmpty() { Rectangle2D.Double b = getBounds(); return b.width <= 0|| b.height <= 0; } /** * Invalidate the figure and remove the cachedTransformedShape, this means * that the figures geometry has changed and it should be redrawn. * @see AbstractAttributedFigure#invalidate() */ public void invalidate() { super.invalidate(); cachedTransformedShape = null; } /* * Drawing code. */ /** * Draws the figure * @see AbstractAttributedFigure#draw(Graphics2D) */ public void draw(Graphics2D g) { drawFigure(g); } /** * This method is invoked before the rendered image of the figure is * a composite figure. * * @param g The graphics context. */ private void drawFigure(Graphics2D g) { Paint paint = SVGAttributeKeys.getFillPaint(this); if (paint != null) { g.setPaint(paint); drawFill(g); } paint = SVGAttributeKeys.getStrokePaint(this); if (paint != null && AttributeKeys.STROKE_WIDTH.get(this) > 0) { g.setPaint(paint); g.setStroke(AttributeKeys.getStroke(this)); drawStroke(g); } } /** * Draws the fill of the ellipse. */ protected void drawFill(Graphics2D g) { g.fill(getTransformedShape()); } /** * Draws the stroke of the ellipse. */ protected void drawStroke(Graphics2D g) { g.draw(getTransformedShape()); } /* * Connecting to the figure. Since this figure does not allow connections * it will always return NULL. */ /** * Return false as no connections can exist. */ public boolean canConnect() { return false; } /** * Return null as no connections can exist. */ public Connector findConnector(Point2D.Double p, ConnectionFigure prototype) { return null; } /** * Return null as no connections can exist. */ public Connector findCompatibleConnector(Connector c, boolean isStartConnector) { return null; } }