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