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    }