001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2013, 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 * [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 * CategoryPointerAnnotation.java
029 * ------------------------------
030 * (C) Copyright 2006-2013, by Object Refinery Limited.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   Peter Kolb (patch 2809117);
034 *
035 * Changes:
036 * --------
037 * 02-Oct-2006 : Version 1 (DG);
038 * 06-Mar-2007 : Implemented hashCode() (DG);
039 * 24-Jun-2009 : Fire change events (see patch 2809117 by PK) (DG);
040 * 30-Mar-2010 : Correct calculation of pointer line (see patch 2954302) (DG);
041 * 02-Jul-2013 : Use ParamChecks (DG);
042 *
043 */
044
045package org.jfree.chart.annotations;
046
047import java.awt.BasicStroke;
048import java.awt.Color;
049import java.awt.Graphics2D;
050import java.awt.Paint;
051import java.awt.Stroke;
052import java.awt.geom.GeneralPath;
053import java.awt.geom.Line2D;
054import java.awt.geom.Rectangle2D;
055import java.io.IOException;
056import java.io.ObjectInputStream;
057import java.io.ObjectOutputStream;
058import java.io.Serializable;
059
060import org.jfree.chart.HashUtilities;
061import org.jfree.chart.axis.CategoryAxis;
062import org.jfree.chart.axis.ValueAxis;
063import org.jfree.chart.event.AnnotationChangeEvent;
064import org.jfree.chart.plot.CategoryPlot;
065import org.jfree.chart.plot.Plot;
066import org.jfree.chart.plot.PlotOrientation;
067import org.jfree.chart.util.ParamChecks;
068import org.jfree.data.category.CategoryDataset;
069import org.jfree.io.SerialUtilities;
070import org.jfree.text.TextUtilities;
071import org.jfree.ui.RectangleEdge;
072import org.jfree.util.ObjectUtilities;
073import org.jfree.util.PublicCloneable;
074
075/**
076 * An arrow and label that can be placed on a {@link CategoryPlot}.  The arrow
077 * is drawn at a user-definable angle so that it points towards the (category,
078 * value) location for the annotation.
079 * <p>
080 * The arrow length (and its offset from the (category, value) location) is
081 * controlled by the tip radius and the base radius attributes.  Imagine two
082 * circles around the (category, value) coordinate: the inner circle defined by
083 * the tip radius, and the outer circle defined by the base radius.  Now, draw
084 * the arrow starting at some point on the outer circle (the point is
085 * determined by the angle), with the arrow tip being drawn at a corresponding
086 * point on the inner circle.
087 *
088 * @since 1.0.3
089 */
090public class CategoryPointerAnnotation extends CategoryTextAnnotation
091        implements Cloneable, PublicCloneable, Serializable {
092
093    /** For serialization. */
094    private static final long serialVersionUID = -4031161445009858551L;
095
096    /** The default tip radius (in Java2D units). */
097    public static final double DEFAULT_TIP_RADIUS = 10.0;
098
099    /** The default base radius (in Java2D units). */
100    public static final double DEFAULT_BASE_RADIUS = 30.0;
101
102    /** The default label offset (in Java2D units). */
103    public static final double DEFAULT_LABEL_OFFSET = 3.0;
104
105    /** The default arrow length (in Java2D units). */
106    public static final double DEFAULT_ARROW_LENGTH = 5.0;
107
108    /** The default arrow width (in Java2D units). */
109    public static final double DEFAULT_ARROW_WIDTH = 3.0;
110
111    /** The angle of the arrow's line (in radians). */
112    private double angle;
113
114    /**
115     * The radius from the (x, y) point to the tip of the arrow (in Java2D
116     * units).
117     */
118    private double tipRadius;
119
120    /**
121     * The radius from the (x, y) point to the start of the arrow line (in
122     * Java2D units).
123     */
124    private double baseRadius;
125
126    /** The length of the arrow head (in Java2D units). */
127    private double arrowLength;
128
129    /** The arrow width (in Java2D units, per side). */
130    private double arrowWidth;
131
132    /** The arrow stroke. */
133    private transient Stroke arrowStroke;
134
135    /** The arrow paint. */
136    private transient Paint arrowPaint;
137
138    /** The radius from the base point to the anchor point for the label. */
139    private double labelOffset;
140
141    /**
142     * Creates a new label and arrow annotation.
143     *
144     * @param label  the label (<code>null</code> permitted).
145     * @param key  the category key.
146     * @param value  the y-value (measured against the chart's range axis).
147     * @param angle  the angle of the arrow's line (in radians).
148     */
149    public CategoryPointerAnnotation(String label, Comparable key, double value,
150            double angle) {
151
152        super(label, key, value);
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 and sends an
177     * {@link AnnotationChangeEvent} to all registered listeners.
178     *
179     * @param angle  the angle (in radians).
180     *
181     * @see #getAngle()
182     */
183    public void setAngle(double angle) {
184        this.angle = angle;
185        fireAnnotationChanged();
186    }
187
188    /**
189     * Returns the tip radius.
190     *
191     * @return The tip radius (in Java2D units).
192     *
193     * @see #setTipRadius(double)
194     */
195    public double getTipRadius() {
196        return this.tipRadius;
197    }
198
199    /**
200     * Sets the tip radius and sends an
201     * {@link AnnotationChangeEvent} to all registered listeners.
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        fireAnnotationChanged();
210    }
211
212    /**
213     * Returns the base radius.
214     *
215     * @return The base radius (in Java2D units).
216     *
217     * @see #setBaseRadius(double)
218     */
219    public double getBaseRadius() {
220        return this.baseRadius;
221    }
222
223    /**
224     * Sets the base radius and sends an
225     * {@link AnnotationChangeEvent} to all registered listeners.
226     *
227     * @param radius  the radius (in Java2D units).
228     *
229     * @see #getBaseRadius()
230     */
231    public void setBaseRadius(double radius) {
232        this.baseRadius = radius;
233        fireAnnotationChanged();
234    }
235
236    /**
237     * Returns the label offset.
238     *
239     * @return The label offset (in Java2D units).
240     *
241     * @see #setLabelOffset(double)
242     */
243    public double getLabelOffset() {
244        return this.labelOffset;
245    }
246
247    /**
248     * Sets the label offset (from the arrow base, continuing in a straight
249     * line, in Java2D units) and sends an
250     * {@link AnnotationChangeEvent} to all registered listeners.
251     *
252     * @param offset  the offset (in Java2D units).
253     *
254     * @see #getLabelOffset()
255     */
256    public void setLabelOffset(double offset) {
257        this.labelOffset = offset;
258        fireAnnotationChanged();
259    }
260
261    /**
262     * Returns the arrow length.
263     *
264     * @return The arrow length.
265     *
266     * @see #setArrowLength(double)
267     */
268    public double getArrowLength() {
269        return this.arrowLength;
270    }
271
272    /**
273     * Sets the arrow length and sends an
274     * {@link AnnotationChangeEvent} to all registered listeners.
275     *
276     * @param length  the length.
277     *
278     * @see #getArrowLength()
279     */
280    public void setArrowLength(double length) {
281        this.arrowLength = length;
282        fireAnnotationChanged();
283    }
284
285    /**
286     * Returns the arrow width.
287     *
288     * @return The arrow width (in Java2D units).
289     *
290     * @see #setArrowWidth(double)
291     */
292    public double getArrowWidth() {
293        return this.arrowWidth;
294    }
295
296    /**
297     * Sets the arrow width and sends an
298     * {@link AnnotationChangeEvent} to all registered listeners.
299     *
300     * @param width  the width (in Java2D units).
301     *
302     * @see #getArrowWidth()
303     */
304    public void setArrowWidth(double width) {
305        this.arrowWidth = width;
306        fireAnnotationChanged();
307    }
308
309    /**
310     * Returns the stroke used to draw the arrow line.
311     *
312     * @return The arrow stroke (never <code>null</code>).
313     *
314     * @see #setArrowStroke(Stroke)
315     */
316    public Stroke getArrowStroke() {
317        return this.arrowStroke;
318    }
319
320    /**
321     * Sets the stroke used to draw the arrow line and sends an
322     * {@link AnnotationChangeEvent} to all registered listeners.
323     *
324     * @param stroke  the stroke (<code>null</code> not permitted).
325     *
326     * @see #getArrowStroke()
327     */
328    public void setArrowStroke(Stroke stroke) {
329        ParamChecks.nullNotPermitted(stroke, "stroke");
330        this.arrowStroke = stroke;
331        fireAnnotationChanged();
332    }
333
334    /**
335     * Returns the paint used for the arrow.
336     *
337     * @return The arrow paint (never <code>null</code>).
338     *
339     * @see #setArrowPaint(Paint)
340     */
341    public Paint getArrowPaint() {
342        return this.arrowPaint;
343    }
344
345    /**
346     * Sets the paint used for the arrow and sends an
347     * {@link AnnotationChangeEvent} to all registered listeners.
348     *
349     * @param paint  the arrow paint (<code>null</code> not permitted).
350     *
351     * @see #getArrowPaint()
352     */
353    public void setArrowPaint(Paint paint) {
354        ParamChecks.nullNotPermitted(paint, "paint");
355        this.arrowPaint = paint;
356        fireAnnotationChanged();
357    }
358
359    /**
360     * Draws the annotation.
361     *
362     * @param g2  the graphics device.
363     * @param plot  the plot.
364     * @param dataArea  the data area.
365     * @param domainAxis  the domain axis.
366     * @param rangeAxis  the range axis.
367     */
368    @Override
369    public void draw(Graphics2D g2, CategoryPlot plot, Rectangle2D dataArea,
370            CategoryAxis domainAxis, ValueAxis rangeAxis) {
371
372        PlotOrientation orientation = plot.getOrientation();
373        RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
374                plot.getDomainAxisLocation(), orientation);
375        RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
376                plot.getRangeAxisLocation(), orientation);
377        CategoryDataset dataset = plot.getDataset();
378        int catIndex = dataset.getColumnIndex(getCategory());
379        int catCount = dataset.getColumnCount();
380        double j2DX = domainAxis.getCategoryMiddle(catIndex, catCount,
381                dataArea, domainEdge);
382        double j2DY = rangeAxis.valueToJava2D(getValue(), dataArea, rangeEdge);
383        if (orientation == PlotOrientation.HORIZONTAL) {
384            double temp = j2DX;
385            j2DX = j2DY;
386            j2DY = temp;
387        }
388        double startX = j2DX + Math.cos(this.angle) * this.baseRadius;
389        double startY = j2DY + Math.sin(this.angle) * this.baseRadius;
390
391        double endX = j2DX + Math.cos(this.angle) * this.tipRadius;
392        double endY = j2DY + Math.sin(this.angle) * this.tipRadius;
393
394        double arrowBaseX = endX + Math.cos(this.angle) * this.arrowLength;
395        double arrowBaseY = endY + Math.sin(this.angle) * this.arrowLength;
396
397        double arrowLeftX = arrowBaseX
398            + Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
399        double arrowLeftY = arrowBaseY
400            + Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
401
402        double arrowRightX = arrowBaseX
403            - Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
404        double arrowRightY = arrowBaseY
405            - Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
406
407        GeneralPath arrow = new GeneralPath();
408        arrow.moveTo((float) endX, (float) endY);
409        arrow.lineTo((float) arrowLeftX, (float) arrowLeftY);
410        arrow.lineTo((float) arrowRightX, (float) arrowRightY);
411        arrow.closePath();
412
413        g2.setStroke(this.arrowStroke);
414        g2.setPaint(this.arrowPaint);
415        Line2D line = new Line2D.Double(startX, startY, arrowBaseX, arrowBaseY);
416        g2.draw(line);
417        g2.fill(arrow);
418
419        // draw the label
420        g2.setFont(getFont());
421        g2.setPaint(getPaint());
422        double labelX = j2DX
423            + Math.cos(this.angle) * (this.baseRadius + this.labelOffset);
424        double labelY = j2DY
425            + Math.sin(this.angle) * (this.baseRadius + this.labelOffset);
426        /* Rectangle2D hotspot = */ TextUtilities.drawAlignedString(getText(),
427                g2, (float) labelX, (float) labelY, getTextAnchor());
428        // TODO: implement the entity for the annotation
429
430    }
431
432    /**
433     * Tests this annotation for equality with an arbitrary object.
434     *
435     * @param obj  the object (<code>null</code> permitted).
436     *
437     * @return <code>true</code> or <code>false</code>.
438     */
439    @Override
440    public boolean equals(Object obj) {
441
442        if (obj == this) {
443            return true;
444        }
445        if (!(obj instanceof CategoryPointerAnnotation)) {
446            return false;
447        }
448        if (!super.equals(obj)) {
449            return false;
450        }
451        CategoryPointerAnnotation that = (CategoryPointerAnnotation) obj;
452        if (this.angle != that.angle) {
453            return false;
454        }
455        if (this.tipRadius != that.tipRadius) {
456            return false;
457        }
458        if (this.baseRadius != that.baseRadius) {
459            return false;
460        }
461        if (this.arrowLength != that.arrowLength) {
462            return false;
463        }
464        if (this.arrowWidth != that.arrowWidth) {
465            return false;
466        }
467        if (!this.arrowPaint.equals(that.arrowPaint)) {
468            return false;
469        }
470        if (!ObjectUtilities.equal(this.arrowStroke, that.arrowStroke)) {
471            return false;
472        }
473        if (this.labelOffset != that.labelOffset) {
474            return false;
475        }
476        return true;
477    }
478
479    /**
480     * Returns a hash code for this instance.
481     *
482     * @return A hash code.
483     */
484    @Override
485    public int hashCode() {
486        int result = 193;
487        long temp = Double.doubleToLongBits(this.angle);
488        result = 37 * result + (int) (temp ^ (temp >>> 32));
489        temp = Double.doubleToLongBits(this.tipRadius);
490        result = 37 * result + (int) (temp ^ (temp >>> 32));
491        temp = Double.doubleToLongBits(this.baseRadius);
492        result = 37 * result + (int) (temp ^ (temp >>> 32));
493        temp = Double.doubleToLongBits(this.arrowLength);
494        result = 37 * result + (int) (temp ^ (temp >>> 32));
495        temp = Double.doubleToLongBits(this.arrowWidth);
496        result = 37 * result + (int) (temp ^ (temp >>> 32));
497        result = 37 * result + HashUtilities.hashCodeForPaint(this.arrowPaint);
498        result = 37 * result + this.arrowStroke.hashCode();
499        temp = Double.doubleToLongBits(this.labelOffset);
500        result = 37 * result + (int) (temp ^ (temp >>> 32));
501        return result;
502    }
503
504    /**
505     * Returns a clone of the annotation.
506     *
507     * @return A clone.
508     *
509     * @throws CloneNotSupportedException  if the annotation can't be cloned.
510     */
511    @Override
512    public Object clone() throws CloneNotSupportedException {
513        return super.clone();
514    }
515
516    /**
517     * Provides serialization support.
518     *
519     * @param stream  the output stream.
520     *
521     * @throws IOException if there is an I/O error.
522     */
523    private void writeObject(ObjectOutputStream stream) throws IOException {
524        stream.defaultWriteObject();
525        SerialUtilities.writePaint(this.arrowPaint, stream);
526        SerialUtilities.writeStroke(this.arrowStroke, stream);
527    }
528
529    /**
530     * Provides serialization support.
531     *
532     * @param stream  the input stream.
533     *
534     * @throws IOException  if there is an I/O error.
535     * @throws ClassNotFoundException  if there is a classpath problem.
536     */
537    private void readObject(ObjectInputStream stream)
538        throws IOException, ClassNotFoundException {
539        stream.defaultReadObject();
540        this.arrowPaint = SerialUtilities.readPaint(stream);
541        this.arrowStroke = SerialUtilities.readStroke(stream);
542    }
543
544}