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 * XYNoteAnnotation.java 029 * --------------------- 030 * (C) Copyright 2003-present, by David Gilbert and Contributors. 031 * 032 * Original Author: David Gilbert (as XYPointerAnnotation.java); 033 * Contributor(s): Yuri Blankenstein (copy to XYNoteAnnotation.java; for ESI TNO); 034 * 035 */ 036package org.jfree.chart.annotations; 037 038import java.awt.BasicStroke; 039import java.awt.Color; 040import java.awt.Graphics2D; 041import java.awt.Paint; 042import java.awt.Shape; 043import java.awt.Stroke; 044import java.awt.geom.Line2D; 045import java.awt.geom.Rectangle2D; 046import java.io.IOException; 047import java.io.ObjectInputStream; 048import java.io.ObjectOutputStream; 049import java.io.Serializable; 050import java.util.Objects; 051 052import org.jfree.chart.HashUtils; 053import org.jfree.chart.axis.ValueAxis; 054import org.jfree.chart.event.AnnotationChangeEvent; 055import org.jfree.chart.plot.Plot; 056import org.jfree.chart.plot.PlotOrientation; 057import org.jfree.chart.plot.PlotRenderingInfo; 058import org.jfree.chart.plot.XYPlot; 059import org.jfree.chart.text.TextUtils; 060import org.jfree.chart.ui.RectangleEdge; 061import org.jfree.chart.util.Args; 062import org.jfree.chart.util.PublicCloneable; 063import org.jfree.chart.util.SerialUtils; 064 065/** 066 * An line and label that can be placed on an {@link XYPlot}. The line is 067 * drawn at a user-definable angle so that it points towards the (x, y) 068 * location for the annotation. 069 * <p> 070 * The line length (and its offset from the (x, y) location) is controlled by 071 * the tip radius and the base radius attributes. Imagine two circles around 072 * the (x, y) coordinate: the inner circle defined by the tip radius, and the 073 * outer circle defined by the base radius. Now, draw the line starting at 074 * some point on the outer circle (the point is determined by the angle), with 075 * the line tip being drawn at a corresponding point on the inner circle. 076 */ 077public class XYNoteAnnotation extends XYTextAnnotation 078 implements Cloneable, PublicCloneable, Serializable { 079 080 /** For serialization. */ 081 private static final long serialVersionUID = -4031161445009858551L; 082 083 /** The default tip radius (in Java2D units). */ 084 public static final double DEFAULT_TIP_RADIUS = 0.0; 085 086 /** The default base radius (in Java2D units). */ 087 public static final double DEFAULT_BASE_RADIUS = 30.0; 088 089 /** The default label offset (in Java2D units). */ 090 public static final double DEFAULT_LABEL_OFFSET = 3.0; 091 092 /** The default line stroke. */ 093 public static final Stroke DEFAULT_LINE_STROKE = new BasicStroke(0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 2.0f, new float[] {2.0f}, 0.0f); 094 095 /** The default line stroke. */ 096 public static final Paint DEFAULT_BACKGROUND_PAINT = new Color(255, 255, 203); 097 098 /** The default line stroke. */ 099 public static final Paint DEFAULT_OUTLINE_PAINT = new Color(255, 204, 102); 100 101 /** The angle of the line's line (in radians). */ 102 private double angle; 103 104 /** 105 * The radius from the (x, y) point to the tip of the line (in Java2D 106 * units). 107 */ 108 private double tipRadius; 109 110 /** 111 * The radius from the (x, y) point to the start of the line line (in 112 * Java2D units). 113 */ 114 private double baseRadius; 115 116 /** The line stroke. */ 117 private transient Stroke lineStroke; 118 119 /** The line paint. */ 120 private transient Paint linePaint; 121 122 /** The radius from the base point to the anchor point for the label. */ 123 private double labelOffset; 124 125 /** 126 * Creates a new label and line annotation. 127 * 128 * @param label the label ({@code null} permitted). 129 * @param x the x-coordinate (measured against the chart's domain axis). 130 * @param y the y-coordinate (measured against the chart's range axis). 131 * @param angle the angle of the line's line (in radians). 132 */ 133 public XYNoteAnnotation(String label, double x, double y, double angle) { 134 super(label, x, y); 135 this.angle = angle; 136 this.tipRadius = DEFAULT_TIP_RADIUS; 137 this.baseRadius = DEFAULT_BASE_RADIUS; 138 this.labelOffset = DEFAULT_LABEL_OFFSET; 139 this.lineStroke = DEFAULT_LINE_STROKE; 140 this.linePaint = Color.BLACK; 141 142 setBackgroundPaint(DEFAULT_BACKGROUND_PAINT); 143 setOutlineVisible(true); 144 setOutlinePaint(DEFAULT_OUTLINE_PAINT); 145 setOutlineStroke(new BasicStroke(1.0f)); 146 } 147 148 /** 149 * Returns the angle of the line. 150 * 151 * @return The angle (in radians). 152 * 153 * @see #setAngle(double) 154 */ 155 public double getAngle() { 156 return this.angle; 157 } 158 159 /** 160 * Sets the angle of the line and sends an 161 * {@link AnnotationChangeEvent} to all registered listeners. 162 * 163 * @param angle the angle (in radians). 164 * 165 * @see #getAngle() 166 */ 167 public void setAngle(double angle) { 168 this.angle = angle; 169 fireAnnotationChanged(); 170 } 171 172 /** 173 * Returns the tip radius. 174 * 175 * @return The tip radius (in Java2D units). 176 * 177 * @see #setTipRadius(double) 178 */ 179 public double getTipRadius() { 180 return this.tipRadius; 181 } 182 183 /** 184 * Sets the tip radius and sends an 185 * {@link AnnotationChangeEvent} to all registered listeners. 186 * 187 * @param radius the radius (in Java2D units). 188 * 189 * @see #getTipRadius() 190 */ 191 public void setTipRadius(double radius) { 192 this.tipRadius = radius; 193 fireAnnotationChanged(); 194 } 195 196 /** 197 * Returns the base radius. 198 * 199 * @return The base radius (in Java2D units). 200 * 201 * @see #setBaseRadius(double) 202 */ 203 public double getBaseRadius() { 204 return this.baseRadius; 205 } 206 207 /** 208 * Sets the base radius and sends an 209 * {@link AnnotationChangeEvent} to all registered listeners. 210 * 211 * @param radius the radius (in Java2D units). 212 * 213 * @see #getBaseRadius() 214 */ 215 public void setBaseRadius(double radius) { 216 this.baseRadius = radius; 217 fireAnnotationChanged(); 218 } 219 220 /** 221 * Returns the label offset. 222 * 223 * @return The label offset (in Java2D units). 224 * 225 * @see #setLabelOffset(double) 226 */ 227 public double getLabelOffset() { 228 return this.labelOffset; 229 } 230 231 /** 232 * Sets the label offset (from the line base, continuing in a straight 233 * line, in Java2D units) and sends an 234 * {@link AnnotationChangeEvent} to all registered listeners. 235 * 236 * @param offset the offset (in Java2D units). 237 * 238 * @see #getLabelOffset() 239 */ 240 public void setLabelOffset(double offset) { 241 this.labelOffset = offset; 242 fireAnnotationChanged(); 243 } 244 245 /** 246 * Returns the stroke used to draw the line line. 247 * 248 * @return The line stroke (never {@code null}). 249 * 250 * @see #setLineStroke(Stroke) 251 */ 252 public Stroke getLineStroke() { 253 return this.lineStroke; 254 } 255 256 /** 257 * Sets the stroke used to draw the line line and sends an 258 * {@link AnnotationChangeEvent} to all registered listeners. 259 * 260 * @param stroke the stroke ({@code null} not permitted). 261 * 262 * @see #getLineStroke() 263 */ 264 public void setLineStroke(Stroke stroke) { 265 Args.nullNotPermitted(stroke, "stroke"); 266 this.lineStroke = stroke; 267 fireAnnotationChanged(); 268 } 269 270 /** 271 * Returns the paint used for the line. 272 * 273 * @return The line paint (never {@code null}). 274 * 275 * @see #setLinePaint(Paint) 276 */ 277 public Paint getLinePaint() { 278 return this.linePaint; 279 } 280 281 /** 282 * Sets the paint used for the line and sends an 283 * {@link AnnotationChangeEvent} to all registered listeners. 284 * 285 * @param paint the line paint ({@code null} not permitted). 286 * 287 * @see #getLinePaint() 288 */ 289 public void setLinePaint(Paint paint) { 290 Args.nullNotPermitted(paint, "paint"); 291 this.linePaint = paint; 292 fireAnnotationChanged(); 293 } 294 295 /** 296 * Draws the annotation. 297 * 298 * @param g2 the graphics device. 299 * @param plot the plot. 300 * @param dataArea the data area. 301 * @param domainAxis the domain axis. 302 * @param rangeAxis the range axis. 303 * @param rendererIndex the renderer index. 304 * @param info the plot rendering info. 305 */ 306 @Override 307 public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea, 308 ValueAxis domainAxis, ValueAxis rangeAxis, int rendererIndex, 309 PlotRenderingInfo info) { 310 311 PlotOrientation orientation = plot.getOrientation(); 312 RectangleEdge domainEdge = Plot.resolveDomainAxisLocation( 313 plot.getDomainAxisLocation(), orientation); 314 RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation( 315 plot.getRangeAxisLocation(), orientation); 316 double j2DX = domainAxis.valueToJava2D(getX(), dataArea, domainEdge); 317 double j2DY = rangeAxis.valueToJava2D(getY(), dataArea, rangeEdge); 318 if (orientation == PlotOrientation.HORIZONTAL) { 319 double temp = j2DX; 320 j2DX = j2DY; 321 j2DY = temp; 322 } 323 double startX = j2DX + Math.cos(this.angle) * this.baseRadius; 324 double startY = j2DY + Math.sin(this.angle) * this.baseRadius; 325 326 double endX = j2DX + Math.cos(this.angle) * this.tipRadius; 327 double endY = j2DY + Math.sin(this.angle) * this.tipRadius; 328 329 g2.setStroke(this.lineStroke); 330 g2.setPaint(this.linePaint); 331 Line2D line = new Line2D.Double(startX, startY, endX, endY); 332 g2.draw(line); 333 334 // draw the label 335 double labelX = j2DX + Math.cos(this.angle) * (this.baseRadius 336 + this.labelOffset); 337 double labelY = j2DY + Math.sin(this.angle) * (this.baseRadius 338 + this.labelOffset); 339 g2.setFont(getFont()); 340 Shape hotspot = TextUtils.calculateRotatedStringBounds( 341 getText(), g2, (float) labelX, (float) labelY, getTextAnchor(), 342 getRotationAngle(), getRotationAnchor()); 343 if (getBackgroundPaint() != null) { 344 g2.setPaint(getBackgroundPaint()); 345 g2.fill(hotspot); 346 } 347 g2.setPaint(getPaint()); 348 TextUtils.drawRotatedString(getText(), g2, (float) labelX, 349 (float) labelY, getTextAnchor(), getRotationAngle(), 350 getRotationAnchor()); 351 if (isOutlineVisible()) { 352 g2.setStroke(getOutlineStroke()); 353 g2.setPaint(getOutlinePaint()); 354 g2.draw(hotspot); 355 } 356 357 String toolTip = getToolTipText(); 358 String url = getURL(); 359 if (toolTip != null || url != null) { 360 addEntity(info, hotspot, rendererIndex, toolTip, url); 361 } 362 363 } 364 365 /** 366 * Tests this annotation for equality with an arbitrary object. 367 * 368 * @param obj the object ({@code null} permitted). 369 * 370 * @return {@code true} or {@code false}. 371 */ 372 @Override 373 public boolean equals(Object obj) { 374 if (obj == this) { 375 return true; 376 } 377 if (!(obj instanceof XYNoteAnnotation)) { 378 return false; 379 } 380 XYNoteAnnotation that = (XYNoteAnnotation) obj; 381 if (this.angle != that.angle) { 382 return false; 383 } 384 if (this.tipRadius != that.tipRadius) { 385 return false; 386 } 387 if (this.baseRadius != that.baseRadius) { 388 return false; 389 } 390 if (!this.linePaint.equals(that.linePaint)) { 391 return false; 392 } 393 if (!Objects.equals(this.lineStroke, that.lineStroke)) { 394 return false; 395 } 396 if (this.labelOffset != that.labelOffset) { 397 return false; 398 } 399 return super.equals(obj); 400 } 401 402 /** 403 * Returns a hash code for this instance. 404 * 405 * @return A hash code. 406 */ 407 @Override 408 public int hashCode() { 409 int result = super.hashCode(); 410 long temp = Double.doubleToLongBits(this.angle); 411 result = 37 * result + (int) (temp ^ (temp >>> 32)); 412 temp = Double.doubleToLongBits(this.tipRadius); 413 result = 37 * result + (int) (temp ^ (temp >>> 32)); 414 temp = Double.doubleToLongBits(this.baseRadius); 415 result = 37 * result + (int) (temp ^ (temp >>> 32)); 416 result = result * 37 + HashUtils.hashCodeForPaint(this.linePaint); 417 result = result * 37 + this.lineStroke.hashCode(); 418 temp = Double.doubleToLongBits(this.labelOffset); 419 result = 37 * result + (int) (temp ^ (temp >>> 32)); 420 return result; 421 } 422 423 /** 424 * Returns a clone of the annotation. 425 * 426 * @return A clone. 427 * 428 * @throws CloneNotSupportedException if the annotation can't be cloned. 429 */ 430 @Override 431 public Object clone() throws CloneNotSupportedException { 432 return super.clone(); 433 } 434 435 /** 436 * Provides serialization support. 437 * 438 * @param stream the output stream. 439 * 440 * @throws IOException if there is an I/O error. 441 */ 442 private void writeObject(ObjectOutputStream stream) throws IOException { 443 stream.defaultWriteObject(); 444 SerialUtils.writePaint(this.linePaint, stream); 445 SerialUtils.writeStroke(this.lineStroke, stream); 446 } 447 448 /** 449 * Provides serialization support. 450 * 451 * @param stream the input stream. 452 * 453 * @throws IOException if there is an I/O error. 454 * @throws ClassNotFoundException if there is a classpath problem. 455 */ 456 private void readObject(ObjectInputStream stream) 457 throws IOException, ClassNotFoundException { 458 stream.defaultReadObject(); 459 this.linePaint = SerialUtils.readPaint(stream); 460 this.lineStroke = SerialUtils.readStroke(stream); 461 } 462}