001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2011, 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-2011, 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 *
042 */
043
044package org.jfree.chart.annotations;
045
046import java.awt.BasicStroke;
047import java.awt.Color;
048import java.awt.Graphics2D;
049import java.awt.Paint;
050import java.awt.Stroke;
051import java.awt.geom.GeneralPath;
052import java.awt.geom.Line2D;
053import java.awt.geom.Rectangle2D;
054import java.io.IOException;
055import java.io.ObjectInputStream;
056import java.io.ObjectOutputStream;
057import java.io.Serializable;
058
059import org.jfree.chart.HashUtilities;
060import org.jfree.chart.axis.CategoryAxis;
061import org.jfree.chart.axis.ValueAxis;
062import org.jfree.chart.event.AnnotationChangeEvent;
063import org.jfree.chart.plot.CategoryPlot;
064import org.jfree.chart.plot.Plot;
065import org.jfree.chart.plot.PlotOrientation;
066import org.jfree.data.category.CategoryDataset;
067import org.jfree.io.SerialUtilities;
068import org.jfree.text.TextUtilities;
069import org.jfree.ui.RectangleEdge;
070import org.jfree.util.ObjectUtilities;
071import org.jfree.util.PublicCloneable;
072
073/**
074 * An arrow and label that can be placed on a {@link CategoryPlot}.  The arrow
075 * is drawn at a user-definable angle so that it points towards the (category,
076 * value) location for the annotation.
077 * <p>
078 * The arrow length (and its offset from the (category, value) location) is
079 * controlled by the tip radius and the base radius attributes.  Imagine two
080 * circles around the (category, value) coordinate: the inner circle defined by
081 * the tip radius, and the outer circle defined by the base radius.  Now, draw
082 * the arrow starting at some point on the outer circle (the point is
083 * determined by the angle), with the arrow tip being drawn at a corresponding
084 * point on the inner circle.
085 *
086 * @since 1.0.3
087 */
088public class CategoryPointerAnnotation extends CategoryTextAnnotation
089        implements Cloneable, PublicCloneable, Serializable {
090
091    /** For serialization. */
092    private static final long serialVersionUID = -4031161445009858551L;
093
094    /** The default tip radius (in Java2D units). */
095    public static final double DEFAULT_TIP_RADIUS = 10.0;
096
097    /** The default base radius (in Java2D units). */
098    public static final double DEFAULT_BASE_RADIUS = 30.0;
099
100    /** The default label offset (in Java2D units). */
101    public static final double DEFAULT_LABEL_OFFSET = 3.0;
102
103    /** The default arrow length (in Java2D units). */
104    public static final double DEFAULT_ARROW_LENGTH = 5.0;
105
106    /** The default arrow width (in Java2D units). */
107    public static final double DEFAULT_ARROW_WIDTH = 3.0;
108
109    /** The angle of the arrow's line (in radians). */
110    private double angle;
111
112    /**
113     * The radius from the (x, y) point to the tip of the arrow (in Java2D
114     * units).
115     */
116    private double tipRadius;
117
118    /**
119     * The radius from the (x, y) point to the start of the arrow line (in
120     * Java2D units).
121     */
122    private double baseRadius;
123
124    /** The length of the arrow head (in Java2D units). */
125    private double arrowLength;
126
127    /** The arrow width (in Java2D units, per side). */
128    private double arrowWidth;
129
130    /** The arrow stroke. */
131    private transient Stroke arrowStroke;
132
133    /** The arrow paint. */
134    private transient Paint arrowPaint;
135
136    /** The radius from the base point to the anchor point for the label. */
137    private double labelOffset;
138
139    /**
140     * Creates a new label and arrow annotation.
141     *
142     * @param label  the label (<code>null</code> permitted).
143     * @param key  the category key.
144     * @param value  the y-value (measured against the chart's range axis).
145     * @param angle  the angle of the arrow's line (in radians).
146     */
147    public CategoryPointerAnnotation(String label, Comparable key, double value,
148            double angle) {
149
150        super(label, key, value);
151        this.angle = angle;
152        this.tipRadius = DEFAULT_TIP_RADIUS;
153        this.baseRadius = DEFAULT_BASE_RADIUS;
154        this.arrowLength = DEFAULT_ARROW_LENGTH;
155        this.arrowWidth = DEFAULT_ARROW_WIDTH;
156        this.labelOffset = DEFAULT_LABEL_OFFSET;
157        this.arrowStroke = new BasicStroke(1.0f);
158        this.arrowPaint = Color.black;
159
160    }
161
162    /**
163     * Returns the angle of the arrow.
164     *
165     * @return The angle (in radians).
166     *
167     * @see #setAngle(double)
168     */
169    public double getAngle() {
170        return this.angle;
171    }
172
173    /**
174     * Sets the angle of the arrow and sends an
175     * {@link AnnotationChangeEvent} to all registered listeners.
176     *
177     * @param angle  the angle (in radians).
178     *
179     * @see #getAngle()
180     */
181    public void setAngle(double angle) {
182        this.angle = angle;
183        fireAnnotationChanged();
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 and sends an
199     * {@link AnnotationChangeEvent} to all registered listeners.
200     *
201     * @param radius  the radius (in Java2D units).
202     *
203     * @see #getTipRadius()
204     */
205    public void setTipRadius(double radius) {
206        this.tipRadius = radius;
207        fireAnnotationChanged();
208    }
209
210    /**
211     * Returns the base radius.
212     *
213     * @return The base radius (in Java2D units).
214     *
215     * @see #setBaseRadius(double)
216     */
217    public double getBaseRadius() {
218        return this.baseRadius;
219    }
220
221    /**
222     * Sets the base radius and sends an
223     * {@link AnnotationChangeEvent} to all registered listeners.
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        fireAnnotationChanged();
232    }
233
234    /**
235     * Returns the label offset.
236     *
237     * @return The label offset (in Java2D units).
238     *
239     * @see #setLabelOffset(double)
240     */
241    public double getLabelOffset() {
242        return this.labelOffset;
243    }
244
245    /**
246     * Sets the label offset (from the arrow base, continuing in a straight
247     * line, in Java2D units) and sends an
248     * {@link AnnotationChangeEvent} to all registered listeners.
249     *
250     * @param offset  the offset (in Java2D units).
251     *
252     * @see #getLabelOffset()
253     */
254    public void setLabelOffset(double offset) {
255        this.labelOffset = offset;
256        fireAnnotationChanged();
257    }
258
259    /**
260     * Returns the arrow length.
261     *
262     * @return The arrow length.
263     *
264     * @see #setArrowLength(double)
265     */
266    public double getArrowLength() {
267        return this.arrowLength;
268    }
269
270    /**
271     * Sets the arrow length and sends an
272     * {@link AnnotationChangeEvent} to all registered listeners.
273     *
274     * @param length  the length.
275     *
276     * @see #getArrowLength()
277     */
278    public void setArrowLength(double length) {
279        this.arrowLength = length;
280        fireAnnotationChanged();
281    }
282
283    /**
284     * Returns the arrow width.
285     *
286     * @return The arrow width (in Java2D units).
287     *
288     * @see #setArrowWidth(double)
289     */
290    public double getArrowWidth() {
291        return this.arrowWidth;
292    }
293
294    /**
295     * Sets the arrow width and sends an
296     * {@link AnnotationChangeEvent} to all registered listeners.
297     *
298     * @param width  the width (in Java2D units).
299     *
300     * @see #getArrowWidth()
301     */
302    public void setArrowWidth(double width) {
303        this.arrowWidth = width;
304        fireAnnotationChanged();
305    }
306
307    /**
308     * Returns the stroke used to draw the arrow line.
309     *
310     * @return The arrow stroke (never <code>null</code>).
311     *
312     * @see #setArrowStroke(Stroke)
313     */
314    public Stroke getArrowStroke() {
315        return this.arrowStroke;
316    }
317
318    /**
319     * Sets the stroke used to draw the arrow line and sends an
320     * {@link AnnotationChangeEvent} to all registered listeners.
321     *
322     * @param stroke  the stroke (<code>null</code> not permitted).
323     *
324     * @see #getArrowStroke()
325     */
326    public void setArrowStroke(Stroke stroke) {
327        if (stroke == null) {
328            throw new IllegalArgumentException("Null 'stroke' not permitted.");
329        }
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        if (paint == null) {
355            throw new IllegalArgumentException("Null 'paint' argument.");
356        }
357        this.arrowPaint = paint;
358        fireAnnotationChanged();
359    }
360
361    /**
362     * Draws the annotation.
363     *
364     * @param g2  the graphics device.
365     * @param plot  the plot.
366     * @param dataArea  the data area.
367     * @param domainAxis  the domain axis.
368     * @param rangeAxis  the range axis.
369     */
370    public void draw(Graphics2D g2, CategoryPlot plot, Rectangle2D dataArea,
371            CategoryAxis domainAxis, ValueAxis rangeAxis) {
372
373        PlotOrientation orientation = plot.getOrientation();
374        RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
375                plot.getDomainAxisLocation(), orientation);
376        RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
377                plot.getRangeAxisLocation(), orientation);
378        CategoryDataset dataset = plot.getDataset();
379        int catIndex = dataset.getColumnIndex(getCategory());
380        int catCount = dataset.getColumnCount();
381        double j2DX = domainAxis.getCategoryMiddle(catIndex, catCount,
382                dataArea, domainEdge);
383        double j2DY = rangeAxis.valueToJava2D(getValue(), dataArea, rangeEdge);
384        if (orientation == PlotOrientation.HORIZONTAL) {
385            double temp = j2DX;
386            j2DX = j2DY;
387            j2DY = temp;
388        }
389        double startX = j2DX + Math.cos(this.angle) * this.baseRadius;
390        double startY = j2DY + Math.sin(this.angle) * this.baseRadius;
391
392        double endX = j2DX + Math.cos(this.angle) * this.tipRadius;
393        double endY = j2DY + Math.sin(this.angle) * this.tipRadius;
394
395        double arrowBaseX = endX + Math.cos(this.angle) * this.arrowLength;
396        double arrowBaseY = endY + Math.sin(this.angle) * this.arrowLength;
397
398        double arrowLeftX = arrowBaseX
399            + Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
400        double arrowLeftY = arrowBaseY
401            + Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
402
403        double arrowRightX = arrowBaseX
404            - Math.cos(this.angle + Math.PI / 2.0) * this.arrowWidth;
405        double arrowRightY = arrowBaseY
406            - Math.sin(this.angle + Math.PI / 2.0) * this.arrowWidth;
407
408        GeneralPath arrow = new GeneralPath();
409        arrow.moveTo((float) endX, (float) endY);
410        arrow.lineTo((float) arrowLeftX, (float) arrowLeftY);
411        arrow.lineTo((float) arrowRightX, (float) arrowRightY);
412        arrow.closePath();
413
414        g2.setStroke(this.arrowStroke);
415        g2.setPaint(this.arrowPaint);
416        Line2D line = new Line2D.Double(startX, startY, arrowBaseX, arrowBaseY);
417        g2.draw(line);
418        g2.fill(arrow);
419
420        // draw the label
421        g2.setFont(getFont());
422        g2.setPaint(getPaint());
423        double labelX = j2DX
424            + Math.cos(this.angle) * (this.baseRadius + this.labelOffset);
425        double labelY = j2DY
426            + Math.sin(this.angle) * (this.baseRadius + this.labelOffset);
427        /* Rectangle2D hotspot = */ TextUtilities.drawAlignedString(getText(),
428                g2, (float) labelX, (float) labelY, getTextAnchor());
429        // TODO: implement the entity for the annotation
430
431    }
432
433    /**
434     * Tests this annotation for equality with an arbitrary object.
435     *
436     * @param obj  the object (<code>null</code> permitted).
437     *
438     * @return <code>true</code> or <code>false</code>.
439     */
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    public int hashCode() {
485        int result = 193;
486        long temp = Double.doubleToLongBits(this.angle);
487        result = 37 * result + (int) (temp ^ (temp >>> 32));
488        temp = Double.doubleToLongBits(this.tipRadius);
489        result = 37 * result + (int) (temp ^ (temp >>> 32));
490        temp = Double.doubleToLongBits(this.baseRadius);
491        result = 37 * result + (int) (temp ^ (temp >>> 32));
492        temp = Double.doubleToLongBits(this.arrowLength);
493        result = 37 * result + (int) (temp ^ (temp >>> 32));
494        temp = Double.doubleToLongBits(this.arrowWidth);
495        result = 37 * result + (int) (temp ^ (temp >>> 32));
496        result = 37 * result + HashUtilities.hashCodeForPaint(this.arrowPaint);
497        result = 37 * result + this.arrowStroke.hashCode();
498        temp = Double.doubleToLongBits(this.labelOffset);
499        result = 37 * result + (int) (temp ^ (temp >>> 32));
500        return result;
501    }
502
503    /**
504     * Returns a clone of the annotation.
505     *
506     * @return A clone.
507     *
508     * @throws CloneNotSupportedException  if the annotation can't be cloned.
509     */
510    public Object clone() throws CloneNotSupportedException {
511        return super.clone();
512    }
513
514    /**
515     * Provides serialization support.
516     *
517     * @param stream  the output stream.
518     *
519     * @throws IOException if there is an I/O error.
520     */
521    private void writeObject(ObjectOutputStream stream) throws IOException {
522        stream.defaultWriteObject();
523        SerialUtilities.writePaint(this.arrowPaint, stream);
524        SerialUtilities.writeStroke(this.arrowStroke, stream);
525    }
526
527    /**
528     * Provides serialization support.
529     *
530     * @param stream  the input stream.
531     *
532     * @throws IOException  if there is an I/O error.
533     * @throws ClassNotFoundException  if there is a classpath problem.
534     */
535    private void readObject(ObjectInputStream stream)
536        throws IOException, ClassNotFoundException {
537        stream.defaultReadObject();
538        this.arrowPaint = SerialUtilities.readPaint(stream);
539        this.arrowStroke = SerialUtilities.readStroke(stream);
540    }
541
542}