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