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