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
041package org.jfree.chart.panel;
042
043import java.awt.Graphics2D;
044import java.awt.Paint;
045import java.awt.Rectangle;
046import java.awt.Shape;
047import java.awt.Stroke;
048import java.awt.geom.Line2D;
049import java.awt.geom.Point2D;
050import java.awt.geom.Rectangle2D;
051import java.beans.PropertyChangeEvent;
052import java.beans.PropertyChangeListener;
053import java.io.Serializable;
054import java.util.ArrayList;
055import java.util.Iterator;
056import java.util.List;
057import org.jfree.chart.ChartPanel;
058import org.jfree.chart.JFreeChart;
059import org.jfree.chart.axis.ValueAxis;
060import org.jfree.chart.plot.Crosshair;
061import org.jfree.chart.plot.PlotOrientation;
062import org.jfree.chart.plot.XYPlot;
063import org.jfree.text.TextUtilities;
064import org.jfree.ui.RectangleAnchor;
065import org.jfree.ui.RectangleEdge;
066import org.jfree.ui.TextAnchor;
067import org.jfree.util.ObjectUtilities;
068import 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 */
075public 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}