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 * XYPointerAnnotation.java 029 * ------------------------ 030 * (C) Copyright 2003-2011, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Peter Kolb (patch 2809117); 034 * 035 * Changes: 036 * -------- 037 * 21-May-2003 : Version 1 (DG); 038 * 10-Jun-2003 : Changed BoundsAnchor to TextAnchor (DG); 039 * 02-Jul-2003 : Added accessor methods and simplified constructor (DG); 040 * 19-Aug-2003 : Implemented Cloneable (DG); 041 * 13-Oct-2003 : Fixed bug where arrow paint is not set correctly (DG); 042 * 21-Jan-2004 : Update for renamed method in ValueAxis (DG); 043 * 29-Sep-2004 : Changes to draw() method signature (DG); 044 * ------------- JFREECHART 1.0.x --------------------------------------------- 045 * 20-Feb-2006 : Correction for equals() method (fixes bug 1435160) (DG); 046 * 12-Jul-2006 : Fix drawing for PlotOrientation.HORIZONTAL, thanks to 047 * Skunk (DG); 048 * 12-Feb-2009 : Added support for rotated label, plus background and 049 * outline (DG); 050 * 18-May-2009 : Fixed typo in hashCode() method (DG); 051 * 24-Jun-2009 : Fire change events (see patch 2809117 by PK) (DG); 052 * 053 */ 054 055package org.jfree.chart.annotations; 056 057import java.awt.BasicStroke; 058import java.awt.Color; 059import java.awt.Graphics2D; 060import java.awt.Paint; 061import java.awt.Shape; 062import java.awt.Stroke; 063import java.awt.geom.GeneralPath; 064import java.awt.geom.Line2D; 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.util.ObjectUtilities; 082import org.jfree.util.PublicCloneable; 083 084/** 085 * An arrow and label that can be placed on an {@link XYPlot}. The arrow is 086 * drawn at a user-definable angle so that it points towards the (x, y) 087 * location for the annotation. 088 * <p> 089 * The arrow length (and its offset from the (x, y) location) is controlled by 090 * the tip radius and the base radius attributes. Imagine two circles around 091 * the (x, y) coordinate: the inner circle defined by the tip radius, and the 092 * outer circle defined by the base radius. Now, draw the arrow starting at 093 * some point on the outer circle (the point is determined by the angle), with 094 * the arrow tip being drawn at a corresponding point on the inner circle. 095 * 096 */ 097public class XYPointerAnnotation extends XYTextAnnotation 098 implements Cloneable, PublicCloneable, Serializable { 099 100 /** For serialization. */ 101 private static final long serialVersionUID = -4031161445009858551L; 102 103 /** The default tip radius (in Java2D units). */ 104 public static final double DEFAULT_TIP_RADIUS = 10.0; 105 106 /** The default base radius (in Java2D units). */ 107 public static final double DEFAULT_BASE_RADIUS = 30.0; 108 109 /** The default label offset (in Java2D units). */ 110 public static final double DEFAULT_LABEL_OFFSET = 3.0; 111 112 /** The default arrow length (in Java2D units). */ 113 public static final double DEFAULT_ARROW_LENGTH = 5.0; 114 115 /** The default arrow width (in Java2D units). */ 116 public static final double DEFAULT_ARROW_WIDTH = 3.0; 117 118 /** The angle of the arrow's line (in radians). */ 119 private double angle; 120 121 /** 122 * The radius from the (x, y) point to the tip of the arrow (in Java2D 123 * units). 124 */ 125 private double tipRadius; 126 127 /** 128 * The radius from the (x, y) point to the start of the arrow line (in 129 * Java2D units). 130 */ 131 private double baseRadius; 132 133 /** The length of the arrow head (in Java2D units). */ 134 private double arrowLength; 135 136 /** The arrow width (in Java2D units, per side). */ 137 private double arrowWidth; 138 139 /** The arrow stroke. */ 140 private transient Stroke arrowStroke; 141 142 /** The arrow paint. */ 143 private transient Paint arrowPaint; 144 145 /** The radius from the base point to the anchor point for the label. */ 146 private double labelOffset; 147 148 /** 149 * Creates a new label and arrow annotation. 150 * 151 * @param label the label (<code>null</code> permitted). 152 * @param x the x-coordinate (measured against the chart's domain axis). 153 * @param y the y-coordinate (measured against the chart's range axis). 154 * @param angle the angle of the arrow's line (in radians). 155 */ 156 public XYPointerAnnotation(String label, double x, double y, double angle) { 157 158 super(label, x, y); 159 this.angle = angle; 160 this.tipRadius = DEFAULT_TIP_RADIUS; 161 this.baseRadius = DEFAULT_BASE_RADIUS; 162 this.arrowLength = DEFAULT_ARROW_LENGTH; 163 this.arrowWidth = DEFAULT_ARROW_WIDTH; 164 this.labelOffset = DEFAULT_LABEL_OFFSET; 165 this.arrowStroke = new BasicStroke(1.0f); 166 this.arrowPaint = Color.black; 167 168 } 169 170 /** 171 * Returns the angle of the arrow. 172 * 173 * @return The angle (in radians). 174 * 175 * @see #setAngle(double) 176 */ 177 public double getAngle() { 178 return this.angle; 179 } 180 181 /** 182 * Sets the angle of the arrow and sends an 183 * {@link AnnotationChangeEvent} to all registered listeners. 184 * 185 * @param angle the angle (in radians). 186 * 187 * @see #getAngle() 188 */ 189 public void setAngle(double angle) { 190 this.angle = angle; 191 fireAnnotationChanged(); 192 } 193 194 /** 195 * Returns the tip radius. 196 * 197 * @return The tip radius (in Java2D units). 198 * 199 * @see #setTipRadius(double) 200 */ 201 public double getTipRadius() { 202 return this.tipRadius; 203 } 204 205 /** 206 * Sets the tip radius and sends an 207 * {@link AnnotationChangeEvent} to all registered listeners. 208 * 209 * @param radius the radius (in Java2D units). 210 * 211 * @see #getTipRadius() 212 */ 213 public void setTipRadius(double radius) { 214 this.tipRadius = radius; 215 fireAnnotationChanged(); 216 } 217 218 /** 219 * Returns the base radius. 220 * 221 * @return The base radius (in Java2D units). 222 * 223 * @see #setBaseRadius(double) 224 */ 225 public double getBaseRadius() { 226 return this.baseRadius; 227 } 228 229 /** 230 * Sets the base radius and sends an 231 * {@link AnnotationChangeEvent} to all registered listeners. 232 * 233 * @param radius the radius (in Java2D units). 234 * 235 * @see #getBaseRadius() 236 */ 237 public void setBaseRadius(double radius) { 238 this.baseRadius = radius; 239 fireAnnotationChanged(); 240 } 241 242 /** 243 * Returns the label offset. 244 * 245 * @return The label offset (in Java2D units). 246 * 247 * @see #setLabelOffset(double) 248 */ 249 public double getLabelOffset() { 250 return this.labelOffset; 251 } 252 253 /** 254 * Sets the label offset (from the arrow base, continuing in a straight 255 * line, in Java2D units) and sends an 256 * {@link AnnotationChangeEvent} to all registered listeners. 257 * 258 * @param offset the offset (in Java2D units). 259 * 260 * @see #getLabelOffset() 261 */ 262 public void setLabelOffset(double offset) { 263 this.labelOffset = offset; 264 fireAnnotationChanged(); 265 } 266 267 /** 268 * Returns the arrow length. 269 * 270 * @return The arrow length. 271 * 272 * @see #setArrowLength(double) 273 */ 274 public double getArrowLength() { 275 return this.arrowLength; 276 } 277 278 /** 279 * Sets the arrow length and sends an 280 * {@link AnnotationChangeEvent} to all registered listeners. 281 * 282 * @param length the length. 283 * 284 * @see #getArrowLength() 285 */ 286 public void setArrowLength(double length) { 287 this.arrowLength = length; 288 fireAnnotationChanged(); 289 } 290 291 /** 292 * Returns the arrow width. 293 * 294 * @return The arrow width (in Java2D units). 295 * 296 * @see #setArrowWidth(double) 297 */ 298 public double getArrowWidth() { 299 return this.arrowWidth; 300 } 301 302 /** 303 * Sets the arrow width and sends an 304 * {@link AnnotationChangeEvent} to all registered listeners. 305 * 306 * @param width the width (in Java2D units). 307 * 308 * @see #getArrowWidth() 309 */ 310 public void setArrowWidth(double width) { 311 this.arrowWidth = width; 312 fireAnnotationChanged(); 313 } 314 315 /** 316 * Returns the stroke used to draw the arrow line. 317 * 318 * @return The arrow stroke (never <code>null</code>). 319 * 320 * @see #setArrowStroke(Stroke) 321 */ 322 public Stroke getArrowStroke() { 323 return this.arrowStroke; 324 } 325 326 /** 327 * Sets the stroke used to draw the arrow line and sends an 328 * {@link AnnotationChangeEvent} to all registered listeners. 329 * 330 * @param stroke the stroke (<code>null</code> not permitted). 331 * 332 * @see #getArrowStroke() 333 */ 334 public void setArrowStroke(Stroke stroke) { 335 if (stroke == null) { 336 throw new IllegalArgumentException("Null 'stroke' not permitted."); 337 } 338 this.arrowStroke = stroke; 339 fireAnnotationChanged(); 340 } 341 342 /** 343 * Returns the paint used for the arrow. 344 * 345 * @return The arrow paint (never <code>null</code>). 346 * 347 * @see #setArrowPaint(Paint) 348 */ 349 public Paint getArrowPaint() { 350 return this.arrowPaint; 351 } 352 353 /** 354 * Sets the paint used for the arrow and sends an 355 * {@link AnnotationChangeEvent} to all registered listeners. 356 * 357 * @param paint the arrow paint (<code>null</code> not permitted). 358 * 359 * @see #getArrowPaint() 360 */ 361 public void setArrowPaint(Paint paint) { 362 if (paint == null) { 363 throw new IllegalArgumentException("Null 'paint' argument."); 364 } 365 this.arrowPaint = paint; 366 fireAnnotationChanged(); 367 } 368 369 /** 370 * Draws the annotation. 371 * 372 * @param g2 the graphics device. 373 * @param plot the plot. 374 * @param dataArea the data area. 375 * @param domainAxis the domain axis. 376 * @param rangeAxis the range axis. 377 * @param rendererIndex the renderer index. 378 * @param info the plot rendering info. 379 */ 380 public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea, 381 ValueAxis domainAxis, ValueAxis rangeAxis, 382 int rendererIndex, 383 PlotRenderingInfo info) { 384 385 PlotOrientation orientation = plot.getOrientation(); 386 RectangleEdge domainEdge = Plot.resolveDomainAxisLocation( 387 plot.getDomainAxisLocation(), orientation); 388 RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation( 389 plot.getRangeAxisLocation(), orientation); 390 double j2DX = domainAxis.valueToJava2D(getX(), dataArea, domainEdge); 391 double j2DY = rangeAxis.valueToJava2D(getY(), dataArea, rangeEdge); 392 if (orientation == PlotOrientation.HORIZONTAL) { 393 double temp = j2DX; 394 j2DX = j2DY; 395 j2DY = temp; 396 } 397 double startX = j2DX + Math.cos(this.angle) * this.baseRadius; 398 double startY = j2DY + Math.sin(this.angle) * this.baseRadius; 399 400 double endX = j2DX + Math.cos(this.angle) * this.tipRadius; 401 double endY = j2DY + Math.sin(this.angle) * this.tipRadius; 402 403 double arrowBaseX = endX + Math.cos(this.angle) * this.arrowLength; 404 double arrowBaseY = endY + Math.sin(this.angle) * this.arrowLength; 405 406 double arrowLeftX = arrowBaseX 407 + Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth; 408 double arrowLeftY = arrowBaseY 409 + Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth; 410 411 double arrowRightX = arrowBaseX 412 - Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth; 413 double arrowRightY = arrowBaseY 414 - Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth; 415 416 GeneralPath arrow = new GeneralPath(); 417 arrow.moveTo((float) endX, (float) endY); 418 arrow.lineTo((float) arrowLeftX, (float) arrowLeftY); 419 arrow.lineTo((float) arrowRightX, (float) arrowRightY); 420 arrow.closePath(); 421 422 g2.setStroke(this.arrowStroke); 423 g2.setPaint(this.arrowPaint); 424 Line2D line = new Line2D.Double(startX, startY, arrowBaseX, arrowBaseY); 425 g2.draw(line); 426 g2.fill(arrow); 427 428 // draw the label 429 double labelX = j2DX + Math.cos(this.angle) * (this.baseRadius 430 + this.labelOffset); 431 double labelY = j2DY + Math.sin(this.angle) * (this.baseRadius 432 + this.labelOffset); 433 g2.setFont(getFont()); 434 Shape hotspot = TextUtilities.calculateRotatedStringBounds( 435 getText(), g2, (float) labelX, (float) labelY, getTextAnchor(), 436 getRotationAngle(), getRotationAnchor()); 437 if (getBackgroundPaint() != null) { 438 g2.setPaint(getBackgroundPaint()); 439 g2.fill(hotspot); 440 } 441 g2.setPaint(getPaint()); 442 TextUtilities.drawRotatedString(getText(), g2, (float) labelX, 443 (float) labelY, getTextAnchor(), getRotationAngle(), 444 getRotationAnchor()); 445 if (isOutlineVisible()) { 446 g2.setStroke(getOutlineStroke()); 447 g2.setPaint(getOutlinePaint()); 448 g2.draw(hotspot); 449 } 450 451 String toolTip = getToolTipText(); 452 String url = getURL(); 453 if (toolTip != null || url != null) { 454 addEntity(info, hotspot, rendererIndex, toolTip, url); 455 } 456 457 } 458 459 /** 460 * Tests this annotation for equality with an arbitrary object. 461 * 462 * @param obj the object (<code>null</code> permitted). 463 * 464 * @return <code>true</code> or <code>false</code>. 465 */ 466 public boolean equals(Object obj) { 467 if (obj == this) { 468 return true; 469 } 470 if (!(obj instanceof XYPointerAnnotation)) { 471 return false; 472 } 473 XYPointerAnnotation that = (XYPointerAnnotation) obj; 474 if (this.angle != that.angle) { 475 return false; 476 } 477 if (this.tipRadius != that.tipRadius) { 478 return false; 479 } 480 if (this.baseRadius != that.baseRadius) { 481 return false; 482 } 483 if (this.arrowLength != that.arrowLength) { 484 return false; 485 } 486 if (this.arrowWidth != that.arrowWidth) { 487 return false; 488 } 489 if (!this.arrowPaint.equals(that.arrowPaint)) { 490 return false; 491 } 492 if (!ObjectUtilities.equal(this.arrowStroke, that.arrowStroke)) { 493 return false; 494 } 495 if (this.labelOffset != that.labelOffset) { 496 return false; 497 } 498 return super.equals(obj); 499 } 500 501 /** 502 * Returns a hash code for this instance. 503 * 504 * @return A hash code. 505 */ 506 public int hashCode() { 507 int result = super.hashCode(); 508 long temp = Double.doubleToLongBits(this.angle); 509 result = 37 * result + (int) (temp ^ (temp >>> 32)); 510 temp = Double.doubleToLongBits(this.tipRadius); 511 result = 37 * result + (int) (temp ^ (temp >>> 32)); 512 temp = Double.doubleToLongBits(this.baseRadius); 513 result = 37 * result + (int) (temp ^ (temp >>> 32)); 514 temp = Double.doubleToLongBits(this.arrowLength); 515 result = 37 * result + (int) (temp ^ (temp >>> 32)); 516 temp = Double.doubleToLongBits(this.arrowWidth); 517 result = 37 * result + (int) (temp ^ (temp >>> 32)); 518 result = result * 37 + HashUtilities.hashCodeForPaint(this.arrowPaint); 519 result = result * 37 + this.arrowStroke.hashCode(); 520 temp = Double.doubleToLongBits(this.labelOffset); 521 result = 37 * result + (int) (temp ^ (temp >>> 32)); 522 return result; 523 } 524 525 /** 526 * Returns a clone of the annotation. 527 * 528 * @return A clone. 529 * 530 * @throws CloneNotSupportedException if the annotation can't be cloned. 531 */ 532 public Object clone() throws CloneNotSupportedException { 533 return super.clone(); 534 } 535 536 /** 537 * Provides serialization support. 538 * 539 * @param stream the output stream. 540 * 541 * @throws IOException if there is an I/O error. 542 */ 543 private void writeObject(ObjectOutputStream stream) throws IOException { 544 stream.defaultWriteObject(); 545 SerialUtilities.writePaint(this.arrowPaint, stream); 546 SerialUtilities.writeStroke(this.arrowStroke, stream); 547 } 548 549 /** 550 * Provides serialization support. 551 * 552 * @param stream the input stream. 553 * 554 * @throws IOException if there is an I/O error. 555 * @throws ClassNotFoundException if there is a classpath problem. 556 */ 557 private void readObject(ObjectInputStream stream) 558 throws IOException, ClassNotFoundException { 559 stream.defaultReadObject(); 560 this.arrowPaint = SerialUtilities.readPaint(stream); 561 this.arrowStroke = SerialUtilities.readStroke(stream); 562 } 563 564}