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, ≤ 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}