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
051package org.jfree.chart.annotations;
052
053import java.awt.BasicStroke;
054import java.awt.Color;
055import java.awt.Graphics2D;
056import java.awt.Paint;
057import java.awt.Stroke;
058import java.awt.geom.GeneralPath;
059import java.awt.geom.Line2D;
060import java.awt.geom.Rectangle2D;
061import java.io.IOException;
062import java.io.ObjectInputStream;
063import java.io.ObjectOutputStream;
064import java.io.Serializable;
065
066import org.jfree.chart.HashUtilities;
067import org.jfree.chart.axis.ValueAxis;
068import org.jfree.chart.plot.Plot;
069import org.jfree.chart.plot.PlotOrientation;
070import org.jfree.chart.plot.PlotRenderingInfo;
071import org.jfree.chart.plot.XYPlot;
072import org.jfree.io.SerialUtilities;
073import org.jfree.text.TextUtilities;
074import org.jfree.ui.RectangleEdge;
075import org.jfree.util.ObjectUtilities;
076import 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 */
091public 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}