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