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    }