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    
044    package org.jfree.chart.annotations;
045    
046    import java.awt.BasicStroke;
047    import java.awt.Color;
048    import java.awt.Graphics2D;
049    import java.awt.Paint;
050    import java.awt.Stroke;
051    import java.awt.geom.GeneralPath;
052    import java.awt.geom.Line2D;
053    import java.awt.geom.Rectangle2D;
054    import java.io.IOException;
055    import java.io.ObjectInputStream;
056    import java.io.ObjectOutputStream;
057    import java.io.Serializable;
058    
059    import org.jfree.chart.HashUtilities;
060    import org.jfree.chart.axis.CategoryAxis;
061    import org.jfree.chart.axis.ValueAxis;
062    import org.jfree.chart.event.AnnotationChangeEvent;
063    import org.jfree.chart.plot.CategoryPlot;
064    import org.jfree.chart.plot.Plot;
065    import org.jfree.chart.plot.PlotOrientation;
066    import org.jfree.data.category.CategoryDataset;
067    import org.jfree.io.SerialUtilities;
068    import org.jfree.text.TextUtilities;
069    import org.jfree.ui.RectangleEdge;
070    import org.jfree.util.ObjectUtilities;
071    import 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     */
088    public 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    }