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