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    }