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