001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert 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 * XYInversePointerAnnotation.java
029 * ------------------------
030 * (C) Copyright 2021-present, by Yuri Blankenstein and Contributors.
031 *
032 * Original Author:  Yuri Blankenstein (for ESI TNO);
033 *
034 */
035package org.jfree.chart.annotations;
036
037import java.awt.Graphics2D;
038import java.awt.Shape;
039import java.awt.geom.Ellipse2D;
040import java.awt.geom.GeneralPath;
041import java.awt.geom.Line2D;
042import java.awt.geom.Point2D;
043import java.awt.geom.Rectangle2D;
044import java.io.Serializable;
045
046import org.jfree.chart.axis.ValueAxis;
047import org.jfree.chart.util.GeomUtil;
048import org.jfree.chart.plot.Plot;
049import org.jfree.chart.plot.PlotOrientation;
050import org.jfree.chart.plot.PlotRenderingInfo;
051import org.jfree.chart.plot.XYPlot;
052import org.jfree.chart.text.TextUtils;
053import org.jfree.chart.ui.RectangleEdge;
054import org.jfree.chart.util.PublicCloneable;
055
056/**
057 * An arrow and label that can be placed on an {@link XYPlot}. The arrow is
058 * drawn at a user-definable angle but points towards the label of the
059 * annotation.
060 * <p>
061 * The arrow length (and its offset from the (x, y) location) is controlled by
062 * the tip radius and the base radius attributes. Imagine two circles around the
063 * (x, y) coordinate: the inner circle defined by the tip radius, and the outer
064 * circle defined by the base radius. Now, draw the arrow starting at some point
065 * on the outer circle (the point is determined by the angle), with the arrow
066 * tip being drawn at a corresponding point on the inner circle.
067 */
068public class XYInversePointerAnnotation extends XYPointerAnnotation
069        implements Cloneable, PublicCloneable, Serializable {
070
071    /** For serialization. */
072    private static final long serialVersionUID = -4031161445009858551L;
073
074    /** The default dot radius (in Java2D units). */
075    public static final double DEFAULT_DOT_RADIUS = 0;
076
077    /** The radius of the dot at the start of the arrow. */
078    private double dotRadius;
079
080    /**
081     * Creates a new label and arrow annotation.
082     *
083     * @param label the label ({@code null} permitted).
084     * @param x     the x-coordinate (measured against the chart's domain axis).
085     * @param y     the y-coordinate (measured against the chart's range axis).
086     * @param angle the angle of the arrow's line (in radians).
087     */
088    public XYInversePointerAnnotation(String label, double x, double y,
089            double angle) {
090        super(label, x, y, angle);
091        this.dotRadius = DEFAULT_DOT_RADIUS;
092    }
093
094    /**
095     * Returns the radius of the dot at the start of the arrow.
096     * 
097     * @return the radius of the dot at the start of the arrow
098     * @see #setDotRadius(double)
099     */
100    public double getDotRadius() {
101        return dotRadius;
102    }
103
104    /**
105     * Sets the radius of the dot at the start of the arrow, &#8804; 0 will omit
106     * the dot.
107     * 
108     * @param dotRadius the radius of the dot at the start of the arrow
109     * @see #getDotRadius()
110     */
111    public void setDotRadius(double dotRadius) {
112        this.dotRadius = dotRadius;
113        fireAnnotationChanged();
114    }
115
116    /**
117     * Draws the annotation.
118     *
119     * @param g2            the graphics device.
120     * @param plot          the plot.
121     * @param dataArea      the data area.
122     * @param domainAxis    the domain axis.
123     * @param rangeAxis     the range axis.
124     * @param rendererIndex the renderer index.
125     * @param info          the plot rendering info.
126     */
127    @Override
128    public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea,
129            ValueAxis domainAxis, ValueAxis rangeAxis, int rendererIndex,
130            PlotRenderingInfo info) {
131
132        PlotOrientation orientation = plot.getOrientation();
133        RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
134                plot.getDomainAxisLocation(), orientation);
135        RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
136                plot.getRangeAxisLocation(), orientation);
137        double j2DX = domainAxis.valueToJava2D(getX(), dataArea, domainEdge);
138        double j2DY = rangeAxis.valueToJava2D(getY(), dataArea, rangeEdge);
139        if (orientation == PlotOrientation.HORIZONTAL) {
140            double temp = j2DX;
141            j2DX = j2DY;
142            j2DY = temp;
143        }
144
145        double startX = j2DX + Math.cos(getAngle()) * getBaseRadius();
146        double startY = j2DY + Math.sin(getAngle()) * getBaseRadius();
147
148        double endX = j2DX + Math.cos(getAngle()) * getTipRadius();
149        double endY = j2DY + Math.sin(getAngle()) * getTipRadius();
150
151        // Already calculate the label bounds to adjust the start point
152        double labelX = j2DX
153                + Math.cos(getAngle()) * (getBaseRadius() + getLabelOffset());
154        double labelY = j2DY
155                + Math.sin(getAngle()) * (getBaseRadius() + getLabelOffset());
156        Shape hotspot = TextUtils.calculateRotatedStringBounds(getText(), g2,
157                (float) labelX, (float) labelY, getTextAnchor(),
158                getRotationAngle(), getRotationAnchor());
159
160        Point2D[] pointOnLabelBounds = GeomUtil.calculateIntersectionPoints(
161                new Line2D.Double(startX, startY, endX, endY),
162                GeomUtil.getLines(hotspot, null));
163        if (pointOnLabelBounds.length > 0) {
164            // Adjust the start point to the intersection with the label bounds
165            startX = pointOnLabelBounds[0].getX();
166            startY = pointOnLabelBounds[0].getY();
167        }
168
169        double arrowBaseX = startX - Math.cos(getAngle()) * getArrowLength();
170        double arrowBaseY = startY - Math.sin(getAngle()) * getArrowLength();
171
172        double arrowLeftX = arrowBaseX
173                + Math.cos(getAngle() + Math.PI / 2.0) * getArrowWidth();
174        double arrowLeftY = arrowBaseY
175                + Math.sin(getAngle() + Math.PI / 2.0) * getArrowWidth();
176
177        double arrowRightX = arrowBaseX
178                - Math.cos(getAngle() + Math.PI / 2.0) * getArrowWidth();
179        double arrowRightY = arrowBaseY
180                - Math.sin(getAngle() + Math.PI / 2.0) * getArrowWidth();
181
182        GeneralPath arrow = new GeneralPath();
183        arrow.moveTo((float) startX, (float) startY);
184        arrow.lineTo((float) arrowLeftX, (float) arrowLeftY);
185        arrow.lineTo((float) arrowRightX, (float) arrowRightY);
186        arrow.closePath();
187
188        g2.setStroke(getArrowStroke());
189        g2.setPaint(getArrowPaint());
190        Line2D line = new Line2D.Double(arrowBaseX, arrowBaseY, endX, endY);
191        g2.draw(line);
192        g2.fill(arrow);
193
194        if (dotRadius > 0) {
195            Ellipse2D.Double dot = new Ellipse2D.Double(endX - dotRadius,
196                    endY - dotRadius, 2 * dotRadius, 2 * dotRadius);
197            g2.fill(dot);
198        }
199
200        // draw the label
201        g2.setFont(getFont());
202        if (getBackgroundPaint() != null) {
203            g2.setPaint(getBackgroundPaint());
204            g2.fill(hotspot);
205        }
206
207        g2.setPaint(getPaint());
208        TextUtils.drawRotatedString(getText(), g2, (float) labelX,
209                (float) labelY, getTextAnchor(), getRotationAngle(),
210                getRotationAnchor());
211        if (isOutlineVisible()) {
212            g2.setStroke(getOutlineStroke());
213            g2.setPaint(getOutlinePaint());
214            g2.draw(hotspot);
215        }
216
217        String toolTip = getToolTipText();
218        String url = getURL();
219        if (toolTip != null || url != null) {
220            addEntity(info, hotspot, rendererIndex, toolTip, url);
221        }
222
223    }
224
225    @Override
226    public int hashCode() {
227        final int prime = 31;
228        int result = super.hashCode();
229        long temp;
230        temp = Double.doubleToLongBits(dotRadius);
231        result = prime * result + (int) (temp ^ (temp >>> 32));
232        return result;
233    }
234
235    @Override
236    public boolean equals(Object obj) {
237        if (this == obj)
238            return true;
239        if (!super.equals(obj))
240            return false;
241        if (getClass() != obj.getClass())
242            return false;
243        XYInversePointerAnnotation other = (XYInversePointerAnnotation) obj;
244        if (Double.doubleToLongBits(dotRadius) != Double
245                .doubleToLongBits(other.dotRadius))
246            return false;
247        return true;
248    }
249
250    /**
251     * Returns a clone of the annotation.
252     *
253     * @return A clone.
254     *
255     * @throws CloneNotSupportedException if the annotation can't be cloned.
256     */
257    @Override
258    public Object clone() throws CloneNotSupportedException {
259        return super.clone();
260    }
261}