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