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 }