001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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, 2007, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * $Id: CategoryPointerAnnotation.java,v 1.1.2.3 2007/03/06 16:12:18 mungady Exp $ 036 * 037 * Changes: 038 * -------- 039 * 02-Oct-2006 : Version 1 (DG); 040 * 06-Mar-2007 : Implemented hashCode() (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.plot.CategoryPlot; 063 import org.jfree.chart.plot.Plot; 064 import org.jfree.chart.plot.PlotOrientation; 065 import org.jfree.data.category.CategoryDataset; 066 import org.jfree.io.SerialUtilities; 067 import org.jfree.text.TextUtilities; 068 import org.jfree.ui.RectangleEdge; 069 import org.jfree.util.ObjectUtilities; 070 import org.jfree.util.PublicCloneable; 071 072 /** 073 * An arrow and label that can be placed on a {@link CategoryPlot}. The arrow 074 * is drawn at a user-definable angle so that it points towards the (category, 075 * value) location for the annotation. 076 * <p> 077 * The arrow length (and its offset from the (category, value) location) is 078 * controlled by the tip radius and the base radius attributes. Imagine two 079 * circles around the (category, value) coordinate: the inner circle defined by 080 * the tip radius, and the outer circle defined by the base radius. Now, draw 081 * the arrow starting at some point on the outer circle (the point is 082 * determined by the angle), with the arrow tip being drawn at a corresponding 083 * point on the inner circle. 084 * 085 * @since 1.0.3 086 */ 087 public class CategoryPointerAnnotation extends CategoryTextAnnotation 088 implements Cloneable, PublicCloneable, 089 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. 175 * 176 * @param angle the angle (in radians). 177 * 178 * @see #getAngle() 179 */ 180 public void setAngle(double angle) { 181 this.angle = angle; 182 } 183 184 /** 185 * Returns the tip radius. 186 * 187 * @return The tip radius (in Java2D units). 188 * 189 * @see #setTipRadius(double) 190 */ 191 public double getTipRadius() { 192 return this.tipRadius; 193 } 194 195 /** 196 * Sets the tip radius. 197 * 198 * @param radius the radius (in Java2D units). 199 * 200 * @see #getTipRadius() 201 */ 202 public void setTipRadius(double radius) { 203 this.tipRadius = radius; 204 } 205 206 /** 207 * Returns the base radius. 208 * 209 * @return The base radius (in Java2D units). 210 * 211 * @see #setBaseRadius(double) 212 */ 213 public double getBaseRadius() { 214 return this.baseRadius; 215 } 216 217 /** 218 * Sets the base radius. 219 * 220 * @param radius the radius (in Java2D units). 221 * 222 * @see #getBaseRadius() 223 */ 224 public void setBaseRadius(double radius) { 225 this.baseRadius = radius; 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). 242 * 243 * @param offset the offset (in Java2D units). 244 * 245 * @see #getLabelOffset() 246 */ 247 public void setLabelOffset(double offset) { 248 this.labelOffset = offset; 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. 264 * 265 * @param length the length. 266 * 267 * @see #getArrowLength() 268 */ 269 public void setArrowLength(double length) { 270 this.arrowLength = length; 271 } 272 273 /** 274 * Returns the arrow width. 275 * 276 * @return The arrow width (in Java2D units). 277 * 278 * @see #setArrowWidth(double) 279 */ 280 public double getArrowWidth() { 281 return this.arrowWidth; 282 } 283 284 /** 285 * Sets the arrow width. 286 * 287 * @param width the width (in Java2D units). 288 * 289 * @see #getArrowWidth() 290 */ 291 public void setArrowWidth(double width) { 292 this.arrowWidth = width; 293 } 294 295 /** 296 * Returns the stroke used to draw the arrow line. 297 * 298 * @return The arrow stroke (never <code>null</code>). 299 * 300 * @see #setArrowStroke(Stroke) 301 */ 302 public Stroke getArrowStroke() { 303 return this.arrowStroke; 304 } 305 306 /** 307 * Sets the stroke used to draw the arrow line. 308 * 309 * @param stroke the stroke (<code>null</code> not permitted). 310 * 311 * @see #getArrowStroke() 312 */ 313 public void setArrowStroke(Stroke stroke) { 314 if (stroke == null) { 315 throw new IllegalArgumentException("Null 'stroke' not permitted."); 316 } 317 this.arrowStroke = stroke; 318 } 319 320 /** 321 * Returns the paint used for the arrow. 322 * 323 * @return The arrow paint (never <code>null</code>). 324 * 325 * @see #setArrowPaint(Paint) 326 */ 327 public Paint getArrowPaint() { 328 return this.arrowPaint; 329 } 330 331 /** 332 * Sets the paint used for the arrow. 333 * 334 * @param paint the arrow paint (<code>null</code> not permitted). 335 * 336 * @see #getArrowPaint() 337 */ 338 public void setArrowPaint(Paint paint) { 339 if (paint == null) { 340 throw new IllegalArgumentException("Null 'paint' argument."); 341 } 342 this.arrowPaint = paint; 343 } 344 345 /** 346 * Draws the annotation. 347 * 348 * @param g2 the graphics device. 349 * @param plot the plot. 350 * @param dataArea the data area. 351 * @param domainAxis the domain axis. 352 * @param rangeAxis the range axis. 353 */ 354 public void draw(Graphics2D g2, CategoryPlot plot, Rectangle2D dataArea, 355 CategoryAxis domainAxis, ValueAxis rangeAxis) { 356 357 PlotOrientation orientation = plot.getOrientation(); 358 RectangleEdge domainEdge = Plot.resolveDomainAxisLocation( 359 plot.getDomainAxisLocation(), orientation); 360 RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation( 361 plot.getRangeAxisLocation(), orientation); 362 CategoryDataset dataset = plot.getDataset(); 363 int catIndex = dataset.getColumnIndex(getCategory()); 364 int catCount = dataset.getColumnCount(); 365 double j2DX = domainAxis.getCategoryMiddle(catIndex, catCount, 366 dataArea, domainEdge); 367 double j2DY = rangeAxis.valueToJava2D(getValue(), dataArea, rangeEdge); 368 if (orientation == PlotOrientation.HORIZONTAL) { 369 double temp = j2DX; 370 j2DX = j2DY; 371 j2DY = temp; 372 } 373 double startX = j2DX + Math.cos(this.angle) * this.baseRadius; 374 double startY = j2DY + Math.sin(this.angle) * this.baseRadius; 375 376 double endX = j2DX + Math.cos(this.angle) * this.tipRadius; 377 double endY = j2DY + Math.sin(this.angle) * this.tipRadius; 378 379 double arrowBaseX = endX + Math.cos(this.angle) * this.arrowLength; 380 double arrowBaseY = endY + Math.sin(this.angle) * this.arrowLength; 381 382 double arrowLeftX = arrowBaseX 383 + Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth; 384 double arrowLeftY = arrowBaseY 385 + Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth; 386 387 double arrowRightX = arrowBaseX 388 - Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth; 389 double arrowRightY = arrowBaseY 390 - Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth; 391 392 GeneralPath arrow = new GeneralPath(); 393 arrow.moveTo((float) endX, (float) endY); 394 arrow.lineTo((float) arrowLeftX, (float) arrowLeftY); 395 arrow.lineTo((float) arrowRightX, (float) arrowRightY); 396 arrow.closePath(); 397 398 g2.setStroke(this.arrowStroke); 399 g2.setPaint(this.arrowPaint); 400 Line2D line = new Line2D.Double(startX, startY, endX, endY); 401 g2.draw(line); 402 g2.fill(arrow); 403 404 // draw the label 405 g2.setFont(getFont()); 406 g2.setPaint(getPaint()); 407 double labelX = j2DX 408 + Math.cos(this.angle) * (this.baseRadius + this.labelOffset); 409 double labelY = j2DY 410 + Math.sin(this.angle) * (this.baseRadius + this.labelOffset); 411 /* Rectangle2D hotspot = */ TextUtilities.drawAlignedString(getText(), 412 g2, (float) labelX, (float) labelY, getTextAnchor()); 413 // TODO: implement the entity for the annotation 414 415 } 416 417 /** 418 * Tests this annotation for equality with an arbitrary object. 419 * 420 * @param obj the object (<code>null</code> permitted). 421 * 422 * @return <code>true</code> or <code>false</code>. 423 */ 424 public boolean equals(Object obj) { 425 426 if (obj == this) { 427 return true; 428 } 429 if (!(obj instanceof CategoryPointerAnnotation)) { 430 return false; 431 } 432 if (!super.equals(obj)) { 433 return false; 434 } 435 CategoryPointerAnnotation that = (CategoryPointerAnnotation) obj; 436 if (this.angle != that.angle) { 437 return false; 438 } 439 if (this.tipRadius != that.tipRadius) { 440 return false; 441 } 442 if (this.baseRadius != that.baseRadius) { 443 return false; 444 } 445 if (this.arrowLength != that.arrowLength) { 446 return false; 447 } 448 if (this.arrowWidth != that.arrowWidth) { 449 return false; 450 } 451 if (!this.arrowPaint.equals(that.arrowPaint)) { 452 return false; 453 } 454 if (!ObjectUtilities.equal(this.arrowStroke, that.arrowStroke)) { 455 return false; 456 } 457 if (this.labelOffset != that.labelOffset) { 458 return false; 459 } 460 return true; 461 } 462 463 /** 464 * Returns a hash code for this instance. 465 * 466 * @return A hash code. 467 */ 468 public int hashCode() { 469 int result = 193; 470 long temp = Double.doubleToLongBits(this.angle); 471 result = 37 * result + (int) (temp ^ (temp >>> 32)); 472 temp = Double.doubleToLongBits(this.tipRadius); 473 result = 37 * result + (int) (temp ^ (temp >>> 32)); 474 temp = Double.doubleToLongBits(this.baseRadius); 475 result = 37 * result + (int) (temp ^ (temp >>> 32)); 476 temp = Double.doubleToLongBits(this.arrowLength); 477 result = 37 * result + (int) (temp ^ (temp >>> 32)); 478 temp = Double.doubleToLongBits(this.arrowWidth); 479 result = 37 * result + (int) (temp ^ (temp >>> 32)); 480 result = 37 * result + HashUtilities.hashCodeForPaint(this.arrowPaint); 481 result = 37 * result + this.arrowStroke.hashCode(); 482 temp = Double.doubleToLongBits(this.labelOffset); 483 result = 37 * result + (int) (temp ^ (temp >>> 32)); 484 return result; 485 } 486 487 /** 488 * Returns a clone of the annotation. 489 * 490 * @return A clone. 491 * 492 * @throws CloneNotSupportedException if the annotation can't be cloned. 493 */ 494 public Object clone() throws CloneNotSupportedException { 495 return super.clone(); 496 } 497 498 /** 499 * Provides serialization support. 500 * 501 * @param stream the output stream. 502 * 503 * @throws IOException if there is an I/O error. 504 */ 505 private void writeObject(ObjectOutputStream stream) throws IOException { 506 stream.defaultWriteObject(); 507 SerialUtilities.writePaint(this.arrowPaint, stream); 508 SerialUtilities.writeStroke(this.arrowStroke, stream); 509 } 510 511 /** 512 * Provides serialization support. 513 * 514 * @param stream the input stream. 515 * 516 * @throws IOException if there is an I/O error. 517 * @throws ClassNotFoundException if there is a classpath problem. 518 */ 519 private void readObject(ObjectInputStream stream) 520 throws IOException, ClassNotFoundException { 521 stream.defaultReadObject(); 522 this.arrowPaint = SerialUtilities.readPaint(stream); 523 this.arrowStroke = SerialUtilities.readStroke(stream); 524 } 525 526 }