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