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