001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, 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-2014 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 * 03-Jul-2013 : Use ParamChecks (DG);
043 *
044 */
045
046package org.jfree.chart.renderer.xy;
047
048import java.awt.BasicStroke;
049import java.awt.Color;
050import java.awt.Graphics2D;
051import java.awt.Paint;
052import java.awt.Shape;
053import java.awt.Stroke;
054import java.awt.geom.Ellipse2D;
055import java.awt.geom.Line2D;
056import java.awt.geom.Rectangle2D;
057import java.io.IOException;
058import java.io.ObjectInputStream;
059import java.io.ObjectOutputStream;
060import java.io.Serializable;
061
062import org.jfree.chart.axis.ValueAxis;
063import org.jfree.chart.entity.EntityCollection;
064import org.jfree.chart.event.RendererChangeEvent;
065import org.jfree.chart.plot.CrosshairState;
066import org.jfree.chart.plot.PlotOrientation;
067import org.jfree.chart.plot.PlotRenderingInfo;
068import org.jfree.chart.plot.XYPlot;
069import org.jfree.chart.renderer.LookupPaintScale;
070import org.jfree.chart.renderer.PaintScale;
071import org.jfree.chart.util.ParamChecks;
072import org.jfree.data.Range;
073import org.jfree.data.general.DatasetUtilities;
074import org.jfree.data.xy.XYDataset;
075import org.jfree.data.xy.XYZDataset;
076import org.jfree.io.SerialUtilities;
077import org.jfree.util.PublicCloneable;
078import org.jfree.util.ShapeUtilities;
079
080/**
081 * A renderer that draws shapes at (x, y) coordinates and, if the dataset
082 * is an instance of {@link XYZDataset}, fills the shapes with a paint that
083 * is based on the z-value (the paint is obtained from a lookup table).  The
084 * renderer also allows for optional guidelines, horizontal and vertical lines
085 * connecting the shape to the edges of the plot.
086 * <br><br>
087 * The example shown here is generated by the
088 * <code>XYShapeRendererDemo1.java</code> program included in the JFreeChart
089 * demo collection:
090 * <br><br>
091 * <img src="../../../../../images/XYShapeRendererSample.png"
092 * alt="XYShapeRendererSample.png">
093 * <br><br>
094 * This renderer has similarities to, but also differences from, the
095 * {@link XYLineAndShapeRenderer}.
096 *
097 * @since 1.0.11
098 */
099public class XYShapeRenderer extends AbstractXYItemRenderer
100        implements XYItemRenderer, Cloneable, Serializable {
101
102    /** Auto generated serial version id. */
103    private static final long serialVersionUID = 8320552104211173221L;
104
105    /** The paint scale (never null). */
106    private PaintScale paintScale;
107
108    /** A flag that controls whether or not the shape outlines are drawn. */
109    private boolean drawOutlines;
110
111    /**
112     * A flag that controls whether or not the outline paint is used (if not,
113     * the regular paint is used).
114     */
115    private boolean useOutlinePaint;
116
117    /**
118     * A flag that controls whether or not the fill paint is used (if not,
119     * the fill paint is used).
120     */
121    private boolean useFillPaint;
122
123    /** Flag indicating if guide lines should be drawn for every item. */
124    private boolean guideLinesVisible;
125
126    /** The paint used for drawing the guide lines (never null). */
127    private transient Paint guideLinePaint;
128
129    /** The stroke used for drawing the guide lines (never null). */
130    private transient Stroke guideLineStroke;
131
132    /**
133     * Creates a new <code>XYShapeRenderer</code> instance with default
134     * attributes.
135     */
136    public XYShapeRenderer() {
137        this.paintScale = new LookupPaintScale();
138        this.useFillPaint = false;
139        this.drawOutlines = false;
140        this.useOutlinePaint = true;
141        this.guideLinesVisible = false;
142        this.guideLinePaint = Color.darkGray;
143        this.guideLineStroke = new BasicStroke();
144        setBaseShape(new Ellipse2D.Double(-5.0, -5.0, 10.0, 10.0));
145        setAutoPopulateSeriesShape(false);
146    }
147
148    /**
149     * Returns the paint scale used by the renderer.
150     *
151     * @return The paint scale (never <code>null</code>).
152     *
153     * @see #setPaintScale(PaintScale)
154     */
155    public PaintScale getPaintScale() {
156        return this.paintScale;
157    }
158
159    /**
160     * Sets the paint scale used by the renderer and sends a
161     * {@link RendererChangeEvent} to all registered listeners.
162     *
163     * @param scale  the scale (<code>null</code> not permitted).
164     *
165     * @see #getPaintScale()
166     */
167    public void setPaintScale(PaintScale scale) {
168        ParamChecks.nullNotPermitted(scale, "scale");
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        ParamChecks.nullNotPermitted(paint, "paint");
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        ParamChecks.nullNotPermitted(stroke, "stroke");
332        this.guideLineStroke = stroke;
333        fireChangeEvent();
334    }
335
336    /**
337     * Returns the lower and upper bounds (range) of the x-values in the
338     * specified dataset.
339     *
340     * @param dataset  the dataset (<code>null</code> permitted).
341     *
342     * @return The range (<code>null</code> if the dataset is <code>null</code>
343     *         or empty).
344     */
345    @Override
346    public Range findDomainBounds(XYDataset dataset) {
347        if (dataset == null) {
348            return null;
349        }
350        Range r = DatasetUtilities.findDomainBounds(dataset, false);
351        if (r == null) {
352            return null;
353        }
354        double offset = 0; // TODO getSeriesShape(n).getBounds().width / 2;
355        return new Range(r.getLowerBound() + offset,
356                         r.getUpperBound() + offset);
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    @Override
369    public Range findRangeBounds(XYDataset dataset) {
370        if (dataset == null) {
371            return null;
372        }
373        Range r = DatasetUtilities.findRangeBounds(dataset, false);
374        if (r == null) {
375            return null;
376        }
377        double offset = 0; // TODO getSeriesShape(n).getBounds().height / 2;
378        return new Range(r.getLowerBound() + offset, r.getUpperBound()
379                + offset);
380    }
381
382    /**
383     * Return the range of z-values in the specified dataset.
384     *  
385     * @param dataset  the dataset (<code>null</code> permitted).
386     * 
387     * @return The range (<code>null</code> if the dataset is <code>null</code>
388     *         or empty).
389     */
390    public Range findZBounds(XYZDataset dataset) {
391        if (dataset != null) {
392            return DatasetUtilities.findZBounds(dataset);
393        }
394        else {
395            return null;
396        }
397    }
398
399    /**
400     * Returns the number of passes required by this renderer.
401     *
402     * @return <code>2</code>.
403     */
404    @Override
405    public int getPassCount() {
406        return 2;
407    }
408
409    /**
410     * Draws the block representing the specified item.
411     *
412     * @param g2  the graphics device.
413     * @param state  the state.
414     * @param dataArea  the data area.
415     * @param info  the plot rendering info.
416     * @param plot  the plot.
417     * @param domainAxis  the x-axis.
418     * @param rangeAxis  the y-axis.
419     * @param dataset  the dataset.
420     * @param series  the series index.
421     * @param item  the item index.
422     * @param crosshairState  the crosshair state.
423     * @param pass  the pass index.
424     */
425    @Override
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;
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;
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    @Override
545    public boolean equals(Object obj) {
546        if (obj == this) {
547            return true;
548        }
549        if (!(obj instanceof XYShapeRenderer)) {
550            return false;
551        }
552        XYShapeRenderer that = (XYShapeRenderer) obj;
553        if (!this.paintScale.equals(that.paintScale)) {
554            return false;
555        }
556        if (this.drawOutlines != that.drawOutlines) {
557            return false;
558        }
559        if (this.useOutlinePaint != that.useOutlinePaint) {
560            return false;
561        }
562        if (this.useFillPaint != that.useFillPaint) {
563            return false;
564        }
565        if (this.guideLinesVisible != that.guideLinesVisible) {
566            return false;
567        }
568        if (!this.guideLinePaint.equals(that.guideLinePaint)) {
569            return false;
570        }
571        if (!this.guideLineStroke.equals(that.guideLineStroke)) {
572            return false;
573        }
574        return super.equals(obj);
575    }
576
577    /**
578     * Returns a clone of this renderer.
579     *
580     * @return A clone of this renderer.
581     *
582     * @throws CloneNotSupportedException if there is a problem creating the
583     *     clone.
584     */
585    @Override
586    public Object clone() throws CloneNotSupportedException {
587        XYShapeRenderer clone = (XYShapeRenderer) super.clone();
588        if (this.paintScale instanceof PublicCloneable) {
589            PublicCloneable pc = (PublicCloneable) this.paintScale;
590            clone.paintScale = (PaintScale) pc.clone();
591        }
592        return clone;
593    }
594
595    /**
596     * Provides serialization support.
597     *
598     * @param stream  the input stream.
599     *
600     * @throws IOException  if there is an I/O error.
601     * @throws ClassNotFoundException  if there is a classpath problem.
602     */
603    private void readObject(ObjectInputStream stream)
604            throws IOException, ClassNotFoundException {
605        stream.defaultReadObject();
606        this.guideLinePaint = SerialUtilities.readPaint(stream);
607        this.guideLineStroke = SerialUtilities.readStroke(stream);
608    }
609
610    /**
611     * Provides serialization support.
612     *
613     * @param stream  the output stream.
614     *
615     * @throws IOException  if there is an I/O error.
616     */
617    private void writeObject(ObjectOutputStream stream) throws IOException {
618        stream.defaultWriteObject();
619        SerialUtilities.writePaint(this.guideLinePaint, stream);
620        SerialUtilities.writeStroke(this.guideLineStroke, stream);
621    }
622
623}