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}