001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2013, 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 * CrosshairOverlay.java 029 * --------------------- 030 * (C) Copyright 2011-2013, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * Changes: 036 * -------- 037 * 09-Apr-2009 : Version 1 (DG); 038 * 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG); 039 * 02-Jul-2013 : Use ParamChecks (DG); 040 * 041 */ 042 043package org.jfree.chart.panel; 044 045import java.awt.Graphics2D; 046import java.awt.Paint; 047import java.awt.Rectangle; 048import java.awt.Shape; 049import java.awt.Stroke; 050import java.awt.geom.Line2D; 051import java.awt.geom.Point2D; 052import java.awt.geom.Rectangle2D; 053import java.beans.PropertyChangeEvent; 054import java.beans.PropertyChangeListener; 055import java.io.Serializable; 056import java.util.ArrayList; 057import java.util.Iterator; 058import java.util.List; 059import org.jfree.chart.ChartPanel; 060import org.jfree.chart.JFreeChart; 061import org.jfree.chart.axis.ValueAxis; 062import org.jfree.chart.event.OverlayChangeEvent; 063import org.jfree.chart.plot.Crosshair; 064import org.jfree.chart.plot.PlotOrientation; 065import org.jfree.chart.plot.XYPlot; 066import org.jfree.chart.util.ParamChecks; 067import org.jfree.text.TextUtilities; 068import org.jfree.ui.RectangleAnchor; 069import org.jfree.ui.RectangleEdge; 070import org.jfree.ui.TextAnchor; 071import org.jfree.util.ObjectUtilities; 072import org.jfree.util.PublicCloneable; 073 074/** 075 * An overlay for a {@link ChartPanel} that draws crosshairs on a plot. 076 * 077 * @since 1.0.13 078 */ 079public class CrosshairOverlay extends AbstractOverlay implements Overlay, 080 PropertyChangeListener, PublicCloneable, Cloneable, Serializable { 081 082 /** Storage for the crosshairs along the x-axis. */ 083 private List xCrosshairs; 084 085 /** Storage for the crosshairs along the y-axis. */ 086 private List yCrosshairs; 087 088 /** 089 * Default constructor. 090 */ 091 public CrosshairOverlay() { 092 super(); 093 this.xCrosshairs = new java.util.ArrayList(); 094 this.yCrosshairs = new java.util.ArrayList(); 095 } 096 097 /** 098 * Adds a crosshair against the domain axis and sends an 099 * {@link OverlayChangeEvent} to all registered listeners. 100 * 101 * @param crosshair the crosshair (<code>null</code> not permitted). 102 * 103 * @see #removeDomainCrosshair(org.jfree.chart.plot.Crosshair) 104 * @see #addRangeCrosshair(org.jfree.chart.plot.Crosshair) 105 */ 106 public void addDomainCrosshair(Crosshair crosshair) { 107 ParamChecks.nullNotPermitted(crosshair, "crosshair"); 108 this.xCrosshairs.add(crosshair); 109 crosshair.addPropertyChangeListener(this); 110 fireOverlayChanged(); 111 } 112 113 /** 114 * Removes a domain axis crosshair and sends an {@link OverlayChangeEvent} 115 * to all registered listeners. 116 * 117 * @param crosshair the crosshair (<code>null</code> not permitted). 118 * 119 * @see #addDomainCrosshair(org.jfree.chart.plot.Crosshair) 120 */ 121 public void removeDomainCrosshair(Crosshair crosshair) { 122 ParamChecks.nullNotPermitted(crosshair, "crosshair"); 123 if (this.xCrosshairs.remove(crosshair)) { 124 crosshair.removePropertyChangeListener(this); 125 fireOverlayChanged(); 126 } 127 } 128 129 /** 130 * Clears all the domain crosshairs from the overlay and sends an 131 * {@link OverlayChangeEvent} to all registered listeners. 132 */ 133 public void clearDomainCrosshairs() { 134 if (this.xCrosshairs.isEmpty()) { 135 return; // nothing to do 136 } 137 List crosshairs = getDomainCrosshairs(); 138 for (int i = 0; i < crosshairs.size(); i++) { 139 Crosshair c = (Crosshair) crosshairs.get(i); 140 this.xCrosshairs.remove(c); 141 c.removePropertyChangeListener(this); 142 } 143 fireOverlayChanged(); 144 } 145 146 /** 147 * Returns a new list containing the domain crosshairs for this overlay. 148 * 149 * @return A list of crosshairs. 150 */ 151 public List getDomainCrosshairs() { 152 return new ArrayList(this.xCrosshairs); 153 } 154 155 /** 156 * Adds a crosshair against the range axis and sends an 157 * {@link OverlayChangeEvent} to all registered listeners. 158 * 159 * @param crosshair the crosshair (<code>null</code> not permitted). 160 */ 161 public void addRangeCrosshair(Crosshair crosshair) { 162 ParamChecks.nullNotPermitted(crosshair, "crosshair"); 163 this.yCrosshairs.add(crosshair); 164 crosshair.addPropertyChangeListener(this); 165 fireOverlayChanged(); 166 } 167 168 /** 169 * Removes a range axis crosshair and sends an {@link OverlayChangeEvent} 170 * to all registered listeners. 171 * 172 * @param crosshair the crosshair (<code>null</code> not permitted). 173 * 174 * @see #addRangeCrosshair(org.jfree.chart.plot.Crosshair) 175 */ 176 public void removeRangeCrosshair(Crosshair crosshair) { 177 ParamChecks.nullNotPermitted(crosshair, "crosshair"); 178 if (this.yCrosshairs.remove(crosshair)) { 179 crosshair.removePropertyChangeListener(this); 180 fireOverlayChanged(); 181 } 182 } 183 184 /** 185 * Clears all the range crosshairs from the overlay and sends an 186 * {@link OverlayChangeEvent} to all registered listeners. 187 */ 188 public void clearRangeCrosshairs() { 189 if (this.yCrosshairs.isEmpty()) { 190 return; // nothing to do 191 } 192 List crosshairs = getRangeCrosshairs(); 193 for (int i = 0; i < crosshairs.size(); i++) { 194 Crosshair c = (Crosshair) crosshairs.get(i); 195 this.yCrosshairs.remove(c); 196 c.removePropertyChangeListener(this); 197 } 198 fireOverlayChanged(); 199 } 200 201 /** 202 * Returns a new list containing the range crosshairs for this overlay. 203 * 204 * @return A list of crosshairs. 205 */ 206 public List getRangeCrosshairs() { 207 return new ArrayList(this.yCrosshairs); 208 } 209 210 /** 211 * Receives a property change event (typically a change in one of the 212 * crosshairs). 213 * 214 * @param e the event. 215 */ 216 @Override 217 public void propertyChange(PropertyChangeEvent e) { 218 fireOverlayChanged(); 219 } 220 221 /** 222 * Paints the crosshairs in the layer. 223 * 224 * @param g2 the graphics target. 225 * @param chartPanel the chart panel. 226 */ 227 @Override 228 public void paintOverlay(Graphics2D g2, ChartPanel chartPanel) { 229 Shape savedClip = g2.getClip(); 230 Rectangle2D dataArea = chartPanel.getScreenDataArea(); 231 g2.clip(dataArea); 232 JFreeChart chart = chartPanel.getChart(); 233 XYPlot plot = (XYPlot) chart.getPlot(); 234 ValueAxis xAxis = plot.getDomainAxis(); 235 RectangleEdge xAxisEdge = plot.getDomainAxisEdge(); 236 Iterator iterator = this.xCrosshairs.iterator(); 237 while (iterator.hasNext()) { 238 Crosshair ch = (Crosshair) iterator.next(); 239 if (ch.isVisible()) { 240 double x = ch.getValue(); 241 double xx = xAxis.valueToJava2D(x, dataArea, xAxisEdge); 242 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 243 drawVerticalCrosshair(g2, dataArea, xx, ch); 244 } 245 else { 246 drawHorizontalCrosshair(g2, dataArea, xx, ch); 247 } 248 } 249 } 250 ValueAxis yAxis = plot.getRangeAxis(); 251 RectangleEdge yAxisEdge = plot.getRangeAxisEdge(); 252 iterator = this.yCrosshairs.iterator(); 253 while (iterator.hasNext()) { 254 Crosshair ch = (Crosshair) iterator.next(); 255 if (ch.isVisible()) { 256 double y = ch.getValue(); 257 double yy = yAxis.valueToJava2D(y, dataArea, yAxisEdge); 258 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 259 drawHorizontalCrosshair(g2, dataArea, yy, ch); 260 } 261 else { 262 drawVerticalCrosshair(g2, dataArea, yy, ch); 263 } 264 } 265 } 266 g2.setClip(savedClip); 267 } 268 269 /** 270 * Draws a crosshair horizontally across the plot. 271 * 272 * @param g2 the graphics target. 273 * @param dataArea the data area. 274 * @param y the y-value in Java2D space. 275 * @param crosshair the crosshair. 276 */ 277 protected void drawHorizontalCrosshair(Graphics2D g2, Rectangle2D dataArea, 278 double y, Crosshair crosshair) { 279 280 if (y >= dataArea.getMinY() && y <= dataArea.getMaxY()) { 281 Line2D line = new Line2D.Double(dataArea.getMinX(), y, 282 dataArea.getMaxX(), y); 283 Paint savedPaint = g2.getPaint(); 284 Stroke savedStroke = g2.getStroke(); 285 g2.setPaint(crosshair.getPaint()); 286 g2.setStroke(crosshair.getStroke()); 287 g2.draw(line); 288 if (crosshair.isLabelVisible()) { 289 String label = crosshair.getLabelGenerator().generateLabel( 290 crosshair); 291 RectangleAnchor anchor = crosshair.getLabelAnchor(); 292 Point2D pt = calculateLabelPoint(line, anchor, 5, 5); 293 float xx = (float) pt.getX(); 294 float yy = (float) pt.getY(); 295 TextAnchor alignPt = textAlignPtForLabelAnchorH(anchor); 296 Shape hotspot = TextUtilities.calculateRotatedStringBounds( 297 label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER); 298 if (!dataArea.contains(hotspot.getBounds2D())) { 299 anchor = flipAnchorV(anchor); 300 pt = calculateLabelPoint(line, anchor, 5, 5); 301 xx = (float) pt.getX(); 302 yy = (float) pt.getY(); 303 alignPt = textAlignPtForLabelAnchorH(anchor); 304 hotspot = TextUtilities.calculateRotatedStringBounds( 305 label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER); 306 } 307 308 g2.setPaint(crosshair.getLabelBackgroundPaint()); 309 g2.fill(hotspot); 310 g2.setPaint(crosshair.getLabelOutlinePaint()); 311 g2.draw(hotspot); 312 TextUtilities.drawAlignedString(label, g2, xx, yy, alignPt); 313 } 314 g2.setPaint(savedPaint); 315 g2.setStroke(savedStroke); 316 } 317 } 318 319 /** 320 * Draws a crosshair vertically on the plot. 321 * 322 * @param g2 the graphics target. 323 * @param dataArea the data area. 324 * @param x the x-value in Java2D space. 325 * @param crosshair the crosshair. 326 */ 327 protected void drawVerticalCrosshair(Graphics2D g2, Rectangle2D dataArea, 328 double x, Crosshair crosshair) { 329 330 if (x >= dataArea.getMinX() && x <= dataArea.getMaxX()) { 331 Line2D line = new Line2D.Double(x, dataArea.getMinY(), x, 332 dataArea.getMaxY()); 333 Paint savedPaint = g2.getPaint(); 334 Stroke savedStroke = g2.getStroke(); 335 g2.setPaint(crosshair.getPaint()); 336 g2.setStroke(crosshair.getStroke()); 337 g2.draw(line); 338 if (crosshair.isLabelVisible()) { 339 String label = crosshair.getLabelGenerator().generateLabel( 340 crosshair); 341 RectangleAnchor anchor = crosshair.getLabelAnchor(); 342 Point2D pt = calculateLabelPoint(line, anchor, 5, 5); 343 float xx = (float) pt.getX(); 344 float yy = (float) pt.getY(); 345 TextAnchor alignPt = textAlignPtForLabelAnchorV(anchor); 346 Shape hotspot = TextUtilities.calculateRotatedStringBounds( 347 label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER); 348 if (!dataArea.contains(hotspot.getBounds2D())) { 349 anchor = flipAnchorH(anchor); 350 pt = calculateLabelPoint(line, anchor, 5, 5); 351 xx = (float) pt.getX(); 352 yy = (float) pt.getY(); 353 alignPt = textAlignPtForLabelAnchorV(anchor); 354 hotspot = TextUtilities.calculateRotatedStringBounds( 355 label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER); 356 } 357 g2.setPaint(crosshair.getLabelBackgroundPaint()); 358 g2.fill(hotspot); 359 g2.setPaint(crosshair.getLabelOutlinePaint()); 360 g2.draw(hotspot); 361 TextUtilities.drawAlignedString(label, g2, xx, yy, alignPt); 362 } 363 g2.setPaint(savedPaint); 364 g2.setStroke(savedStroke); 365 } 366 } 367 368 /** 369 * Calculates the anchor point for a label. 370 * 371 * @param line the line for the crosshair. 372 * @param anchor the anchor point. 373 * @param deltaX the x-offset. 374 * @param deltaY the y-offset. 375 * 376 * @return The anchor point. 377 */ 378 private Point2D calculateLabelPoint(Line2D line, RectangleAnchor anchor, 379 double deltaX, double deltaY) { 380 double x, y; 381 boolean left = (anchor == RectangleAnchor.BOTTOM_LEFT 382 || anchor == RectangleAnchor.LEFT 383 || anchor == RectangleAnchor.TOP_LEFT); 384 boolean right = (anchor == RectangleAnchor.BOTTOM_RIGHT 385 || anchor == RectangleAnchor.RIGHT 386 || anchor == RectangleAnchor.TOP_RIGHT); 387 boolean top = (anchor == RectangleAnchor.TOP_LEFT 388 || anchor == RectangleAnchor.TOP 389 || anchor == RectangleAnchor.TOP_RIGHT); 390 boolean bottom = (anchor == RectangleAnchor.BOTTOM_LEFT 391 || anchor == RectangleAnchor.BOTTOM 392 || anchor == RectangleAnchor.BOTTOM_RIGHT); 393 Rectangle rect = line.getBounds(); 394 395 // we expect the line to be vertical or horizontal 396 if (line.getX1() == line.getX2()) { // vertical 397 x = line.getX1(); 398 y = (line.getY1() + line.getY2()) / 2.0; 399 if (left) { 400 x = x - deltaX; 401 } 402 if (right) { 403 x = x + deltaX; 404 } 405 if (top) { 406 y = Math.min(line.getY1(), line.getY2()) + deltaY; 407 } 408 if (bottom) { 409 y = Math.max(line.getY1(), line.getY2()) - deltaY; 410 } 411 } 412 else { // horizontal 413 x = (line.getX1() + line.getX2()) / 2.0; 414 y = line.getY1(); 415 if (left) { 416 x = Math.min(line.getX1(), line.getX2()) + deltaX; 417 } 418 if (right) { 419 x = Math.max(line.getX1(), line.getX2()) - deltaX; 420 } 421 if (top) { 422 y = y - deltaY; 423 } 424 if (bottom) { 425 y = y + deltaY; 426 } 427 } 428 return new Point2D.Double(x, y); 429 } 430 431 /** 432 * Returns the text anchor that is used to align a label to its anchor 433 * point. 434 * 435 * @param anchor the anchor. 436 * 437 * @return The text alignment point. 438 */ 439 private TextAnchor textAlignPtForLabelAnchorV(RectangleAnchor anchor) { 440 TextAnchor result = TextAnchor.CENTER; 441 if (anchor.equals(RectangleAnchor.TOP_LEFT)) { 442 result = TextAnchor.TOP_RIGHT; 443 } 444 else if (anchor.equals(RectangleAnchor.TOP)) { 445 result = TextAnchor.TOP_CENTER; 446 } 447 else if (anchor.equals(RectangleAnchor.TOP_RIGHT)) { 448 result = TextAnchor.TOP_LEFT; 449 } 450 else if (anchor.equals(RectangleAnchor.LEFT)) { 451 result = TextAnchor.HALF_ASCENT_RIGHT; 452 } 453 else if (anchor.equals(RectangleAnchor.RIGHT)) { 454 result = TextAnchor.HALF_ASCENT_LEFT; 455 } 456 else if (anchor.equals(RectangleAnchor.BOTTOM_LEFT)) { 457 result = TextAnchor.BOTTOM_RIGHT; 458 } 459 else if (anchor.equals(RectangleAnchor.BOTTOM)) { 460 result = TextAnchor.BOTTOM_CENTER; 461 } 462 else if (anchor.equals(RectangleAnchor.BOTTOM_RIGHT)) { 463 result = TextAnchor.BOTTOM_LEFT; 464 } 465 return result; 466 } 467 468 /** 469 * Returns the text anchor that is used to align a label to its anchor 470 * point. 471 * 472 * @param anchor the anchor. 473 * 474 * @return The text alignment point. 475 */ 476 private TextAnchor textAlignPtForLabelAnchorH(RectangleAnchor anchor) { 477 TextAnchor result = TextAnchor.CENTER; 478 if (anchor.equals(RectangleAnchor.TOP_LEFT)) { 479 result = TextAnchor.BOTTOM_LEFT; 480 } 481 else if (anchor.equals(RectangleAnchor.TOP)) { 482 result = TextAnchor.BOTTOM_CENTER; 483 } 484 else if (anchor.equals(RectangleAnchor.TOP_RIGHT)) { 485 result = TextAnchor.BOTTOM_RIGHT; 486 } 487 else if (anchor.equals(RectangleAnchor.LEFT)) { 488 result = TextAnchor.HALF_ASCENT_LEFT; 489 } 490 else if (anchor.equals(RectangleAnchor.RIGHT)) { 491 result = TextAnchor.HALF_ASCENT_RIGHT; 492 } 493 else if (anchor.equals(RectangleAnchor.BOTTOM_LEFT)) { 494 result = TextAnchor.TOP_LEFT; 495 } 496 else if (anchor.equals(RectangleAnchor.BOTTOM)) { 497 result = TextAnchor.TOP_CENTER; 498 } 499 else if (anchor.equals(RectangleAnchor.BOTTOM_RIGHT)) { 500 result = TextAnchor.TOP_RIGHT; 501 } 502 return result; 503 } 504 505 private RectangleAnchor flipAnchorH(RectangleAnchor anchor) { 506 RectangleAnchor result = anchor; 507 if (anchor.equals(RectangleAnchor.TOP_LEFT)) { 508 result = RectangleAnchor.TOP_RIGHT; 509 } 510 else if (anchor.equals(RectangleAnchor.TOP_RIGHT)) { 511 result = RectangleAnchor.TOP_LEFT; 512 } 513 else if (anchor.equals(RectangleAnchor.LEFT)) { 514 result = RectangleAnchor.RIGHT; 515 } 516 else if (anchor.equals(RectangleAnchor.RIGHT)) { 517 result = RectangleAnchor.LEFT; 518 } 519 else if (anchor.equals(RectangleAnchor.BOTTOM_LEFT)) { 520 result = RectangleAnchor.BOTTOM_RIGHT; 521 } 522 else if (anchor.equals(RectangleAnchor.BOTTOM_RIGHT)) { 523 result = RectangleAnchor.BOTTOM_LEFT; 524 } 525 return result; 526 } 527 528 private RectangleAnchor flipAnchorV(RectangleAnchor anchor) { 529 RectangleAnchor result = anchor; 530 if (anchor.equals(RectangleAnchor.TOP_LEFT)) { 531 result = RectangleAnchor.BOTTOM_LEFT; 532 } 533 else if (anchor.equals(RectangleAnchor.TOP_RIGHT)) { 534 result = RectangleAnchor.BOTTOM_RIGHT; 535 } 536 else if (anchor.equals(RectangleAnchor.TOP)) { 537 result = RectangleAnchor.BOTTOM; 538 } 539 else if (anchor.equals(RectangleAnchor.BOTTOM)) { 540 result = RectangleAnchor.TOP; 541 } 542 else if (anchor.equals(RectangleAnchor.BOTTOM_LEFT)) { 543 result = RectangleAnchor.TOP_LEFT; 544 } 545 else if (anchor.equals(RectangleAnchor.BOTTOM_RIGHT)) { 546 result = RectangleAnchor.TOP_RIGHT; 547 } 548 return result; 549 } 550 551 /** 552 * Tests this overlay for equality with an arbitrary object. 553 * 554 * @param obj the object (<code>null</code> permitted). 555 * 556 * @return A boolean. 557 */ 558 @Override 559 public boolean equals(Object obj) { 560 if (obj == this) { 561 return true; 562 } 563 if (!(obj instanceof CrosshairOverlay)) { 564 return false; 565 } 566 CrosshairOverlay that = (CrosshairOverlay) obj; 567 if (!this.xCrosshairs.equals(that.xCrosshairs)) { 568 return false; 569 } 570 if (!this.yCrosshairs.equals(that.yCrosshairs)) { 571 return false; 572 } 573 return true; 574 } 575 576 /** 577 * Returns a clone of this instance. 578 * 579 * @return A clone of this instance. 580 * 581 * @throws java.lang.CloneNotSupportedException if there is some problem 582 * with the cloning. 583 */ 584 @Override 585 public Object clone() throws CloneNotSupportedException { 586 CrosshairOverlay clone = (CrosshairOverlay) super.clone(); 587 clone.xCrosshairs = (List) ObjectUtilities.deepClone(this.xCrosshairs); 588 clone.yCrosshairs = (List) ObjectUtilities.deepClone(this.yCrosshairs); 589 return clone; 590 } 591 592}