001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2021, 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 * XYLineAndShapeRenderer.java
029 * ---------------------------
030 * (C) Copyright 2004-2021, by David Gilbert.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 *
035 */
036
037package org.jfree.chart.renderer.xy;
038
039import java.awt.Graphics2D;
040import java.awt.Paint;
041import java.awt.Shape;
042import java.awt.Stroke;
043import java.awt.geom.GeneralPath;
044import java.awt.geom.Line2D;
045import java.awt.geom.Rectangle2D;
046import java.io.IOException;
047import java.io.ObjectInputStream;
048import java.io.ObjectOutputStream;
049import java.io.Serializable;
050import java.util.HashMap;
051import java.util.Map;
052import java.util.Objects;
053
054import org.jfree.chart.legend.LegendItem;
055import org.jfree.chart.axis.ValueAxis;
056import org.jfree.chart.entity.EntityCollection;
057import org.jfree.chart.event.RendererChangeEvent;
058import org.jfree.chart.plot.CrosshairState;
059import org.jfree.chart.plot.PlotOrientation;
060import org.jfree.chart.plot.PlotRenderingInfo;
061import org.jfree.chart.plot.XYPlot;
062import org.jfree.chart.api.RectangleEdge;
063import org.jfree.chart.internal.LineUtils;
064import org.jfree.chart.internal.Args;
065import org.jfree.chart.api.PublicCloneable;
066import org.jfree.chart.internal.CloneUtils;
067import org.jfree.chart.internal.SerialUtils;
068import org.jfree.chart.internal.ShapeUtils;
069import org.jfree.data.xy.XYDataset;
070
071/**
072 * A renderer that connects data points with lines and/or draws shapes at each
073 * data point.  This renderer is designed for use with the {@link XYPlot}
074 * class.  The example shown here is generated by
075 * the {@code XYLineAndShapeRendererDemo2.java} program included in the
076 * JFreeChart demo collection:
077 * <br><br>
078 * <img src="doc-files/XYLineAndShapeRendererSample.png"
079 * alt="XYLineAndShapeRendererSample.png">
080 *
081 */
082public class XYLineAndShapeRenderer extends AbstractXYItemRenderer
083        implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {
084
085    /** For serialization. */
086    private static final long serialVersionUID = -7435246895986425885L;
087
088    /**
089     * A table of flags that control (per series) whether or not lines are
090     * visible.
091     */
092    private Map<Integer, Boolean> seriesLinesVisibleMap;
093
094    /** The default value returned by the getLinesVisible() method. */
095    private boolean defaultLinesVisible;
096
097    /** The shape that is used to represent a line in the legend. */
098    private transient Shape legendLine;
099
100    /**
101     * A table of flags that control (per series) whether or not shapes are
102     * visible.
103     */
104    private Map<Integer, Boolean> seriesShapesVisibleMap;
105
106    /** The default value returned by the getShapeVisible() method. */
107    private boolean defaultShapesVisible;
108
109    /**
110     * A table of flags that control (per series) whether or not shapes are
111     * filled.
112     */
113    private Map<Integer, Boolean> seriesShapesFilledMap;
114
115    /** The default value returned by the getShapeFilled() method. */
116    private boolean defaultShapesFilled;
117
118    /** A flag that controls whether outlines are drawn for shapes. */
119    private boolean drawOutlines;
120
121    /**
122     * A flag that controls whether the fill paint is used for filling
123     * shapes.
124     */
125    private boolean useFillPaint;
126
127    /**
128     * A flag that controls whether the outline paint is used for drawing shape
129     * outlines.
130     */
131    private boolean useOutlinePaint;
132
133    /**
134     * A flag that controls whether or not each series is drawn as a single
135     * path.
136     */
137    private boolean drawSeriesLineAsPath;
138
139    /**
140     * Creates a new renderer with both lines and shapes visible.
141     */
142    public XYLineAndShapeRenderer() {
143        this(true, true);
144    }
145
146    /**
147     * Creates a new renderer.
148     *
149     * @param lines  lines visible?
150     * @param shapes  shapes visible?
151     */
152    public XYLineAndShapeRenderer(boolean lines, boolean shapes) {
153        this.seriesLinesVisibleMap = new HashMap<>();
154        this.defaultLinesVisible = lines;
155        this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
156
157        this.seriesShapesVisibleMap = new HashMap<>();
158        this.defaultShapesVisible = shapes;
159
160        this.useFillPaint = false;     // use item paint for fills by default
161        this.seriesShapesFilledMap = new HashMap<>();
162        this.defaultShapesFilled = true;
163
164        this.drawOutlines = true;
165        this.useOutlinePaint = false;  // use item paint for outlines by
166                                       // default, not outline paint
167
168        this.drawSeriesLineAsPath = false;
169    }
170
171    /**
172     * Returns a flag that controls whether or not each series is drawn as a
173     * single path.  The default value is {@code false}.
174     *
175     * @return A boolean.
176     *
177     * @see #setDrawSeriesLineAsPath(boolean)
178     */
179    public boolean getDrawSeriesLineAsPath() {
180        return this.drawSeriesLineAsPath;
181    }
182
183    /**
184     * Sets the flag that controls whether or not each series is drawn as a
185     * single path and sends a {@link RendererChangeEvent} to all registered
186     * listeners.
187     *
188     * @param flag  the flag.
189     *
190     * @see #getDrawSeriesLineAsPath()
191     */
192    public void setDrawSeriesLineAsPath(boolean flag) {
193        if (this.drawSeriesLineAsPath != flag) {
194            this.drawSeriesLineAsPath = flag;
195            fireChangeEvent();
196        }
197    }
198
199    /**
200     * Returns the number of passes through the data that the renderer requires
201     * in order to draw the chart.  Most charts will require a single pass, but
202     * some require two passes.
203     *
204     * @return The pass count.
205     */
206    @Override
207    public int getPassCount() {
208        return 2;
209    }
210
211    // LINES VISIBLE
212
213    /**
214     * Returns the flag used to control whether or not the shape for an item is
215     * visible.
216     *
217     * @param series  the series index (zero-based).
218     * @param item  the item index (zero-based).
219     *
220     * @return A boolean.
221     */
222    public boolean getItemLineVisible(int series, int item) {
223        Boolean flag = getSeriesLinesVisible(series);
224        if (flag != null) {
225            return flag;
226        }
227        return this.defaultLinesVisible;
228    }
229
230    /**
231     * Returns the flag used to control whether or not the lines for a series
232     * are visible.
233     *
234     * @param series  the series index (zero-based).
235     *
236     * @return The flag (possibly {@code null}).
237     *
238     * @see #setSeriesLinesVisible(int, Boolean)
239     */
240    public Boolean getSeriesLinesVisible(int series) {
241        return this.seriesLinesVisibleMap.get(series);
242    }
243
244    /**
245     * Sets the 'lines visible' flag for a series and sends a
246     * {@link RendererChangeEvent} to all registered listeners.
247     *
248     * @param series  the series index (zero-based).
249     * @param flag  the flag ({@code null} permitted).
250     *
251     * @see #getSeriesLinesVisible(int)
252     */
253    public void setSeriesLinesVisible(int series, Boolean flag) {
254        this.seriesLinesVisibleMap.put(series, flag);
255        fireChangeEvent();
256    }
257
258    /**
259     * Sets the 'lines visible' flag for a series and sends a
260     * {@link RendererChangeEvent} to all registered listeners.
261     *
262     * @param series  the series index (zero-based).
263     * @param visible  the flag.
264     *
265     * @see #getSeriesLinesVisible(int)
266     */
267    public void setSeriesLinesVisible(int series, boolean visible) {
268        setSeriesLinesVisible(series, Boolean.valueOf(visible));
269    }
270
271    /**
272     * Returns the default 'lines visible' attribute.
273     *
274     * @return The default flag.
275     *
276     * @see #setDefaultLinesVisible(boolean)
277     */
278    public boolean getDefaultLinesVisible() {
279        return this.defaultLinesVisible;
280    }
281
282    /**
283     * Sets the default 'lines visible' flag and sends a
284     * {@link RendererChangeEvent} to all registered listeners.
285     *
286     * @param flag  the flag.
287     *
288     * @see #getDefaultLinesVisible()
289     */
290    public void setDefaultLinesVisible(boolean flag) {
291        this.defaultLinesVisible = flag;
292        fireChangeEvent();
293    }
294
295    /**
296     * Returns the shape used to represent a line in the legend.
297     *
298     * @return The legend line (never {@code null}).
299     *
300     * @see #setLegendLine(Shape)
301     */
302    public Shape getLegendLine() {
303        return this.legendLine;
304    }
305
306    /**
307     * Sets the shape used as a line in each legend item and sends a
308     * {@link RendererChangeEvent} to all registered listeners.
309     *
310     * @param line  the line ({@code null} not permitted).
311     *
312     * @see #getLegendLine()
313     */
314    public void setLegendLine(Shape line) {
315        Args.nullNotPermitted(line, "line");
316        this.legendLine = line;
317        fireChangeEvent();
318    }
319
320    // SHAPES VISIBLE
321
322    /**
323     * Returns the flag used to control whether or not the shape for an item is
324     * visible.
325     * <p>
326     * The default implementation passes control to the
327     * {@code getSeriesShapesVisible()} method. You can override this method
328     * if you require different behaviour.
329     *
330     * @param series  the series index (zero-based).
331     * @param item  the item index (zero-based).
332     *
333     * @return A boolean.
334     */
335    public boolean getItemShapeVisible(int series, int item) {
336        Boolean flag = getSeriesShapesVisible(series);
337        if (flag != null) {
338            return flag;
339        }
340        return this.defaultShapesVisible;
341    }
342
343    /**
344     * Returns the flag used to control whether or not the shapes for a series
345     * are visible.
346     *
347     * @param series  the series index (zero-based).
348     *
349     * @return A boolean.
350     *
351     * @see #setSeriesShapesVisible(int, Boolean)
352     */
353    public Boolean getSeriesShapesVisible(int series) {
354        return this.seriesShapesVisibleMap.get(series);
355    }
356
357    /**
358     * Sets the 'shapes visible' flag for a series and sends a
359     * {@link RendererChangeEvent} to all registered listeners.
360     *
361     * @param series  the series index (zero-based).
362     * @param visible  the flag.
363     *
364     * @see #getSeriesShapesVisible(int)
365     */
366    public void setSeriesShapesVisible(int series, boolean visible) {
367        setSeriesShapesVisible(series, Boolean.valueOf(visible));
368    }
369
370    /**
371     * Sets the 'shapes visible' flag for a series and sends a
372     * {@link RendererChangeEvent} to all registered listeners.
373     *
374     * @param series  the series index (zero-based).
375     * @param flag  the flag.
376     *
377     * @see #getSeriesShapesVisible(int)
378     */
379    public void setSeriesShapesVisible(int series, Boolean flag) {
380        this.seriesShapesVisibleMap.put(series, flag);
381        fireChangeEvent();
382    }
383
384    /**
385     * Returns the default 'shape visible' attribute.
386     *
387     * @return The default flag.
388     *
389     * @see #setDefaultShapesVisible(boolean)
390     */
391    public boolean getDefaultShapesVisible() {
392        return this.defaultShapesVisible;
393    }
394
395    /**
396     * Sets the default 'shapes visible' flag and sends a
397     * {@link RendererChangeEvent} to all registered listeners.
398     *
399     * @param flag  the flag.
400     *
401     * @see #getDefaultShapesVisible()
402     */
403    public void setDefaultShapesVisible(boolean flag) {
404        this.defaultShapesVisible = flag;
405        fireChangeEvent();
406    }
407
408    // SHAPES FILLED
409
410    /**
411     * Returns the flag used to control whether or not the shape for an item
412     * is filled.
413     * <p>
414     * The default implementation passes control to the
415     * {@code getSeriesShapesFilled} method. You can override this method
416     * if you require different behaviour.
417     *
418     * @param series  the series index (zero-based).
419     * @param item  the item index (zero-based).
420     *
421     * @return A boolean.
422     */
423    public boolean getItemShapeFilled(int series, int item) {
424        Boolean flag = getSeriesShapesFilled(series);
425        if (flag != null) {
426            return flag;
427        }
428        return this.defaultShapesFilled;
429       
430    }
431
432    /**
433     * Returns the flag used to control whether or not the shapes for a series
434     * are filled.
435     *
436     * @param series  the series index (zero-based).
437     *
438     * @return A boolean.
439     *
440     * @see #setSeriesShapesFilled(int, Boolean)
441     */
442    public Boolean getSeriesShapesFilled(int series) {
443        return this.seriesShapesFilledMap.get(series);
444    }
445
446    /**
447     * Sets the 'shapes filled' flag for a series and sends a
448     * {@link RendererChangeEvent} to all registered listeners.
449     *
450     * @param series  the series index (zero-based).
451     * @param flag  the flag.
452     *
453     * @see #getSeriesShapesFilled(int)
454     */
455    public void setSeriesShapesFilled(int series, boolean flag) {
456        setSeriesShapesFilled(series, Boolean.valueOf(flag));
457    }
458
459    /**
460     * Sets the 'shapes filled' flag for a series and sends a
461     * {@link RendererChangeEvent} to all registered listeners.
462     *
463     * @param series  the series index (zero-based).
464     * @param flag  the flag.
465     *
466     * @see #getSeriesShapesFilled(int)
467     */
468    public void setSeriesShapesFilled(int series, Boolean flag) {
469        this.seriesShapesFilledMap.put(series, flag);
470        fireChangeEvent();
471    }
472
473    /**
474     * Returns the default 'shape filled' attribute.
475     *
476     * @return The default flag.
477     *
478     * @see #setDefaultShapesFilled(boolean)
479     */
480    public boolean getDefaultShapesFilled() {
481        return this.defaultShapesFilled;
482    }
483
484    /**
485     * Sets the default 'shapes filled' flag and sends a
486     * {@link RendererChangeEvent} to all registered listeners.
487     *
488     * @param flag  the flag.
489     *
490     * @see #getDefaultShapesFilled()
491     */
492    public void setDefaultShapesFilled(boolean flag) {
493        this.defaultShapesFilled = flag;
494        fireChangeEvent();
495    }
496
497    /**
498     * Returns {@code true} if outlines should be drawn for shapes, and
499     * {@code false} otherwise.
500     *
501     * @return A boolean.
502     *
503     * @see #setDrawOutlines(boolean)
504     */
505    public boolean getDrawOutlines() {
506        return this.drawOutlines;
507    }
508
509    /**
510     * Sets the flag that controls whether outlines are drawn for
511     * shapes, and sends a {@link RendererChangeEvent} to all registered
512     * listeners.
513     * <P>
514     * In some cases, shapes look better if they do NOT have an outline, but
515     * this flag allows you to set your own preference.
516     *
517     * @param flag  the flag.
518     *
519     * @see #getDrawOutlines()
520     */
521    public void setDrawOutlines(boolean flag) {
522        this.drawOutlines = flag;
523        fireChangeEvent();
524    }
525
526    /**
527     * Returns {@code true} if the renderer should use the fill paint
528     * setting to fill shapes, and {@code false} if it should just
529     * use the regular paint.
530     * <p>
531     * Refer to {@code XYLineAndShapeRendererDemo2.java} to see the
532     * effect of this flag.
533     *
534     * @return A boolean.
535     *
536     * @see #setUseFillPaint(boolean)
537     * @see #getUseOutlinePaint()
538     */
539    public boolean getUseFillPaint() {
540        return this.useFillPaint;
541    }
542
543    /**
544     * Sets the flag that controls whether the fill paint is used to fill
545     * shapes, and sends a {@link RendererChangeEvent} to all
546     * registered listeners.
547     *
548     * @param flag  the flag.
549     *
550     * @see #getUseFillPaint()
551     */
552    public void setUseFillPaint(boolean flag) {
553        this.useFillPaint = flag;
554        fireChangeEvent();
555    }
556
557    /**
558     * Returns {@code true} if the renderer should use the outline paint
559     * setting to draw shape outlines, and {@code false} if it should just
560     * use the regular paint.
561     *
562     * @return A boolean.
563     *
564     * @see #setUseOutlinePaint(boolean)
565     * @see #getUseFillPaint()
566     */
567    public boolean getUseOutlinePaint() {
568        return this.useOutlinePaint;
569    }
570
571    /**
572     * Sets the flag that controls whether the outline paint is used to draw
573     * shape outlines, and sends a {@link RendererChangeEvent} to all
574     * registered listeners.
575     * <p>
576     * Refer to {@code XYLineAndShapeRendererDemo2.java} to see the
577     * effect of this flag.
578     *
579     * @param flag  the flag.
580     *
581     * @see #getUseOutlinePaint()
582     */
583    public void setUseOutlinePaint(boolean flag) {
584        this.useOutlinePaint = flag;
585        fireChangeEvent();
586    }
587
588    /**
589     * Records the state for the renderer.  This is used to preserve state
590     * information between calls to the drawItem() method for a single chart
591     * drawing.
592     */
593    public static class State extends XYItemRendererState {
594
595        /** The path for the current series. */
596        public GeneralPath seriesPath;
597
598        /**
599         * A flag that indicates if the last (x, y) point was 'good'
600         * (non-null).
601         */
602        private boolean lastPointGood;
603
604        /**
605         * Creates a new state instance.
606         *
607         * @param info  the plot rendering info.
608         */
609        public State(PlotRenderingInfo info) {
610            super(info);
611            this.seriesPath = new GeneralPath();
612        }
613
614        /**
615         * Returns a flag that indicates if the last point drawn (in the
616         * current series) was 'good' (non-null).
617         *
618         * @return A boolean.
619         */
620        public boolean isLastPointGood() {
621            return this.lastPointGood;
622        }
623
624        /**
625         * Sets a flag that indicates if the last point drawn (in the current
626         * series) was 'good' (non-null).
627         *
628         * @param good  the flag.
629         */
630        public void setLastPointGood(boolean good) {
631            this.lastPointGood = good;
632        }
633
634        /**
635         * This method is called by the {@link XYPlot} at the start of each
636         * series pass.  We reset the state for the current series.
637         *
638         * @param dataset  the dataset.
639         * @param series  the series index.
640         * @param firstItem  the first item index for this pass.
641         * @param lastItem  the last item index for this pass.
642         * @param pass  the current pass index.
643         * @param passCount  the number of passes.
644         */
645        @Override
646        public void startSeriesPass(XYDataset dataset, int series,
647                int firstItem, int lastItem, int pass, int passCount) {
648            this.seriesPath.reset();
649            this.lastPointGood = false;
650            super.startSeriesPass(dataset, series, firstItem, lastItem, pass,
651                    passCount);
652       }
653
654    }
655
656    /**
657     * Initialises the renderer.
658     * <P>
659     * This method will be called before the first item is rendered, giving the
660     * renderer an opportunity to initialise any state information it wants to
661     * maintain.  The renderer can do nothing if it chooses.
662     *
663     * @param g2  the graphics device.
664     * @param dataArea  the area inside the axes.
665     * @param plot  the plot.
666     * @param data  the data.
667     * @param info  an optional info collection object to return data back to
668     *              the caller.
669     *
670     * @return The renderer state.
671     */
672    @Override
673    public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
674            XYPlot plot, XYDataset data, PlotRenderingInfo info) {
675        return new State(info);
676    }
677
678    /**
679     * Draws the visual representation of a single data item.
680     *
681     * @param g2  the graphics device.
682     * @param state  the renderer state.
683     * @param dataArea  the area within which the data is being drawn.
684     * @param info  collects information about the drawing.
685     * @param plot  the plot (can be used to obtain standard color
686     *              information etc).
687     * @param domainAxis  the domain axis.
688     * @param rangeAxis  the range axis.
689     * @param dataset  the dataset.
690     * @param series  the series index (zero-based).
691     * @param item  the item index (zero-based).
692     * @param crosshairState  crosshair information for the plot
693     *                        ({@code null} permitted).
694     * @param pass  the pass index.
695     */
696    @Override
697    public void drawItem(Graphics2D g2, XYItemRendererState state,
698            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
699            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
700            int series, int item, CrosshairState crosshairState, int pass) {
701
702        // do nothing if item is not visible
703        if (!getItemVisible(series, item)) {
704            return;
705        }
706
707        // first pass draws the background (lines, for instance)
708        if (isLinePass(pass)) {
709            if (getItemLineVisible(series, item)) {
710                if (this.drawSeriesLineAsPath) {
711                    drawPrimaryLineAsPath(state, g2, plot, dataset, pass,
712                            series, item, domainAxis, rangeAxis, dataArea);
713                }
714                else {
715                    drawPrimaryLine(state, g2, plot, dataset, pass, series,
716                            item, domainAxis, rangeAxis, dataArea);
717                }
718            }
719        }
720        // second pass adds shapes where the items are ..
721        else if (isItemPass(pass)) {
722
723            // setup for collecting optional entity info...
724            EntityCollection entities = null;
725            if (info != null && info.getOwner() != null) {
726                entities = info.getOwner().getEntityCollection();
727            }
728
729            drawSecondaryPass(g2, plot, dataset, pass, series, item,
730                    domainAxis, dataArea, rangeAxis, crosshairState, entities);
731        }
732    }
733
734    /**
735     * Returns {@code true} if the specified pass is the one for drawing
736     * lines.
737     *
738     * @param pass  the pass.
739     *
740     * @return A boolean.
741     */
742    protected boolean isLinePass(int pass) {
743        return pass == 0;
744    }
745
746    /**
747     * Returns {@code true} if the specified pass is the one for drawing
748     * items.
749     *
750     * @param pass  the pass.
751     *
752     * @return A boolean.
753     */
754    protected boolean isItemPass(int pass) {
755        return pass == 1;
756    }
757
758    /**
759     * Draws the item (first pass). This method draws the lines
760     * connecting the items.
761     *
762     * @param g2  the graphics device.
763     * @param state  the renderer state.
764     * @param dataArea  the area within which the data is being drawn.
765     * @param plot  the plot (can be used to obtain standard color
766     *              information etc).
767     * @param domainAxis  the domain axis.
768     * @param rangeAxis  the range axis.
769     * @param dataset  the dataset.
770     * @param pass  the pass.
771     * @param series  the series index (zero-based).
772     * @param item  the item index (zero-based).
773     */
774    protected void drawPrimaryLine(XYItemRendererState state,
775                                   Graphics2D g2,
776                                   XYPlot plot,
777                                   XYDataset dataset,
778                                   int pass,
779                                   int series,
780                                   int item,
781                                   ValueAxis domainAxis,
782                                   ValueAxis rangeAxis,
783                                   Rectangle2D dataArea) {
784        if (item == 0) {
785            return;
786        }
787
788        // get the data point...
789        double x1 = dataset.getXValue(series, item);
790        double y1 = dataset.getYValue(series, item);
791        if (Double.isNaN(y1) || Double.isNaN(x1)) {
792            return;
793        }
794
795        double x0 = dataset.getXValue(series, item - 1);
796        double y0 = dataset.getYValue(series, item - 1);
797        if (Double.isNaN(y0) || Double.isNaN(x0)) {
798            return;
799        }
800
801        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
802        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
803
804        double transX0 = domainAxis.valueToJava2D(x0, dataArea, xAxisLocation);
805        double transY0 = rangeAxis.valueToJava2D(y0, dataArea, yAxisLocation);
806
807        double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
808        double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
809
810        // only draw if we have good values
811        if (Double.isNaN(transX0) || Double.isNaN(transY0)
812            || Double.isNaN(transX1) || Double.isNaN(transY1)) {
813            return;
814        }
815
816        PlotOrientation orientation = plot.getOrientation();
817        boolean visible;
818        if (orientation == PlotOrientation.HORIZONTAL) {
819            state.workingLine.setLine(transY0, transX0, transY1, transX1);
820        }
821        else if (orientation == PlotOrientation.VERTICAL) {
822            state.workingLine.setLine(transX0, transY0, transX1, transY1);
823        }
824        visible = LineUtils.clipLine(state.workingLine, dataArea);
825        if (visible) {
826            drawFirstPassShape(g2, pass, series, item, state.workingLine);
827        }
828    }
829
830    /**
831     * Draws the first pass shape.
832     *
833     * @param g2  the graphics device.
834     * @param pass  the pass.
835     * @param series  the series index.
836     * @param item  the item index.
837     * @param shape  the shape.
838     */
839    protected void drawFirstPassShape(Graphics2D g2, int pass, int series,
840                                      int item, Shape shape) {
841        g2.setStroke(getItemStroke(series, item));
842        g2.setPaint(getItemPaint(series, item));
843        g2.draw(shape);
844    }
845
846
847    /**
848     * Draws the item (first pass). This method draws the lines
849     * connecting the items. Instead of drawing separate lines,
850     * a {@code GeneralPath} is constructed and drawn at the end of
851     * the series painting.
852     *
853     * @param g2  the graphics device.
854     * @param state  the renderer state.
855     * @param plot  the plot (can be used to obtain standard color information
856     *              etc).
857     * @param dataset  the dataset.
858     * @param pass  the pass.
859     * @param series  the series index (zero-based).
860     * @param item  the item index (zero-based).
861     * @param domainAxis  the domain axis.
862     * @param rangeAxis  the range axis.
863     * @param dataArea  the area within which the data is being drawn.
864     */
865    protected void drawPrimaryLineAsPath(XYItemRendererState state,
866            Graphics2D g2, XYPlot plot, XYDataset dataset, int pass,
867            int series, int item, ValueAxis domainAxis, ValueAxis rangeAxis,
868            Rectangle2D dataArea) {
869
870        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
871        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
872
873        // get the data point...
874        double x1 = dataset.getXValue(series, item);
875        double y1 = dataset.getYValue(series, item);
876        double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
877        double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
878
879        State s = (State) state;
880        // update path to reflect latest point
881        if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) {
882            float x = (float) transX1;
883            float y = (float) transY1;
884            PlotOrientation orientation = plot.getOrientation();
885            if (orientation == PlotOrientation.HORIZONTAL) {
886                x = (float) transY1;
887                y = (float) transX1;
888            }
889            if (s.isLastPointGood()) {
890                s.seriesPath.lineTo(x, y);
891            }
892            else {
893                s.seriesPath.moveTo(x, y);
894            }
895            s.setLastPointGood(true);
896        } else {
897            s.setLastPointGood(false);
898        }
899        // if this is the last item, draw the path ...
900        if (item == s.getLastItemIndex()) {
901            // draw path
902            drawFirstPassShape(g2, pass, series, item, s.seriesPath);
903        }
904    }
905
906    /**
907     * Draws the item shapes and adds chart entities (second pass). This method
908     * draws the shapes which mark the item positions. If {@code entities}
909     * is not {@code null} it will be populated with entity information
910     * for points that fall within the data area.
911     *
912     * @param g2  the graphics device.
913     * @param plot  the plot (can be used to obtain standard color
914     *              information etc).
915     * @param domainAxis  the domain axis.
916     * @param dataArea  the area within which the data is being drawn.
917     * @param rangeAxis  the range axis.
918     * @param dataset  the dataset.
919     * @param pass  the pass.
920     * @param series  the series index (zero-based).
921     * @param item  the item index (zero-based).
922     * @param crosshairState  the crosshair state.
923     * @param entities the entity collection.
924     */
925    protected void drawSecondaryPass(Graphics2D g2, XYPlot plot, 
926            XYDataset dataset, int pass, int series, int item,
927            ValueAxis domainAxis, Rectangle2D dataArea, ValueAxis rangeAxis,
928            CrosshairState crosshairState, EntityCollection entities) {
929
930        Shape entityArea = null;
931
932        // get the data point...
933        double x1 = dataset.getXValue(series, item);
934        double y1 = dataset.getYValue(series, item);
935        if (Double.isNaN(y1) || Double.isNaN(x1)) {
936            return;
937        }
938
939        PlotOrientation orientation = plot.getOrientation();
940        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
941        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
942        double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
943        double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
944
945        if (getItemShapeVisible(series, item)) {
946            Shape shape = getItemShape(series, item);
947            if (orientation == PlotOrientation.HORIZONTAL) {
948                shape = ShapeUtils.createTranslatedShape(shape, transY1,
949                        transX1);
950            }
951            else if (orientation == PlotOrientation.VERTICAL) {
952                shape = ShapeUtils.createTranslatedShape(shape, transX1,
953                        transY1);
954            }
955            entityArea = shape;
956            if (shape.intersects(dataArea)) {
957                if (getItemShapeFilled(series, item)) {
958                    if (this.useFillPaint) {
959                        g2.setPaint(getItemFillPaint(series, item));
960                    }
961                    else {
962                        g2.setPaint(getItemPaint(series, item));
963                    }
964                    g2.fill(shape);
965                }
966                if (this.drawOutlines) {
967                    if (getUseOutlinePaint()) {
968                        g2.setPaint(getItemOutlinePaint(series, item));
969                    }
970                    else {
971                        g2.setPaint(getItemPaint(series, item));
972                    }
973                    g2.setStroke(getItemOutlineStroke(series, item));
974                    g2.draw(shape);
975                }
976            }
977        }
978
979        double xx = transX1;
980        double yy = transY1;
981        if (orientation == PlotOrientation.HORIZONTAL) {
982            xx = transY1;
983            yy = transX1;
984        }
985
986        // draw the item label if there is one...
987        if (isItemLabelVisible(series, item)) {
988            drawItemLabel(g2, orientation, dataset, series, item, xx, yy,
989                    (y1 < 0.0));
990        }
991
992        int datasetIndex = plot.indexOf(dataset);
993        updateCrosshairValues(crosshairState, x1, y1, datasetIndex,
994                transX1, transY1, orientation);
995
996        // add an entity for the item, but only if it falls within the data
997        // area...
998        if (entities != null && ShapeUtils.isPointInRect(dataArea, xx, yy)) {
999            addEntity(entities, entityArea, dataset, series, item, xx, yy);
1000        }
1001    }
1002
1003
1004    /**
1005     * Returns a legend item for the specified series.
1006     *
1007     * @param datasetIndex  the dataset index (zero-based).
1008     * @param series  the series index (zero-based).
1009     *
1010     * @return A legend item for the series (possibly {@code null}).
1011     */
1012    @Override
1013    public LegendItem getLegendItem(int datasetIndex, int series) {
1014        XYPlot plot = getPlot();
1015        if (plot == null) {
1016            return null;
1017        }
1018
1019        XYDataset dataset = plot.getDataset(datasetIndex);
1020        if (dataset == null) {
1021            return null;
1022        }
1023
1024        if (!getItemVisible(series, 0)) {
1025            return null;
1026        }
1027        String label = getLegendItemLabelGenerator().generateLabel(dataset,
1028                series);
1029        String description = label;
1030        String toolTipText = null;
1031        if (getLegendItemToolTipGenerator() != null) {
1032            toolTipText = getLegendItemToolTipGenerator().generateLabel(
1033                    dataset, series);
1034        }
1035        String urlText = null;
1036        if (getLegendItemURLGenerator() != null) {
1037            urlText = getLegendItemURLGenerator().generateLabel(dataset,
1038                    series);
1039        }
1040        boolean shapeIsVisible = getItemShapeVisible(series, 0);
1041        Shape shape = lookupLegendShape(series);
1042        boolean shapeIsFilled = getItemShapeFilled(series, 0);
1043        Paint fillPaint = (this.useFillPaint ? lookupSeriesFillPaint(series)
1044                : lookupSeriesPaint(series));
1045        boolean shapeOutlineVisible = this.drawOutlines;
1046        Paint outlinePaint = (this.useOutlinePaint ? lookupSeriesOutlinePaint(
1047                series) : lookupSeriesPaint(series));
1048        Stroke outlineStroke = lookupSeriesOutlineStroke(series);
1049        boolean lineVisible = getItemLineVisible(series, 0);
1050        Stroke lineStroke = lookupSeriesStroke(series);
1051        Paint linePaint = lookupSeriesPaint(series);
1052        LegendItem result = new LegendItem(label, description, toolTipText,
1053                urlText, shapeIsVisible, shape, shapeIsFilled, fillPaint,
1054                shapeOutlineVisible, outlinePaint, outlineStroke, lineVisible,
1055                this.legendLine, lineStroke, linePaint);
1056        result.setLabelFont(lookupLegendTextFont(series));
1057        Paint labelPaint = lookupLegendTextPaint(series);
1058        if (labelPaint != null) {
1059            result.setLabelPaint(labelPaint);
1060        }
1061        result.setSeriesKey(dataset.getSeriesKey(series));
1062        result.setSeriesIndex(series);
1063        result.setDataset(dataset);
1064        result.setDatasetIndex(datasetIndex);
1065
1066        return result;
1067    }
1068
1069    /**
1070     * Returns a clone of the renderer.
1071     *
1072     * @return A clone.
1073     *
1074     * @throws CloneNotSupportedException if the clone cannot be created.
1075     */
1076    @Override
1077    public Object clone() throws CloneNotSupportedException {
1078        XYLineAndShapeRenderer clone = (XYLineAndShapeRenderer) super.clone();
1079        clone.seriesLinesVisibleMap = new HashMap<>(this.seriesLinesVisibleMap);
1080        clone.legendLine = CloneUtils.clone(this.legendLine);
1081        clone.seriesShapesVisibleMap = new HashMap<>(this.seriesShapesVisibleMap);
1082        clone.seriesShapesFilledMap = new HashMap<>(this.seriesShapesFilledMap);
1083        return clone;
1084    }
1085
1086    /**
1087     * Tests this renderer for equality with an arbitrary object.
1088     *
1089     * @param obj  the object ({@code null} permitted).
1090     *
1091     * @return {@code true} or {@code false}.
1092     */
1093    @Override
1094    public boolean equals(Object obj) {
1095        if (obj == this) {
1096            return true;
1097        }
1098        if (!(obj instanceof XYLineAndShapeRenderer)) {
1099            return false;
1100        }
1101        if (!super.equals(obj)) {
1102            return false;
1103        }
1104        XYLineAndShapeRenderer that = (XYLineAndShapeRenderer) obj;
1105        if (!Objects.equals(this.seriesLinesVisibleMap, that.seriesLinesVisibleMap)) {
1106            return false;
1107        }
1108        if (this.defaultLinesVisible != that.defaultLinesVisible) {
1109            return false;
1110        }
1111        if (!ShapeUtils.equal(this.legendLine, that.legendLine)) {
1112            return false;
1113        }
1114        if (!Objects.equals(this.seriesShapesVisibleMap, that.seriesShapesVisibleMap)) {
1115            return false;
1116        }
1117        if (this.defaultShapesVisible != that.defaultShapesVisible) {
1118            return false;
1119        }
1120        if (!Objects.equals(this.seriesShapesFilledMap, that.seriesShapesFilledMap)) {
1121            return false;
1122        }
1123        if (this.defaultShapesFilled != that.defaultShapesFilled) {
1124            return false;
1125        }
1126        if (this.drawOutlines != that.drawOutlines) {
1127            return false;
1128        }
1129        if (this.useOutlinePaint != that.useOutlinePaint) {
1130            return false;
1131        }
1132        if (this.useFillPaint != that.useFillPaint) {
1133            return false;
1134        }
1135        if (this.drawSeriesLineAsPath != that.drawSeriesLineAsPath) {
1136            return false;
1137        }
1138        return true;
1139    }
1140
1141    @Override
1142    public int hashCode() {
1143        int result = super.hashCode();
1144        result = 31 * result + seriesLinesVisibleMap.hashCode();
1145        result = 31 * result + (defaultLinesVisible ? 1 : 0);
1146        result = 31 * result + seriesShapesVisibleMap.hashCode();
1147        result = 31 * result + (defaultShapesVisible ? 1 : 0);
1148        result = 31 * result + seriesShapesFilledMap.hashCode();
1149        result = 31 * result + (defaultShapesFilled ? 1 : 0);
1150        result = 31 * result + (drawOutlines ? 1 : 0);
1151        result = 31 * result + (useFillPaint ? 1 : 0);
1152        result = 31 * result + (useOutlinePaint ? 1 : 0);
1153        result = 31 * result + (drawSeriesLineAsPath ? 1 : 0);
1154        return result;
1155    }
1156
1157    /**
1158     * Provides serialization support.
1159     *
1160     * @param stream  the input stream.
1161     *
1162     * @throws IOException  if there is an I/O error.
1163     * @throws ClassNotFoundException  if there is a classpath problem.
1164     */
1165    private void readObject(ObjectInputStream stream)
1166            throws IOException, ClassNotFoundException {
1167        stream.defaultReadObject();
1168        this.legendLine = SerialUtils.readShape(stream);
1169    }
1170
1171    /**
1172     * Provides serialization support.
1173     *
1174     * @param stream  the output stream.
1175     *
1176     * @throws IOException  if there is an I/O error.
1177     */
1178    private void writeObject(ObjectOutputStream stream) throws IOException {
1179        stream.defaultWriteObject();
1180        SerialUtils.writeShape(this.legendLine, stream);
1181    }
1182
1183}