    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 * XYShapeRenderer.java
    029 * --------------------
    030 * (C) Copyright 2008-2011 by Andreas Haumer, xS+S and Contributors.
    031 *
    032 * Original Author:  Martin Hoeller (x Software + Systeme  xS+S - Andreas
    033 *                       Haumer);
    034 * Contributor(s):   David Gilbert (for Object Refinery Limited);
    035 *
    036 * Changes:
    037 * --------
    038 * 17-Sep-2008 : Version 1, based on a contribution from Martin Hoeller with
    039 *               amendments by David Gilbert (DG);
    040 * 16-Feb-2010 : Added findZBounds() (patch 2952086) (MH);
    041 * 19-Oct-2011 : Fixed NPE in findRangeBounds() (bug 3026341) (DG);
    042 *
    043 */
    044
    045package org.jfree.chart.renderer.xy;
    046
    047import java.awt.BasicStroke;
    048import java.awt.Color;
    049import java.awt.Graphics2D;
    050import java.awt.Paint;
    051import java.awt.Shape;
    052import java.awt.Stroke;
    053import java.awt.geom.Ellipse2D;
    054import java.awt.geom.Line2D;
    055import java.awt.geom.Rectangle2D;
    056import java.io.IOException;
    057import java.io.ObjectInputStream;
    058import java.io.ObjectOutputStream;
    059import java.io.Serializable;
    060
    061import org.jfree.chart.axis.ValueAxis;
    062import org.jfree.chart.entity.EntityCollection;
    063import org.jfree.chart.event.RendererChangeEvent;
    064import org.jfree.chart.plot.CrosshairState;
    065import org.jfree.chart.plot.PlotOrientation;
    066import org.jfree.chart.plot.PlotRenderingInfo;
    067import org.jfree.chart.plot.XYPlot;
    068import org.jfree.chart.renderer.LookupPaintScale;
    069import org.jfree.chart.renderer.PaintScale;
    070import org.jfree.data.Range;
    071import org.jfree.data.general.DatasetUtilities;
    072import org.jfree.data.xy.XYDataset;
    073import org.jfree.data.xy.XYZDataset;
    074import org.jfree.io.SerialUtilities;
    075import org.jfree.util.PublicCloneable;
    076import org.jfree.util.ShapeUtilities;
    077
    078/**
    079 * A renderer that draws shapes at (x, y) coordinates and, if the dataset
    080 * is an instance of {@link XYZDataset}, fills the shapes with a paint that
    081 * is based on the z-value (the paint is obtained from a lookup table).  The
    082 * renderer also allows for optional guidelines, horizontal and vertical lines
    083 * connecting the shape to the edges of the plot.
    084 * <br><br>
    085 * The example shown here is generated by the
    086 * <code>XYShapeRendererDemo1.java</code> program included in the JFreeChart
    087 * demo collection:
    088 * <br><br>
    089 * <img src="../../../../../images/XYShapeRendererSample.png"
    090 * alt="XYShapeRendererSample.png" />
    091 * <br><br>
    092 * This renderer has similarities to, but also differences from, the
    093 * {@link XYLineAndShapeRenderer}.
    094 *
    095 * @since 1.0.11
    096 */
    097public class XYShapeRenderer extends AbstractXYItemRenderer
    098        implements XYItemRenderer, Cloneable, Serializable {
    099
    100    /** Auto generated serial version id. */
    101    private static final long serialVersionUID = 8320552104211173221L;
    102
    103    /** The paint scale (never null). */
    104    private PaintScale paintScale;
    105
    106    /** A flag that controls whether or not the shape outlines are drawn. */
    107    private boolean drawOutlines;
    108
    109    /**
    110     * A flag that controls whether or not the outline paint is used (if not,
    111     * the regular paint is used).
    112     */
    113    private boolean useOutlinePaint;
    114
    115    /**
    116     * A flag that controls whether or not the fill paint is used (if not,
    117     * the fill paint is used).
    118     */
    119    private boolean useFillPaint;
    120
    121    /** Flag indicating if guide lines should be drawn for every item. */
    122    private boolean guideLinesVisible;
    123
    124    /** The paint used for drawing the guide lines (never null). */
    125    private transient Paint guideLinePaint;
    126
    127    /** The stroke used for drawing the guide lines (never null). */
    128    private transient Stroke guideLineStroke;
    129
    130    /**
    131     * Creates a new <code>XYShapeRenderer</code> instance with default
    132     * attributes.
    133     */
    134    public XYShapeRenderer() {
    135        this.paintScale = new LookupPaintScale();
    136        this.useFillPaint = false;
    137        this.drawOutlines = false;
    138        this.useOutlinePaint = true;
    139        this.guideLinesVisible = false;
    140        this.guideLinePaint = Color.darkGray;
    141        this.guideLineStroke = new BasicStroke();
    142        setBaseShape(new Ellipse2D.Double(-5.0, -5.0, 10.0, 10.0));
    143        setAutoPopulateSeriesShape(false);
    144    }
    145
    146    /**
    147     * Returns the paint scale used by the renderer.
    148     *
    149     * @return The paint scale (never <code>null</code>).
    150     *
    151     * @see #setPaintScale(PaintScale)
    152     */
    153    public PaintScale getPaintScale() {
    154        return this.paintScale;
    155    }
    156
    157    /**
    158     * Sets the paint scale used by the renderer and sends a
    159     * {@link RendererChangeEvent} to all registered listeners.
    160     *
    161     * @param scale  the scale (<code>null</code> not permitted).
    162     *
    163     * @see #getPaintScale()
    164     */
    165    public void setPaintScale(PaintScale scale) {
    166        if (scale == null) {
    167            throw new IllegalArgumentException("Null 'scale' argument.");
    168        }
    169        this.paintScale = scale;
    170        notifyListeners(new RendererChangeEvent(this));
    171    }
    172
    173    /**
    174     * Returns <code>true</code> if outlines should be drawn for shapes, and
    175     * <code>false</code> otherwise.
    176     *
    177     * @return A boolean.
    178     *
    179     * @see #setDrawOutlines(boolean)
    180     */
    181    public boolean getDrawOutlines() {
    182        return this.drawOutlines;
    183    }
    184
    185    /**
    186     * Sets the flag that controls whether outlines are drawn for
    187     * shapes, and sends a {@link RendererChangeEvent} to all registered
    188     * listeners.
    189     * <P>
    190     * In some cases, shapes look better if they do NOT have an outline, but
    191     * this flag allows you to set your own preference.
    192     *
    193     * @param flag  the flag.
    194     *
    195     * @see #getDrawOutlines()
    196     */
    197    public void setDrawOutlines(boolean flag) {
    198        this.drawOutlines = flag;
    199        fireChangeEvent();
    200    }
    201
    202    /**
    203     * Returns <code>true</code> if the renderer should use the fill paint
    204     * setting to fill shapes, and <code>false</code> if it should just
    205     * use the regular paint.
    206     * <p>
    207     * Refer to <code>XYLineAndShapeRendererDemo2.java</code> to see the
    208     * effect of this flag.
    209     *
    210     * @return A boolean.
    211     *
    212     * @see #setUseFillPaint(boolean)
    213     * @see #getUseOutlinePaint()
    214     */
    215    public boolean getUseFillPaint() {
    216        return this.useFillPaint;
    217    }
    218
    219    /**
    220     * Sets the flag that controls whether the fill paint is used to fill
    221     * shapes, and sends a {@link RendererChangeEvent} to all
    222     * registered listeners.
    223     *
    224     * @param flag  the flag.
    225     *
    226     * @see #getUseFillPaint()
    227     */
    228    public void setUseFillPaint(boolean flag) {
    229        this.useFillPaint = flag;
    230        fireChangeEvent();
    231    }
    232
    233    /**
    234     * Returns the flag that controls whether the outline paint is used for
    235     * shape outlines.  If not, the regular series paint is used.
    236     *
    237     * @return A boolean.
    238     *
    239     * @see #setUseOutlinePaint(boolean)
    240     */
    241    public boolean getUseOutlinePaint() {
    242        return this.useOutlinePaint;
    243    }
    244
    245    /**
    246     * Sets the flag that controls whether the outline paint is used for shape
    247     * outlines, and sends a {@link RendererChangeEvent} to all registered
    248     * listeners.
    249     *
    250     * @param use  the flag.
    251     *
    252     * @see #getUseOutlinePaint()
    253     */
    254    public void setUseOutlinePaint(boolean use) {
    255        this.useOutlinePaint = use;
    256        fireChangeEvent();
    257    }
    258
    259    /**
    260     * Returns a flag that controls whether or not guide lines are drawn for
    261     * each data item (the lines are horizontal and vertical "crosshairs"
    262     * linking the data point to the axes).
    263     *
    264     * @return A boolean.
    265     *
    266     * @see #setGuideLinesVisible(boolean)
    267     */
    268    public boolean isGuideLinesVisible() {
    269        return this.guideLinesVisible;
    270    }
    271
    272    /**
    273     * Sets the flag that controls whether or not guide lines are drawn for
    274     * each data item and sends a {@link RendererChangeEvent} to all registered
    275     * listeners.
    276     *
    277     * @param visible  the new flag value.
    278     *
    279     * @see #isGuideLinesVisible()
    280     */
    281    public void setGuideLinesVisible(boolean visible) {
    282        this.guideLinesVisible = visible;
    283        fireChangeEvent();
    284    }
    285
    286    /**
    287     * Returns the paint used to draw the guide lines.
    288     *
    289     * @return The paint (never <code>null</code>).
    290     *
    291     * @see #setGuideLinePaint(Paint)
    292     */
    293    public Paint getGuideLinePaint() {
    294        return this.guideLinePaint;
    295    }
    296
    297    /**
    298     * Sets the paint used to draw the guide lines and sends a
    299     * {@link RendererChangeEvent} to all registered listeners.
    300     *
    301     * @param paint  the paint (<code>null</code> not permitted).
    302     *
    303     * @see #getGuideLinePaint()
    304     */
    305    public void setGuideLinePaint(Paint paint) {
    306        if (paint == null) {
    307            throw new IllegalArgumentException("Null 'paint' argument.");
    308        }
    309        this.guideLinePaint = paint;
    310        fireChangeEvent();
    311    }
    312
    313    /**
    314     * Returns the stroke used to draw the guide lines.
    315     *
    316     * @return The stroke.
    317     *
    318     * @see #setGuideLineStroke(Stroke)
    319     */
    320    public Stroke getGuideLineStroke() {
    321        return this.guideLineStroke;
    322    }
    323
    324    /**
    325     * Sets the stroke used to draw the guide lines and sends a
    326     * {@link RendererChangeEvent} to all registered listeners.
    327     *
    328     * @param stroke  the stroke (<code>null</code> not permitted).
    329     *
    330     * @see #getGuideLineStroke()
    331     */
    332    public void setGuideLineStroke(Stroke stroke) {
    333        if (stroke == null) {
    334            throw new IllegalArgumentException("Null 'stroke' argument.");
    335        }
    336        this.guideLineStroke = stroke;
    337        fireChangeEvent();
    338    }
    339
    340    /**
    341     * Returns the lower and upper bounds (range) of the x-values in the
    342     * specified dataset.
    343     *
    344     * @param dataset  the dataset (<code>null</code> permitted).
    345     *
    346     * @return The range (<code>null</code> if the dataset is <code>null</code>
    347     *         or empty).
    348     */
    349    public Range findDomainBounds(XYDataset dataset) {
    350        if (dataset == null) {
    351            return null;
    352        }
    353        Range r = DatasetUtilities.findDomainBounds(dataset, false);
    354        if (r == null) {
    355            return null;
    356        }
    357        double offset = 0; // TODO getSeriesShape(n).getBounds().width / 2;
    358        return new Range(r.getLowerBound() + offset,
    359                         r.getUpperBound() + offset);
    360    }
    361
    362    /**
    363     * Returns the range of values the renderer requires to display all the
    364     * items from the specified dataset.
    365     *
    366     * @param dataset  the dataset (<code>null</code> permitted).
    367     *
    368     * @return The range (<code>null</code> if the dataset is <code>null</code>
    369     *         or empty).
    370     */
    371    public Range findRangeBounds(XYDataset dataset) {
    372        if (dataset == null) {
    373            return null;
    374        }
    375        Range r = DatasetUtilities.findRangeBounds(dataset, false);
    376        if (r == null) {
    377            return null;
    378        }
    379        double offset = 0; // TODO getSeriesShape(n).getBounds().height / 2;
    380        return new Range(r.getLowerBound() + offset, r.getUpperBound()
    381                + offset);
    382    }
    383
    384    /**
    385     * Return the range of z-values in the specified dataset.
    386     *
    387     * @param dataset  the dataset (<code>null</code> permitted).
    388     *
    389     * @return The range (<code>null</code> if the dataset is <code>null</code>
    390     *         or empty).
    391     */
    392    public Range findZBounds(XYZDataset dataset) {
    393        if (dataset != null) {
    394            return DatasetUtilities.findZBounds(dataset);
    395        }
    396        else {
    397            return null;
    398        }
    399    }
    400
    401    /**
    402     * Returns the number of passes required by this renderer.
    403     *
    404     * @return <code>2</code>.
    405     */
    406    public int getPassCount() {
    407        return 2;
    408    }
    409
    410    /**
    411     * Draws the block representing the specified item.
    412     *
    413     * @param g2  the graphics device.
    414     * @param state  the state.
    415     * @param dataArea  the data area.
    416     * @param info  the plot rendering info.
    417     * @param plot  the plot.
    418     * @param domainAxis  the x-axis.
    419     * @param rangeAxis  the y-axis.
    420     * @param dataset  the dataset.
    421     * @param series  the series index.
    422     * @param item  the item index.
    423     * @param crosshairState  the crosshair state.
    424     * @param pass  the pass index.
    425     */
    426    public void drawItem(Graphics2D g2, XYItemRendererState state,
    427            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
    428            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
    429            int series, int item, CrosshairState crosshairState, int pass) {
    430
    431        Shape hotspot = null;
    432        EntityCollection entities = null;
    433        if (info != null) {
    434            entities = info.getOwner().getEntityCollection();
    435        }
    436
    437        double x = dataset.getXValue(series, item);
    438        double y = dataset.getYValue(series, item);
    439        if (Double.isNaN(x) || Double.isNaN(y)) {
    440            // can't draw anything
    441            return;
    442        }
    443
    444        double transX = domainAxis.valueToJava2D(x, dataArea,
    445                plot.getDomainAxisEdge());
    446        double transY = rangeAxis.valueToJava2D(y, dataArea,
    447                plot.getRangeAxisEdge());
    448
    449        PlotOrientation orientation = plot.getOrientation();
    450
    451        // draw optional guide lines
    452        if ((pass == 0) && this.guideLinesVisible) {
    453            g2.setStroke(this.guideLineStroke);
    454            g2.setPaint(this.guideLinePaint);
    455            if (orientation == PlotOrientation.HORIZONTAL) {
    456                g2.draw(new Line2D.Double(transY, dataArea.getMinY(), transY,
    457                        dataArea.getMaxY()));
    458                g2.draw(new Line2D.Double(dataArea.getMinX(), transX,
    459                        dataArea.getMaxX(), transX));
    460            }
    461            else {
    462                g2.draw(new Line2D.Double(transX, dataArea.getMinY(), transX,
    463                        dataArea.getMaxY()));
    464                g2.draw(new Line2D.Double(dataArea.getMinX(), transY,
    465                        dataArea.getMaxX(), transY));
    466            }
    467        }
    468        else if (pass == 1) {
    469            Shape shape = getItemShape(series, item);
    470            if (orientation == PlotOrientation.HORIZONTAL) {
    471                shape = ShapeUtilities.createTranslatedShape(shape, transY,
    472                        transX);
    473            }
    474            else if (orientation == PlotOrientation.VERTICAL) {
    475                shape = ShapeUtilities.createTranslatedShape(shape, transX,
    476                        transY);
    477            }
    478            hotspot = shape;
    479            if (shape.intersects(dataArea)) {
    480                //if (getItemShapeFilled(series, item)) {
    481                    g2.setPaint(getPaint(dataset, series, item));
    482                    g2.fill(shape);
    483               //}
    484                if (this.drawOutlines) {
    485                    if (getUseOutlinePaint()) {
    486                        g2.setPaint(getItemOutlinePaint(series, item));
    487                    }
    488                    else {
    489                        g2.setPaint(getItemPaint(series, item));
    490                    }
    491                    g2.setStroke(getItemOutlineStroke(series, item));
    492                    g2.draw(shape);
    493                }
    494            }
    495
    496            // add an entity for the item...
    497            if (entities != null) {
    498                addEntity(entities, hotspot, dataset, series, item, transX,
    499                        transY);
    500            }
    501        }
    502    }
    503
    504    /**
    505     * Get the paint for a given series and item from a dataset.
    506     *
    507     * @param dataset  the dataset..
    508     * @param series  the series index.
    509     * @param item  the item index.
    510     *
    511     * @return The paint.
    512     */
    513    protected Paint getPaint(XYDataset dataset, int series, int item) {
    514        Paint p = null;
    515        if (dataset instanceof XYZDataset) {
    516            double z = ((XYZDataset) dataset).getZValue(series, item);
    517            p = this.paintScale.getPaint(z);
    518        }
    519        else {
    520            if (this.useFillPaint) {
    521                p = getItemFillPaint(series, item);
    522            }
    523            else {
    524                p = getItemPaint(series, item);
    525            }
    526        }
    527        return p;
    528    }
    529
    530    /**
    531     * Tests this instance for equality with an arbitrary object.  This method
    532     * returns <code>true</code> if and only if:
    533     * <ul>
    534     * <li><code>obj</code> is an instance of <code>XYShapeRenderer</code> (not
    535     *     <code>null</code>);</li>
    536     * <li><code>obj</code> has the same field values as this
    537     *     <code>XYShapeRenderer</code>;</li>
    538     * </ul>
    539     *
    540     * @param obj  the object (<code>null</code> permitted).
    541     *
    542     * @return A boolean.
    543     */
    544    public boolean equals(Object obj) {
    545        if (obj == this) {
    546            return true;
    547        }
    548        if (!(obj instanceof XYShapeRenderer)) {
    549            return false;
    550        }
    551        XYShapeRenderer that = (XYShapeRenderer) obj;
    552        if (!this.paintScale.equals(that.paintScale)) {
    553            return false;
    554        }
    555        if (this.drawOutlines != that.drawOutlines) {
    556            return false;
    557        }
    558        if (this.useOutlinePaint != that.useOutlinePaint) {
    559            return false;
    560        }
    561        if (this.useFillPaint != that.useFillPaint) {
    562            return false;
    563        }
    564        if (this.guideLinesVisible != that.guideLinesVisible) {
    565            return false;
    566        }
    567        if (!this.guideLinePaint.equals(that.guideLinePaint)) {
    568            return false;
    569        }
    570        if (!this.guideLineStroke.equals(that.guideLineStroke)) {
    571            return false;
    572        }
    573        return super.equals(obj);
    574    }
    575
    576    /**
    577     * Returns a clone of this renderer.
    578     *
    579     * @return A clone of this renderer.
    580     *
    581     * @throws CloneNotSupportedException if there is a problem creating the
    582     *     clone.
    583     */
    584    public Object clone() throws CloneNotSupportedException {
    585        XYShapeRenderer clone = (XYShapeRenderer) super.clone();
    586        if (this.paintScale instanceof PublicCloneable) {
    587            PublicCloneable pc = (PublicCloneable) this.paintScale;
    588            clone.paintScale = (PaintScale) pc.clone();
    589        }
    590        return clone;
    591    }
    592
    593    /**
    594     * Provides serialization support.
    595     *
    596     * @param stream  the input stream.
    597     *
    598     * @throws IOException  if there is an I/O error.
    599     * @throws ClassNotFoundException  if there is a classpath problem.
    600     */
    601    private void readObject(ObjectInputStream stream)
    602            throws IOException, ClassNotFoundException {
    603        stream.defaultReadObject();
    604        this.guideLinePaint = SerialUtilities.readPaint(stream);
    605        this.guideLineStroke = SerialUtilities.readStroke(stream);
    606    }
    607
    608    /**
    609     * Provides serialization support.
    610     *
    611     * @param stream  the output stream.
    612     *
    613     * @throws IOException  if there is an I/O error.
    614     */
    615    private void writeObject(ObjectOutputStream stream) throws IOException {
    616        stream.defaultWriteObject();
    617        SerialUtilities.writePaint(this.guideLinePaint, stream);
    618        SerialUtilities.writeStroke(this.guideLineStroke, stream);
    619    }
    620
    621}