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