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