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