    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
    044    package org.jfree.chart.annotations;
    045
    046    import java.awt.BasicStroke;
    047    import java.awt.Color;
    048    import java.awt.Graphics2D;
    049    import java.awt.Paint;
    050    import java.awt.Stroke;
    051    import java.awt.geom.GeneralPath;
    052    import java.awt.geom.Line2D;
    053    import java.awt.geom.Rectangle2D;
    054    import java.io.IOException;
    055    import java.io.ObjectInputStream;
    056    import java.io.ObjectOutputStream;
    057    import java.io.Serializable;
    058
    059    import org.jfree.chart.HashUtilities;
    060    import org.jfree.chart.axis.CategoryAxis;
    061    import org.jfree.chart.axis.ValueAxis;
    062    import org.jfree.chart.event.AnnotationChangeEvent;
    063    import org.jfree.chart.plot.CategoryPlot;
    064    import org.jfree.chart.plot.Plot;
    065    import org.jfree.chart.plot.PlotOrientation;
    066    import org.jfree.data.category.CategoryDataset;
    067    import org.jfree.io.SerialUtilities;
    068    import org.jfree.text.TextUtilities;
    069    import org.jfree.ui.RectangleEdge;
    070    import org.jfree.util.ObjectUtilities;
    071    import 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     */
    088    public 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    }