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