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