    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2007, 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     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * ------------------------------
    028     * CategoryPointerAnnotation.java
    029     * ------------------------------
    030     * (C) Copyright 2006, 2007, by Object Refinery Limited.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   -;
    034     *
    035     * $Id: CategoryPointerAnnotation.java,v 1.1.2.3 2007/03/06 16:12:18 mungady Exp $
    036     *
    037     * Changes:
    038     * --------
    039     * 02-Oct-2006 : Version 1 (DG);
    040     * 06-Mar-2007 : Implemented hashCode() (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.plot.CategoryPlot;
    063    import org.jfree.chart.plot.Plot;
    064    import org.jfree.chart.plot.PlotOrientation;
    065    import org.jfree.data.category.CategoryDataset;
    066    import org.jfree.io.SerialUtilities;
    067    import org.jfree.text.TextUtilities;
    068    import org.jfree.ui.RectangleEdge;
    069    import org.jfree.util.ObjectUtilities;
    070    import org.jfree.util.PublicCloneable;
    071
    072    /**
    073     * An arrow and label that can be placed on a {@link CategoryPlot}.  The arrow
    074     * is drawn at a user-definable angle so that it points towards the (category,
    075     * value) location for the annotation.
    076     * <p>
    077     * The arrow length (and its offset from the (category, value) location) is
    078     * controlled by the tip radius and the base radius attributes.  Imagine two
    079     * circles around the (category, value) coordinate: the inner circle defined by
    080     * the tip radius, and the outer circle defined by the base radius.  Now, draw
    081     * the arrow starting at some point on the outer circle (the point is
    082     * determined by the angle), with the arrow tip being drawn at a corresponding
    083     * point on the inner circle.
    084     *
    085     * @since 1.0.3
    086     */
    087    public class CategoryPointerAnnotation extends CategoryTextAnnotation
    088                                     implements Cloneable, PublicCloneable,
    089                                                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.
    175         *
    176         * @param angle  the angle (in radians).
    177         *
    178         * @see #getAngle()
    179         */
    180        public void setAngle(double angle) {
    181            this.angle = angle;
    182        }
    183
    184        /**
    185         * Returns the tip radius.
    186         *
    187         * @return The tip radius (in Java2D units).
    188         *
    189         * @see #setTipRadius(double)
    190         */
    191        public double getTipRadius() {
    192            return this.tipRadius;
    193        }
    194
    195        /**
    196         * Sets the tip radius.
    197         *
    198         * @param radius  the radius (in Java2D units).
    199         *
    200         * @see #getTipRadius()
    201         */
    202        public void setTipRadius(double radius) {
    203            this.tipRadius = radius;
    204        }
    205
    206        /**
    207         * Returns the base radius.
    208         *
    209         * @return The base radius (in Java2D units).
    210         *
    211         * @see #setBaseRadius(double)
    212         */
    213        public double getBaseRadius() {
    214            return this.baseRadius;
    215        }
    216
    217        /**
    218         * Sets the base radius.
    219         *
    220         * @param radius  the radius (in Java2D units).
    221         *
    222         * @see #getBaseRadius()
    223         */
    224        public void setBaseRadius(double radius) {
    225            this.baseRadius = radius;
    226        }
    227
    228        /**
    229         * Returns the label offset.
    230         *
    231         * @return The label offset (in Java2D units).
    232         *
    233         * @see #setLabelOffset(double)
    234         */
    235        public double getLabelOffset() {
    236            return this.labelOffset;
    237        }
    238
    239        /**
    240         * Sets the label offset (from the arrow base, continuing in a straight
    241         * line, in Java2D units).
    242         *
    243         * @param offset  the offset (in Java2D units).
    244         *
    245         * @see #getLabelOffset()
    246         */
    247        public void setLabelOffset(double offset) {
    248            this.labelOffset = offset;
    249        }
    250
    251        /**
    252         * Returns the arrow length.
    253         *
    254         * @return The arrow length.
    255         *
    256         * @see #setArrowLength(double)
    257         */
    258        public double getArrowLength() {
    259            return this.arrowLength;
    260        }
    261
    262        /**
    263         * Sets the arrow length.
    264         *
    265         * @param length  the length.
    266         *
    267         * @see #getArrowLength()
    268         */
    269        public void setArrowLength(double length) {
    270            this.arrowLength = length;
    271        }
    272
    273        /**
    274         * Returns the arrow width.
    275         *
    276         * @return The arrow width (in Java2D units).
    277         *
    278         * @see #setArrowWidth(double)
    279         */
    280        public double getArrowWidth() {
    281            return this.arrowWidth;
    282        }
    283
    284        /**
    285         * Sets the arrow width.
    286         *
    287         * @param width  the width (in Java2D units).
    288         *
    289         * @see #getArrowWidth()
    290         */
    291        public void setArrowWidth(double width) {
    292            this.arrowWidth = width;
    293        }
    294
    295        /**
    296         * Returns the stroke used to draw the arrow line.
    297         *
    298         * @return The arrow stroke (never <code>null</code>).
    299         *
    300         * @see #setArrowStroke(Stroke)
    301         */
    302        public Stroke getArrowStroke() {
    303            return this.arrowStroke;
    304        }
    305
    306        /**
    307         * Sets the stroke used to draw the arrow line.
    308         *
    309         * @param stroke  the stroke (<code>null</code> not permitted).
    310         *
    311         * @see #getArrowStroke()
    312         */
    313        public void setArrowStroke(Stroke stroke) {
    314            if (stroke == null) {
    315                throw new IllegalArgumentException("Null 'stroke' not permitted.");
    316            }
    317            this.arrowStroke = stroke;
    318        }
    319
    320        /**
    321         * Returns the paint used for the arrow.
    322         *
    323         * @return The arrow paint (never <code>null</code>).
    324         *
    325         * @see #setArrowPaint(Paint)
    326         */
    327        public Paint getArrowPaint() {
    328            return this.arrowPaint;
    329        }
    330
    331        /**
    332         * Sets the paint used for the arrow.
    333         *
    334         * @param paint  the arrow paint (<code>null</code> not permitted).
    335         *
    336         * @see #getArrowPaint()
    337         */
    338        public void setArrowPaint(Paint paint) {
    339            if (paint == null) {
    340                throw new IllegalArgumentException("Null 'paint' argument.");
    341            }
    342            this.arrowPaint = paint;
    343        }
    344
    345        /**
    346         * Draws the annotation.
    347         *
    348         * @param g2  the graphics device.
    349         * @param plot  the plot.
    350         * @param dataArea  the data area.
    351         * @param domainAxis  the domain axis.
    352         * @param rangeAxis  the range axis.
    353         */
    354        public void draw(Graphics2D g2, CategoryPlot plot, Rectangle2D dataArea,
    355                CategoryAxis domainAxis, ValueAxis rangeAxis) {
    356
    357            PlotOrientation orientation = plot.getOrientation();
    358            RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
    359                    plot.getDomainAxisLocation(), orientation);
    360            RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
    361                    plot.getRangeAxisLocation(), orientation);
    362            CategoryDataset dataset = plot.getDataset();
    363            int catIndex = dataset.getColumnIndex(getCategory());
    364            int catCount = dataset.getColumnCount();
    365            double j2DX = domainAxis.getCategoryMiddle(catIndex, catCount,
    366                    dataArea, domainEdge);
    367            double j2DY = rangeAxis.valueToJava2D(getValue(), dataArea, rangeEdge);
    368            if (orientation == PlotOrientation.HORIZONTAL) {
    369                double temp = j2DX;
    370                j2DX = j2DY;
    371                j2DY = temp;
    372            }
    373            double startX = j2DX + Math.cos(this.angle) * this.baseRadius;
    374            double startY = j2DY + Math.sin(this.angle) * this.baseRadius;
    375
    376            double endX = j2DX + Math.cos(this.angle) * this.tipRadius;
    377            double endY = j2DY + Math.sin(this.angle) * this.tipRadius;
    378
    379            double arrowBaseX = endX + Math.cos(this.angle) * this.arrowLength;
    380            double arrowBaseY = endY + Math.sin(this.angle) * this.arrowLength;
    381
    382            double arrowLeftX = arrowBaseX
    383                + Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
    384            double arrowLeftY = arrowBaseY
    385                + Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
    386
    387            double arrowRightX = arrowBaseX
    388                - Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
    389            double arrowRightY = arrowBaseY
    390                - Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
    391
    392            GeneralPath arrow = new GeneralPath();
    393            arrow.moveTo((float) endX, (float) endY);
    394            arrow.lineTo((float) arrowLeftX, (float) arrowLeftY);
    395            arrow.lineTo((float) arrowRightX, (float) arrowRightY);
    396            arrow.closePath();
    397
    398            g2.setStroke(this.arrowStroke);
    399            g2.setPaint(this.arrowPaint);
    400            Line2D line = new Line2D.Double(startX, startY, endX, endY);
    401            g2.draw(line);
    402            g2.fill(arrow);
    403
    404            // draw the label
    405            g2.setFont(getFont());
    406            g2.setPaint(getPaint());
    407            double labelX = j2DX
    408                + Math.cos(this.angle) * (this.baseRadius + this.labelOffset);
    409            double labelY = j2DY
    410                + Math.sin(this.angle) * (this.baseRadius + this.labelOffset);
    411            /* Rectangle2D hotspot = */ TextUtilities.drawAlignedString(getText(),
    412                    g2, (float) labelX, (float) labelY, getTextAnchor());
    413            // TODO: implement the entity for the annotation
    414
    415        }
    416
    417        /**
    418         * Tests this annotation for equality with an arbitrary object.
    419         *
    420         * @param obj  the object (<code>null</code> permitted).
    421         *
    422         * @return <code>true</code> or <code>false</code>.
    423         */
    424        public boolean equals(Object obj) {
    425
    426            if (obj == this) {
    427                return true;
    428            }
    429            if (!(obj instanceof CategoryPointerAnnotation)) {
    430                return false;
    431            }
    432            if (!super.equals(obj)) {
    433                return false;
    434            }
    435            CategoryPointerAnnotation that = (CategoryPointerAnnotation) obj;
    436            if (this.angle != that.angle) {
    437                return false;
    438            }
    439            if (this.tipRadius != that.tipRadius) {
    440                return false;
    441            }
    442            if (this.baseRadius != that.baseRadius) {
    443                return false;
    444            }
    445            if (this.arrowLength != that.arrowLength) {
    446                return false;
    447            }
    448            if (this.arrowWidth != that.arrowWidth) {
    449                return false;
    450            }
    451            if (!this.arrowPaint.equals(that.arrowPaint)) {
    452                return false;
    453            }
    454            if (!ObjectUtilities.equal(this.arrowStroke, that.arrowStroke)) {
    455                return false;
    456            }
    457            if (this.labelOffset != that.labelOffset) {
    458                return false;
    459            }
    460            return true;
    461        }
    462
    463        /**
    464         * Returns a hash code for this instance.
    465         *
    466         * @return A hash code.
    467         */
    468        public int hashCode() {
    469            int result = 193;
    470            long temp = Double.doubleToLongBits(this.angle);
    471            result = 37 * result + (int) (temp ^ (temp >>> 32));
    472            temp = Double.doubleToLongBits(this.tipRadius);
    473            result = 37 * result + (int) (temp ^ (temp >>> 32));
    474            temp = Double.doubleToLongBits(this.baseRadius);
    475            result = 37 * result + (int) (temp ^ (temp >>> 32));
    476            temp = Double.doubleToLongBits(this.arrowLength);
    477            result = 37 * result + (int) (temp ^ (temp >>> 32));
    478            temp = Double.doubleToLongBits(this.arrowWidth);
    479            result = 37 * result + (int) (temp ^ (temp >>> 32));
    480            result = 37 * result + HashUtilities.hashCodeForPaint(this.arrowPaint);
    481            result = 37 * result + this.arrowStroke.hashCode();
    482            temp = Double.doubleToLongBits(this.labelOffset);
    483            result = 37 * result + (int) (temp ^ (temp >>> 32));
    484            return result;
    485        }
    486
    487        /**
    488         * Returns a clone of the annotation.
    489         *
    490         * @return A clone.
    491         *
    492         * @throws CloneNotSupportedException  if the annotation can't be cloned.
    493         */
    494        public Object clone() throws CloneNotSupportedException {
    495            return super.clone();
    496        }
    497
    498        /**
    499         * Provides serialization support.
    500         *
    501         * @param stream  the output stream.
    502         *
    503         * @throws IOException if there is an I/O error.
    504         */
    505        private void writeObject(ObjectOutputStream stream) throws IOException {
    506            stream.defaultWriteObject();
    507            SerialUtilities.writePaint(this.arrowPaint, stream);
    508            SerialUtilities.writeStroke(this.arrowStroke, stream);
    509        }
    510
    511        /**
    512         * Provides serialization support.
    513         *
    514         * @param stream  the input stream.
    515         *
    516         * @throws IOException  if there is an I/O error.
    517         * @throws ClassNotFoundException  if there is a classpath problem.
    518         */
    519        private void readObject(ObjectInputStream stream)
    520            throws IOException, ClassNotFoundException {
    521            stream.defaultReadObject();
    522            this.arrowPaint = SerialUtilities.readPaint(stream);
    523            this.arrowStroke = SerialUtilities.readStroke(stream);
    524        }
    525
    526    }