001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2022, 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-2022, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Peter Kolb (patch 2809117); 034 * 035 */ 036 037package org.jfree.chart.annotations; 038 039import java.awt.BasicStroke; 040import java.awt.Color; 041import java.awt.Graphics2D; 042import java.awt.Paint; 043import java.awt.Shape; 044import java.awt.Stroke; 045import java.awt.geom.GeneralPath; 046import java.awt.geom.Line2D; 047import java.awt.geom.Rectangle2D; 048import java.io.IOException; 049import java.io.ObjectInputStream; 050import java.io.ObjectOutputStream; 051import java.io.Serializable; 052import java.util.Objects; 053 054import org.jfree.chart.internal.HashUtils; 055import org.jfree.chart.axis.ValueAxis; 056import org.jfree.chart.event.AnnotationChangeEvent; 057import org.jfree.chart.plot.Plot; 058import org.jfree.chart.plot.PlotOrientation; 059import org.jfree.chart.plot.PlotRenderingInfo; 060import org.jfree.chart.plot.XYPlot; 061import org.jfree.chart.text.TextUtils; 062import org.jfree.chart.api.RectangleEdge; 063import org.jfree.chart.internal.Args; 064import org.jfree.chart.api.PublicCloneable; 065import org.jfree.chart.internal.SerialUtils; 066 067/** 068 * An arrow and label that can be placed on an {@link XYPlot}. The arrow is 069 * drawn at a user-definable angle so that it points towards the (x, y) 070 * location for the annotation. 071 * <p> 072 * The arrow length (and its offset from the (x, y) location) is controlled by 073 * the tip radius and the base radius attributes. Imagine two circles around 074 * the (x, y) coordinate: the inner circle defined by the tip radius, and the 075 * outer circle defined by the base radius. Now, draw the arrow starting at 076 * some point on the outer circle (the point is determined by the angle), with 077 * the arrow tip being drawn at a corresponding point on the inner circle. 078 */ 079public class XYPointerAnnotation extends XYTextAnnotation 080 implements Cloneable, PublicCloneable, Serializable { 081 082 /** For serialization. */ 083 private static final long serialVersionUID = -4031161445009858551L; 084 085 /** The default tip radius (in Java2D units). */ 086 public static final double DEFAULT_TIP_RADIUS = 10.0; 087 088 /** The default base radius (in Java2D units). */ 089 public static final double DEFAULT_BASE_RADIUS = 30.0; 090 091 /** The default label offset (in Java2D units). */ 092 public static final double DEFAULT_LABEL_OFFSET = 3.0; 093 094 /** The default arrow length (in Java2D units). */ 095 public static final double DEFAULT_ARROW_LENGTH = 5.0; 096 097 /** The default arrow width (in Java2D units). */ 098 public static final double DEFAULT_ARROW_WIDTH = 3.0; 099 100 /** The angle of the arrow's line (in radians). */ 101 private double angle; 102 103 /** 104 * The radius from the (x, y) point to the tip of the arrow (in Java2D 105 * units). 106 */ 107 private double tipRadius; 108 109 /** 110 * The radius from the (x, y) point to the start of the arrow line (in 111 * Java2D units). 112 */ 113 private double baseRadius; 114 115 /** The length of the arrow head (in Java2D units). */ 116 private double arrowLength; 117 118 /** The arrow width (in Java2D units, per side). */ 119 private double arrowWidth; 120 121 /** The arrow stroke. */ 122 private transient Stroke arrowStroke; 123 124 /** The arrow paint. */ 125 private transient Paint arrowPaint; 126 127 /** The radius from the base point to the anchor point for the label. */ 128 private double labelOffset; 129 130 /** 131 * Creates a new label and arrow annotation. 132 * 133 * @param label the label ({@code null} permitted). 134 * @param x the x-coordinate (measured against the chart's domain axis). 135 * @param y the y-coordinate (measured against the chart's range axis). 136 * @param angle the angle of the arrow's line (in radians). 137 */ 138 public XYPointerAnnotation(String label, double x, double y, double angle) { 139 140 super(label, x, y); 141 Args.requireFinite(x, "x"); 142 Args.requireFinite(y, "y"); 143 Args.requireFinite(angle, "angle"); 144 this.angle = angle; 145 this.tipRadius = DEFAULT_TIP_RADIUS; 146 this.baseRadius = DEFAULT_BASE_RADIUS; 147 this.arrowLength = DEFAULT_ARROW_LENGTH; 148 this.arrowWidth = DEFAULT_ARROW_WIDTH; 149 this.labelOffset = DEFAULT_LABEL_OFFSET; 150 this.arrowStroke = new BasicStroke(1.0f); 151 this.arrowPaint = Color.BLACK; 152 153 } 154 155 /** 156 * Returns the angle of the arrow. 157 * 158 * @return The angle (in radians). 159 * 160 * @see #setAngle(double) 161 */ 162 public double getAngle() { 163 return this.angle; 164 } 165 166 /** 167 * Sets the angle of the arrow and sends an 168 * {@link AnnotationChangeEvent} to all registered listeners. 169 * 170 * @param angle the angle (in radians). 171 * 172 * @see #getAngle() 173 */ 174 public void setAngle(double angle) { 175 this.angle = angle; 176 fireAnnotationChanged(); 177 } 178 179 /** 180 * Returns the tip radius. 181 * 182 * @return The tip radius (in Java2D units). 183 * 184 * @see #setTipRadius(double) 185 */ 186 public double getTipRadius() { 187 return this.tipRadius; 188 } 189 190 /** 191 * Sets the tip radius and sends an 192 * {@link AnnotationChangeEvent} to all registered listeners. 193 * 194 * @param radius the radius (in Java2D units). 195 * 196 * @see #getTipRadius() 197 */ 198 public void setTipRadius(double radius) { 199 this.tipRadius = radius; 200 fireAnnotationChanged(); 201 } 202 203 /** 204 * Returns the base radius. 205 * 206 * @return The base radius (in Java2D units). 207 * 208 * @see #setBaseRadius(double) 209 */ 210 public double getBaseRadius() { 211 return this.baseRadius; 212 } 213 214 /** 215 * Sets the base radius and sends an 216 * {@link AnnotationChangeEvent} to all registered listeners. 217 * 218 * @param radius the radius (in Java2D units). 219 * 220 * @see #getBaseRadius() 221 */ 222 public void setBaseRadius(double radius) { 223 this.baseRadius = radius; 224 fireAnnotationChanged(); 225 } 226 227 /** 228 * Returns the label offset. 229 * 230 * @return The label offset (in Java2D units). 231 * 232 * @see #setLabelOffset(double) 233 */ 234 public double getLabelOffset() { 235 return this.labelOffset; 236 } 237 238 /** 239 * Sets the label offset (from the arrow base, continuing in a straight 240 * line, in Java2D units) and sends an 241 * {@link AnnotationChangeEvent} to all registered listeners. 242 * 243 * @param offset the offset (in Java2D units). 244 * 245 * @see #getLabelOffset() 246 */ 247 public void setLabelOffset(double offset) { 248 this.labelOffset = offset; 249 fireAnnotationChanged(); 250 } 251 252 /** 253 * Returns the arrow length. 254 * 255 * @return The arrow length. 256 * 257 * @see #setArrowLength(double) 258 */ 259 public double getArrowLength() { 260 return this.arrowLength; 261 } 262 263 /** 264 * Sets the arrow length and sends an 265 * {@link AnnotationChangeEvent} to all registered listeners. 266 * 267 * @param length the length. 268 * 269 * @see #getArrowLength() 270 */ 271 public void setArrowLength(double length) { 272 this.arrowLength = length; 273 fireAnnotationChanged(); 274 } 275 276 /** 277 * Returns the arrow width. 278 * 279 * @return The arrow width (in Java2D units). 280 * 281 * @see #setArrowWidth(double) 282 */ 283 public double getArrowWidth() { 284 return this.arrowWidth; 285 } 286 287 /** 288 * Sets the arrow width and sends an 289 * {@link AnnotationChangeEvent} to all registered listeners. 290 * 291 * @param width the width (in Java2D units). 292 * 293 * @see #getArrowWidth() 294 */ 295 public void setArrowWidth(double width) { 296 this.arrowWidth = width; 297 fireAnnotationChanged(); 298 } 299 300 /** 301 * Returns the stroke used to draw the arrow line. 302 * 303 * @return The arrow stroke (never {@code null}). 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 and sends an 313 * {@link AnnotationChangeEvent} to all registered listeners. 314 * 315 * @param stroke the stroke ({@code null} not permitted). 316 * 317 * @see #getArrowStroke() 318 */ 319 public void setArrowStroke(Stroke stroke) { 320 Args.nullNotPermitted(stroke, "stroke"); 321 this.arrowStroke = stroke; 322 fireAnnotationChanged(); 323 } 324 325 /** 326 * Returns the paint used for the arrow. 327 * 328 * @return The arrow paint (never {@code null}). 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 and sends an 338 * {@link AnnotationChangeEvent} to all registered listeners. 339 * 340 * @param paint the arrow paint ({@code null} not permitted). 341 * 342 * @see #getArrowPaint() 343 */ 344 public void setArrowPaint(Paint paint) { 345 Args.nullNotPermitted(paint, "paint"); 346 this.arrowPaint = paint; 347 fireAnnotationChanged(); 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 @Override 362 public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea, 363 ValueAxis domainAxis, ValueAxis rangeAxis, 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, arrowBaseX, arrowBaseY); 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 = TextUtils.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 TextUtils.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} permitted). 444 * 445 * @return {@code true} or {@code false}. 446 */ 447 @Override 448 public boolean equals(Object obj) { 449 if (obj == this) { 450 return true; 451 } 452 if (!(obj instanceof XYPointerAnnotation)) { 453 return false; 454 } 455 XYPointerAnnotation that = (XYPointerAnnotation) obj; 456 if (this.angle != that.angle) { 457 return false; 458 } 459 if (this.tipRadius != that.tipRadius) { 460 return false; 461 } 462 if (this.baseRadius != that.baseRadius) { 463 return false; 464 } 465 if (this.arrowLength != that.arrowLength) { 466 return false; 467 } 468 if (this.arrowWidth != that.arrowWidth) { 469 return false; 470 } 471 if (!this.arrowPaint.equals(that.arrowPaint)) { 472 return false; 473 } 474 if (!Objects.equals(this.arrowStroke, that.arrowStroke)) { 475 return false; 476 } 477 if (this.labelOffset != that.labelOffset) { 478 return false; 479 } 480 return super.equals(obj); 481 } 482 483 /** 484 * Returns a hash code for this instance. 485 * 486 * @return A hash code. 487 */ 488 @Override 489 public int hashCode() { 490 int result = super.hashCode(); 491 long temp = Double.doubleToLongBits(this.angle); 492 result = 37 * result + (int) (temp ^ (temp >>> 32)); 493 temp = Double.doubleToLongBits(this.tipRadius); 494 result = 37 * result + (int) (temp ^ (temp >>> 32)); 495 temp = Double.doubleToLongBits(this.baseRadius); 496 result = 37 * result + (int) (temp ^ (temp >>> 32)); 497 temp = Double.doubleToLongBits(this.arrowLength); 498 result = 37 * result + (int) (temp ^ (temp >>> 32)); 499 temp = Double.doubleToLongBits(this.arrowWidth); 500 result = 37 * result + (int) (temp ^ (temp >>> 32)); 501 result = result * 37 + HashUtils.hashCodeForPaint(this.arrowPaint); 502 result = result * 37 + this.arrowStroke.hashCode(); 503 temp = Double.doubleToLongBits(this.labelOffset); 504 result = 37 * result + (int) (temp ^ (temp >>> 32)); 505 return result; 506 } 507 508 /** 509 * Returns a clone of the annotation. 510 * 511 * @return A clone. 512 * 513 * @throws CloneNotSupportedException if the annotation can't be cloned. 514 */ 515 @Override 516 public Object clone() throws CloneNotSupportedException { 517 return super.clone(); 518 } 519 520 /** 521 * Provides serialization support. 522 * 523 * @param stream the output stream. 524 * 525 * @throws IOException if there is an I/O error. 526 */ 527 private void writeObject(ObjectOutputStream stream) throws IOException { 528 stream.defaultWriteObject(); 529 SerialUtils.writePaint(this.arrowPaint, stream); 530 SerialUtils.writeStroke(this.arrowStroke, stream); 531 } 532 533 /** 534 * Provides serialization support. 535 * 536 * @param stream the input stream. 537 * 538 * @throws IOException if there is an I/O error. 539 * @throws ClassNotFoundException if there is a classpath problem. 540 */ 541 private void readObject(ObjectInputStream stream) 542 throws IOException, ClassNotFoundException { 543 stream.defaultReadObject(); 544 this.arrowPaint = SerialUtils.readPaint(stream); 545 this.arrowStroke = SerialUtils.readStroke(stream); 546 } 547 548}