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