001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2006, 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, 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.2 2006/10/03 16:21:43 mungady Exp $ 036 * 037 * Changes: 038 * -------- 039 * 02-Oct-2006 : Version 1 (DG); 040 * 041 */ 042 043 package org.jfree.chart.annotations; 044 045 import java.awt.BasicStroke; 046 import java.awt.Color; 047 import java.awt.Graphics2D; 048 import java.awt.Paint; 049 import java.awt.Stroke; 050 import java.awt.geom.GeneralPath; 051 import java.awt.geom.Line2D; 052 import java.awt.geom.Rectangle2D; 053 import java.io.IOException; 054 import java.io.ObjectInputStream; 055 import java.io.ObjectOutputStream; 056 import java.io.Serializable; 057 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 public double getAngle() { 166 return this.angle; 167 } 168 169 /** 170 * Sets the angle of the arrow. 171 * 172 * @param angle the angle (in radians). 173 */ 174 public void setAngle(double angle) { 175 this.angle = angle; 176 } 177 178 /** 179 * Returns the tip radius. 180 * 181 * @return The tip radius (in Java2D units). 182 */ 183 public double getTipRadius() { 184 return this.tipRadius; 185 } 186 187 /** 188 * Sets the tip radius. 189 * 190 * @param radius the radius (in Java2D units). 191 */ 192 public void setTipRadius(double radius) { 193 this.tipRadius = radius; 194 } 195 196 /** 197 * Returns the base radius. 198 * 199 * @return The base radius (in Java2D units). 200 */ 201 public double getBaseRadius() { 202 return this.baseRadius; 203 } 204 205 /** 206 * Sets the base radius. 207 * 208 * @param radius the radius (in Java2D units). 209 */ 210 public void setBaseRadius(double radius) { 211 this.baseRadius = radius; 212 } 213 214 /** 215 * Returns the label offset. 216 * 217 * @return The label offset (in Java2D units). 218 */ 219 public double getLabelOffset() { 220 return this.labelOffset; 221 } 222 223 /** 224 * Sets the label offset (from the arrow base, continuing in a straight 225 * line, in Java2D units). 226 * 227 * @param offset the offset (in Java2D units). 228 */ 229 public void setLabelOffset(double offset) { 230 this.labelOffset = offset; 231 } 232 233 /** 234 * Returns the arrow length. 235 * 236 * @return The arrow length. 237 */ 238 public double getArrowLength() { 239 return this.arrowLength; 240 } 241 242 /** 243 * Sets the arrow length. 244 * 245 * @param length the length. 246 */ 247 public void setArrowLength(double length) { 248 this.arrowLength = length; 249 } 250 251 /** 252 * Returns the arrow width. 253 * 254 * @return The arrow width (in Java2D units). 255 */ 256 public double getArrowWidth() { 257 return this.arrowWidth; 258 } 259 260 /** 261 * Sets the arrow width. 262 * 263 * @param width the width (in Java2D units). 264 */ 265 public void setArrowWidth(double width) { 266 this.arrowWidth = width; 267 } 268 269 /** 270 * Returns the stroke used to draw the arrow line. 271 * 272 * @return The arrow stroke (never <code>null</code>). 273 */ 274 public Stroke getArrowStroke() { 275 return this.arrowStroke; 276 } 277 278 /** 279 * Sets the stroke used to draw the arrow line. 280 * 281 * @param stroke the stroke (<code>null</code> not permitted). 282 */ 283 public void setArrowStroke(Stroke stroke) { 284 if (stroke == null) { 285 throw new IllegalArgumentException("Null 'stroke' not permitted."); 286 } 287 this.arrowStroke = stroke; 288 } 289 290 /** 291 * Returns the paint used for the arrow. 292 * 293 * @return The arrow paint (never <code>null</code>). 294 */ 295 public Paint getArrowPaint() { 296 return this.arrowPaint; 297 } 298 299 /** 300 * Sets the paint used for the arrow. 301 * 302 * @param paint the arrow paint (<code>null</code> not permitted). 303 */ 304 public void setArrowPaint(Paint paint) { 305 if (paint == null) { 306 throw new IllegalArgumentException("Null 'paint' argument."); 307 } 308 this.arrowPaint = paint; 309 } 310 311 /** 312 * Draws the annotation. 313 * 314 * @param g2 the graphics device. 315 * @param plot the plot. 316 * @param dataArea the data area. 317 * @param domainAxis the domain axis. 318 * @param rangeAxis the range axis. 319 */ 320 public void draw(Graphics2D g2, CategoryPlot plot, Rectangle2D dataArea, 321 CategoryAxis domainAxis, ValueAxis rangeAxis) { 322 323 PlotOrientation orientation = plot.getOrientation(); 324 RectangleEdge domainEdge = Plot.resolveDomainAxisLocation( 325 plot.getDomainAxisLocation(), orientation); 326 RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation( 327 plot.getRangeAxisLocation(), orientation); 328 CategoryDataset dataset = plot.getDataset(); 329 int catIndex = dataset.getColumnIndex(getCategory()); 330 int catCount = dataset.getColumnCount(); 331 double j2DX = domainAxis.getCategoryMiddle(catIndex, catCount, 332 dataArea, domainEdge); 333 double j2DY = rangeAxis.valueToJava2D(getValue(), dataArea, rangeEdge); 334 if (orientation == PlotOrientation.HORIZONTAL) { 335 double temp = j2DX; 336 j2DX = j2DY; 337 j2DY = temp; 338 } 339 double startX = j2DX + Math.cos(this.angle) * this.baseRadius; 340 double startY = j2DY + Math.sin(this.angle) * this.baseRadius; 341 342 double endX = j2DX + Math.cos(this.angle) * this.tipRadius; 343 double endY = j2DY + Math.sin(this.angle) * this.tipRadius; 344 345 double arrowBaseX = endX + Math.cos(this.angle) * this.arrowLength; 346 double arrowBaseY = endY + Math.sin(this.angle) * this.arrowLength; 347 348 double arrowLeftX = arrowBaseX 349 + Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth; 350 double arrowLeftY = arrowBaseY 351 + Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth; 352 353 double arrowRightX = arrowBaseX 354 - Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth; 355 double arrowRightY = arrowBaseY 356 - Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth; 357 358 GeneralPath arrow = new GeneralPath(); 359 arrow.moveTo((float) endX, (float) endY); 360 arrow.lineTo((float) arrowLeftX, (float) arrowLeftY); 361 arrow.lineTo((float) arrowRightX, (float) arrowRightY); 362 arrow.closePath(); 363 364 g2.setStroke(this.arrowStroke); 365 g2.setPaint(this.arrowPaint); 366 Line2D line = new Line2D.Double(startX, startY, endX, endY); 367 g2.draw(line); 368 g2.fill(arrow); 369 370 // draw the label 371 g2.setFont(getFont()); 372 g2.setPaint(getPaint()); 373 double labelX = j2DX 374 + Math.cos(this.angle) * (this.baseRadius + this.labelOffset); 375 double labelY = j2DY 376 + Math.sin(this.angle) * (this.baseRadius + this.labelOffset); 377 /* Rectangle2D hotspot = */ TextUtilities.drawAlignedString(getText(), 378 g2, (float) labelX, (float) labelY, getTextAnchor()); 379 // TODO: implement the entity for the annotation 380 381 } 382 383 /** 384 * Tests this annotation for equality with an arbitrary object. 385 * 386 * @param obj the object (<code>null</code> permitted). 387 * 388 * @return <code>true</code> or <code>false</code>. 389 */ 390 public boolean equals(Object obj) { 391 392 if (obj == null) { 393 return false; 394 } 395 if (obj == this) { 396 return true; 397 } 398 if (!(obj instanceof CategoryPointerAnnotation)) { 399 return false; 400 } 401 if (!super.equals(obj)) { 402 return false; 403 } 404 CategoryPointerAnnotation that = (CategoryPointerAnnotation) obj; 405 if (this.angle != that.angle) { 406 return false; 407 } 408 if (this.tipRadius != that.tipRadius) { 409 return false; 410 } 411 if (this.baseRadius != that.baseRadius) { 412 return false; 413 } 414 if (this.arrowLength != that.arrowLength) { 415 return false; 416 } 417 if (this.arrowWidth != that.arrowWidth) { 418 return false; 419 } 420 if (!this.arrowPaint.equals(that.arrowPaint)) { 421 return false; 422 } 423 if (!ObjectUtilities.equal(this.arrowStroke, that.arrowStroke)) { 424 return false; 425 } 426 if (this.labelOffset != that.labelOffset) { 427 return false; 428 } 429 return true; 430 } 431 432 /** 433 * Returns a clone of the annotation. 434 * 435 * @return A clone. 436 * 437 * @throws CloneNotSupportedException if the annotation can't be cloned. 438 */ 439 public Object clone() throws CloneNotSupportedException { 440 return super.clone(); 441 } 442 443 /** 444 * Provides serialization support. 445 * 446 * @param stream the output stream. 447 * 448 * @throws IOException if there is an I/O error. 449 */ 450 private void writeObject(ObjectOutputStream stream) throws IOException { 451 stream.defaultWriteObject(); 452 SerialUtilities.writePaint(this.arrowPaint, stream); 453 SerialUtilities.writeStroke(this.arrowStroke, stream); 454 } 455 456 /** 457 * Provides serialization support. 458 * 459 * @param stream the input stream. 460 * 461 * @throws IOException if there is an I/O error. 462 * @throws ClassNotFoundException if there is a classpath problem. 463 */ 464 private void readObject(ObjectInputStream stream) 465 throws IOException, ClassNotFoundException { 466 stream.defaultReadObject(); 467 this.arrowPaint = SerialUtilities.readPaint(stream); 468 this.arrowStroke = SerialUtilities.readStroke(stream); 469 } 470 471 }