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