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