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