    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
    042package org.jfree.chart.panel;
    043
    044import java.awt.Graphics2D;
    045import java.awt.Paint;
    046import java.awt.Rectangle;
    047import java.awt.Shape;
    048import java.awt.Stroke;
    049import java.awt.geom.Line2D;
    050import java.awt.geom.Point2D;
    051import java.awt.geom.Rectangle2D;
    052import java.beans.PropertyChangeEvent;
    053import java.beans.PropertyChangeListener;
    054import java.io.Serializable;
    055import java.util.ArrayList;
    056import java.util.Iterator;
    057import java.util.List;
    058import org.jfree.chart.ChartPanel;
    059import org.jfree.chart.JFreeChart;
    060import org.jfree.chart.axis.ValueAxis;
    061import org.jfree.chart.event.OverlayChangeEvent;
    062import org.jfree.chart.plot.Crosshair;
    063import org.jfree.chart.plot.PlotOrientation;
    064import org.jfree.chart.plot.XYPlot;
    065import org.jfree.text.TextUtilities;
    066import org.jfree.ui.RectangleAnchor;
    067import org.jfree.ui.RectangleEdge;
    068import org.jfree.ui.TextAnchor;
    069import org.jfree.util.ObjectUtilities;
    070import 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 */
    077public 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}