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