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