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 }