    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     * XYTextAnnotation.java
    029     * ---------------------
    030     * (C) Copyright 2002-2011, by Object Refinery Limited and Contributors.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   Peter Kolb (patch 2809117);
    034     *
    035     * Changes:
    036     * --------
    037     * 28-Aug-2002 : Version 1 (DG);
    038     * 07-Nov-2002 : Fixed errors reported by Checkstyle (DG);
    039     * 13-Jan-2003 : Reviewed Javadocs (DG);
    040     * 26-Mar-2003 : Implemented Serializable (DG);
    041     * 02-Jul-2003 : Added new text alignment and rotation options (DG);
    042     * 19-Aug-2003 : Implemented Cloneable (DG);
    043     * 17-Jan-2003 : Added fix for bug 878706, where the annotation is placed
    044     *               incorrectly for a plot with horizontal orientation (thanks to
    045     *               Ed Yu for the fix) (DG);
    046     * 21-Jan-2004 : Update for renamed method in ValueAxis (DG);
    047     * ------------- JFREECHART 1.0.x ---------------------------------------------
    048     * 26-Jan-2006 : Fixed equals() method (bug 1415480) (DG);
    049     * 06-Mar-2007 : Added argument checks, re-implemented hashCode() method (DG);
    050     * 12-Feb-2009 : Added background paint and outline paint/stroke (DG);
    051     * 01-Apr-2009 : Fixed bug in hotspot calculation (DG);
    052     * 24-Jun-2009 : Fire change events (see patch 2809117) (DG);
    053     *
    054     */
    055
    056    package org.jfree.chart.annotations;
    057
    058    import java.awt.BasicStroke;
    059    import java.awt.Color;
    060    import java.awt.Font;
    061    import java.awt.Graphics2D;
    062    import java.awt.Paint;
    063    import java.awt.Shape;
    064    import java.awt.Stroke;
    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.ui.TextAnchor;
    082    import org.jfree.util.PaintUtilities;
    083    import org.jfree.util.PublicCloneable;
    084
    085    /**
    086     * A text annotation that can be placed at a particular (x, y) location on an
    087     * {@link XYPlot}.
    088     */
    089    public class XYTextAnnotation extends AbstractXYAnnotation
    090            implements Cloneable, PublicCloneable, Serializable {
    091
    092        /** For serialization. */
    093        private static final long serialVersionUID = -2946063342782506328L;
    094
    095        /** The default font. */
    096        public static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN,
    097                10);
    098
    099        /** The default paint. */
    100        public static final Paint DEFAULT_PAINT = Color.black;
    101
    102        /** The default text anchor. */
    103        public static final TextAnchor DEFAULT_TEXT_ANCHOR = TextAnchor.CENTER;
    104
    105        /** The default rotation anchor. */
    106        public static final TextAnchor DEFAULT_ROTATION_ANCHOR = TextAnchor.CENTER;
    107
    108        /** The default rotation angle. */
    109        public static final double DEFAULT_ROTATION_ANGLE = 0.0;
    110
    111        /** The text. */
    112        private String text;
    113
    114        /** The font. */
    115        private Font font;
    116
    117        /** The paint. */
    118        private transient Paint paint;
    119
    120        /** The x-coordinate. */
    121        private double x;
    122
    123        /** The y-coordinate. */
    124        private double y;
    125
    126        /** The text anchor (to be aligned with (x, y)). */
    127        private TextAnchor textAnchor;
    128
    129        /** The rotation anchor. */
    130        private TextAnchor rotationAnchor;
    131
    132        /** The rotation angle. */
    133        private double rotationAngle;
    134
    135        /**
    136         * The background paint (possibly null).
    137         *
    138         * @since 1.0.13
    139         */
    140        private transient Paint backgroundPaint;
    141
    142        /**
    143         * The flag that controls the visibility of the outline.
    144         *
    145         * @since 1.0.13
    146         */
    147        private boolean outlineVisible;
    148
    149        /**
    150         * The outline paint (never null).
    151         *
    152         * @since 1.0.13
    153         */
    154        private transient Paint outlinePaint;
    155
    156        /**
    157         * The outline stroke (never null).
    158         *
    159         * @since 1.0.13
    160         */
    161        private transient Stroke outlineStroke;
    162
    163        /**
    164         * Creates a new annotation to be displayed at the given coordinates.  The
    165         * coordinates are specified in data space (they will be converted to
    166         * Java2D space for display).
    167         *
    168         * @param text  the text (<code>null</code> not permitted).
    169         * @param x  the x-coordinate (in data space).
    170         * @param y  the y-coordinate (in data space).
    171         */
    172        public XYTextAnnotation(String text, double x, double y) {
    173            super();
    174            if (text == null) {
    175                throw new IllegalArgumentException("Null 'text' argument.");
    176            }
    177            this.text = text;
    178            this.font = DEFAULT_FONT;
    179            this.paint = DEFAULT_PAINT;
    180            this.x = x;
    181            this.y = y;
    182            this.textAnchor = DEFAULT_TEXT_ANCHOR;
    183            this.rotationAnchor = DEFAULT_ROTATION_ANCHOR;
    184            this.rotationAngle = DEFAULT_ROTATION_ANGLE;
    185
    186            // by default the outline and background won't be visible
    187            this.backgroundPaint = null;
    188            this.outlineVisible = false;
    189            this.outlinePaint = Color.black;
    190            this.outlineStroke = new BasicStroke(0.5f);
    191        }
    192
    193        /**
    194         * Returns the text for the annotation.
    195         *
    196         * @return The text (never <code>null</code>).
    197         *
    198         * @see #setText(String)
    199         */
    200        public String getText() {
    201            return this.text;
    202        }
    203
    204        /**
    205         * Sets the text for the annotation.
    206         *
    207         * @param text  the text (<code>null</code> not permitted).
    208         *
    209         * @see #getText()
    210         */
    211        public void setText(String text) {
    212            if (text == null) {
    213                throw new IllegalArgumentException("Null 'text' argument.");
    214            }
    215            this.text = text;
    216        }
    217
    218        /**
    219         * Returns the font for the annotation.
    220         *
    221         * @return The font (never <code>null</code>).
    222         *
    223         * @see #setFont(Font)
    224         */
    225        public Font getFont() {
    226            return this.font;
    227        }
    228
    229        /**
    230         * Sets the font for the annotation and sends an
    231         * {@link AnnotationChangeEvent} to all registered listeners.
    232         *
    233         * @param font  the font (<code>null</code> not permitted).
    234         *
    235         * @see #getFont()
    236         */
    237        public void setFont(Font font) {
    238            if (font == null) {
    239                throw new IllegalArgumentException("Null 'font' argument.");
    240            }
    241            this.font = font;
    242            fireAnnotationChanged();
    243        }
    244
    245        /**
    246         * Returns the paint for the annotation.
    247         *
    248         * @return The paint (never <code>null</code>).
    249         *
    250         * @see #setPaint(Paint)
    251         */
    252        public Paint getPaint() {
    253            return this.paint;
    254        }
    255
    256        /**
    257         * Sets the paint for the annotation and sends an
    258         * {@link AnnotationChangeEvent} to all registered listeners.
    259         *
    260         * @param paint  the paint (<code>null</code> not permitted).
    261         *
    262         * @see #getPaint()
    263         */
    264        public void setPaint(Paint paint) {
    265            if (paint == null) {
    266                throw new IllegalArgumentException("Null 'paint' argument.");
    267            }
    268            this.paint = paint;
    269            fireAnnotationChanged();
    270        }
    271
    272        /**
    273         * Returns the text anchor.
    274         *
    275         * @return The text anchor (never <code>null</code>).
    276         *
    277         * @see #setTextAnchor(TextAnchor)
    278         */
    279        public TextAnchor getTextAnchor() {
    280            return this.textAnchor;
    281        }
    282
    283        /**
    284         * Sets the text anchor (the point on the text bounding rectangle that is
    285         * aligned to the (x, y) coordinate of the annotation) and sends an
    286         * {@link AnnotationChangeEvent} to all registered listeners.
    287         *
    288         * @param anchor  the anchor point (<code>null</code> not permitted).
    289         *
    290         * @see #getTextAnchor()
    291         */
    292        public void setTextAnchor(TextAnchor anchor) {
    293            if (anchor == null) {
    294                throw new IllegalArgumentException("Null 'anchor' argument.");
    295            }
    296            this.textAnchor = anchor;
    297            fireAnnotationChanged();
    298        }
    299
    300        /**
    301         * Returns the rotation anchor.
    302         *
    303         * @return The rotation anchor point (never <code>null</code>).
    304         *
    305         * @see #setRotationAnchor(TextAnchor)
    306         */
    307        public TextAnchor getRotationAnchor() {
    308            return this.rotationAnchor;
    309        }
    310
    311        /**
    312         * Sets the rotation anchor point and sends an
    313         * {@link AnnotationChangeEvent} to all registered listeners.
    314         *
    315         * @param anchor  the anchor (<code>null</code> not permitted).
    316         *
    317         * @see #getRotationAnchor()
    318         */
    319        public void setRotationAnchor(TextAnchor anchor) {
    320            if (anchor == null) {
    321                throw new IllegalArgumentException("Null 'anchor' argument.");
    322            }
    323            this.rotationAnchor = anchor;
    324            fireAnnotationChanged();
    325        }
    326
    327        /**
    328         * Returns the rotation angle.
    329         *
    330         * @return The rotation angle.
    331         *
    332         * @see #setRotationAngle(double)
    333         */
    334        public double getRotationAngle() {
    335            return this.rotationAngle;
    336        }
    337
    338        /**
    339         * Sets the rotation angle and sends an {@link AnnotationChangeEvent} to
    340         * all registered listeners.  The angle is measured clockwise in radians.
    341         *
    342         * @param angle  the angle (in radians).
    343         *
    344         * @see #getRotationAngle()
    345         */
    346        public void setRotationAngle(double angle) {
    347            this.rotationAngle = angle;
    348            fireAnnotationChanged();
    349        }
    350
    351        /**
    352         * Returns the x coordinate for the text anchor point (measured against the
    353         * domain axis).
    354         *
    355         * @return The x coordinate (in data space).
    356         *
    357         * @see #setX(double)
    358         */
    359        public double getX() {
    360            return this.x;
    361        }
    362
    363        /**
    364         * Sets the x coordinate for the text anchor point (measured against the
    365         * domain axis) and sends an {@link AnnotationChangeEvent} to all
    366         * registered listeners.
    367         *
    368         * @param x  the x coordinate (in data space).
    369         *
    370         * @see #getX()
    371         */
    372        public void setX(double x) {
    373            this.x = x;
    374            fireAnnotationChanged();
    375        }
    376
    377        /**
    378         * Returns the y coordinate for the text anchor point (measured against the
    379         * range axis).
    380         *
    381         * @return The y coordinate (in data space).
    382         *
    383         * @see #setY(double)
    384         */
    385        public double getY() {
    386            return this.y;
    387        }
    388
    389        /**
    390         * Sets the y coordinate for the text anchor point (measured against the
    391         * range axis) and sends an {@link AnnotationChangeEvent} to all registered
    392         * listeners.
    393         *
    394         * @param y  the y coordinate.
    395         *
    396         * @see #getY()
    397         */
    398        public void setY(double y) {
    399            this.y = y;
    400            fireAnnotationChanged();
    401        }
    402
    403        /**
    404         * Returns the background paint for the annotation.
    405         *
    406         * @return The background paint (possibly <code>null</code>).
    407         *
    408         * @see #setBackgroundPaint(Paint)
    409         *
    410         * @since 1.0.13
    411         */
    412        public Paint getBackgroundPaint() {
    413            return this.backgroundPaint;
    414        }
    415
    416        /**
    417         * Sets the background paint for the annotation and sends an
    418         * {@link AnnotationChangeEvent} to all registered listeners.
    419         *
    420         * @param paint  the paint (<code>null</code> permitted).
    421         *
    422         * @see #getBackgroundPaint()
    423         *
    424         * @since 1.0.13
    425         */
    426        public void setBackgroundPaint(Paint paint) {
    427            this.backgroundPaint = paint;
    428            fireAnnotationChanged();
    429        }
    430
    431        /**
    432         * Returns the outline paint for the annotation.
    433         *
    434         * @return The outline paint (never <code>null</code>).
    435         *
    436         * @see #setOutlinePaint(Paint)
    437         *
    438         * @since 1.0.13
    439         */
    440        public Paint getOutlinePaint() {
    441            return this.outlinePaint;
    442        }
    443
    444        /**
    445         * Sets the outline paint for the annotation and sends an
    446         * {@link AnnotationChangeEvent} to all registered listeners.
    447         *
    448         * @param paint  the paint (<code>null</code> not permitted).
    449         *
    450         * @see #getOutlinePaint()
    451         *
    452         * @since 1.0.13
    453         */
    454        public void setOutlinePaint(Paint paint) {
    455            if (paint == null) {
    456                throw new IllegalArgumentException("Null 'paint' argument.");
    457            }
    458            this.outlinePaint = paint;
    459            fireAnnotationChanged();
    460        }
    461
    462        /**
    463         * Returns the outline stroke for the annotation.
    464         *
    465         * @return The outline stroke (never <code>null</code>).
    466         *
    467         * @see #setOutlineStroke(Stroke)
    468         *
    469         * @since 1.0.13
    470         */
    471        public Stroke getOutlineStroke() {
    472            return this.outlineStroke;
    473        }
    474
    475        /**
    476         * Sets the outline stroke for the annotation and sends an
    477         * {@link AnnotationChangeEvent} to all registered listeners.
    478         *
    479         * @param stroke  the stroke (<code>null</code> not permitted).
    480         *
    481         * @see #getOutlineStroke()
    482         *
    483         * @since 1.0.13
    484         */
    485        public void setOutlineStroke(Stroke stroke) {
    486            if (stroke == null) {
    487                throw new IllegalArgumentException("Null 'stroke' argument.");
    488            }
    489            this.outlineStroke = stroke;
    490            fireAnnotationChanged();
    491        }
    492
    493        /**
    494         * Returns the flag that controls whether or not the outline is drawn.
    495         *
    496         * @return A boolean.
    497         *
    498         * @since 1.0.13
    499         */
    500        public boolean isOutlineVisible() {
    501            return this.outlineVisible;
    502        }
    503
    504        /**
    505         * Sets the flag that controls whether or not the outline is drawn and
    506         * sends an {@link AnnotationChangeEvent} to all registered listeners.
    507         *
    508         * @param visible  the new flag value.
    509         *
    510         * @since 1.0.13
    511         */
    512        public void setOutlineVisible(boolean visible) {
    513            this.outlineVisible = visible;
    514            fireAnnotationChanged();
    515        }
    516
    517        /**
    518         * Draws the annotation.
    519         *
    520         * @param g2  the graphics device.
    521         * @param plot  the plot.
    522         * @param dataArea  the data area.
    523         * @param domainAxis  the domain axis.
    524         * @param rangeAxis  the range axis.
    525         * @param rendererIndex  the renderer index.
    526         * @param info  an optional info object that will be populated with
    527         *              entity information.
    528         */
    529        public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea,
    530                         ValueAxis domainAxis, ValueAxis rangeAxis,
    531                         int rendererIndex,
    532                         PlotRenderingInfo info) {
    533
    534            PlotOrientation orientation = plot.getOrientation();
    535            RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
    536                    plot.getDomainAxisLocation(), orientation);
    537            RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
    538                    plot.getRangeAxisLocation(), orientation);
    539
    540            float anchorX = (float) domainAxis.valueToJava2D(
    541                    this.x, dataArea, domainEdge);
    542            float anchorY = (float) rangeAxis.valueToJava2D(
    543                    this.y, dataArea, rangeEdge);
    544
    545            if (orientation == PlotOrientation.HORIZONTAL) {
    546                float tempAnchor = anchorX;
    547                anchorX = anchorY;
    548                anchorY = tempAnchor;
    549            }
    550
    551            g2.setFont(getFont());
    552            Shape hotspot = TextUtilities.calculateRotatedStringBounds(
    553                    getText(), g2, anchorX, anchorY, getTextAnchor(),
    554                    getRotationAngle(), getRotationAnchor());
    555            if (this.backgroundPaint != null) {
    556                g2.setPaint(this.backgroundPaint);
    557                g2.fill(hotspot);
    558            }
    559            g2.setPaint(getPaint());
    560            TextUtilities.drawRotatedString(getText(), g2, anchorX, anchorY,
    561                    getTextAnchor(), getRotationAngle(), getRotationAnchor());
    562            if (this.outlineVisible) {
    563                g2.setStroke(this.outlineStroke);
    564                g2.setPaint(this.outlinePaint);
    565                g2.draw(hotspot);
    566            }
    567
    568            String toolTip = getToolTipText();
    569            String url = getURL();
    570            if (toolTip != null || url != null) {
    571                addEntity(info, hotspot, rendererIndex, toolTip, url);
    572            }
    573
    574        }
    575
    576        /**
    577         * Tests this annotation for equality with an arbitrary object.
    578         *
    579         * @param obj  the object (<code>null</code> permitted).
    580         *
    581         * @return A boolean.
    582         */
    583        public boolean equals(Object obj) {
    584            if (obj == this) {
    585                return true;
    586            }
    587            if (!(obj instanceof XYTextAnnotation)) {
    588                return false;
    589            }
    590            XYTextAnnotation that = (XYTextAnnotation) obj;
    591            if (!this.text.equals(that.text)) {
    592                return false;
    593            }
    594            if (this.x != that.x) {
    595                return false;
    596            }
    597            if (this.y != that.y) {
    598                return false;
    599            }
    600            if (!this.font.equals(that.font)) {
    601                return false;
    602            }
    603            if (!PaintUtilities.equal(this.paint, that.paint)) {
    604                return false;
    605            }
    606            if (!this.rotationAnchor.equals(that.rotationAnchor)) {
    607                return false;
    608            }
    609            if (this.rotationAngle != that.rotationAngle) {
    610                return false;
    611            }
    612            if (!this.textAnchor.equals(that.textAnchor)) {
    613                return false;
    614            }
    615            if (this.outlineVisible != that.outlineVisible) {
    616                return false;
    617            }
    618            if (!PaintUtilities.equal(this.backgroundPaint, that.backgroundPaint)) {
    619                return false;
    620            }
    621            if (!PaintUtilities.equal(this.outlinePaint, that.outlinePaint)) {
    622                return false;
    623            }
    624            if (!(this.outlineStroke.equals(that.outlineStroke))) {
    625                return false;
    626            }
    627            return super.equals(obj);
    628        }
    629
    630        /**
    631         * Returns a hash code for the object.
    632         *
    633         * @return A hash code.
    634         */
    635        public int hashCode() {
    636            int result = 193;
    637            result = 37 * this.text.hashCode();
    638            result = 37 * this.font.hashCode();
    639            result = 37 * result + HashUtilities.hashCodeForPaint(this.paint);
    640            long temp = Double.doubleToLongBits(this.x);
    641            result = 37 * result + (int) (temp ^ (temp >>> 32));
    642            temp = Double.doubleToLongBits(this.y);
    643            result = 37 * result + (int) (temp ^ (temp >>> 32));
    644            result = 37 * result + this.textAnchor.hashCode();
    645            result = 37 * result + this.rotationAnchor.hashCode();
    646            temp = Double.doubleToLongBits(this.rotationAngle);
    647            result = 37 * result + (int) (temp ^ (temp >>> 32));
    648            return result;
    649        }
    650
    651        /**
    652         * Returns a clone of the annotation.
    653         *
    654         * @return A clone.
    655         *
    656         * @throws CloneNotSupportedException  if the annotation can't be cloned.
    657         */
    658        public Object clone() throws CloneNotSupportedException {
    659            return super.clone();
    660        }
    661
    662        /**
    663         * Provides serialization support.
    664         *
    665         * @param stream  the output stream.
    666         *
    667         * @throws IOException  if there is an I/O error.
    668         */
    669        private void writeObject(ObjectOutputStream stream) throws IOException {
    670            stream.defaultWriteObject();
    671            SerialUtilities.writePaint(this.paint, stream);
    672            SerialUtilities.writePaint(this.backgroundPaint, stream);
    673            SerialUtilities.writePaint(this.outlinePaint, stream);
    674            SerialUtilities.writeStroke(this.outlineStroke, stream);
    675        }
    676
    677        /**
    678         * Provides serialization support.
    679         *
    680         * @param stream  the input stream.
    681         *
    682         * @throws IOException  if there is an I/O error.
    683         * @throws ClassNotFoundException  if there is a classpath problem.
    684         */
    685        private void readObject(ObjectInputStream stream)
    686            throws IOException, ClassNotFoundException {
    687            stream.defaultReadObject();
    688            this.paint = SerialUtilities.readPaint(stream);
    689            this.backgroundPaint = SerialUtilities.readPaint(stream);
    690            this.outlinePaint = SerialUtilities.readPaint(stream);
    691            this.outlineStroke = SerialUtilities.readStroke(stream);
    692        }
    693
    694    }