001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, 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     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025     * in the United States and other countries.]
026     *
027     * ---------------------------
028     * StandardXYItemRenderer.java
029     * ---------------------------
030     * (C) Copyright 2001-2008, by Object Refinery Limited and Contributors.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   Mark Watson (www.markwatson.com);
034     *                   Jonathan Nash;
035     *                   Andreas Schneider;
036     *                   Norbert Kiesel (for TBD Networks);
037     *                   Christian W. Zuckschwerdt;
038     *                   Bill Kelemen;
039     *                   Nicolas Brodu (for Astrium and EADS Corporate Research
040     *                   Center);
041     *
042     * Changes:
043     * --------
044     * 19-Oct-2001 : Version 1, based on code by Mark Watson (DG);
045     * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
046     * 21-Dec-2001 : Added working line instance to improve performance (DG);
047     * 22-Jan-2002 : Added code to lock crosshairs to data points.  Based on code
048     *               by Jonathan Nash (DG);
049     * 23-Jan-2002 : Added DrawInfo parameter to drawItem() method (DG);
050     * 28-Mar-2002 : Added a property change listener mechanism so that the
051     *               renderer no longer needs to be immutable (DG);
052     * 02-Apr-2002 : Modified to handle null values (DG);
053     * 09-Apr-2002 : Modified draw method to return void.  Removed the translated
054     *               zero from the drawItem method.  Override the initialise()
055     *               method to calculate it (DG);
056     * 13-May-2002 : Added code from Andreas Schneider to allow changing
057     *               shapes/colors per item (DG);
058     * 24-May-2002 : Incorporated tooltips into chart entities (DG);
059     * 25-Jun-2002 : Removed redundant code (DG);
060     * 05-Aug-2002 : Incorporated URLs for HTML image maps into chart entities (RA);
061     * 08-Aug-2002 : Added discontinuous lines option contributed by
062     *               Norbert Kiesel (DG);
063     * 20-Aug-2002 : Added user definable default values to be returned by
064     *               protected methods unless overridden by a subclass (DG);
065     * 23-Sep-2002 : Updated for changes in the XYItemRenderer interface (DG);
066     * 02-Oct-2002 : Fixed errors reported by Checkstyle (DG);
067     * 25-Mar-2003 : Implemented Serializable (DG);
068     * 01-May-2003 : Modified drawItem() method signature (DG);
069     * 15-May-2003 : Modified to take into account the plot orientation (DG);
070     * 29-Jul-2003 : Amended code that doesn't compile with JDK 1.2.2 (DG);
071     * 30-Jul-2003 : Modified entity constructor (CZ);
072     * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
073     * 24-Aug-2003 : Added null/NaN checks in drawItem (BK);
074     * 08-Sep-2003 : Fixed serialization (NB);
075     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
076     * 21-Jan-2004 : Override for getLegendItem() method (DG);
077     * 27-Jan-2004 : Moved working line into state object (DG);
078     * 10-Feb-2004 : Changed drawItem() method to make cut-and-paste overriding
079     *               easier (DG);
080     * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState.  Renamed
081     *               XYToolTipGenerator --> XYItemLabelGenerator (DG);
082     * 08-Jun-2004 : Modified to use getX() and getY() methods (DG);
083     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
084     *               getYValue() (DG);
085     * 25-Aug-2004 : Created addEntity() method in superclass (DG);
086     * 08-Oct-2004 : Added 'gapThresholdType' as suggested by Mike Watts (DG);
087     * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
088     * 23-Feb-2005 : Fixed getLegendItem() method to show lines.  Fixed bug
089     *               1077108 (shape not visible for first item in series) (DG);
090     * 10-Apr-2005 : Fixed item label positioning with horizontal orientation (DG);
091     * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG);
092     * 27-Apr-2005 : Use generator for series label in legend (DG);
093     * ------------- JFREECHART 1.0.x ---------------------------------------------
094     * 15-Jun-2006 : Fixed bug (1380480) for rendering series as path (DG);
095     * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
096     * 14-Mar-2007 : Fixed problems with the equals() and clone() methods (DG);
097     * 23-Mar-2007 : Clean-up of shapesFilled attributes (DG);
098     * 20-Apr-2007 : Updated getLegendItem() and drawItem() for renderer
099     *               change (DG);
100     * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem()
101     *               method (DG);
102     * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG);
103     * 08-Jun-2007 : Fixed bug in entity creation (DG);
104     * 21-Nov-2007 : Deprecated override flag methods (DG);
105     * 02-Jun-2008 : Fixed tooltips for data items at lower edges of data area (DG);
106     * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG);
107     *
108     */
109    
110    package org.jfree.chart.renderer.xy;
111    
112    import java.awt.Graphics2D;
113    import java.awt.Image;
114    import java.awt.Paint;
115    import java.awt.Point;
116    import java.awt.Shape;
117    import java.awt.Stroke;
118    import java.awt.geom.GeneralPath;
119    import java.awt.geom.Line2D;
120    import java.awt.geom.Rectangle2D;
121    import java.io.IOException;
122    import java.io.ObjectInputStream;
123    import java.io.ObjectOutputStream;
124    import java.io.Serializable;
125    
126    import org.jfree.chart.LegendItem;
127    import org.jfree.chart.axis.ValueAxis;
128    import org.jfree.chart.entity.EntityCollection;
129    import org.jfree.chart.event.RendererChangeEvent;
130    import org.jfree.chart.labels.XYToolTipGenerator;
131    import org.jfree.chart.plot.CrosshairState;
132    import org.jfree.chart.plot.Plot;
133    import org.jfree.chart.plot.PlotOrientation;
134    import org.jfree.chart.plot.PlotRenderingInfo;
135    import org.jfree.chart.plot.XYPlot;
136    import org.jfree.chart.urls.XYURLGenerator;
137    import org.jfree.data.xy.XYDataset;
138    import org.jfree.io.SerialUtilities;
139    import org.jfree.ui.RectangleEdge;
140    import org.jfree.util.BooleanList;
141    import org.jfree.util.BooleanUtilities;
142    import org.jfree.util.ObjectUtilities;
143    import org.jfree.util.PublicCloneable;
144    import org.jfree.util.ShapeUtilities;
145    import org.jfree.util.UnitType;
146    
147    /**
148     * Standard item renderer for an {@link XYPlot}.  This class can draw (a)
149     * shapes at each point, or (b) lines between points, or (c) both shapes and
150     * lines.
151     * <P>
152     * This renderer has been retained for historical reasons and, in general, you
153     * should use the {@link XYLineAndShapeRenderer} class instead.
154     */
155    public class StandardXYItemRenderer extends AbstractXYItemRenderer
156            implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {
157    
158        /** For serialization. */
159        private static final long serialVersionUID = -3271351259436865995L;
160    
161        /** Constant for the type of rendering (shapes only). */
162        public static final int SHAPES = 1;
163    
164        /** Constant for the type of rendering (lines only). */
165        public static final int LINES = 2;
166    
167        /** Constant for the type of rendering (shapes and lines). */
168        public static final int SHAPES_AND_LINES = SHAPES | LINES;
169    
170        /** Constant for the type of rendering (images only). */
171        public static final int IMAGES = 4;
172    
173        /** Constant for the type of rendering (discontinuous lines). */
174        public static final int DISCONTINUOUS = 8;
175    
176        /** Constant for the type of rendering (discontinuous lines). */
177        public static final int DISCONTINUOUS_LINES = LINES | DISCONTINUOUS;
178    
179        /** A flag indicating whether or not shapes are drawn at each XY point. */
180        private boolean baseShapesVisible;
181    
182        /** A flag indicating whether or not lines are drawn between XY points. */
183        private boolean plotLines;
184    
185        /** A flag indicating whether or not images are drawn between XY points. */
186        private boolean plotImages;
187    
188        /** A flag controlling whether or not discontinuous lines are used. */
189        private boolean plotDiscontinuous;
190    
191        /** Specifies how the gap threshold value is interpreted. */
192        private UnitType gapThresholdType = UnitType.RELATIVE;
193    
194        /** Threshold for deciding when to discontinue a line. */
195        private double gapThreshold = 1.0;
196    
197        /**
198         * A flag that controls whether or not shapes are filled for ALL series.
199         *
200         * @deprecated As of 1.0.8, this override should not be used.
201         */
202        private Boolean shapesFilled;
203    
204        /**
205         * A table of flags that control (per series) whether or not shapes are
206         * filled.
207         */
208        private BooleanList seriesShapesFilled;
209    
210        /** The default value returned by the getShapeFilled() method. */
211        private boolean baseShapesFilled;
212    
213        /**
214         * A flag that controls whether or not each series is drawn as a single
215         * path.
216         */
217        private boolean drawSeriesLineAsPath;
218    
219        /**
220         * The shape that is used to represent a line in the legend.
221         * This should never be set to <code>null</code>.
222         */
223        private transient Shape legendLine;
224    
225        /**
226         * Constructs a new renderer.
227         */
228        public StandardXYItemRenderer() {
229            this(LINES, null);
230        }
231    
232        /**
233         * Constructs a new renderer.  To specify the type of renderer, use one of
234         * the constants: {@link #SHAPES}, {@link #LINES} or
235         * {@link #SHAPES_AND_LINES}.
236         *
237         * @param type  the type.
238         */
239        public StandardXYItemRenderer(int type) {
240            this(type, null);
241        }
242    
243        /**
244         * Constructs a new renderer.  To specify the type of renderer, use one of
245         * the constants: {@link #SHAPES}, {@link #LINES} or
246         * {@link #SHAPES_AND_LINES}.
247         *
248         * @param type  the type of renderer.
249         * @param toolTipGenerator  the item label generator (<code>null</code>
250         *                          permitted).
251         */
252        public StandardXYItemRenderer(int type,
253                                      XYToolTipGenerator toolTipGenerator) {
254            this(type, toolTipGenerator, null);
255        }
256    
257        /**
258         * Constructs a new renderer.  To specify the type of renderer, use one of
259         * the constants: {@link #SHAPES}, {@link #LINES} or
260         * {@link #SHAPES_AND_LINES}.
261         *
262         * @param type  the type of renderer.
263         * @param toolTipGenerator  the item label generator (<code>null</code>
264         *                          permitted).
265         * @param urlGenerator  the URL generator.
266         */
267        public StandardXYItemRenderer(int type,
268                                      XYToolTipGenerator toolTipGenerator,
269                                      XYURLGenerator urlGenerator) {
270    
271            super();
272            setBaseToolTipGenerator(toolTipGenerator);
273            setURLGenerator(urlGenerator);
274            if ((type & SHAPES) != 0) {
275                this.baseShapesVisible = true;
276            }
277            if ((type & LINES) != 0) {
278                this.plotLines = true;
279            }
280            if ((type & IMAGES) != 0) {
281                this.plotImages = true;
282            }
283            if ((type & DISCONTINUOUS) != 0) {
284                this.plotDiscontinuous = true;
285            }
286    
287            this.shapesFilled = null;
288            this.seriesShapesFilled = new BooleanList();
289            this.baseShapesFilled = true;
290            this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
291            this.drawSeriesLineAsPath = false;
292        }
293    
294        /**
295         * Returns true if shapes are being plotted by the renderer.
296         *
297         * @return <code>true</code> if shapes are being plotted by the renderer.
298         *
299         * @see #setBaseShapesVisible
300         */
301        public boolean getBaseShapesVisible() {
302            return this.baseShapesVisible;
303        }
304    
305        /**
306         * Sets the flag that controls whether or not a shape is plotted at each
307         * data point.
308         *
309         * @param flag  the flag.
310         *
311         * @see #getBaseShapesVisible
312         */
313        public void setBaseShapesVisible(boolean flag) {
314            if (this.baseShapesVisible != flag) {
315                this.baseShapesVisible = flag;
316                fireChangeEvent();
317            }
318        }
319    
320        // SHAPES FILLED
321    
322        /**
323         * Returns the flag used to control whether or not the shape for an item is
324         * filled.
325         * <p>
326         * The default implementation passes control to the
327         * <code>getSeriesShapesFilled</code> 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         * @see #getSeriesShapesFilled(int)
336         */
337        public boolean getItemShapeFilled(int series, int item) {
338            // return the overall setting, if there is one...
339            if (this.shapesFilled != null) {
340                return this.shapesFilled.booleanValue();
341            }
342    
343            // otherwise look up the paint table
344            Boolean flag = this.seriesShapesFilled.getBoolean(series);
345            if (flag != null) {
346                return flag.booleanValue();
347            }
348            else {
349                return this.baseShapesFilled;
350            }
351        }
352    
353        /**
354         * Returns the override flag that controls whether or not shapes are filled
355         * for ALL series.
356         *
357         * @return The flag (possibly <code>null</code>).
358         *
359         * @since 1.0.5
360         *
361         * @deprecated As of 1.0.8, you should avoid using this method and rely
362         *             on just the per-series ({@link #getSeriesShapesFilled(int)})
363         *             and base-level ({@link #getBaseShapesFilled()}) settings.
364         */
365        public Boolean getShapesFilled() {
366            return this.shapesFilled;
367        }
368    
369        /**
370         * Sets the override flag that controls whether or not shapes are filled
371         * for ALL series and sends a {@link RendererChangeEvent} to all registered
372         * listeners.
373         *
374         * @param filled  the flag.
375         *
376         * @see #setShapesFilled(Boolean)
377         *
378         * @deprecated As of 1.0.8, you should avoid using this method and rely
379         *             on just the per-series ({@link #setSeriesShapesFilled(int,
380         *             Boolean)}) and base-level ({@link #setBaseShapesVisible(
381         *             boolean)}) settings.
382         */
383        public void setShapesFilled(boolean filled) {
384            // here we use BooleanUtilities to remain compatible with JDKs < 1.4
385            setShapesFilled(BooleanUtilities.valueOf(filled));
386        }
387    
388        /**
389         * Sets the override flag that controls whether or not shapes are filled
390         * for ALL series and sends a {@link RendererChangeEvent} to all registered
391         * listeners.
392         *
393         * @param filled  the flag (<code>null</code> permitted).
394         *
395         * @see #setShapesFilled(boolean)
396         *
397         * @deprecated As of 1.0.8, you should avoid using this method and rely
398         *             on just the per-series ({@link #setSeriesShapesFilled(int,
399         *             Boolean)}) and base-level ({@link #setBaseShapesVisible(
400         *             boolean)}) settings.
401         */
402        public void setShapesFilled(Boolean filled) {
403            this.shapesFilled = filled;
404            fireChangeEvent();
405        }
406    
407        /**
408         * Returns the flag used to control whether or not the shapes for a series
409         * are filled.
410         *
411         * @param series  the series index (zero-based).
412         *
413         * @return A boolean.
414         */
415        public Boolean getSeriesShapesFilled(int series) {
416            return this.seriesShapesFilled.getBoolean(series);
417        }
418    
419        /**
420         * Sets the 'shapes filled' flag for a series and sends a
421         * {@link RendererChangeEvent} to all registered listeners.
422         *
423         * @param series  the series index (zero-based).
424         * @param flag  the flag.
425         *
426         * @see #getSeriesShapesFilled(int)
427         */
428        public void setSeriesShapesFilled(int series, Boolean flag) {
429            this.seriesShapesFilled.setBoolean(series, flag);
430            fireChangeEvent();
431        }
432    
433        /**
434         * Returns the base 'shape filled' attribute.
435         *
436         * @return The base flag.
437         *
438         * @see #setBaseShapesFilled(boolean)
439         */
440        public boolean getBaseShapesFilled() {
441            return this.baseShapesFilled;
442        }
443    
444        /**
445         * Sets the base 'shapes filled' flag and sends a
446         * {@link RendererChangeEvent} to all registered listeners.
447         *
448         * @param flag  the flag.
449         *
450         * @see #getBaseShapesFilled()
451         */
452        public void setBaseShapesFilled(boolean flag) {
453            this.baseShapesFilled = flag;
454        }
455    
456        /**
457         * Returns true if lines are being plotted by the renderer.
458         *
459         * @return <code>true</code> if lines are being plotted by the renderer.
460         *
461         * @see #setPlotLines(boolean)
462         */
463        public boolean getPlotLines() {
464            return this.plotLines;
465        }
466    
467        /**
468         * Sets the flag that controls whether or not a line is plotted between
469         * each data point and sends a {@link RendererChangeEvent} to all
470         * registered listeners.
471         *
472         * @param flag  the flag.
473         *
474         * @see #getPlotLines()
475         */
476        public void setPlotLines(boolean flag) {
477            if (this.plotLines != flag) {
478                this.plotLines = flag;
479                fireChangeEvent();
480            }
481        }
482    
483        /**
484         * Returns the gap threshold type (relative or absolute).
485         *
486         * @return The type.
487         *
488         * @see #setGapThresholdType(UnitType)
489         */
490        public UnitType getGapThresholdType() {
491            return this.gapThresholdType;
492        }
493    
494        /**
495         * Sets the gap threshold type and sends a {@link RendererChangeEvent} to
496         * all registered listeners.
497         *
498         * @param thresholdType  the type (<code>null</code> not permitted).
499         *
500         * @see #getGapThresholdType()
501         */
502        public void setGapThresholdType(UnitType thresholdType) {
503            if (thresholdType == null) {
504                throw new IllegalArgumentException(
505                        "Null 'thresholdType' argument.");
506            }
507            this.gapThresholdType = thresholdType;
508            fireChangeEvent();
509        }
510    
511        /**
512         * Returns the gap threshold for discontinuous lines.
513         *
514         * @return The gap threshold.
515         *
516         * @see #setGapThreshold(double)
517         */
518        public double getGapThreshold() {
519            return this.gapThreshold;
520        }
521    
522        /**
523         * Sets the gap threshold for discontinuous lines and sends a
524         * {@link RendererChangeEvent} to all registered listeners.
525         *
526         * @param t  the threshold.
527         *
528         * @see #getGapThreshold()
529         */
530        public void setGapThreshold(double t) {
531            this.gapThreshold = t;
532            fireChangeEvent();
533        }
534    
535        /**
536         * Returns true if images are being plotted by the renderer.
537         *
538         * @return <code>true</code> if images are being plotted by the renderer.
539         *
540         * @see #setPlotImages(boolean)
541         */
542        public boolean getPlotImages() {
543            return this.plotImages;
544        }
545    
546        /**
547         * Sets the flag that controls whether or not an image is drawn at each
548         * data point and sends a {@link RendererChangeEvent} to all registered
549         * listeners.
550         *
551         * @param flag  the flag.
552         *
553         * @see #getPlotImages()
554         */
555        public void setPlotImages(boolean flag) {
556            if (this.plotImages != flag) {
557                this.plotImages = flag;
558                fireChangeEvent();
559            }
560        }
561    
562        /**
563         * Returns a flag that controls whether or not the renderer shows
564         * discontinuous lines.
565         *
566         * @return <code>true</code> if lines should be discontinuous.
567         */
568        public boolean getPlotDiscontinuous() {
569            return this.plotDiscontinuous;
570        }
571    
572        /**
573         * Sets the flag that controls whether or not the renderer shows
574         * discontinuous lines, and sends a {@link RendererChangeEvent} to all
575         * registered listeners.
576         *
577         * @param flag  the new flag value.
578         *
579         * @since 1.0.5
580         */
581        public void setPlotDiscontinuous(boolean flag) {
582            if (this.plotDiscontinuous != flag) {
583                this.plotDiscontinuous = flag;
584                fireChangeEvent();
585            }
586        }
587    
588        /**
589         * Returns a flag that controls whether or not each series is drawn as a
590         * single path.
591         *
592         * @return A boolean.
593         *
594         * @see #setDrawSeriesLineAsPath(boolean)
595         */
596        public boolean getDrawSeriesLineAsPath() {
597            return this.drawSeriesLineAsPath;
598        }
599    
600        /**
601         * Sets the flag that controls whether or not each series is drawn as a
602         * single path.
603         *
604         * @param flag  the flag.
605         *
606         * @see #getDrawSeriesLineAsPath()
607         */
608        public void setDrawSeriesLineAsPath(boolean flag) {
609            this.drawSeriesLineAsPath = flag;
610        }
611    
612        /**
613         * Returns the shape used to represent a line in the legend.
614         *
615         * @return The legend line (never <code>null</code>).
616         *
617         * @see #setLegendLine(Shape)
618         */
619        public Shape getLegendLine() {
620            return this.legendLine;
621        }
622    
623        /**
624         * Sets the shape used as a line in each legend item and sends a
625         * {@link RendererChangeEvent} to all registered listeners.
626         *
627         * @param line  the line (<code>null</code> not permitted).
628         *
629         * @see #getLegendLine()
630         */
631        public void setLegendLine(Shape line) {
632            if (line == null) {
633                throw new IllegalArgumentException("Null 'line' argument.");
634            }
635            this.legendLine = line;
636            fireChangeEvent();
637        }
638    
639        /**
640         * Returns a legend item for a series.
641         *
642         * @param datasetIndex  the dataset index (zero-based).
643         * @param series  the series index (zero-based).
644         *
645         * @return A legend item for the series.
646         */
647        public LegendItem getLegendItem(int datasetIndex, int series) {
648            XYPlot plot = getPlot();
649            if (plot == null) {
650                return null;
651            }
652            LegendItem result = null;
653            XYDataset dataset = plot.getDataset(datasetIndex);
654            if (dataset != null) {
655                if (getItemVisible(series, 0)) {
656                    String label = getLegendItemLabelGenerator().generateLabel(
657                            dataset, series);
658                    String description = label;
659                    String toolTipText = null;
660                    if (getLegendItemToolTipGenerator() != null) {
661                        toolTipText = getLegendItemToolTipGenerator().generateLabel(
662                                dataset, series);
663                    }
664                    String urlText = null;
665                    if (getLegendItemURLGenerator() != null) {
666                        urlText = getLegendItemURLGenerator().generateLabel(
667                                dataset, series);
668                    }
669                    Shape shape = lookupLegendShape(series);
670                    boolean shapeFilled = getItemShapeFilled(series, 0);
671                    Paint paint = lookupSeriesPaint(series);
672                    Paint linePaint = paint;
673                    Stroke lineStroke = lookupSeriesStroke(series);
674                    result = new LegendItem(label, description, toolTipText,
675                            urlText, this.baseShapesVisible, shape, shapeFilled,
676                            paint, !shapeFilled, paint, lineStroke,
677                            this.plotLines, this.legendLine, lineStroke, linePaint);
678                    result.setLabelFont(lookupLegendTextFont(series));
679                    Paint labelPaint = lookupLegendTextPaint(series);
680                    if (labelPaint != null) {
681                        result.setLabelPaint(labelPaint);
682                    }
683                    result.setDataset(dataset);
684                    result.setDatasetIndex(datasetIndex);
685                    result.setSeriesKey(dataset.getSeriesKey(series));
686                    result.setSeriesIndex(series);
687                }
688            }
689            return result;
690        }
691    
692        /**
693         * Records the state for the renderer.  This is used to preserve state
694         * information between calls to the drawItem() method for a single chart
695         * drawing.
696         */
697        public static class State extends XYItemRendererState {
698    
699            /** The path for the current series. */
700            public GeneralPath seriesPath;
701    
702            /** The series index. */
703            private int seriesIndex;
704    
705            /**
706             * A flag that indicates if the last (x, y) point was 'good'
707             * (non-null).
708             */
709            private boolean lastPointGood;
710    
711            /**
712             * Creates a new state instance.
713             *
714             * @param info  the plot rendering info.
715             */
716            public State(PlotRenderingInfo info) {
717                super(info);
718            }
719    
720            /**
721             * Returns a flag that indicates if the last point drawn (in the
722             * current series) was 'good' (non-null).
723             *
724             * @return A boolean.
725             */
726            public boolean isLastPointGood() {
727                return this.lastPointGood;
728            }
729    
730            /**
731             * Sets a flag that indicates if the last point drawn (in the current
732             * series) was 'good' (non-null).
733             *
734             * @param good  the flag.
735             */
736            public void setLastPointGood(boolean good) {
737                this.lastPointGood = good;
738            }
739    
740            /**
741             * Returns the series index for the current path.
742             *
743             * @return The series index for the current path.
744             */
745            public int getSeriesIndex() {
746                return this.seriesIndex;
747            }
748    
749            /**
750             * Sets the series index for the current path.
751             *
752             * @param index  the index.
753             */
754            public void setSeriesIndex(int index) {
755                this.seriesIndex = index;
756            }
757        }
758    
759        /**
760         * Initialises the renderer.
761         * <P>
762         * This method will be called before the first item is rendered, giving the
763         * renderer an opportunity to initialise any state information it wants to
764         * maintain. The renderer can do nothing if it chooses.
765         *
766         * @param g2  the graphics device.
767         * @param dataArea  the area inside the axes.
768         * @param plot  the plot.
769         * @param data  the data.
770         * @param info  an optional info collection object to return data back to
771         *              the caller.
772         *
773         * @return The renderer state.
774         */
775        public XYItemRendererState initialise(Graphics2D g2,
776                                              Rectangle2D dataArea,
777                                              XYPlot plot,
778                                              XYDataset data,
779                                              PlotRenderingInfo info) {
780    
781            State state = new State(info);
782            state.seriesPath = new GeneralPath();
783            state.seriesIndex = -1;
784            return state;
785    
786        }
787    
788        /**
789         * Draws the visual representation of a single data item.
790         *
791         * @param g2  the graphics device.
792         * @param state  the renderer state.
793         * @param dataArea  the area within which the data is being drawn.
794         * @param info  collects information about the drawing.
795         * @param plot  the plot (can be used to obtain standard color information
796         *              etc).
797         * @param domainAxis  the domain axis.
798         * @param rangeAxis  the range axis.
799         * @param dataset  the dataset.
800         * @param series  the series index (zero-based).
801         * @param item  the item index (zero-based).
802         * @param crosshairState  crosshair information for the plot
803         *                        (<code>null</code> permitted).
804         * @param pass  the pass index.
805         */
806        public void drawItem(Graphics2D g2,
807                             XYItemRendererState state,
808                             Rectangle2D dataArea,
809                             PlotRenderingInfo info,
810                             XYPlot plot,
811                             ValueAxis domainAxis,
812                             ValueAxis rangeAxis,
813                             XYDataset dataset,
814                             int series,
815                             int item,
816                             CrosshairState crosshairState,
817                             int pass) {
818    
819            boolean itemVisible = getItemVisible(series, item);
820    
821            // setup for collecting optional entity info...
822            Shape entityArea = null;
823            EntityCollection entities = null;
824            if (info != null) {
825                entities = info.getOwner().getEntityCollection();
826            }
827    
828            PlotOrientation orientation = plot.getOrientation();
829            Paint paint = getItemPaint(series, item);
830            Stroke seriesStroke = getItemStroke(series, item);
831            g2.setPaint(paint);
832            g2.setStroke(seriesStroke);
833    
834            // get the data point...
835            double x1 = dataset.getXValue(series, item);
836            double y1 = dataset.getYValue(series, item);
837            if (Double.isNaN(x1) || Double.isNaN(y1)) {
838                itemVisible = false;
839            }
840    
841            RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
842            RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
843            double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
844            double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
845    
846            if (getPlotLines()) {
847                if (this.drawSeriesLineAsPath) {
848                    State s = (State) state;
849                    if (s.getSeriesIndex() != series) {
850                        // we are starting a new series path
851                        s.seriesPath.reset();
852                        s.lastPointGood = false;
853                        s.setSeriesIndex(series);
854                    }
855    
856                    // update path to reflect latest point
857                    if (itemVisible && !Double.isNaN(transX1)
858                            && !Double.isNaN(transY1)) {
859                        float x = (float) transX1;
860                        float y = (float) transY1;
861                        if (orientation == PlotOrientation.HORIZONTAL) {
862                            x = (float) transY1;
863                            y = (float) transX1;
864                        }
865                        if (s.isLastPointGood()) {
866                            // TODO: check threshold
867                            s.seriesPath.lineTo(x, y);
868                        }
869                        else {
870                            s.seriesPath.moveTo(x, y);
871                        }
872                        s.setLastPointGood(true);
873                    }
874                    else {
875                        s.setLastPointGood(false);
876                    }
877                    if (item == dataset.getItemCount(series) - 1) {
878                        if (s.seriesIndex == series) {
879                            // draw path
880                            g2.setStroke(lookupSeriesStroke(series));
881                            g2.setPaint(lookupSeriesPaint(series));
882                            g2.draw(s.seriesPath);
883                        }
884                    }
885                }
886    
887                else if (item != 0 && itemVisible) {
888                    // get the previous data point...
889                    double x0 = dataset.getXValue(series, item - 1);
890                    double y0 = dataset.getYValue(series, item - 1);
891                    if (!Double.isNaN(x0) && !Double.isNaN(y0)) {
892                        boolean drawLine = true;
893                        if (getPlotDiscontinuous()) {
894                            // only draw a line if the gap between the current and
895                            // previous data point is within the threshold
896                            int numX = dataset.getItemCount(series);
897                            double minX = dataset.getXValue(series, 0);
898                            double maxX = dataset.getXValue(series, numX - 1);
899                            if (this.gapThresholdType == UnitType.ABSOLUTE) {
900                                drawLine = Math.abs(x1 - x0) <= this.gapThreshold;
901                            }
902                            else {
903                                drawLine = Math.abs(x1 - x0) <= ((maxX - minX)
904                                    / numX * getGapThreshold());
905                            }
906                        }
907                        if (drawLine) {
908                            double transX0 = domainAxis.valueToJava2D(x0, dataArea,
909                                    xAxisLocation);
910                            double transY0 = rangeAxis.valueToJava2D(y0, dataArea,
911                                    yAxisLocation);
912    
913                            // only draw if we have good values
914                            if (Double.isNaN(transX0) || Double.isNaN(transY0)
915                                || Double.isNaN(transX1) || Double.isNaN(transY1)) {
916                                return;
917                            }
918    
919                            if (orientation == PlotOrientation.HORIZONTAL) {
920                                state.workingLine.setLine(transY0, transX0,
921                                        transY1, transX1);
922                            }
923                            else if (orientation == PlotOrientation.VERTICAL) {
924                                state.workingLine.setLine(transX0, transY0,
925                                        transX1, transY1);
926                            }
927    
928                            if (state.workingLine.intersects(dataArea)) {
929                                g2.draw(state.workingLine);
930                            }
931                        }
932                    }
933                }
934            }
935    
936            // we needed to get this far even for invisible items, to ensure that
937            // seriesPath updates happened, but now there is nothing more we need
938            // to do for non-visible items...
939            if (!itemVisible) {
940                return;
941            }
942    
943            if (getBaseShapesVisible()) {
944    
945                Shape shape = getItemShape(series, item);
946                if (orientation == PlotOrientation.HORIZONTAL) {
947                    shape = ShapeUtilities.createTranslatedShape(shape, transY1,
948                            transX1);
949                }
950                else if (orientation == PlotOrientation.VERTICAL) {
951                    shape = ShapeUtilities.createTranslatedShape(shape, transX1,
952                            transY1);
953                }
954                if (shape.intersects(dataArea)) {
955                    if (getItemShapeFilled(series, item)) {
956                        g2.fill(shape);
957                    }
958                    else {
959                        g2.draw(shape);
960                    }
961                }
962                entityArea = shape;
963    
964            }
965    
966            if (getPlotImages()) {
967                Image image = getImage(plot, series, item, transX1, transY1);
968                if (image != null) {
969                    Point hotspot = getImageHotspot(plot, series, item, transX1,
970                            transY1, image);
971                    g2.drawImage(image, (int) (transX1 - hotspot.getX()),
972                            (int) (transY1 - hotspot.getY()), null);
973                    entityArea = new Rectangle2D.Double(transX1 - hotspot.getX(),
974                            transY1 - hotspot.getY(), image.getWidth(null),
975                            image.getHeight(null));
976                }
977    
978            }
979    
980            double xx = transX1;
981            double yy = transY1;
982            if (orientation == PlotOrientation.HORIZONTAL) {
983                xx = transY1;
984                yy = transX1;
985            }
986    
987            // draw the item label if there is one...
988            if (isItemLabelVisible(series, item)) {
989                drawItemLabel(g2, orientation, dataset, series, item, xx, yy,
990                        (y1 < 0.0));
991            }
992    
993            int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
994            int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
995            updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex,
996                    rangeAxisIndex, transX1, transY1, orientation);
997    
998            // add an entity for the item...
999            if (entities != null && isPointInRect(dataArea, xx, yy)) {
1000                addEntity(entities, entityArea, dataset, series, item, xx, yy);
1001            }
1002    
1003        }
1004    
1005        /**
1006         * Tests this renderer for equality with another object.
1007         *
1008         * @param obj  the object (<code>null</code> permitted).
1009         *
1010         * @return A boolean.
1011         */
1012        public boolean equals(Object obj) {
1013    
1014            if (obj == this) {
1015                return true;
1016            }
1017            if (!(obj instanceof StandardXYItemRenderer)) {
1018                return false;
1019            }
1020            StandardXYItemRenderer that = (StandardXYItemRenderer) obj;
1021            if (this.baseShapesVisible != that.baseShapesVisible) {
1022                return false;
1023            }
1024            if (this.plotLines != that.plotLines) {
1025                return false;
1026            }
1027            if (this.plotImages != that.plotImages) {
1028                return false;
1029            }
1030            if (this.plotDiscontinuous != that.plotDiscontinuous) {
1031                return false;
1032            }
1033            if (this.gapThresholdType != that.gapThresholdType) {
1034                return false;
1035            }
1036            if (this.gapThreshold != that.gapThreshold) {
1037                return false;
1038            }
1039            if (!ObjectUtilities.equal(this.shapesFilled, that.shapesFilled)) {
1040                return false;
1041            }
1042            if (!this.seriesShapesFilled.equals(that.seriesShapesFilled)) {
1043                return false;
1044            }
1045            if (this.baseShapesFilled != that.baseShapesFilled) {
1046                return false;
1047            }
1048            if (this.drawSeriesLineAsPath != that.drawSeriesLineAsPath) {
1049                return false;
1050            }
1051            if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) {
1052                return false;
1053            }
1054            return super.equals(obj);
1055    
1056        }
1057    
1058        /**
1059         * Returns a clone of the renderer.
1060         *
1061         * @return A clone.
1062         *
1063         * @throws CloneNotSupportedException  if the renderer cannot be cloned.
1064         */
1065        public Object clone() throws CloneNotSupportedException {
1066            StandardXYItemRenderer clone = (StandardXYItemRenderer) super.clone();
1067            clone.seriesShapesFilled
1068                    = (BooleanList) this.seriesShapesFilled.clone();
1069            clone.legendLine = ShapeUtilities.clone(this.legendLine);
1070            return clone;
1071        }
1072    
1073        ////////////////////////////////////////////////////////////////////////////
1074        // PROTECTED METHODS
1075        // These provide the opportunity to subclass the standard renderer and
1076        // create custom effects.
1077        ////////////////////////////////////////////////////////////////////////////
1078    
1079        /**
1080         * Returns the image used to draw a single data item.
1081         *
1082         * @param plot  the plot (can be used to obtain standard color information
1083         *              etc).
1084         * @param series  the series index.
1085         * @param item  the item index.
1086         * @param x  the x value of the item.
1087         * @param y  the y value of the item.
1088         *
1089         * @return The image.
1090         *
1091         * @see #getPlotImages()
1092         */
1093        protected Image getImage(Plot plot, int series, int item,
1094                                 double x, double y) {
1095            // this method must be overridden if you want to display images
1096            return null;
1097        }
1098    
1099        /**
1100         * Returns the hotspot of the image used to draw a single data item.
1101         * The hotspot is the point relative to the top left of the image
1102         * that should indicate the data item. The default is the center of the
1103         * image.
1104         *
1105         * @param plot  the plot (can be used to obtain standard color information
1106         *              etc).
1107         * @param image  the image (can be used to get size information about the
1108         *               image)
1109         * @param series  the series index
1110         * @param item  the item index
1111         * @param x  the x value of the item
1112         * @param y  the y value of the item
1113         *
1114         * @return The hotspot used to draw the data item.
1115         */
1116        protected Point getImageHotspot(Plot plot, int series, int item,
1117                                        double x, double y, Image image) {
1118    
1119            int height = image.getHeight(null);
1120            int width = image.getWidth(null);
1121            return new Point(width / 2, height / 2);
1122    
1123        }
1124    
1125        /**
1126         * Provides serialization support.
1127         *
1128         * @param stream  the input stream.
1129         *
1130         * @throws IOException  if there is an I/O error.
1131         * @throws ClassNotFoundException  if there is a classpath problem.
1132         */
1133        private void readObject(ObjectInputStream stream)
1134                throws IOException, ClassNotFoundException {
1135            stream.defaultReadObject();
1136            this.legendLine = SerialUtilities.readShape(stream);
1137        }
1138    
1139        /**
1140         * Provides serialization support.
1141         *
1142         * @param stream  the output stream.
1143         *
1144         * @throws IOException  if there is an I/O error.
1145         */
1146        private void writeObject(ObjectOutputStream stream) throws IOException {
1147            stream.defaultWriteObject();
1148            SerialUtilities.writeShape(this.legendLine, stream);
1149        }
1150    
1151    }