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