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