    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 * CategoryPointerAnnotation.java
    029 * ------------------------------
    030 * (C) Copyright 2006-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 * 02-Oct-2006 : Version 1 (DG);
    038 * 06-Mar-2007 : Implemented hashCode() (DG);
    039 * 24-Jun-2009 : Fire change events (see patch 2809117 by PK) (DG);
    040 * 30-Mar-2010 : Correct calculation of pointer line (see patch 2954302) (DG);
    041 *
    042 */
    043
    044package org.jfree.chart.annotations;
    045
    046import java.awt.BasicStroke;
    047import java.awt.Color;
    048import java.awt.Graphics2D;
    049import java.awt.Paint;
    050import java.awt.Stroke;
    051import java.awt.geom.GeneralPath;
    052import java.awt.geom.Line2D;
    053import java.awt.geom.Rectangle2D;
    054import java.io.IOException;
    055import java.io.ObjectInputStream;
    056import java.io.ObjectOutputStream;
    057import java.io.Serializable;
    058
    059import org.jfree.chart.HashUtilities;
    060import org.jfree.chart.axis.CategoryAxis;
    061import org.jfree.chart.axis.ValueAxis;
    062import org.jfree.chart.event.AnnotationChangeEvent;
    063import org.jfree.chart.plot.CategoryPlot;
    064import org.jfree.chart.plot.Plot;
    065import org.jfree.chart.plot.PlotOrientation;
    066import org.jfree.data.category.CategoryDataset;
    067import org.jfree.io.SerialUtilities;
    068import org.jfree.text.TextUtilities;
    069import org.jfree.ui.RectangleEdge;
    070import org.jfree.util.ObjectUtilities;
    071import org.jfree.util.PublicCloneable;
    072
    073/**
    074 * An arrow and label that can be placed on a {@link CategoryPlot}.  The arrow
    075 * is drawn at a user-definable angle so that it points towards the (category,
    076 * value) location for the annotation.
    077 * <p>
    078 * The arrow length (and its offset from the (category, value) location) is
    079 * controlled by the tip radius and the base radius attributes.  Imagine two
    080 * circles around the (category, value) coordinate: the inner circle defined by
    081 * the tip radius, and the outer circle defined by the base radius.  Now, draw
    082 * the arrow starting at some point on the outer circle (the point is
    083 * determined by the angle), with the arrow tip being drawn at a corresponding
    084 * point on the inner circle.
    085 *
    086 * @since 1.0.3
    087 */
    088public class CategoryPointerAnnotation extends CategoryTextAnnotation
    089        implements Cloneable, PublicCloneable, Serializable {
    090
    091    /** For serialization. */
    092    private static final long serialVersionUID = -4031161445009858551L;
    093
    094    /** The default tip radius (in Java2D units). */
    095    public static final double DEFAULT_TIP_RADIUS = 10.0;
    096
    097    /** The default base radius (in Java2D units). */
    098    public static final double DEFAULT_BASE_RADIUS = 30.0;
    099
    100    /** The default label offset (in Java2D units). */
    101    public static final double DEFAULT_LABEL_OFFSET = 3.0;
    102
    103    /** The default arrow length (in Java2D units). */
    104    public static final double DEFAULT_ARROW_LENGTH = 5.0;
    105
    106    /** The default arrow width (in Java2D units). */
    107    public static final double DEFAULT_ARROW_WIDTH = 3.0;
    108
    109    /** The angle of the arrow's line (in radians). */
    110    private double angle;
    111
    112    /**
    113     * The radius from the (x, y) point to the tip of the arrow (in Java2D
    114     * units).
    115     */
    116    private double tipRadius;
    117
    118    /**
    119     * The radius from the (x, y) point to the start of the arrow line (in
    120     * Java2D units).
    121     */
    122    private double baseRadius;
    123
    124    /** The length of the arrow head (in Java2D units). */
    125    private double arrowLength;
    126
    127    /** The arrow width (in Java2D units, per side). */
    128    private double arrowWidth;
    129
    130    /** The arrow stroke. */
    131    private transient Stroke arrowStroke;
    132
    133    /** The arrow paint. */
    134    private transient Paint arrowPaint;
    135
    136    /** The radius from the base point to the anchor point for the label. */
    137    private double labelOffset;
    138
    139    /**
    140     * Creates a new label and arrow annotation.
    141     *
    142     * @param label  the label (<code>null</code> permitted).
    143     * @param key  the category key.
    144     * @param value  the y-value (measured against the chart's range axis).
    145     * @param angle  the angle of the arrow's line (in radians).
    146     */
    147    public CategoryPointerAnnotation(String label, Comparable key, double value,
    148            double angle) {
    149
    150        super(label, key, value);
    151        this.angle = angle;
    152        this.tipRadius = DEFAULT_TIP_RADIUS;
    153        this.baseRadius = DEFAULT_BASE_RADIUS;
    154        this.arrowLength = DEFAULT_ARROW_LENGTH;
    155        this.arrowWidth = DEFAULT_ARROW_WIDTH;
    156        this.labelOffset = DEFAULT_LABEL_OFFSET;
    157        this.arrowStroke = new BasicStroke(1.0f);
    158        this.arrowPaint = Color.black;
    159
    160    }
    161
    162    /**
    163     * Returns the angle of the arrow.
    164     *
    165     * @return The angle (in radians).
    166     *
    167     * @see #setAngle(double)
    168     */
    169    public double getAngle() {
    170        return this.angle;
    171    }
    172
    173    /**
    174     * Sets the angle of the arrow and sends an
    175     * {@link AnnotationChangeEvent} to all registered listeners.
    176     *
    177     * @param angle  the angle (in radians).
    178     *
    179     * @see #getAngle()
    180     */
    181    public void setAngle(double angle) {
    182        this.angle = angle;
    183        fireAnnotationChanged();
    184    }
    185
    186    /**
    187     * Returns the tip radius.
    188     *
    189     * @return The tip radius (in Java2D units).
    190     *
    191     * @see #setTipRadius(double)
    192     */
    193    public double getTipRadius() {
    194        return this.tipRadius;
    195    }
    196
    197    /**
    198     * Sets the tip radius and sends an
    199     * {@link AnnotationChangeEvent} to all registered listeners.
    200     *
    201     * @param radius  the radius (in Java2D units).
    202     *
    203     * @see #getTipRadius()
    204     */
    205    public void setTipRadius(double radius) {
    206        this.tipRadius = radius;
    207        fireAnnotationChanged();
    208    }
    209
    210    /**
    211     * Returns the base radius.
    212     *
    213     * @return The base radius (in Java2D units).
    214     *
    215     * @see #setBaseRadius(double)
    216     */
    217    public double getBaseRadius() {
    218        return this.baseRadius;
    219    }
    220
    221    /**
    222     * Sets the base radius and sends an
    223     * {@link AnnotationChangeEvent} to all registered listeners.
    224     *
    225     * @param radius  the radius (in Java2D units).
    226     *
    227     * @see #getBaseRadius()
    228     */
    229    public void setBaseRadius(double radius) {
    230        this.baseRadius = radius;
    231        fireAnnotationChanged();
    232    }
    233
    234    /**
    235     * Returns the label offset.
    236     *
    237     * @return The label offset (in Java2D units).
    238     *
    239     * @see #setLabelOffset(double)
    240     */
    241    public double getLabelOffset() {
    242        return this.labelOffset;
    243    }
    244
    245    /**
    246     * Sets the label offset (from the arrow base, continuing in a straight
    247     * line, in Java2D units) and sends an
    248     * {@link AnnotationChangeEvent} to all registered listeners.
    249     *
    250     * @param offset  the offset (in Java2D units).
    251     *
    252     * @see #getLabelOffset()
    253     */
    254    public void setLabelOffset(double offset) {
    255        this.labelOffset = offset;
    256        fireAnnotationChanged();
    257    }
    258
    259    /**
    260     * Returns the arrow length.
    261     *
    262     * @return The arrow length.
    263     *
    264     * @see #setArrowLength(double)
    265     */
    266    public double getArrowLength() {
    267        return this.arrowLength;
    268    }
    269
    270    /**
    271     * Sets the arrow length and sends an
    272     * {@link AnnotationChangeEvent} to all registered listeners.
    273     *
    274     * @param length  the length.
    275     *
    276     * @see #getArrowLength()
    277     */
    278    public void setArrowLength(double length) {
    279        this.arrowLength = length;
    280        fireAnnotationChanged();
    281    }
    282
    283    /**
    284     * Returns the arrow width.
    285     *
    286     * @return The arrow width (in Java2D units).
    287     *
    288     * @see #setArrowWidth(double)
    289     */
    290    public double getArrowWidth() {
    291        return this.arrowWidth;
    292    }
    293
    294    /**
    295     * Sets the arrow width and sends an
    296     * {@link AnnotationChangeEvent} to all registered listeners.
    297     *
    298     * @param width  the width (in Java2D units).
    299     *
    300     * @see #getArrowWidth()
    301     */
    302    public void setArrowWidth(double width) {
    303        this.arrowWidth = width;
    304        fireAnnotationChanged();
    305    }
    306
    307    /**
    308     * Returns the stroke used to draw the arrow line.
    309     *
    310     * @return The arrow stroke (never <code>null</code>).
    311     *
    312     * @see #setArrowStroke(Stroke)
    313     */
    314    public Stroke getArrowStroke() {
    315        return this.arrowStroke;
    316    }
    317
    318    /**
    319     * Sets the stroke used to draw the arrow line and sends an
    320     * {@link AnnotationChangeEvent} to all registered listeners.
    321     *
    322     * @param stroke  the stroke (<code>null</code> not permitted).
    323     *
    324     * @see #getArrowStroke()
    325     */
    326    public void setArrowStroke(Stroke stroke) {
    327        if (stroke == null) {
    328            throw new IllegalArgumentException("Null 'stroke' not permitted.");
    329        }
    330        this.arrowStroke = stroke;
    331        fireAnnotationChanged();
    332    }
    333
    334    /**
    335     * Returns the paint used for the arrow.
    336     *
    337     * @return The arrow paint (never <code>null</code>).
    338     *
    339     * @see #setArrowPaint(Paint)
    340     */
    341    public Paint getArrowPaint() {
    342        return this.arrowPaint;
    343    }
    344
    345    /**
    346     * Sets the paint used for the arrow and sends an
    347     * {@link AnnotationChangeEvent} to all registered listeners.
    348     *
    349     * @param paint  the arrow paint (<code>null</code> not permitted).
    350     *
    351     * @see #getArrowPaint()
    352     */
    353    public void setArrowPaint(Paint paint) {
    354        if (paint == null) {
    355            throw new IllegalArgumentException("Null 'paint' argument.");
    356        }
    357        this.arrowPaint = paint;
    358        fireAnnotationChanged();
    359    }
    360
    361    /**
    362     * Draws the annotation.
    363     *
    364     * @param g2  the graphics device.
    365     * @param plot  the plot.
    366     * @param dataArea  the data area.
    367     * @param domainAxis  the domain axis.
    368     * @param rangeAxis  the range axis.
    369     */
    370    public void draw(Graphics2D g2, CategoryPlot plot, Rectangle2D dataArea,
    371            CategoryAxis domainAxis, ValueAxis rangeAxis) {
    372
    373        PlotOrientation orientation = plot.getOrientation();
    374        RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
    375                plot.getDomainAxisLocation(), orientation);
    376        RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
    377                plot.getRangeAxisLocation(), orientation);
    378        CategoryDataset dataset = plot.getDataset();
    379        int catIndex = dataset.getColumnIndex(getCategory());
    380        int catCount = dataset.getColumnCount();
    381        double j2DX = domainAxis.getCategoryMiddle(catIndex, catCount,
    382                dataArea, domainEdge);
    383        double j2DY = rangeAxis.valueToJava2D(getValue(), dataArea, rangeEdge);
    384        if (orientation == PlotOrientation.HORIZONTAL) {
    385            double temp = j2DX;
    386            j2DX = j2DY;
    387            j2DY = temp;
    388        }
    389        double startX = j2DX + Math.cos(this.angle) * this.baseRadius;
    390        double startY = j2DY + Math.sin(this.angle) * this.baseRadius;
    391
    392        double endX = j2DX + Math.cos(this.angle) * this.tipRadius;
    393        double endY = j2DY + Math.sin(this.angle) * this.tipRadius;
    394
    395        double arrowBaseX = endX + Math.cos(this.angle) * this.arrowLength;
    396        double arrowBaseY = endY + Math.sin(this.angle) * this.arrowLength;
    397
    398        double arrowLeftX = arrowBaseX
    399            + Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
    400        double arrowLeftY = arrowBaseY
    401            + Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
    402
    403        double arrowRightX = arrowBaseX
    404            - Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
    405        double arrowRightY = arrowBaseY
    406            - Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
    407
    408        GeneralPath arrow = new GeneralPath();
    409        arrow.moveTo((float) endX, (float) endY);
    410        arrow.lineTo((float) arrowLeftX, (float) arrowLeftY);
    411        arrow.lineTo((float) arrowRightX, (float) arrowRightY);
    412        arrow.closePath();
    413
    414        g2.setStroke(this.arrowStroke);
    415        g2.setPaint(this.arrowPaint);
    416        Line2D line = new Line2D.Double(startX, startY, arrowBaseX, arrowBaseY);
    417        g2.draw(line);
    418        g2.fill(arrow);
    419
    420        // draw the label
    421        g2.setFont(getFont());
    422        g2.setPaint(getPaint());
    423        double labelX = j2DX
    424            + Math.cos(this.angle) * (this.baseRadius + this.labelOffset);
    425        double labelY = j2DY
    426            + Math.sin(this.angle) * (this.baseRadius + this.labelOffset);
    427        /* Rectangle2D hotspot = */ TextUtilities.drawAlignedString(getText(),
    428                g2, (float) labelX, (float) labelY, getTextAnchor());
    429        // TODO: implement the entity for the annotation
    430
    431    }
    432
    433    /**
    434     * Tests this annotation for equality with an arbitrary object.
    435     *
    436     * @param obj  the object (<code>null</code> permitted).
    437     *
    438     * @return <code>true</code> or <code>false</code>.
    439     */
    440    public boolean equals(Object obj) {
    441
    442        if (obj == this) {
    443            return true;
    444        }
    445        if (!(obj instanceof CategoryPointerAnnotation)) {
    446            return false;
    447        }
    448        if (!super.equals(obj)) {
    449            return false;
    450        }
    451        CategoryPointerAnnotation that = (CategoryPointerAnnotation) obj;
    452        if (this.angle != that.angle) {
    453            return false;
    454        }
    455        if (this.tipRadius != that.tipRadius) {
    456            return false;
    457        }
    458        if (this.baseRadius != that.baseRadius) {
    459            return false;
    460        }
    461        if (this.arrowLength != that.arrowLength) {
    462            return false;
    463        }
    464        if (this.arrowWidth != that.arrowWidth) {
    465            return false;
    466        }
    467        if (!this.arrowPaint.equals(that.arrowPaint)) {
    468            return false;
    469        }
    470        if (!ObjectUtilities.equal(this.arrowStroke, that.arrowStroke)) {
    471            return false;
    472        }
    473        if (this.labelOffset != that.labelOffset) {
    474            return false;
    475        }
    476        return true;
    477    }
    478
    479    /**
    480     * Returns a hash code for this instance.
    481     *
    482     * @return A hash code.
    483     */
    484    public int hashCode() {
    485        int result = 193;
    486        long temp = Double.doubleToLongBits(this.angle);
    487        result = 37 * result + (int) (temp ^ (temp >>> 32));
    488        temp = Double.doubleToLongBits(this.tipRadius);
    489        result = 37 * result + (int) (temp ^ (temp >>> 32));
    490        temp = Double.doubleToLongBits(this.baseRadius);
    491        result = 37 * result + (int) (temp ^ (temp >>> 32));
    492        temp = Double.doubleToLongBits(this.arrowLength);
    493        result = 37 * result + (int) (temp ^ (temp >>> 32));
    494        temp = Double.doubleToLongBits(this.arrowWidth);
    495        result = 37 * result + (int) (temp ^ (temp >>> 32));
    496        result = 37 * result + HashUtilities.hashCodeForPaint(this.arrowPaint);
    497        result = 37 * result + this.arrowStroke.hashCode();
    498        temp = Double.doubleToLongBits(this.labelOffset);
    499        result = 37 * result + (int) (temp ^ (temp >>> 32));
    500        return result;
    501    }
    502
    503    /**
    504     * Returns a clone of the annotation.
    505     *
    506     * @return A clone.
    507     *
    508     * @throws CloneNotSupportedException  if the annotation can't be cloned.
    509     */
    510    public Object clone() throws CloneNotSupportedException {
    511        return super.clone();
    512    }
    513
    514    /**
    515     * Provides serialization support.
    516     *
    517     * @param stream  the output stream.
    518     *
    519     * @throws IOException if there is an I/O error.
    520     */
    521    private void writeObject(ObjectOutputStream stream) throws IOException {
    522        stream.defaultWriteObject();
    523        SerialUtilities.writePaint(this.arrowPaint, stream);
    524        SerialUtilities.writeStroke(this.arrowStroke, stream);
    525    }
    526
    527    /**
    528     * Provides serialization support.
    529     *
    530     * @param stream  the input stream.
    531     *
    532     * @throws IOException  if there is an I/O error.
    533     * @throws ClassNotFoundException  if there is a classpath problem.
    534     */
    535    private void readObject(ObjectInputStream stream)
    536        throws IOException, ClassNotFoundException {
    537        stream.defaultReadObject();
    538        this.arrowPaint = SerialUtilities.readPaint(stream);
    539        this.arrowStroke = SerialUtilities.readStroke(stream);
    540    }
    541
    542}