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    }