001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ------------------------
028 * XYPointerAnnotation.java
029 * ------------------------
030 * (C) Copyright 2003-present, by David Gilbert.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Peter Kolb (patch 2809117);
034                     Tracy Hiltbrand (equals/hashCode comply with EqualsVerifier);
035 *
036 */
037
038package org.jfree.chart.annotations;
039
040import java.awt.BasicStroke;
041import java.awt.Color;
042import java.awt.Graphics2D;
043import java.awt.Paint;
044import java.awt.Shape;
045import java.awt.Stroke;
046import java.awt.geom.GeneralPath;
047import java.awt.geom.Line2D;
048import java.awt.geom.Rectangle2D;
049import java.io.IOException;
050import java.io.ObjectInputStream;
051import java.io.ObjectOutputStream;
052import java.io.Serializable;
053import java.util.Objects;
054
055import org.jfree.chart.HashUtils;
056import org.jfree.chart.axis.ValueAxis;
057import org.jfree.chart.event.AnnotationChangeEvent;
058import org.jfree.chart.plot.Plot;
059import org.jfree.chart.plot.PlotOrientation;
060import org.jfree.chart.plot.PlotRenderingInfo;
061import org.jfree.chart.plot.XYPlot;
062import org.jfree.chart.text.TextUtils;
063import org.jfree.chart.ui.RectangleEdge;
064import org.jfree.chart.util.Args;
065import org.jfree.chart.util.PaintUtils;
066import org.jfree.chart.util.PublicCloneable;
067import org.jfree.chart.util.SerialUtils;
068
069/**
070 * An arrow and label that can be placed on an {@link XYPlot}.  The arrow is
071 * drawn at a user-definable angle so that it points towards the (x, y)
072 * location for the annotation.
073 * <p>
074 * The arrow length (and its offset from the (x, y) location) is controlled by
075 * the tip radius and the base radius attributes.  Imagine two circles around
076 * the (x, y) coordinate: the inner circle defined by the tip radius, and the
077 * outer circle defined by the base radius.  Now, draw the arrow starting at
078 * some point on the outer circle (the point is determined by the angle), with
079 * the arrow tip being drawn at a corresponding point on the inner circle.
080 */
081public class XYPointerAnnotation extends XYTextAnnotation
082        implements Cloneable, PublicCloneable, Serializable {
083
084    /** For serialization. */
085    private static final long serialVersionUID = -4031161445009858551L;
086
087    /** The default tip radius (in Java2D units). */
088    public static final double DEFAULT_TIP_RADIUS = 10.0;
089
090    /** The default base radius (in Java2D units). */
091    public static final double DEFAULT_BASE_RADIUS = 30.0;
092
093    /** The default label offset (in Java2D units). */
094    public static final double DEFAULT_LABEL_OFFSET = 3.0;
095
096    /** The default arrow length (in Java2D units). */
097    public static final double DEFAULT_ARROW_LENGTH = 5.0;
098
099    /** The default arrow width (in Java2D units). */
100    public static final double DEFAULT_ARROW_WIDTH = 3.0;
101
102    /** The angle of the arrow's line (in radians). */
103    private double angle;
104
105    /**
106     * The radius from the (x, y) point to the tip of the arrow (in Java2D
107     * units).
108     */
109    private double tipRadius;
110
111    /**
112     * The radius from the (x, y) point to the start of the arrow line (in
113     * Java2D units).
114     */
115    private double baseRadius;
116
117    /** The length of the arrow head (in Java2D units). */
118    private double arrowLength;
119
120    /** The arrow width (in Java2D units, per side). */
121    private double arrowWidth;
122
123    /** The arrow stroke. */
124    private transient Stroke arrowStroke;
125
126    /** The arrow paint. */
127    private transient Paint arrowPaint;
128
129    /** The radius from the base point to the anchor point for the label. */
130    private double labelOffset;
131
132    /**
133     * Creates a new label and arrow annotation.
134     *
135     * @param label  the label ({@code null} permitted).
136     * @param x  the x-coordinate (measured against the chart's domain axis).
137     * @param y  the y-coordinate (measured against the chart's range axis).
138     * @param angle  the angle of the arrow's line (in radians).
139     */
140    public XYPointerAnnotation(String label, double x, double y, double angle) {
141
142        super(label, x, y);
143        Args.requireFinite(x, "x");
144        Args.requireFinite(y, "y");
145        Args.requireFinite(angle, "angle");
146        this.angle = angle;
147        this.tipRadius = DEFAULT_TIP_RADIUS;
148        this.baseRadius = DEFAULT_BASE_RADIUS;
149        this.arrowLength = DEFAULT_ARROW_LENGTH;
150        this.arrowWidth = DEFAULT_ARROW_WIDTH;
151        this.labelOffset = DEFAULT_LABEL_OFFSET;
152        this.arrowStroke = new BasicStroke(1.0f);
153        this.arrowPaint = Color.BLACK;
154
155    }
156
157    /**
158     * Returns the angle of the arrow.
159     *
160     * @return The angle (in radians).
161     *
162     * @see #setAngle(double)
163     */
164    public double getAngle() {
165        return this.angle;
166    }
167
168    /**
169     * Sets the angle of the arrow and sends an
170     * {@link AnnotationChangeEvent} to all registered listeners.
171     *
172     * @param angle  the angle (in radians).
173     *
174     * @see #getAngle()
175     */
176    public void setAngle(double angle) {
177        this.angle = angle;
178        fireAnnotationChanged();
179    }
180
181    /**
182     * Returns the tip radius.
183     *
184     * @return The tip radius (in Java2D units).
185     *
186     * @see #setTipRadius(double)
187     */
188    public double getTipRadius() {
189        return this.tipRadius;
190    }
191
192    /**
193     * Sets the tip radius and sends an
194     * {@link AnnotationChangeEvent} to all registered listeners.
195     *
196     * @param radius  the radius (in Java2D units).
197     *
198     * @see #getTipRadius()
199     */
200    public void setTipRadius(double radius) {
201        this.tipRadius = radius;
202        fireAnnotationChanged();
203    }
204
205    /**
206     * Returns the base radius.
207     *
208     * @return The base radius (in Java2D units).
209     *
210     * @see #setBaseRadius(double)
211     */
212    public double getBaseRadius() {
213        return this.baseRadius;
214    }
215
216    /**
217     * Sets the base radius and sends an
218     * {@link AnnotationChangeEvent} to all registered listeners.
219     *
220     * @param radius  the radius (in Java2D units).
221     *
222     * @see #getBaseRadius()
223     */
224    public void setBaseRadius(double radius) {
225        this.baseRadius = radius;
226        fireAnnotationChanged();
227    }
228
229    /**
230     * Returns the label offset.
231     *
232     * @return The label offset (in Java2D units).
233     *
234     * @see #setLabelOffset(double)
235     */
236    public double getLabelOffset() {
237        return this.labelOffset;
238    }
239
240    /**
241     * Sets the label offset (from the arrow base, continuing in a straight
242     * line, in Java2D units) and sends an
243     * {@link AnnotationChangeEvent} to all registered listeners.
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        fireAnnotationChanged();
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 and sends an
267     * {@link AnnotationChangeEvent} to all registered listeners.
268     *
269     * @param length  the length.
270     *
271     * @see #getArrowLength()
272     */
273    public void setArrowLength(double length) {
274        this.arrowLength = length;
275        fireAnnotationChanged();
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 and sends an
291     * {@link AnnotationChangeEvent} to all registered listeners.
292     *
293     * @param width  the width (in Java2D units).
294     *
295     * @see #getArrowWidth()
296     */
297    public void setArrowWidth(double width) {
298        this.arrowWidth = width;
299        fireAnnotationChanged();
300    }
301
302    /**
303     * Returns the stroke used to draw the arrow line.
304     *
305     * @return The arrow stroke (never {@code null}).
306     *
307     * @see #setArrowStroke(Stroke)
308     */
309    public Stroke getArrowStroke() {
310        return this.arrowStroke;
311    }
312
313    /**
314     * Sets the stroke used to draw the arrow line and sends an
315     * {@link AnnotationChangeEvent} to all registered listeners.
316     *
317     * @param stroke  the stroke ({@code null} not permitted).
318     *
319     * @see #getArrowStroke()
320     */
321    public void setArrowStroke(Stroke stroke) {
322        Args.nullNotPermitted(stroke, "stroke");
323        this.arrowStroke = stroke;
324        fireAnnotationChanged();
325    }
326
327    /**
328     * Returns the paint used for the arrow.
329     *
330     * @return The arrow paint (never {@code null}).
331     *
332     * @see #setArrowPaint(Paint)
333     */
334    public Paint getArrowPaint() {
335        return this.arrowPaint;
336    }
337
338    /**
339     * Sets the paint used for the arrow and sends an
340     * {@link AnnotationChangeEvent} to all registered listeners.
341     *
342     * @param paint  the arrow paint ({@code null} not permitted).
343     *
344     * @see #getArrowPaint()
345     */
346    public void setArrowPaint(Paint paint) {
347        Args.nullNotPermitted(paint, "paint");
348        this.arrowPaint = paint;
349        fireAnnotationChanged();
350    }
351
352    /**
353     * Draws the annotation.
354     *
355     * @param g2  the graphics device.
356     * @param plot  the plot.
357     * @param dataArea  the data area.
358     * @param domainAxis  the domain axis.
359     * @param rangeAxis  the range axis.
360     * @param rendererIndex  the renderer index.
361     * @param info  the plot rendering info.
362     */
363    @Override
364    public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea,
365            ValueAxis domainAxis, ValueAxis rangeAxis, int rendererIndex, 
366            PlotRenderingInfo info) {
367
368        PlotOrientation orientation = plot.getOrientation();
369        RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
370                plot.getDomainAxisLocation(), orientation);
371        RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
372                plot.getRangeAxisLocation(), orientation);
373        double j2DX = domainAxis.valueToJava2D(getX(), dataArea, domainEdge);
374        double j2DY = rangeAxis.valueToJava2D(getY(), dataArea, rangeEdge);
375        if (orientation == PlotOrientation.HORIZONTAL) {
376            double temp = j2DX;
377            j2DX = j2DY;
378            j2DY = temp;
379        }
380        double startX = j2DX + Math.cos(this.angle) * this.baseRadius;
381        double startY = j2DY + Math.sin(this.angle) * this.baseRadius;
382
383        double endX = j2DX + Math.cos(this.angle) * this.tipRadius;
384        double endY = j2DY + Math.sin(this.angle) * this.tipRadius;
385
386        double arrowBaseX = endX + Math.cos(this.angle) * this.arrowLength;
387        double arrowBaseY = endY + Math.sin(this.angle) * this.arrowLength;
388
389        double arrowLeftX = arrowBaseX
390                + Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
391        double arrowLeftY = arrowBaseY
392                + Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
393
394        double arrowRightX = arrowBaseX
395                - Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
396        double arrowRightY = arrowBaseY
397                - Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
398
399        GeneralPath arrow = new GeneralPath();
400        arrow.moveTo((float) endX, (float) endY);
401        arrow.lineTo((float) arrowLeftX, (float) arrowLeftY);
402        arrow.lineTo((float) arrowRightX, (float) arrowRightY);
403        arrow.closePath();
404
405        g2.setStroke(this.arrowStroke);
406        g2.setPaint(this.arrowPaint);
407        Line2D line = new Line2D.Double(startX, startY, arrowBaseX, arrowBaseY);
408        g2.draw(line);
409        g2.fill(arrow);
410
411        // draw the label
412        double labelX = j2DX + Math.cos(this.angle) * (this.baseRadius
413                + this.labelOffset);
414        double labelY = j2DY + Math.sin(this.angle) * (this.baseRadius
415                + this.labelOffset);
416        g2.setFont(getFont());
417        Shape hotspot = TextUtils.calculateRotatedStringBounds(
418                getText(), g2, (float) labelX, (float) labelY, getTextAnchor(),
419                getRotationAngle(), getRotationAnchor());
420        if (getBackgroundPaint() != null) {
421            g2.setPaint(getBackgroundPaint());
422            g2.fill(hotspot);
423        }
424        g2.setPaint(getPaint());
425        TextUtils.drawRotatedString(getText(), g2, (float) labelX,
426                (float) labelY, getTextAnchor(), getRotationAngle(),
427                getRotationAnchor());
428        if (isOutlineVisible()) {
429            g2.setStroke(getOutlineStroke());
430            g2.setPaint(getOutlinePaint());
431            g2.draw(hotspot);
432        }
433
434        String toolTip = getToolTipText();
435        String url = getURL();
436        if (toolTip != null || url != null) {
437            addEntity(info, hotspot, rendererIndex, toolTip, url);
438        }
439
440    }
441
442    /**
443     * Tests this annotation for equality with an arbitrary object.
444     *
445     * @param obj  the object ({@code null} permitted).
446     *
447     * @return {@code true} or {@code false}.
448     */
449    @Override
450    public boolean equals(Object obj) {
451        if (obj == this) {
452            return true;
453        }
454        if (!(obj instanceof XYPointerAnnotation)) {
455            return false;
456        }
457        XYPointerAnnotation that = (XYPointerAnnotation) obj;
458        if (Double.doubleToLongBits(this.angle) != 
459            Double.doubleToLongBits(that.angle)) {
460            return false;
461        }
462        if (Double.doubleToLongBits(this.tipRadius) !=
463            Double.doubleToLongBits(that.tipRadius)) {
464            return false;
465        }
466        if (Double.doubleToLongBits(this.baseRadius) != 
467            Double.doubleToLongBits(that.baseRadius)) {
468            return false;
469        }
470        if (Double.doubleToLongBits(this.arrowLength) != 
471            Double.doubleToLongBits(that.arrowLength)) {
472            return false;
473        }
474        if (Double.doubleToLongBits(this.arrowWidth) != 
475            Double.doubleToLongBits(that.arrowWidth)) {
476            return false;
477        }
478        if (!PaintUtils.equal(this.arrowPaint, that.arrowPaint)) {
479            return false;
480        }
481        if (!Objects.equals(this.arrowStroke, that.arrowStroke)) {
482            return false;
483        }
484        if (Double.doubleToLongBits(this.labelOffset) != 
485            Double.doubleToLongBits(that.labelOffset)) {
486            return false;
487        }
488
489        // fix the "equals not symmetric" problem
490        if (!that.canEqual(this)) {
491            return false;
492        }
493
494        return super.equals(obj);
495    }
496
497    /**
498     * Ensures symmetry between super/subclass implementations of equals. For
499     * more detail, see http://jqno.nl/equalsverifier/manual/inheritance.
500     *
501     * @param other Object
502     * 
503     * @return true ONLY if the parameter is THIS class type
504     */
505    @Override
506    public boolean canEqual(Object other) {
507        // fix the "equals not symmetric" problem
508        return (other instanceof XYPointerAnnotation);
509    }
510
511    /**
512     * Returns a hash code for this instance.
513     *
514     * @return A hash code.
515     */
516    @Override
517    public int hashCode() {
518        int hash = super.hashCode(); // equals calls superclass, hashCode must also
519        hash = 41 * hash + (int) (Double.doubleToLongBits(this.angle) ^ 
520                                 (Double.doubleToLongBits(this.angle) >>> 32));
521        hash = 41 * hash + (int) (Double.doubleToLongBits(this.tipRadius) ^ 
522                                 (Double.doubleToLongBits(this.tipRadius) >>> 32));
523        hash = 41 * hash + (int) (Double.doubleToLongBits(this.baseRadius) ^ 
524                                 (Double.doubleToLongBits(this.baseRadius) >>> 32));
525        hash = 41 * hash + (int) (Double.doubleToLongBits(this.arrowLength) ^ 
526                                 (Double.doubleToLongBits(this.arrowLength) >>> 32));
527        hash = 41 * hash + (int) (Double.doubleToLongBits(this.arrowWidth) ^ 
528                                 (Double.doubleToLongBits(this.arrowWidth) >>> 32));
529        hash = 41 * hash + HashUtils.hashCodeForPaint(this.arrowPaint);
530        hash = 41 * hash + Objects.hashCode(this.arrowStroke);
531        hash = 41 * hash + (int) (Double.doubleToLongBits(this.labelOffset) ^ 
532                                 (Double.doubleToLongBits(this.labelOffset) >>> 32));
533        return hash;
534    }
535
536    /**
537     * Returns a clone of the annotation.
538     *
539     * @return A clone.
540     *
541     * @throws CloneNotSupportedException  if the annotation can't be cloned.
542     */
543    @Override
544    public Object clone() throws CloneNotSupportedException {
545        return super.clone();
546    }
547
548    /**
549     * Provides serialization support.
550     *
551     * @param stream  the output stream.
552     *
553     * @throws IOException if there is an I/O error.
554     */
555    private void writeObject(ObjectOutputStream stream) throws IOException {
556        stream.defaultWriteObject();
557        SerialUtils.writePaint(this.arrowPaint, stream);
558        SerialUtils.writeStroke(this.arrowStroke, stream);
559    }
560
561    /**
562     * Provides serialization support.
563     *
564     * @param stream  the input stream.
565     *
566     * @throws IOException  if there is an I/O error.
567     * @throws ClassNotFoundException  if there is a classpath problem.
568     */
569    private void readObject(ObjectInputStream stream)
570        throws IOException, ClassNotFoundException {
571        stream.defaultReadObject();
572        this.arrowPaint = SerialUtils.readPaint(stream);
573        this.arrowStroke = SerialUtils.readStroke(stream);
574    }
575
576}