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