001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2009, 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-2009, 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 * 12-Feb-2009 : Added support for rotated label, plus background and
049 * outline (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.Shape;
060 import java.awt.Stroke;
061 import java.awt.geom.GeneralPath;
062 import java.awt.geom.Line2D;
063 import java.awt.geom.Rectangle2D;
064 import java.io.IOException;
065 import java.io.ObjectInputStream;
066 import java.io.ObjectOutputStream;
067 import java.io.Serializable;
068
069 import org.jfree.chart.HashUtilities;
070 import org.jfree.chart.axis.ValueAxis;
071 import org.jfree.chart.plot.Plot;
072 import org.jfree.chart.plot.PlotOrientation;
073 import org.jfree.chart.plot.PlotRenderingInfo;
074 import org.jfree.chart.plot.XYPlot;
075 import org.jfree.io.SerialUtilities;
076 import org.jfree.text.TextUtilities;
077 import org.jfree.ui.RectangleEdge;
078 import org.jfree.util.ObjectUtilities;
079 import org.jfree.util.PublicCloneable;
080
081 /**
082 * An arrow and label that can be placed on an {@link XYPlot}. The arrow is
083 * drawn at a user-definable angle so that it points towards the (x, y)
084 * location for the annotation.
085 * <p>
086 * The arrow length (and its offset from the (x, y) location) is controlled by
087 * the tip radius and the base radius attributes. Imagine two circles around
088 * the (x, y) coordinate: the inner circle defined by the tip radius, and the
089 * outer circle defined by the base radius. Now, draw the arrow starting at
090 * some point on the outer circle (the point is determined by the angle), with
091 * the arrow tip being drawn at a corresponding point on the inner circle.
092 *
093 */
094 public class XYPointerAnnotation extends XYTextAnnotation
095 implements Cloneable, PublicCloneable, 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 double labelX = j2DX + Math.cos(this.angle) * (this.baseRadius
411 + this.labelOffset);
412 double labelY = j2DY + Math.sin(this.angle) * (this.baseRadius
413 + this.labelOffset);
414 g2.setFont(getFont());
415 Shape hotspot = TextUtilities.calculateRotatedStringBounds(
416 getText(), g2, (float) labelX, (float) labelY, getTextAnchor(),
417 getRotationAngle(), getRotationAnchor());
418 if (getBackgroundPaint() != null) {
419 g2.setPaint(getBackgroundPaint());
420 g2.fill(hotspot);
421 }
422 g2.setPaint(getPaint());
423 TextUtilities.drawRotatedString(getText(), g2, (float) labelX,
424 (float) labelY, getTextAnchor(), getRotationAngle(),
425 getRotationAnchor());
426 if (isOutlineVisible()) {
427 g2.setStroke(getOutlineStroke());
428 g2.setPaint(getOutlinePaint());
429 g2.draw(hotspot);
430 }
431
432 String toolTip = getToolTipText();
433 String url = getURL();
434 if (toolTip != null || url != null) {
435 addEntity(info, hotspot, rendererIndex, toolTip, url);
436 }
437
438 }
439
440 /**
441 * Tests this annotation for equality with an arbitrary object.
442 *
443 * @param obj the object (<code>null</code> permitted).
444 *
445 * @return <code>true</code> or <code>false</code>.
446 */
447 public boolean equals(Object obj) {
448 if (obj == this) {
449 return true;
450 }
451 if (!(obj instanceof XYPointerAnnotation)) {
452 return false;
453 }
454 XYPointerAnnotation that = (XYPointerAnnotation) obj;
455 if (this.angle != that.angle) {
456 return false;
457 }
458 if (this.tipRadius != that.tipRadius) {
459 return false;
460 }
461 if (this.baseRadius != that.baseRadius) {
462 return false;
463 }
464 if (this.arrowLength != that.arrowLength) {
465 return false;
466 }
467 if (this.arrowWidth != that.arrowWidth) {
468 return false;
469 }
470 if (!this.arrowPaint.equals(that.arrowPaint)) {
471 return false;
472 }
473 if (!ObjectUtilities.equal(this.arrowStroke, that.arrowStroke)) {
474 return false;
475 }
476 if (this.labelOffset != that.labelOffset) {
477 return false;
478 }
479 return super.equals(obj);
480 }
481
482 /**
483 * Returns a hash code for this instance.
484 *
485 * @return A hash code.
486 */
487 public int hashCode() {
488 int result = super.hashCode();
489 long temp = Double.doubleToLongBits(this.angle);
490 result = 37 * result + (int) (temp ^ (temp >>> 32));
491 temp = Double.doubleToLongBits(this.tipRadius);
492 result = 37 * result + (int) (temp ^ (temp >>> 32));
493 temp = Double.doubleToLongBits(this.baseRadius);
494 result = 37 * result + (int) (temp ^ (temp >>> 32));
495 temp = Double.doubleToLongBits(this.arrowLength);
496 result = 37 * result + (int) (temp ^ (temp >>> 32));
497 temp = Double.doubleToLongBits(this.arrowWidth);
498 result = 37 * result + (int) (temp ^ (temp >>> 32));
499 result = result * 37 + HashUtilities.hashCodeForPaint(this.arrowPaint);
500 result = result * 37 + this.arrowStroke.hashCode();
501 temp = Double.doubleToLongBits(this.labelOffset);
502 result = 37 * result + (int) (temp ^ (temp >>> 32));
503 return super.hashCode();
504 }
505
506 /**
507 * Returns a clone of the annotation.
508 *
509 * @return A clone.
510 *
511 * @throws CloneNotSupportedException if the annotation can't be cloned.
512 */
513 public Object clone() throws CloneNotSupportedException {
514 return super.clone();
515 }
516
517 /**
518 * Provides serialization support.
519 *
520 * @param stream the output stream.
521 *
522 * @throws IOException if there is an I/O error.
523 */
524 private void writeObject(ObjectOutputStream stream) throws IOException {
525 stream.defaultWriteObject();
526 SerialUtilities.writePaint(this.arrowPaint, stream);
527 SerialUtilities.writeStroke(this.arrowStroke, stream);
528 }
529
530 /**
531 * Provides serialization support.
532 *
533 * @param stream the input stream.
534 *
535 * @throws IOException if there is an I/O error.
536 * @throws ClassNotFoundException if there is a classpath problem.
537 */
538 private void readObject(ObjectInputStream stream)
539 throws IOException, ClassNotFoundException {
540 stream.defaultReadObject();
541 this.arrowPaint = SerialUtilities.readPaint(stream);
542 this.arrowStroke = SerialUtilities.readStroke(stream);
543 }
544
545 }