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