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