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