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