    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
    055    package org.jfree.chart.annotations;
    056
    057    import java.awt.BasicStroke;
    058    import java.awt.Color;
    059    import java.awt.Graphics2D;
    060    import java.awt.Paint;
    061    import java.awt.Shape;
    062    import java.awt.Stroke;
    063    import java.awt.geom.GeneralPath;
    064    import java.awt.geom.Line2D;
    065    import java.awt.geom.Rectangle2D;
    066    import java.io.IOException;
    067    import java.io.ObjectInputStream;
    068    import java.io.ObjectOutputStream;
    069    import java.io.Serializable;
    070
    071    import org.jfree.chart.HashUtilities;
    072    import org.jfree.chart.axis.ValueAxis;
    073    import org.jfree.chart.event.AnnotationChangeEvent;
    074    import org.jfree.chart.plot.Plot;
    075    import org.jfree.chart.plot.PlotOrientation;
    076    import org.jfree.chart.plot.PlotRenderingInfo;
    077    import org.jfree.chart.plot.XYPlot;
    078    import org.jfree.io.SerialUtilities;
    079    import org.jfree.text.TextUtilities;
    080    import org.jfree.ui.RectangleEdge;
    081    import org.jfree.util.ObjectUtilities;
    082    import 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     */
    097    public 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    }