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