    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors.
    006 *
    007 * Project Info:  http://www.jfree.org/jfreechart/index.html
    008 *
    009 * This library is free software; you can redistribute it and/or modify it
    010 * under the terms of the GNU Lesser General Public License as published by
    011 * the Free Software Foundation; either version 2.1 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This library is distributed in the hope that it will be useful, but
    015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017 * License for more details.
    018 *
    019 * You should have received a copy of the GNU Lesser General Public
    020 * License along with this library; if not, write to the Free Software
    021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022 * USA.
    023 *
    024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    025 * Other names may be trademarks of their respective owners.]
    026 *
    027 * ------------------------
    028 * XYPointerAnnotation.java
    029 * ------------------------
    030 * (C) Copyright 2003-2011, by Object Refinery Limited.
    031 *
    032 * Original Author:  David Gilbert (for Object Refinery Limited);
    033 * Contributor(s):   Peter Kolb (patch 2809117);
    034 *
    035 * Changes:
    036 * --------
    037 * 21-May-2003 : Version 1 (DG);
    038 * 10-Jun-2003 : Changed BoundsAnchor to TextAnchor (DG);
    039 * 02-Jul-2003 : Added accessor methods and simplified constructor (DG);
    040 * 19-Aug-2003 : Implemented Cloneable (DG);
    041 * 13-Oct-2003 : Fixed bug where arrow paint is not set correctly (DG);
    042 * 21-Jan-2004 : Update for renamed method in ValueAxis (DG);
    043 * 29-Sep-2004 : Changes to draw() method signature (DG);
    044 * ------------- JFREECHART 1.0.x ---------------------------------------------
    045 * 20-Feb-2006 : Correction for equals() method (fixes bug 1435160) (DG);
    046 * 12-Jul-2006 : Fix drawing for PlotOrientation.HORIZONTAL, thanks to
    047 *               Skunk (DG);
    048 * 12-Feb-2009 : Added support for rotated label, plus background and
    049 *               outline (DG);
    050 * 18-May-2009 : Fixed typo in hashCode() method (DG);
    051 * 24-Jun-2009 : Fire change events (see patch 2809117 by PK) (DG);
    052 *
    053 */
    054
    055package org.jfree.chart.annotations;
    056
    057import java.awt.BasicStroke;
    058import java.awt.Color;
    059import java.awt.Graphics2D;
    060import java.awt.Paint;
    061import java.awt.Shape;
    062import java.awt.Stroke;
    063import java.awt.geom.GeneralPath;
    064import java.awt.geom.Line2D;
    065import java.awt.geom.Rectangle2D;
    066import java.io.IOException;
    067import java.io.ObjectInputStream;
    068import java.io.ObjectOutputStream;
    069import java.io.Serializable;
    070
    071import org.jfree.chart.HashUtilities;
    072import org.jfree.chart.axis.ValueAxis;
    073import org.jfree.chart.event.AnnotationChangeEvent;
    074import org.jfree.chart.plot.Plot;
    075import org.jfree.chart.plot.PlotOrientation;
    076import org.jfree.chart.plot.PlotRenderingInfo;
    077import org.jfree.chart.plot.XYPlot;
    078import org.jfree.io.SerialUtilities;
    079import org.jfree.text.TextUtilities;
    080import org.jfree.ui.RectangleEdge;
    081import org.jfree.util.ObjectUtilities;
    082import org.jfree.util.PublicCloneable;
    083
    084/**
    085 * An arrow and label that can be placed on an {@link XYPlot}.  The arrow is
    086 * drawn at a user-definable angle so that it points towards the (x, y)
    087 * location for the annotation.
    088 * <p>
    089 * The arrow length (and its offset from the (x, y) location) is controlled by
    090 * the tip radius and the base radius attributes.  Imagine two circles around
    091 * the (x, y) coordinate: the inner circle defined by the tip radius, and the
    092 * outer circle defined by the base radius.  Now, draw the arrow starting at
    093 * some point on the outer circle (the point is determined by the angle), with
    094 * the arrow tip being drawn at a corresponding point on the inner circle.
    095 *
    096 */
    097public class XYPointerAnnotation extends XYTextAnnotation
    098        implements Cloneable, PublicCloneable, Serializable {
    099
    100    /** For serialization. */
    101    private static final long serialVersionUID = -4031161445009858551L;
    102
    103    /** The default tip radius (in Java2D units). */
    104    public static final double DEFAULT_TIP_RADIUS = 10.0;
    105
    106    /** The default base radius (in Java2D units). */
    107    public static final double DEFAULT_BASE_RADIUS = 30.0;
    108
    109    /** The default label offset (in Java2D units). */
    110    public static final double DEFAULT_LABEL_OFFSET = 3.0;
    111
    112    /** The default arrow length (in Java2D units). */
    113    public static final double DEFAULT_ARROW_LENGTH = 5.0;
    114
    115    /** The default arrow width (in Java2D units). */
    116    public static final double DEFAULT_ARROW_WIDTH = 3.0;
    117
    118    /** The angle of the arrow's line (in radians). */
    119    private double angle;
    120
    121    /**
    122     * The radius from the (x, y) point to the tip of the arrow (in Java2D
    123     * units).
    124     */
    125    private double tipRadius;
    126
    127    /**
    128     * The radius from the (x, y) point to the start of the arrow line (in
    129     * Java2D units).
    130     */
    131    private double baseRadius;
    132
    133    /** The length of the arrow head (in Java2D units). */
    134    private double arrowLength;
    135
    136    /** The arrow width (in Java2D units, per side). */
    137    private double arrowWidth;
    138
    139    /** The arrow stroke. */
    140    private transient Stroke arrowStroke;
    141
    142    /** The arrow paint. */
    143    private transient Paint arrowPaint;
    144
    145    /** The radius from the base point to the anchor point for the label. */
    146    private double labelOffset;
    147
    148    /**
    149     * Creates a new label and arrow annotation.
    150     *
    151     * @param label  the label (<code>null</code> permitted).
    152     * @param x  the x-coordinate (measured against the chart's domain axis).
    153     * @param y  the y-coordinate (measured against the chart's range axis).
    154     * @param angle  the angle of the arrow's line (in radians).
    155     */
    156    public XYPointerAnnotation(String label, double x, double y, double angle) {
    157
    158        super(label, x, y);
    159        this.angle = angle;
    160        this.tipRadius = DEFAULT_TIP_RADIUS;
    161        this.baseRadius = DEFAULT_BASE_RADIUS;
    162        this.arrowLength = DEFAULT_ARROW_LENGTH;
    163        this.arrowWidth = DEFAULT_ARROW_WIDTH;
    164        this.labelOffset = DEFAULT_LABEL_OFFSET;
    165        this.arrowStroke = new BasicStroke(1.0f);
    166        this.arrowPaint = Color.black;
    167
    168    }
    169
    170    /**
    171     * Returns the angle of the arrow.
    172     *
    173     * @return The angle (in radians).
    174     *
    175     * @see #setAngle(double)
    176     */
    177    public double getAngle() {
    178        return this.angle;
    179    }
    180
    181    /**
    182     * Sets the angle of the arrow and sends an
    183     * {@link AnnotationChangeEvent} to all registered listeners.
    184     *
    185     * @param angle  the angle (in radians).
    186     *
    187     * @see #getAngle()
    188     */
    189    public void setAngle(double angle) {
    190        this.angle = angle;
    191        fireAnnotationChanged();
    192    }
    193
    194    /**
    195     * Returns the tip radius.
    196     *
    197     * @return The tip radius (in Java2D units).
    198     *
    199     * @see #setTipRadius(double)
    200     */
    201    public double getTipRadius() {
    202        return this.tipRadius;
    203    }
    204
    205    /**
    206     * Sets the tip radius and sends an
    207     * {@link AnnotationChangeEvent} to all registered listeners.
    208     *
    209     * @param radius  the radius (in Java2D units).
    210     *
    211     * @see #getTipRadius()
    212     */
    213    public void setTipRadius(double radius) {
    214        this.tipRadius = radius;
    215        fireAnnotationChanged();
    216    }
    217
    218    /**
    219     * Returns the base radius.
    220     *
    221     * @return The base radius (in Java2D units).
    222     *
    223     * @see #setBaseRadius(double)
    224     */
    225    public double getBaseRadius() {
    226        return this.baseRadius;
    227    }
    228
    229    /**
    230     * Sets the base radius and sends an
    231     * {@link AnnotationChangeEvent} to all registered listeners.
    232     *
    233     * @param radius  the radius (in Java2D units).
    234     *
    235     * @see #getBaseRadius()
    236     */
    237    public void setBaseRadius(double radius) {
    238        this.baseRadius = radius;
    239        fireAnnotationChanged();
    240    }
    241
    242    /**
    243     * Returns the label offset.
    244     *
    245     * @return The label offset (in Java2D units).
    246     *
    247     * @see #setLabelOffset(double)
    248     */
    249    public double getLabelOffset() {
    250        return this.labelOffset;
    251    }
    252
    253    /**
    254     * Sets the label offset (from the arrow base, continuing in a straight
    255     * line, in Java2D units) and sends an
    256     * {@link AnnotationChangeEvent} to all registered listeners.
    257     *
    258     * @param offset  the offset (in Java2D units).
    259     *
    260     * @see #getLabelOffset()
    261     */
    262    public void setLabelOffset(double offset) {
    263        this.labelOffset = offset;
    264        fireAnnotationChanged();
    265    }
    266
    267    /**
    268     * Returns the arrow length.
    269     *
    270     * @return The arrow length.
    271     *
    272     * @see #setArrowLength(double)
    273     */
    274    public double getArrowLength() {
    275        return this.arrowLength;
    276    }
    277
    278    /**
    279     * Sets the arrow length and sends an
    280     * {@link AnnotationChangeEvent} to all registered listeners.
    281     *
    282     * @param length  the length.
    283     *
    284     * @see #getArrowLength()
    285     */
    286    public void setArrowLength(double length) {
    287        this.arrowLength = length;
    288        fireAnnotationChanged();
    289    }
    290
    291    /**
    292     * Returns the arrow width.
    293     *
    294     * @return The arrow width (in Java2D units).
    295     *
    296     * @see #setArrowWidth(double)
    297     */
    298    public double getArrowWidth() {
    299        return this.arrowWidth;
    300    }
    301
    302    /**
    303     * Sets the arrow width and sends an
    304     * {@link AnnotationChangeEvent} to all registered listeners.
    305     *
    306     * @param width  the width (in Java2D units).
    307     *
    308     * @see #getArrowWidth()
    309     */
    310    public void setArrowWidth(double width) {
    311        this.arrowWidth = width;
    312        fireAnnotationChanged();
    313    }
    314
    315    /**
    316     * Returns the stroke used to draw the arrow line.
    317     *
    318     * @return The arrow stroke (never <code>null</code>).
    319     *
    320     * @see #setArrowStroke(Stroke)
    321     */
    322    public Stroke getArrowStroke() {
    323        return this.arrowStroke;
    324    }
    325
    326    /**
    327     * Sets the stroke used to draw the arrow line and sends an
    328     * {@link AnnotationChangeEvent} to all registered listeners.
    329     *
    330     * @param stroke  the stroke (<code>null</code> not permitted).
    331     *
    332     * @see #getArrowStroke()
    333     */
    334    public void setArrowStroke(Stroke stroke) {
    335        if (stroke == null) {
    336            throw new IllegalArgumentException("Null 'stroke' not permitted.");
    337        }
    338        this.arrowStroke = stroke;
    339        fireAnnotationChanged();
    340    }
    341
    342    /**
    343     * Returns the paint used for the arrow.
    344     *
    345     * @return The arrow paint (never <code>null</code>).
    346     *
    347     * @see #setArrowPaint(Paint)
    348     */
    349    public Paint getArrowPaint() {
    350        return this.arrowPaint;
    351    }
    352
    353    /**
    354     * Sets the paint used for the arrow and sends an
    355     * {@link AnnotationChangeEvent} to all registered listeners.
    356     *
    357     * @param paint  the arrow paint (<code>null</code> not permitted).
    358     *
    359     * @see #getArrowPaint()
    360     */
    361    public void setArrowPaint(Paint paint) {
    362        if (paint == null) {
    363            throw new IllegalArgumentException("Null 'paint' argument.");
    364        }
    365        this.arrowPaint = paint;
    366        fireAnnotationChanged();
    367    }
    368
    369    /**
    370     * Draws the annotation.
    371     *
    372     * @param g2  the graphics device.
    373     * @param plot  the plot.
    374     * @param dataArea  the data area.
    375     * @param domainAxis  the domain axis.
    376     * @param rangeAxis  the range axis.
    377     * @param rendererIndex  the renderer index.
    378     * @param info  the plot rendering info.
    379     */
    380    public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea,
    381                     ValueAxis domainAxis, ValueAxis rangeAxis,
    382                     int rendererIndex,
    383                     PlotRenderingInfo info) {
    384
    385        PlotOrientation orientation = plot.getOrientation();
    386        RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
    387                plot.getDomainAxisLocation(), orientation);
    388        RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
    389                plot.getRangeAxisLocation(), orientation);
    390        double j2DX = domainAxis.valueToJava2D(getX(), dataArea, domainEdge);
    391        double j2DY = rangeAxis.valueToJava2D(getY(), dataArea, rangeEdge);
    392        if (orientation == PlotOrientation.HORIZONTAL) {
    393            double temp = j2DX;
    394            j2DX = j2DY;
    395            j2DY = temp;
    396        }
    397        double startX = j2DX + Math.cos(this.angle) * this.baseRadius;
    398        double startY = j2DY + Math.sin(this.angle) * this.baseRadius;
    399
    400        double endX = j2DX + Math.cos(this.angle) * this.tipRadius;
    401        double endY = j2DY + Math.sin(this.angle) * this.tipRadius;
    402
    403        double arrowBaseX = endX + Math.cos(this.angle) * this.arrowLength;
    404        double arrowBaseY = endY + Math.sin(this.angle) * this.arrowLength;
    405
    406        double arrowLeftX = arrowBaseX
    407                + Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
    408        double arrowLeftY = arrowBaseY
    409                + Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
    410
    411        double arrowRightX = arrowBaseX
    412                - Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
    413        double arrowRightY = arrowBaseY
    414                - Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
    415
    416        GeneralPath arrow = new GeneralPath();
    417        arrow.moveTo((float) endX, (float) endY);
    418        arrow.lineTo((float) arrowLeftX, (float) arrowLeftY);
    419        arrow.lineTo((float) arrowRightX, (float) arrowRightY);
    420        arrow.closePath();
    421
    422        g2.setStroke(this.arrowStroke);
    423        g2.setPaint(this.arrowPaint);
    424        Line2D line = new Line2D.Double(startX, startY, arrowBaseX, arrowBaseY);
    425        g2.draw(line);
    426        g2.fill(arrow);
    427
    428        // draw the label
    429        double labelX = j2DX + Math.cos(this.angle) * (this.baseRadius
    430                + this.labelOffset);
    431        double labelY = j2DY + Math.sin(this.angle) * (this.baseRadius
    432                + this.labelOffset);
    433        g2.setFont(getFont());
    434        Shape hotspot = TextUtilities.calculateRotatedStringBounds(
    435                getText(), g2, (float) labelX, (float) labelY, getTextAnchor(),
    436                getRotationAngle(), getRotationAnchor());
    437        if (getBackgroundPaint() != null) {
    438            g2.setPaint(getBackgroundPaint());
    439            g2.fill(hotspot);
    440        }
    441        g2.setPaint(getPaint());
    442        TextUtilities.drawRotatedString(getText(), g2, (float) labelX,
    443                (float) labelY, getTextAnchor(), getRotationAngle(),
    444                getRotationAnchor());
    445        if (isOutlineVisible()) {
    446            g2.setStroke(getOutlineStroke());
    447            g2.setPaint(getOutlinePaint());
    448            g2.draw(hotspot);
    449        }
    450
    451        String toolTip = getToolTipText();
    452        String url = getURL();
    453        if (toolTip != null || url != null) {
    454            addEntity(info, hotspot, rendererIndex, toolTip, url);
    455        }
    456
    457    }
    458
    459    /**
    460     * Tests this annotation for equality with an arbitrary object.
    461     *
    462     * @param obj  the object (<code>null</code> permitted).
    463     *
    464     * @return <code>true</code> or <code>false</code>.
    465     */
    466    public boolean equals(Object obj) {
    467        if (obj == this) {
    468            return true;
    469        }
    470        if (!(obj instanceof XYPointerAnnotation)) {
    471            return false;
    472        }
    473        XYPointerAnnotation that = (XYPointerAnnotation) obj;
    474        if (this.angle != that.angle) {
    475            return false;
    476        }
    477        if (this.tipRadius != that.tipRadius) {
    478            return false;
    479        }
    480        if (this.baseRadius != that.baseRadius) {
    481            return false;
    482        }
    483        if (this.arrowLength != that.arrowLength) {
    484            return false;
    485        }
    486        if (this.arrowWidth != that.arrowWidth) {
    487            return false;
    488        }
    489        if (!this.arrowPaint.equals(that.arrowPaint)) {
    490            return false;
    491        }
    492        if (!ObjectUtilities.equal(this.arrowStroke, that.arrowStroke)) {
    493            return false;
    494        }
    495        if (this.labelOffset != that.labelOffset) {
    496            return false;
    497        }
    498        return super.equals(obj);
    499    }
    500
    501    /**
    502     * Returns a hash code for this instance.
    503     *
    504     * @return A hash code.
    505     */
    506    public int hashCode() {
    507        int result = super.hashCode();
    508        long temp = Double.doubleToLongBits(this.angle);
    509        result = 37 * result + (int) (temp ^ (temp >>> 32));
    510        temp = Double.doubleToLongBits(this.tipRadius);
    511        result = 37 * result + (int) (temp ^ (temp >>> 32));
    512        temp = Double.doubleToLongBits(this.baseRadius);
    513        result = 37 * result + (int) (temp ^ (temp >>> 32));
    514        temp = Double.doubleToLongBits(this.arrowLength);
    515        result = 37 * result + (int) (temp ^ (temp >>> 32));
    516        temp = Double.doubleToLongBits(this.arrowWidth);
    517        result = 37 * result + (int) (temp ^ (temp >>> 32));
    518        result = result * 37 + HashUtilities.hashCodeForPaint(this.arrowPaint);
    519        result = result * 37 + this.arrowStroke.hashCode();
    520        temp = Double.doubleToLongBits(this.labelOffset);
    521        result = 37 * result + (int) (temp ^ (temp >>> 32));
    522        return result;
    523    }
    524
    525    /**
    526     * Returns a clone of the annotation.
    527     *
    528     * @return A clone.
    529     *
    530     * @throws CloneNotSupportedException  if the annotation can't be cloned.
    531     */
    532    public Object clone() throws CloneNotSupportedException {
    533        return super.clone();
    534    }
    535
    536    /**
    537     * Provides serialization support.
    538     *
    539     * @param stream  the output stream.
    540     *
    541     * @throws IOException if there is an I/O error.
    542     */
    543    private void writeObject(ObjectOutputStream stream) throws IOException {
    544        stream.defaultWriteObject();
    545        SerialUtilities.writePaint(this.arrowPaint, stream);
    546        SerialUtilities.writeStroke(this.arrowStroke, stream);
    547    }
    548
    549    /**
    550     * Provides serialization support.
    551     *
    552     * @param stream  the input stream.
    553     *
    554     * @throws IOException  if there is an I/O error.
    555     * @throws ClassNotFoundException  if there is a classpath problem.
    556     */
    557    private void readObject(ObjectInputStream stream)
    558        throws IOException, ClassNotFoundException {
    559        stream.defaultReadObject();
    560        this.arrowPaint = SerialUtilities.readPaint(stream);
    561        this.arrowStroke = SerialUtilities.readStroke(stream);
    562    }
    563
    564}