001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, 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-2022, by David Gilbert.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   John Matthews, Michal Wozniak;
034 *
035 */
036
037package org.jfree.chart.swing;
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.JFreeChart;
054import org.jfree.chart.axis.ValueAxis;
055import org.jfree.chart.plot.Crosshair;
056import org.jfree.chart.plot.PlotOrientation;
057import org.jfree.chart.plot.XYPlot;
058import org.jfree.chart.text.TextUtils;
059import org.jfree.chart.api.RectangleAnchor;
060import org.jfree.chart.api.RectangleEdge;
061import org.jfree.chart.text.TextAnchor;
062import org.jfree.chart.internal.CloneUtils;
063import org.jfree.chart.internal.Args;
064import org.jfree.chart.api.PublicCloneable;
065
066/**
067 * An overlay for a {@link ChartPanel} that draws crosshairs on a chart.  If 
068 * you are using the JavaFX extensions for JFreeChart, then you should use
069 * the {@code CrosshairOverlayFX} class.
070 */
071public class CrosshairOverlay extends AbstractOverlay implements Overlay,
072        PropertyChangeListener, PublicCloneable, Cloneable, Serializable {
073
074    /** Storage for the crosshairs along the x-axis. */
075    protected List<Crosshair> xCrosshairs;
076
077    /** Storage for the crosshairs along the y-axis. */
078    protected List<Crosshair> yCrosshairs;
079
080    /**
081     * Creates a new overlay that initially contains no crosshairs.
082     */
083    public CrosshairOverlay() {
084        super();
085        this.xCrosshairs = new ArrayList<>();
086        this.yCrosshairs = new ArrayList<>();
087    }
088
089    /**
090     * Adds a crosshair against the domain axis (x-axis) and sends an
091     * {@link OverlayChangeEvent} to all registered listeners.
092     *
093     * @param crosshair  the crosshair ({@code null} not permitted).
094     *
095     * @see #removeDomainCrosshair(org.jfree.chart.plot.Crosshair)
096     * @see #addRangeCrosshair(org.jfree.chart.plot.Crosshair)
097     */
098    public void addDomainCrosshair(Crosshair crosshair) {
099        Args.nullNotPermitted(crosshair, "crosshair");
100        this.xCrosshairs.add(crosshair);
101        crosshair.addPropertyChangeListener(this);
102        fireOverlayChanged();
103    }
104
105    /**
106     * Removes a domain axis crosshair and sends an {@link OverlayChangeEvent}
107     * to all registered listeners.
108     *
109     * @param crosshair  the crosshair ({@code null} not permitted).
110     *
111     * @see #addDomainCrosshair(org.jfree.chart.plot.Crosshair)
112     */
113    public void removeDomainCrosshair(Crosshair crosshair) {
114        Args.nullNotPermitted(crosshair, "crosshair");
115        if (this.xCrosshairs.remove(crosshair)) {
116            crosshair.removePropertyChangeListener(this);
117            fireOverlayChanged();
118        }
119    }
120
121    /**
122     * Clears all the domain crosshairs from the overlay and sends an
123     * {@link OverlayChangeEvent} to all registered listeners (unless there
124     * were no crosshairs to begin with).
125     */
126    public void clearDomainCrosshairs() {
127        if (this.xCrosshairs.isEmpty()) {
128            return;  // nothing to do - avoids firing change event
129        }
130        for (Crosshair c : getDomainCrosshairs()) {
131            this.xCrosshairs.remove(c);
132            c.removePropertyChangeListener(this);
133        }
134        fireOverlayChanged();
135    }
136
137    /**
138     * Returns a new list containing the domain crosshairs for this overlay.
139     *
140     * @return A list of crosshairs.
141     */
142    public List<Crosshair> getDomainCrosshairs() {
143        return new ArrayList<>(this.xCrosshairs);
144    }
145
146    /**
147     * Adds a crosshair against the range axis and sends an
148     * {@link OverlayChangeEvent} to all registered listeners.
149     *
150     * @param crosshair  the crosshair ({@code null} not permitted).
151     */
152    public void addRangeCrosshair(Crosshair crosshair) {
153        Args.nullNotPermitted(crosshair, "crosshair");
154        this.yCrosshairs.add(crosshair);
155        crosshair.addPropertyChangeListener(this);
156        fireOverlayChanged();
157    }
158
159    /**
160     * Removes a range axis crosshair and sends an {@link OverlayChangeEvent}
161     * to all registered listeners.
162     *
163     * @param crosshair  the crosshair ({@code null} not permitted).
164     *
165     * @see #addRangeCrosshair(org.jfree.chart.plot.Crosshair)
166     */
167    public void removeRangeCrosshair(Crosshair crosshair) {
168        Args.nullNotPermitted(crosshair, "crosshair");
169        if (this.yCrosshairs.remove(crosshair)) {
170            crosshair.removePropertyChangeListener(this);
171            fireOverlayChanged();
172        }
173    }
174
175    /**
176     * Clears all the range crosshairs from the overlay and sends an
177     * {@link OverlayChangeEvent} to all registered listeners (unless there
178     * were no crosshairs to begin with).
179     */
180    public void clearRangeCrosshairs() {
181        if (this.yCrosshairs.isEmpty()) {
182            return;  // nothing to do - avoids change notification
183        }
184        for (Crosshair c : getRangeCrosshairs()) {
185            this.yCrosshairs.remove(c);
186            c.removePropertyChangeListener(this);
187        }
188        fireOverlayChanged();
189    }
190
191    /**
192     * Returns a new list containing the range crosshairs for this overlay.
193     *
194     * @return A list of crosshairs.
195     */
196    public List<Crosshair> getRangeCrosshairs() {
197        return new ArrayList<>(this.yCrosshairs);
198    }
199
200    /**
201     * Receives a property change event (typically a change in one of the
202     * crosshairs).
203     *
204     * @param e  the event.
205     */
206    @Override
207    public void propertyChange(PropertyChangeEvent e) {
208        fireOverlayChanged();
209    }
210
211    /**
212     * Renders the crosshairs in the overlay on top of the chart that has just
213     * been rendered in the specified {@code chartPanel}.  This method is
214     * called by the JFreeChart framework, you won't normally call it from
215     * user code.
216     *
217     * @param g2  the graphics target.
218     * @param chartPanel  the chart panel.
219     */
220    @Override
221    public void paintOverlay(Graphics2D g2, ChartPanel chartPanel) {
222        Shape savedClip = g2.getClip();
223        Rectangle2D dataArea = chartPanel.getScreenDataArea();
224        g2.clip(dataArea);
225        JFreeChart chart = chartPanel.getChart();
226        XYPlot plot = (XYPlot) chart.getPlot();
227        ValueAxis xAxis = plot.getDomainAxis();
228        RectangleEdge xAxisEdge = plot.getDomainAxisEdge();
229        for (Crosshair ch : getDomainCrosshairs()) {
230            if (ch.isVisible()) {
231                double x = ch.getValue();
232                double xx = xAxis.valueToJava2D(x, dataArea, xAxisEdge);
233                if (plot.getOrientation() == PlotOrientation.VERTICAL) {
234                    drawVerticalCrosshair(g2, dataArea, xx, ch);
235                } else {
236                    drawHorizontalCrosshair(g2, dataArea, xx, ch);
237                }
238            }
239        }
240        ValueAxis yAxis = plot.getRangeAxis();
241        RectangleEdge yAxisEdge = plot.getRangeAxisEdge();
242        for (Crosshair ch : getRangeCrosshairs()) {
243            if (ch.isVisible()) {
244                double y = ch.getValue();
245                double yy = yAxis.valueToJava2D(y, dataArea, yAxisEdge);
246                if (plot.getOrientation() == PlotOrientation.VERTICAL) {
247                    drawHorizontalCrosshair(g2, dataArea, yy, ch);
248                } else {
249                    drawVerticalCrosshair(g2, dataArea, yy, ch);
250                }
251            }
252        }
253        g2.setClip(savedClip);
254    }
255
256    /**
257     * Draws a crosshair horizontally across the plot.
258     *
259     * @param g2  the graphics target.
260     * @param dataArea  the data area.
261     * @param y  the y-value in Java2D space.
262     * @param crosshair  the crosshair.
263     */
264    protected void drawHorizontalCrosshair(Graphics2D g2, Rectangle2D dataArea,
265            double y, Crosshair crosshair) {
266
267        if (y >= dataArea.getMinY() && y <= dataArea.getMaxY()) {
268            Line2D line = new Line2D.Double(dataArea.getMinX(), y,
269                    dataArea.getMaxX(), y);
270            Paint savedPaint = g2.getPaint();
271            Stroke savedStroke = g2.getStroke();
272            g2.setPaint(crosshair.getPaint());
273            g2.setStroke(crosshair.getStroke());
274            g2.draw(line);
275            if (crosshair.isLabelVisible()) {
276                String label = crosshair.getLabelGenerator().generateLabel(
277                        crosshair);
278                if (label != null && !label.isEmpty()) {
279                    Font savedFont = g2.getFont();
280                    g2.setFont(crosshair.getLabelFont());
281                    RectangleAnchor anchor = crosshair.getLabelAnchor();
282                    Point2D pt = calculateLabelPoint(line, anchor, crosshair.getLabelXOffset(), crosshair.getLabelYOffset());
283                    float xx = (float) pt.getX();
284                    float yy = (float) pt.getY();
285                    TextAnchor alignPt = textAlignPtForLabelAnchorH(anchor);
286                    Shape hotspot = TextUtils.calculateRotatedStringBounds(
287                            label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER);
288                    if (!dataArea.contains(hotspot.getBounds2D())) {
289                        anchor = flipAnchorV(anchor);
290                        pt = calculateLabelPoint(line, anchor, crosshair.getLabelXOffset(), crosshair.getLabelYOffset());
291                        xx = (float) pt.getX();
292                        yy = (float) pt.getY();
293                        alignPt = textAlignPtForLabelAnchorH(anchor);
294                        hotspot = TextUtils.calculateRotatedStringBounds(
295                               label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER);
296                    }
297
298                    g2.setPaint(crosshair.getLabelBackgroundPaint());
299                    g2.fill(hotspot);
300                    if (crosshair.isLabelOutlineVisible()) {
301                        g2.setPaint(crosshair.getLabelOutlinePaint());
302                        g2.setStroke(crosshair.getLabelOutlineStroke());
303                        g2.draw(hotspot);
304                    }
305                    g2.setPaint(crosshair.getLabelPaint());
306                    TextUtils.drawAlignedString(label, g2, xx, yy, alignPt);
307                    g2.setFont(savedFont);
308                }
309            }
310            g2.setPaint(savedPaint);
311            g2.setStroke(savedStroke);
312        }
313    }
314
315    /**
316     * Draws a crosshair vertically on the plot.
317     *
318     * @param g2  the graphics target.
319     * @param dataArea  the data area.
320     * @param x  the x-value in Java2D space.
321     * @param crosshair  the crosshair.
322     */
323    protected void drawVerticalCrosshair(Graphics2D g2, Rectangle2D dataArea,
324            double x, Crosshair crosshair) {
325
326        if (x >= dataArea.getMinX() && x <= dataArea.getMaxX()) {
327            Line2D line = new Line2D.Double(x, dataArea.getMinY(), x,
328                    dataArea.getMaxY());
329            Paint savedPaint = g2.getPaint();
330            Stroke savedStroke = g2.getStroke();
331            g2.setPaint(crosshair.getPaint());
332            g2.setStroke(crosshair.getStroke());
333            g2.draw(line);
334            if (crosshair.isLabelVisible()) {
335                String label = crosshair.getLabelGenerator().generateLabel(
336                        crosshair);
337                if (label != null && !label.isEmpty()) {
338                    Font savedFont = g2.getFont();
339                    g2.setFont(crosshair.getLabelFont());
340                    RectangleAnchor anchor = crosshair.getLabelAnchor();
341                    Point2D pt = calculateLabelPoint(line, anchor, crosshair.getLabelXOffset(), crosshair.getLabelYOffset());
342                    float xx = (float) pt.getX();
343                    float yy = (float) pt.getY();
344                    TextAnchor alignPt = textAlignPtForLabelAnchorV(anchor);
345                    Shape hotspot = TextUtils.calculateRotatedStringBounds(
346                            label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER);
347                    if (!dataArea.contains(hotspot.getBounds2D())) {
348                        anchor = flipAnchorH(anchor);
349                        pt = calculateLabelPoint(line, anchor, crosshair.getLabelXOffset(), crosshair.getLabelYOffset());
350                        xx = (float) pt.getX();
351                        yy = (float) pt.getY();
352                        alignPt = textAlignPtForLabelAnchorV(anchor);
353                        hotspot = TextUtils.calculateRotatedStringBounds(
354                               label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER);
355                    }
356                    g2.setPaint(crosshair.getLabelBackgroundPaint());
357                    g2.fill(hotspot);
358                    if (crosshair.isLabelOutlineVisible()) {
359                        g2.setPaint(crosshair.getLabelOutlinePaint());
360                        g2.setStroke(crosshair.getLabelOutlineStroke());
361                        g2.draw(hotspot);
362                    }
363                    g2.setPaint(crosshair.getLabelPaint());
364                    TextUtils.drawAlignedString(label, g2, xx, yy, alignPt);
365                    g2.setFont(savedFont);
366                }
367            }
368            g2.setPaint(savedPaint);
369            g2.setStroke(savedStroke);
370        }
371    }
372
373    /**
374     * Calculates the anchor point for a label.
375     *
376     * @param line  the line for the crosshair.
377     * @param anchor  the anchor point.
378     * @param deltaX  the x-offset.
379     * @param deltaY  the y-offset.
380     *
381     * @return The anchor point.
382     */
383    private Point2D calculateLabelPoint(Line2D line, RectangleAnchor anchor,
384            double deltaX, double deltaY) {
385        double x, y;
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} permitted).
560     *
561     * @return A boolean.
562     */
563    @Override
564    public boolean equals(Object obj) {
565        if (obj == this) {
566            return true;
567        }
568        if (!(obj instanceof CrosshairOverlay)) {
569            return false;
570        }
571        CrosshairOverlay that = (CrosshairOverlay) obj;
572        if (!this.xCrosshairs.equals(that.xCrosshairs)) {
573            return false;
574        }
575        if (!this.yCrosshairs.equals(that.yCrosshairs)) {
576            return false;
577        }
578        return true;
579    }
580
581    /**
582     * Returns a clone of this instance.
583     *
584     * @return A clone of this instance.
585     *
586     * @throws java.lang.CloneNotSupportedException if there is some problem
587     *     with the cloning.
588     */
589    @Override
590    public Object clone() throws CloneNotSupportedException {
591        CrosshairOverlay clone = (CrosshairOverlay) super.clone();
592        clone.xCrosshairs = (List) CloneUtils.cloneList(this.xCrosshairs);
593        clone.yCrosshairs = (List) CloneUtils.cloneList(this.yCrosshairs);
594        return clone;
595    }
596
597}