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