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}