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