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