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 * XYNoteAnnotation.java
029 * ---------------------
030 * (C) Copyright 2003-present, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert (as XYPointerAnnotation.java);
033 * Contributor(s):   Yuri Blankenstein (copy to XYNoteAnnotation.java; for ESI TNO);
034 *
035 */
036package org.jfree.chart.annotations;
037
038import java.awt.BasicStroke;
039import java.awt.Color;
040import java.awt.Graphics2D;
041import java.awt.Paint;
042import java.awt.Shape;
043import java.awt.Stroke;
044import java.awt.geom.Line2D;
045import java.awt.geom.Rectangle2D;
046import java.io.IOException;
047import java.io.ObjectInputStream;
048import java.io.ObjectOutputStream;
049import java.io.Serializable;
050import java.util.Objects;
051
052import org.jfree.chart.HashUtils;
053import org.jfree.chart.axis.ValueAxis;
054import org.jfree.chart.event.AnnotationChangeEvent;
055import org.jfree.chart.plot.Plot;
056import org.jfree.chart.plot.PlotOrientation;
057import org.jfree.chart.plot.PlotRenderingInfo;
058import org.jfree.chart.plot.XYPlot;
059import org.jfree.chart.text.TextUtils;
060import org.jfree.chart.ui.RectangleEdge;
061import org.jfree.chart.util.Args;
062import org.jfree.chart.util.PublicCloneable;
063import org.jfree.chart.util.SerialUtils;
064
065/**
066 * An line and label that can be placed on an {@link XYPlot}.  The line is
067 * drawn at a user-definable angle so that it points towards the (x, y)
068 * location for the annotation.
069 * <p>
070 * The line length (and its offset from the (x, y) location) is controlled by
071 * the tip radius and the base radius attributes.  Imagine two circles around
072 * the (x, y) coordinate: the inner circle defined by the tip radius, and the
073 * outer circle defined by the base radius.  Now, draw the line starting at
074 * some point on the outer circle (the point is determined by the angle), with
075 * the line tip being drawn at a corresponding point on the inner circle.
076 */
077public class XYNoteAnnotation extends XYTextAnnotation
078        implements Cloneable, PublicCloneable, Serializable {
079
080    /** For serialization. */
081    private static final long serialVersionUID = -4031161445009858551L;
082
083    /** The default tip radius (in Java2D units). */
084    public static final double DEFAULT_TIP_RADIUS = 0.0;
085
086    /** The default base radius (in Java2D units). */
087    public static final double DEFAULT_BASE_RADIUS = 30.0;
088
089    /** The default label offset (in Java2D units). */
090    public static final double DEFAULT_LABEL_OFFSET = 3.0;
091    
092    /** The default line stroke. */
093    public static final Stroke DEFAULT_LINE_STROKE = new BasicStroke(0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 2.0f, new float[] {2.0f}, 0.0f);
094
095    /** The default line stroke. */
096    public static final Paint DEFAULT_BACKGROUND_PAINT = new Color(255, 255, 203);
097
098    /** The default line stroke. */
099    public static final Paint DEFAULT_OUTLINE_PAINT = new Color(255, 204, 102);
100
101    /** The angle of the line's line (in radians). */
102    private double angle;
103
104    /**
105     * The radius from the (x, y) point to the tip of the line (in Java2D
106     * units).
107     */
108    private double tipRadius;
109
110    /**
111     * The radius from the (x, y) point to the start of the line line (in
112     * Java2D units).
113     */
114    private double baseRadius;
115
116    /** The line stroke. */
117    private transient Stroke lineStroke;
118
119    /** The line paint. */
120    private transient Paint linePaint;
121
122    /** The radius from the base point to the anchor point for the label. */
123    private double labelOffset;
124
125    /**
126     * Creates a new label and line annotation.
127     *
128     * @param label  the label ({@code null} permitted).
129     * @param x  the x-coordinate (measured against the chart's domain axis).
130     * @param y  the y-coordinate (measured against the chart's range axis).
131     * @param angle  the angle of the line's line (in radians).
132     */
133    public XYNoteAnnotation(String label, double x, double y, double angle) {
134        super(label, x, y);
135        this.angle = angle;
136        this.tipRadius = DEFAULT_TIP_RADIUS;
137        this.baseRadius = DEFAULT_BASE_RADIUS;
138        this.labelOffset = DEFAULT_LABEL_OFFSET;
139        this.lineStroke = DEFAULT_LINE_STROKE;
140        this.linePaint = Color.BLACK;
141
142        setBackgroundPaint(DEFAULT_BACKGROUND_PAINT);
143        setOutlineVisible(true);
144        setOutlinePaint(DEFAULT_OUTLINE_PAINT);
145        setOutlineStroke(new BasicStroke(1.0f));
146    }
147
148    /**
149     * Returns the angle of the line.
150     *
151     * @return The angle (in radians).
152     *
153     * @see #setAngle(double)
154     */
155    public double getAngle() {
156        return this.angle;
157    }
158
159    /**
160     * Sets the angle of the line and sends an
161     * {@link AnnotationChangeEvent} to all registered listeners.
162     *
163     * @param angle  the angle (in radians).
164     *
165     * @see #getAngle()
166     */
167    public void setAngle(double angle) {
168        this.angle = angle;
169        fireAnnotationChanged();
170    }
171
172    /**
173     * Returns the tip radius.
174     *
175     * @return The tip radius (in Java2D units).
176     *
177     * @see #setTipRadius(double)
178     */
179    public double getTipRadius() {
180        return this.tipRadius;
181    }
182
183    /**
184     * Sets the tip radius and sends an
185     * {@link AnnotationChangeEvent} to all registered listeners.
186     *
187     * @param radius  the radius (in Java2D units).
188     *
189     * @see #getTipRadius()
190     */
191    public void setTipRadius(double radius) {
192        this.tipRadius = radius;
193        fireAnnotationChanged();
194    }
195
196    /**
197     * Returns the base radius.
198     *
199     * @return The base radius (in Java2D units).
200     *
201     * @see #setBaseRadius(double)
202     */
203    public double getBaseRadius() {
204        return this.baseRadius;
205    }
206
207    /**
208     * Sets the base radius and sends an
209     * {@link AnnotationChangeEvent} to all registered listeners.
210     *
211     * @param radius  the radius (in Java2D units).
212     *
213     * @see #getBaseRadius()
214     */
215    public void setBaseRadius(double radius) {
216        this.baseRadius = radius;
217        fireAnnotationChanged();
218    }
219
220    /**
221     * Returns the label offset.
222     *
223     * @return The label offset (in Java2D units).
224     *
225     * @see #setLabelOffset(double)
226     */
227    public double getLabelOffset() {
228        return this.labelOffset;
229    }
230
231    /**
232     * Sets the label offset (from the line base, continuing in a straight
233     * line, in Java2D units) and sends an
234     * {@link AnnotationChangeEvent} to all registered listeners.
235     *
236     * @param offset  the offset (in Java2D units).
237     *
238     * @see #getLabelOffset()
239     */
240    public void setLabelOffset(double offset) {
241        this.labelOffset = offset;
242        fireAnnotationChanged();
243    }
244
245    /**
246     * Returns the stroke used to draw the line line.
247     *
248     * @return The line stroke (never {@code null}).
249     *
250     * @see #setLineStroke(Stroke)
251     */
252    public Stroke getLineStroke() {
253        return this.lineStroke;
254    }
255
256    /**
257     * Sets the stroke used to draw the line line and sends an
258     * {@link AnnotationChangeEvent} to all registered listeners.
259     *
260     * @param stroke  the stroke ({@code null} not permitted).
261     *
262     * @see #getLineStroke()
263     */
264    public void setLineStroke(Stroke stroke) {
265        Args.nullNotPermitted(stroke, "stroke");
266        this.lineStroke = stroke;
267        fireAnnotationChanged();
268    }
269
270    /**
271     * Returns the paint used for the line.
272     *
273     * @return The line paint (never {@code null}).
274     *
275     * @see #setLinePaint(Paint)
276     */
277    public Paint getLinePaint() {
278        return this.linePaint;
279    }
280
281    /**
282     * Sets the paint used for the line and sends an
283     * {@link AnnotationChangeEvent} to all registered listeners.
284     *
285     * @param paint  the line paint ({@code null} not permitted).
286     *
287     * @see #getLinePaint()
288     */
289    public void setLinePaint(Paint paint) {
290        Args.nullNotPermitted(paint, "paint");
291        this.linePaint = paint;
292        fireAnnotationChanged();
293    }
294
295    /**
296     * Draws the annotation.
297     *
298     * @param g2  the graphics device.
299     * @param plot  the plot.
300     * @param dataArea  the data area.
301     * @param domainAxis  the domain axis.
302     * @param rangeAxis  the range axis.
303     * @param rendererIndex  the renderer index.
304     * @param info  the plot rendering info.
305     */
306    @Override
307    public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea,
308            ValueAxis domainAxis, ValueAxis rangeAxis, int rendererIndex, 
309            PlotRenderingInfo info) {
310
311        PlotOrientation orientation = plot.getOrientation();
312        RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
313                plot.getDomainAxisLocation(), orientation);
314        RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
315                plot.getRangeAxisLocation(), orientation);
316        double j2DX = domainAxis.valueToJava2D(getX(), dataArea, domainEdge);
317        double j2DY = rangeAxis.valueToJava2D(getY(), dataArea, rangeEdge);
318        if (orientation == PlotOrientation.HORIZONTAL) {
319            double temp = j2DX;
320            j2DX = j2DY;
321            j2DY = temp;
322        }
323        double startX = j2DX + Math.cos(this.angle) * this.baseRadius;
324        double startY = j2DY + Math.sin(this.angle) * this.baseRadius;
325
326        double endX = j2DX + Math.cos(this.angle) * this.tipRadius;
327        double endY = j2DY + Math.sin(this.angle) * this.tipRadius;
328
329        g2.setStroke(this.lineStroke);
330        g2.setPaint(this.linePaint);
331        Line2D line = new Line2D.Double(startX, startY, endX, endY);
332        g2.draw(line);
333
334        // draw the label
335        double labelX = j2DX + Math.cos(this.angle) * (this.baseRadius
336                + this.labelOffset);
337        double labelY = j2DY + Math.sin(this.angle) * (this.baseRadius
338                + this.labelOffset);
339        g2.setFont(getFont());
340        Shape hotspot = TextUtils.calculateRotatedStringBounds(
341                getText(), g2, (float) labelX, (float) labelY, getTextAnchor(),
342                getRotationAngle(), getRotationAnchor());
343        if (getBackgroundPaint() != null) {
344            g2.setPaint(getBackgroundPaint());
345            g2.fill(hotspot);
346        }
347        g2.setPaint(getPaint());
348        TextUtils.drawRotatedString(getText(), g2, (float) labelX,
349                (float) labelY, getTextAnchor(), getRotationAngle(),
350                getRotationAnchor());
351        if (isOutlineVisible()) {
352            g2.setStroke(getOutlineStroke());
353            g2.setPaint(getOutlinePaint());
354            g2.draw(hotspot);
355        }
356
357        String toolTip = getToolTipText();
358        String url = getURL();
359        if (toolTip != null || url != null) {
360            addEntity(info, hotspot, rendererIndex, toolTip, url);
361        }
362
363    }
364
365    /**
366     * Tests this annotation for equality with an arbitrary object.
367     *
368     * @param obj  the object ({@code null} permitted).
369     *
370     * @return {@code true} or {@code false}.
371     */
372    @Override
373    public boolean equals(Object obj) {
374        if (obj == this) {
375            return true;
376        }
377        if (!(obj instanceof XYNoteAnnotation)) {
378            return false;
379        }
380        XYNoteAnnotation that = (XYNoteAnnotation) obj;
381        if (this.angle != that.angle) {
382            return false;
383        }
384        if (this.tipRadius != that.tipRadius) {
385            return false;
386        }
387        if (this.baseRadius != that.baseRadius) {
388            return false;
389        }
390        if (!this.linePaint.equals(that.linePaint)) {
391            return false;
392        }
393        if (!Objects.equals(this.lineStroke, that.lineStroke)) {
394            return false;
395        }
396        if (this.labelOffset != that.labelOffset) {
397            return false;
398        }
399        return super.equals(obj);
400    }
401
402    /**
403     * Returns a hash code for this instance.
404     *
405     * @return A hash code.
406     */
407    @Override
408    public int hashCode() {
409        int result = super.hashCode();
410        long temp = Double.doubleToLongBits(this.angle);
411        result = 37 * result + (int) (temp ^ (temp >>> 32));
412        temp = Double.doubleToLongBits(this.tipRadius);
413        result = 37 * result + (int) (temp ^ (temp >>> 32));
414        temp = Double.doubleToLongBits(this.baseRadius);
415        result = 37 * result + (int) (temp ^ (temp >>> 32));
416        result = result * 37 + HashUtils.hashCodeForPaint(this.linePaint);
417        result = result * 37 + this.lineStroke.hashCode();
418        temp = Double.doubleToLongBits(this.labelOffset);
419        result = 37 * result + (int) (temp ^ (temp >>> 32));
420        return result;
421    }
422
423    /**
424     * Returns a clone of the annotation.
425     *
426     * @return A clone.
427     *
428     * @throws CloneNotSupportedException  if the annotation can't be cloned.
429     */
430    @Override
431    public Object clone() throws CloneNotSupportedException {
432        return super.clone();
433    }
434
435    /**
436     * Provides serialization support.
437     *
438     * @param stream  the output stream.
439     *
440     * @throws IOException if there is an I/O error.
441     */
442    private void writeObject(ObjectOutputStream stream) throws IOException {
443        stream.defaultWriteObject();
444        SerialUtils.writePaint(this.linePaint, stream);
445        SerialUtils.writeStroke(this.lineStroke, stream);
446    }
447
448    /**
449     * Provides serialization support.
450     *
451     * @param stream  the input stream.
452     *
453     * @throws IOException  if there is an I/O error.
454     * @throws ClassNotFoundException  if there is a classpath problem.
455     */
456    private void readObject(ObjectInputStream stream)
457        throws IOException, ClassNotFoundException {
458        stream.defaultReadObject();
459        this.linePaint = SerialUtils.readPaint(stream);
460        this.lineStroke = SerialUtils.readStroke(stream);
461    }
462}