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}