001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors.
006     *
007     * Project Info:  http://www.jfree.org/jfreechart/index.html
008     *
009     * This library is free software; you can redistribute it and/or modify it
010     * under the terms of the GNU Lesser General Public License as published by
011     * the Free Software Foundation; either version 2.1 of the License, or
012     * (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but
015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017     * License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this library; if not, write to the Free Software
021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022     * USA.
023     *
024     * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025     * Other names may be trademarks of their respective owners.]
026     *
027     * -----------------------------
028     * DefaultPolarItemRenderer.java
029     * -----------------------------
030     * (C) Copyright 2004-2011, by Solution Engineering, Inc. and
031     *     Contributors.
032     *
033     * Original Author:  Daniel Bridenbecker, Solution Engineering, Inc.;
034     * Contributor(s):   David Gilbert (for Object Refinery Limited);
035     *                   Martin Hoeller (patch 2850344);
036     *
037     * Changes
038     * -------
039     * 19-Jan-2004 : Version 1, contributed by DB with minor changes by DG (DG);
040     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
041     *               getYValue() (DG);
042     * 04-Oct-2004 : Renamed BooleanUtils --> BooleanUtilities (DG);
043     * 20-Apr-2005 : Update for change to LegendItem class (DG);
044     * ------------- JFREECHART 1.0.x ---------------------------------------------
045     * 04-Aug-2006 : Implemented equals() and clone() (DG);
046     * 02-Feb-2007 : Removed author tags from all over JFreeChart sources (DG);
047     * 14-Mar-2007 : Fixed clone() method (DG);
048     * 04-May-2007 : Fixed lookup for series paint and stroke (DG);
049     * 18-May-2007 : Set dataset for LegendItem (DG);
050     * 03-Sep-2009 : Applied patch 2850344 by Martin Hoeller (DG);
051     * 27-Nov-2009 : Updated for modification to PolarItemRenderer interface (DG);
052     * 03-Oct-2011 : Fixed potential NPE in equals() (MH);
053     * 03-Oct-2011 : Added flag to connectFirstAndLastPoint (MH);
054     * 03-Oct-2011 : Added tooltip and URL generator support (MH);
055     * 03-Oct-2011 : Added some configuration options for the legend (MH);
056     * 03-Oct-2011 : Added support for PolarPlot's angleOffset and direction (MH);
057     * 16-Oct-2011 : Fixed serialization problems with fillComposite (MH);
058     */
059    
060    package org.jfree.chart.renderer;
061    
062    import java.awt.AlphaComposite;
063    import java.awt.Composite;
064    import java.awt.Graphics2D;
065    import java.awt.Paint;
066    import java.awt.Point;
067    import java.awt.Shape;
068    import java.awt.Stroke;
069    import java.awt.geom.Ellipse2D;
070    import java.awt.geom.GeneralPath;
071    import java.awt.geom.Line2D;
072    import java.awt.geom.PathIterator;
073    import java.awt.geom.Rectangle2D;
074    import java.io.IOException;
075    import java.io.ObjectInputStream;
076    import java.io.ObjectOutputStream;
077    import java.util.Iterator;
078    import java.util.List;
079    
080    import org.jfree.chart.LegendItem;
081    import org.jfree.chart.axis.NumberTick;
082    import org.jfree.chart.axis.ValueAxis;
083    import org.jfree.chart.entity.EntityCollection;
084    import org.jfree.chart.entity.XYItemEntity;
085    import org.jfree.chart.event.RendererChangeEvent;
086    import org.jfree.chart.labels.XYSeriesLabelGenerator;
087    import org.jfree.chart.labels.XYToolTipGenerator;
088    import org.jfree.chart.plot.DrawingSupplier;
089    import org.jfree.chart.plot.PlotOrientation;
090    import org.jfree.chart.plot.PlotRenderingInfo;
091    import org.jfree.chart.plot.PolarPlot;
092    import org.jfree.chart.renderer.xy.AbstractXYItemRenderer;
093    import org.jfree.chart.urls.XYURLGenerator;
094    import org.jfree.data.xy.XYDataset;
095    import org.jfree.io.SerialUtilities;
096    import org.jfree.text.TextUtilities;
097    import org.jfree.util.BooleanList;
098    import org.jfree.util.BooleanUtilities;
099    import org.jfree.util.ObjectList;
100    import org.jfree.util.ObjectUtilities;
101    import org.jfree.util.PublicCloneable;
102    import org.jfree.util.ShapeUtilities;
103    
104    /**
105     * A renderer that can be used with the {@link PolarPlot} class.
106     */
107    public class DefaultPolarItemRenderer extends AbstractRenderer
108            implements PolarItemRenderer {
109    
110        /** The plot that the renderer is assigned to. */
111        private PolarPlot plot;
112    
113        /** Flags that control whether the renderer fills each series or not. */
114        private BooleanList seriesFilled;
115    
116        /**
117         * Flag that controls whether an outline is drawn for filled series or
118         * not.
119         *
120         * @since 1.0.14
121         */
122        private boolean drawOutlineWhenFilled;
123    
124        /**
125         * The composite to use when filling series.
126         * 
127         * @since 1.0.14
128         */
129        private transient Composite fillComposite;
130    
131        /**
132         * A flag that controls whether the fill paint is used for filling
133         * shapes.
134         * 
135         * @since 1.0.14
136         */
137        private boolean useFillPaint;
138    
139        /**
140         * The shape that is used to represent a line in the legend.
141         * 
142         * @since 1.0.14
143         */
144        private transient Shape legendLine;
145    
146        /**
147         * Flag that controls whether item shapes are visible or not.
148         * 
149         * @since 1.0.14
150         */
151        private boolean shapesVisible;
152    
153        /**
154         * Flag that controls if the first and last point of the dataset should be
155         * connected or not.
156         * 
157         *  @since 1.0.14
158         */
159        private boolean connectFirstAndLastPoint;
160        
161        /**
162         * A list of tool tip generators (one per series).
163         * 
164         * @since 1.0.14
165         */
166        private ObjectList toolTipGeneratorList;
167    
168        /**
169         * The base tool tip generator.
170         * 
171         * @since 1.0.14
172         */
173        private XYToolTipGenerator baseToolTipGenerator;
174    
175        /**
176         * The URL text generator.
177         * 
178         * @since 1.0.14
179         */
180        private XYURLGenerator urlGenerator;
181    
182        /**
183         * The legend item tool tip generator.
184         * 
185         * @since 1.0.14
186         */
187        private XYSeriesLabelGenerator legendItemToolTipGenerator;
188    
189        /**
190         * The legend item URL generator.
191         * 
192         * @since 1.0.14
193         */
194        private XYSeriesLabelGenerator legendItemURLGenerator;
195    
196    
197        /**
198         * Creates a new instance of DefaultPolarItemRenderer
199         */
200        public DefaultPolarItemRenderer() {
201            this.seriesFilled = new BooleanList();
202            this.drawOutlineWhenFilled = true;
203            this.fillComposite = AlphaComposite.getInstance(
204                    AlphaComposite.SRC_OVER, 0.3f);
205            this.useFillPaint = false;     // use item paint for fills by default
206            this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
207            this.shapesVisible = true;
208            this.connectFirstAndLastPoint = true;
209            
210            this.toolTipGeneratorList = new ObjectList();
211            this.urlGenerator = null;
212            this.legendItemToolTipGenerator = null;
213            this.legendItemURLGenerator = null;
214        }
215    
216        /**
217         * Set the plot associated with this renderer.
218         *
219         * @param plot  the plot.
220         *
221         * @see #getPlot()
222         */
223        public void setPlot(PolarPlot plot) {
224            this.plot = plot;
225        }
226    
227        /**
228         * Return the plot associated with this renderer.
229         *
230         * @return The plot.
231         *
232         * @see #setPlot(PolarPlot)
233         */
234        public PolarPlot getPlot() {
235            return this.plot;
236        }
237    
238        /**
239         * Returns <code>true</code> if the renderer will draw an outline around
240         * a filled polygon, <code>false</code> otherwise.
241         *
242         * @return A boolean.
243         *
244         * @since 1.0.14
245         */
246        public boolean getDrawOutlineWhenFilled() {
247            return this.drawOutlineWhenFilled;
248        }
249    
250        /**
251         * Set the flag that controls whether the outline around a filled
252         * polygon will be drawn or not and sends a {@link RendererChangeEvent}
253         * to all registered listeners.
254         *
255         * @param drawOutlineWhenFilled  the flag.
256         *
257         * @since 1.0.14
258         */
259        public void setDrawOutlineWhenFilled(boolean drawOutlineWhenFilled) {
260            this.drawOutlineWhenFilled = drawOutlineWhenFilled;
261            fireChangeEvent();
262        }
263    
264        /**
265         * Get the composite that is used for filling.
266         *
267         * @return The composite (never <code>null</code>).
268         *
269         * @since 1.0.14
270         */
271        public Composite getFillComposite() {
272            return this.fillComposite;
273        }
274    
275        /**
276         * Set the composite which will be used for filling polygons and sends a
277         * {@link RendererChangeEvent} to all registered listeners.
278         *
279         * @param composite  the composite to use (<code>null</code> not
280         *         permitted).
281         *
282         * @since 1.0.14
283         */
284        public void setFillComposite(Composite composite) {
285            if (composite == null) {
286                throw new IllegalArgumentException("Null 'composite' argument.");
287            }
288            this.fillComposite = composite;
289            fireChangeEvent();
290        }
291    
292        /**
293         * Returns <code>true</code> if a shape will be drawn for every item, or
294         * <code>false</code> if not.
295         *
296         * @return A boolean.
297         *
298         * @since 1.0.14
299         */
300        public boolean getShapesVisible() {
301            return this.shapesVisible;
302        }
303    
304        /**
305         * Set the flag that controls whether a shape will be drawn for every
306         * item, or not and sends a {@link RendererChangeEvent} to all registered
307         * listeners.
308         *
309         * @param visible  the flag.
310         *
311         * @since 1.0.14
312         */
313        public void setShapesVisible(boolean visible) {
314            this.shapesVisible = visible;
315            fireChangeEvent();
316        }
317    
318        /**
319         * Returns <code>true</code> if first and last point of a series will be
320         * connected, <code>false</code> otherwise.
321         * 
322         * @return The current status of the flag.
323         * 
324         * @since 1.0.14
325         */
326        public boolean getConnectFirstAndLastPoint() {
327            return this.connectFirstAndLastPoint;
328        }
329    
330        /**
331         * Set the flag that controls whether the first and last point of a series
332         * will be connected or not and sends a {@link RendererChangeEvent} to all
333         * registered listeners.
334         * 
335         * @param connect the flag.
336         * 
337         * @since 1.0.14
338         */
339        public void setConnectFirstAndLastPoint(boolean connect)
340        {
341            this.connectFirstAndLastPoint = connect;
342            fireChangeEvent();
343        }
344    
345        /**
346         * Returns the drawing supplier from the plot.
347         *
348         * @return The drawing supplier.
349         */
350        public DrawingSupplier getDrawingSupplier() {
351            DrawingSupplier result = null;
352            PolarPlot p = getPlot();
353            if (p != null) {
354                result = p.getDrawingSupplier();
355            }
356            return result;
357        }
358    
359        /**
360         * Returns <code>true</code> if the renderer should fill the specified
361         * series, and <code>false</code> otherwise.
362         *
363         * @param series  the series index (zero-based).
364         *
365         * @return A boolean.
366         */
367        public boolean isSeriesFilled(int series) {
368            boolean result = false;
369            Boolean b = this.seriesFilled.getBoolean(series);
370            if (b != null) {
371                result = b.booleanValue();
372            }
373            return result;
374        }
375    
376        /**
377         * Sets a flag that controls whether or not a series is filled.
378         *
379         * @param series  the series index.
380         * @param filled  the flag.
381         */
382        public void setSeriesFilled(int series, boolean filled) {
383            this.seriesFilled.setBoolean(series, BooleanUtilities.valueOf(filled));
384        }
385    
386        /**
387         * Returns <code>true</code> if the renderer should use the fill paint
388         * setting to fill shapes, and <code>false</code> if it should just
389         * use the regular paint.
390         *
391         * @return A boolean.
392         *
393         * @see #setUseFillPaint(boolean)
394         * @since 1.0.14
395         */
396        public boolean getUseFillPaint() {
397            return this.useFillPaint;
398        }
399    
400        /**
401         * Sets the flag that controls whether the fill paint is used to fill
402         * shapes, and sends a {@link RendererChangeEvent} to all
403         * registered listeners.
404         *
405         * @param flag  the flag.
406         *
407         * @see #getUseFillPaint()
408         * @since 1.0.14
409         */
410        public void setUseFillPaint(boolean flag) {
411            this.useFillPaint = flag;
412            fireChangeEvent();
413        }
414    
415        /**
416         * Returns the shape used to represent a line in the legend.
417         *
418         * @return The legend line (never <code>null</code>).
419         *
420         * @see #setLegendLine(Shape)
421         */
422        public Shape getLegendLine() {
423            return this.legendLine;
424        }
425    
426        /**
427         * Sets the shape used as a line in each legend item and sends a
428         * {@link RendererChangeEvent} to all registered listeners.
429         *
430         * @param line  the line (<code>null</code> not permitted).
431         *
432         * @see #getLegendLine()
433         */
434        public void setLegendLine(Shape line) {
435            if (line == null) {
436                throw new IllegalArgumentException("Null 'line' argument.");
437            }
438            this.legendLine = line;
439            fireChangeEvent();
440        }
441    
442        /**
443         * Adds an entity to the collection.
444         *
445         * @param entities  the entity collection being populated.
446         * @param area  the entity area (if <code>null</code> a default will be
447         *              used).
448         * @param dataset  the dataset.
449         * @param series  the series.
450         * @param item  the item.
451         * @param entityX  the entity's center x-coordinate in user space (only
452         *                 used if <code>area</code> is <code>null</code>).
453         * @param entityY  the entity's center y-coordinate in user space (only
454         *                 used if <code>area</code> is <code>null</code>).
455         */
456        // this method was copied from AbstractXYItemRenderer on 03-Oct-2011
457        protected void addEntity(EntityCollection entities, Shape area,
458                                 XYDataset dataset, int series, int item,
459                                 double entityX, double entityY) {
460            if (!getItemCreateEntity(series, item)) {
461                return;
462            }
463            Shape hotspot = area;
464            if (hotspot == null) {
465                double r = getDefaultEntityRadius();
466                double w = r * 2;
467                if (getPlot().getOrientation() == PlotOrientation.VERTICAL) {
468                    hotspot = new Ellipse2D.Double(entityX - r, entityY - r, w, w);
469                }
470                else {
471                    hotspot = new Ellipse2D.Double(entityY - r, entityX - r, w, w);
472                }
473            }
474            String tip = null;
475            XYToolTipGenerator generator = getToolTipGenerator(series, item);
476            if (generator != null) {
477                tip = generator.generateToolTip(dataset, series, item);
478            }
479            String url = null;
480            if (getURLGenerator() != null) {
481                url = getURLGenerator().generateURL(dataset, series, item);
482            }
483            XYItemEntity entity = new XYItemEntity(hotspot, dataset, series, item,
484                    tip, url);
485            entities.add(entity);
486        }
487    
488        /**
489         * Plots the data for a given series.
490         *
491         * @param g2  the drawing surface.
492         * @param dataArea  the data area.
493         * @param info  collects plot rendering info.
494         * @param plot  the plot.
495         * @param dataset  the dataset.
496         * @param seriesIndex  the series index.
497         */
498        public void drawSeries(Graphics2D g2, Rectangle2D dataArea,
499                PlotRenderingInfo info, PolarPlot plot, XYDataset dataset,
500                int seriesIndex) {
501    
502            
503            GeneralPath poly = null;
504            ValueAxis axis = plot.getAxisForDataset(plot.indexOf(dataset));
505            final int numPoints = dataset.getItemCount(seriesIndex);
506            for (int i = 0; i < numPoints; i++) {
507                double theta = dataset.getXValue(seriesIndex, i);
508                double radius = dataset.getYValue(seriesIndex, i);
509                Point p = plot.translateToJava2D(theta, radius, axis, dataArea);
510                if (poly == null) {
511                    poly = new GeneralPath();
512                    poly.moveTo(p.x, p.y);
513                }
514                else {
515                    poly.lineTo(p.x, p.y);
516                }
517            }
518    
519            if (getConnectFirstAndLastPoint()) {
520                poly.closePath();
521            }
522    
523            g2.setPaint(lookupSeriesPaint(seriesIndex));
524            g2.setStroke(lookupSeriesStroke(seriesIndex));
525            if (isSeriesFilled(seriesIndex)) {
526                Composite savedComposite = g2.getComposite();
527                g2.setComposite(this.fillComposite);
528                g2.fill(poly);
529                g2.setComposite(savedComposite);
530                if (this.drawOutlineWhenFilled) {
531                    // draw the outline of the filled polygon
532                    g2.setPaint(lookupSeriesOutlinePaint(seriesIndex));
533                    g2.draw(poly);
534                }
535            }
536            else {
537                // just the lines, no filling
538                g2.draw(poly);
539            }
540            
541            // draw the item shapes
542            if (this.shapesVisible) {
543                // setup for collecting optional entity info...
544                EntityCollection entities = null;
545                if (info != null) {
546                    entities = info.getOwner().getEntityCollection();
547                }
548    
549                PathIterator pi = poly.getPathIterator(null);
550                int i = 0;
551                while (!pi.isDone()) {
552                    final float[] coords = new float[6];
553                    final int segType = pi.currentSegment(coords);
554                    pi.next();
555                    if (segType != PathIterator.SEG_LINETO &&
556                            segType != PathIterator.SEG_MOVETO)
557                        continue;
558                    final int x = Math.round(coords[0]);
559                    final int y = Math.round(coords[1]);
560                    final Shape shape = ShapeUtilities.createTranslatedShape(
561                            getItemShape(seriesIndex, i++), x,  y);
562    
563                    Paint paint;
564                    if (useFillPaint)
565                        paint = lookupSeriesFillPaint(seriesIndex);
566                    else
567                        paint = lookupSeriesPaint(seriesIndex);
568                    g2.setPaint(paint);
569                    g2.fill(shape);
570                    if (isSeriesFilled(seriesIndex) && this.drawOutlineWhenFilled) {
571                        g2.setPaint(lookupSeriesOutlinePaint(seriesIndex));
572                        g2.setStroke(lookupSeriesOutlineStroke(seriesIndex));
573                        g2.draw(shape);
574                    }
575    
576                    // add an entity for the item, but only if it falls within the
577                    // data area...
578                    if (entities != null &&
579                            AbstractXYItemRenderer.isPointInRect(dataArea, x, y)) {
580                        addEntity(entities, shape, dataset, seriesIndex, i-1, x, y);
581                    }
582                }
583            }
584        }
585    
586        /**
587         * Draw the angular gridlines - the spokes.
588         *
589         * @param g2  the drawing surface.
590         * @param plot  the plot.
591         * @param ticks  the ticks.
592         * @param dataArea  the data area.
593         */
594        public void drawAngularGridLines(Graphics2D g2, PolarPlot plot,
595                    List ticks, Rectangle2D dataArea) {
596    
597            g2.setFont(plot.getAngleLabelFont());
598            g2.setStroke(plot.getAngleGridlineStroke());
599            g2.setPaint(plot.getAngleGridlinePaint());
600    
601            double axisMin = plot.getAxis().getLowerBound();
602            double maxRadius = plot.getAxis().getUpperBound();
603            Point center = plot.translateValueThetaRadiusToJava2D(axisMin, axisMin,
604                    dataArea);
605            Iterator iterator = ticks.iterator();
606            while (iterator.hasNext()) {
607                NumberTick tick = (NumberTick) iterator.next();
608                double tickVal = tick.getNumber().doubleValue();
609                Point p = plot.translateValueThetaRadiusToJava2D(
610                        tickVal, maxRadius, dataArea);
611                g2.setPaint(plot.getAngleGridlinePaint());
612                g2.drawLine(center.x, center.y, p.x, p.y);
613                if (plot.isAngleLabelsVisible()) {
614                    int x = p.x;
615                    int y = p.y;
616                    g2.setPaint(plot.getAngleLabelPaint());
617                    TextUtilities.drawAlignedString(tick.getText(), g2, x, y,
618                            tick.getTextAnchor());
619                }
620            }
621         }
622    
623        /**
624         * Draw the radial gridlines - the rings.
625         *
626         * @param g2  the drawing surface.
627         * @param plot  the plot.
628         * @param radialAxis  the radial axis.
629         * @param ticks  the ticks.
630         * @param dataArea  the data area.
631         */
632        public void drawRadialGridLines(Graphics2D g2,
633                                        PolarPlot plot,
634                                        ValueAxis radialAxis,
635                                        List ticks,
636                                        Rectangle2D dataArea) {
637    
638            g2.setFont(radialAxis.getTickLabelFont());
639            g2.setPaint(plot.getRadiusGridlinePaint());
640            g2.setStroke(plot.getRadiusGridlineStroke());
641    
642            double axisMin = radialAxis.getLowerBound();
643            Point center = plot.translateValueThetaRadiusToJava2D(axisMin, axisMin,
644                    dataArea);
645    
646            Iterator iterator = ticks.iterator();
647            while (iterator.hasNext()) {
648                NumberTick tick = (NumberTick) iterator.next();
649                double angleDegrees = plot.isCounterClockwise() ? plot.getAngleOffset() : -plot.getAngleOffset();
650                Point p = plot.translateValueThetaRadiusToJava2D(angleDegrees,
651                        tick.getNumber().doubleValue(), dataArea);
652                int r = p.x - center.x;
653                int upperLeftX = center.x - r;
654                int upperLeftY = center.y - r;
655                int d = 2 * r;
656                Ellipse2D ring = new Ellipse2D.Double(upperLeftX, upperLeftY, d, d);
657                g2.setPaint(plot.getRadiusGridlinePaint());
658                g2.draw(ring);
659            }
660        }
661    
662        /**
663         * Return the legend for the given series.
664         *
665         * @param series  the series index.
666         *
667         * @return The legend item.
668         */
669        public LegendItem getLegendItem(int series) {
670            LegendItem result = null;
671            PolarPlot plot = getPlot();
672            if (plot == null) {
673                return null;
674            }
675            XYDataset dataset = plot.getDataset(plot.getIndexOf(this));
676            if (dataset == null) {
677                return null;
678            }
679            
680            String toolTipText = null;
681            if (getLegendItemToolTipGenerator() != null) {
682                toolTipText = getLegendItemToolTipGenerator().generateLabel(
683                        dataset, series);
684            }
685            String urlText = null;
686            if (getLegendItemURLGenerator() != null) {
687                urlText = getLegendItemURLGenerator().generateLabel(dataset,
688                        series);
689            }
690    
691            String label = dataset.getSeriesKey(series).toString();
692            String description = label;
693            Shape shape = lookupSeriesShape(series);
694            Paint paint;
695            if (this.useFillPaint) {
696                paint = lookupSeriesFillPaint(series);
697            }
698            else {
699                paint = lookupSeriesPaint(series);
700            }
701            Stroke stroke = lookupSeriesStroke(series);
702            Paint outlinePaint = lookupSeriesOutlinePaint(series);
703            Stroke outlineStroke = lookupSeriesOutlineStroke(series);
704            boolean shapeOutlined = isSeriesFilled(series)
705                    && this.drawOutlineWhenFilled;
706            result = new LegendItem(label, description, toolTipText, urlText,
707                    getShapesVisible(), shape, /* shapeFilled=*/ true, paint,
708                    shapeOutlined, outlinePaint, outlineStroke, 
709                    /* lineVisible= */ true, this.legendLine, stroke, paint);
710            result.setToolTipText(toolTipText);
711            result.setURLText(urlText);
712            result.setDataset(dataset);
713    
714            return result;
715        }
716    
717        /**
718         * @since 1.0.14
719         */
720        public XYToolTipGenerator getToolTipGenerator(int series, int item)
721        {
722            XYToolTipGenerator generator
723                = (XYToolTipGenerator) this.toolTipGeneratorList.get(series);
724            if (generator == null) {
725                generator = this.baseToolTipGenerator;
726            }
727            return generator;
728        }
729    
730        /**
731         * @since 1.0.14
732         */
733        public XYToolTipGenerator getSeriesToolTipGenerator(int series)
734        {
735            return (XYToolTipGenerator) this.toolTipGeneratorList.get(series);
736        }
737    
738        /**
739         * @since 1.0.14
740         */
741        public void setSeriesToolTipGenerator(int series,
742                XYToolTipGenerator generator)
743        {
744            this.toolTipGeneratorList.set(series, generator);
745            fireChangeEvent();
746        }
747    
748        /**
749         * @since 1.0.14
750         */
751        public XYToolTipGenerator getBaseToolTipGenerator()
752        {
753            return this.baseToolTipGenerator;
754        }
755    
756        /**
757         * @since 1.0.14
758         */
759        public void setBaseToolTipGenerator(XYToolTipGenerator generator)
760        {
761            this.baseToolTipGenerator = generator;
762            fireChangeEvent();
763        }
764    
765        /**
766         * @since 1.0.14
767         */
768        public XYURLGenerator getURLGenerator()
769        {
770            return this.urlGenerator;
771        }
772    
773        /**
774         * @since 1.0.14
775         */
776        public void setURLGenerator(XYURLGenerator urlGenerator)
777        {
778            this.urlGenerator = urlGenerator;
779            fireChangeEvent();
780        }
781    
782        /**
783         * Returns the legend item tool tip generator.
784         *
785         * @return The tool tip generator (possibly <code>null</code>).
786         *
787         * @see #setLegendItemToolTipGenerator(XYSeriesLabelGenerator)
788         * @since 1.0.14
789         */
790        public XYSeriesLabelGenerator getLegendItemToolTipGenerator() {
791            return this.legendItemToolTipGenerator;
792        }
793    
794        /**
795         * Sets the legend item tool tip generator and sends a
796         * {@link RendererChangeEvent} to all registered listeners.
797         *
798         * @param generator  the generator (<code>null</code> permitted).
799         *
800         * @see #getLegendItemToolTipGenerator()
801         * @since 1.0.14
802         */
803        public void setLegendItemToolTipGenerator(
804                XYSeriesLabelGenerator generator) {
805            this.legendItemToolTipGenerator = generator;
806            fireChangeEvent();
807        }
808    
809        /**
810         * Returns the legend item URL generator.
811         *
812         * @return The URL generator (possibly <code>null</code>).
813         *
814         * @see #setLegendItemURLGenerator(XYSeriesLabelGenerator)
815         * @since 1.0.14
816         */
817        public XYSeriesLabelGenerator getLegendItemURLGenerator() {
818            return this.legendItemURLGenerator;
819        }
820    
821        /**
822         * Sets the legend item URL generator and sends a
823         * {@link RendererChangeEvent} to all registered listeners.
824         *
825         * @param generator  the generator (<code>null</code> permitted).
826         *
827         * @see #getLegendItemURLGenerator()
828         * @since 1.0.14
829         */
830        public void setLegendItemURLGenerator(XYSeriesLabelGenerator generator) {
831            this.legendItemURLGenerator = generator;
832            fireChangeEvent();
833        }
834    
835        /**
836         * Tests this renderer for equality with an arbitrary object.
837         *
838         * @param obj  the object (<code>null</code> not permitted).
839         *
840         * @return <code>true</code> if this renderer is equal to <code>obj</code>,
841         *     and <code>false</code> otherwise.
842         */
843        public boolean equals(Object obj) {
844            if (obj == null) {
845                return false;
846            }
847            if (!(obj instanceof DefaultPolarItemRenderer)) {
848                return false;
849            }
850            DefaultPolarItemRenderer that = (DefaultPolarItemRenderer) obj;
851            if (!this.seriesFilled.equals(that.seriesFilled)) {
852                return false;
853            }
854            if (this.drawOutlineWhenFilled != that.drawOutlineWhenFilled) {
855                return false;
856            }
857            if (!ObjectUtilities.equal(this.fillComposite, that.fillComposite)) {
858                return false;
859            }
860            if (this.useFillPaint != that.useFillPaint) {
861                return false;
862            }
863            if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) {
864                return false;
865            }
866            if (this.shapesVisible != that.shapesVisible) {
867                return false;
868            }
869            if (this.connectFirstAndLastPoint != that.connectFirstAndLastPoint) {
870                return false;
871            }
872            if (!this.toolTipGeneratorList.equals(that.toolTipGeneratorList)) {
873                return false;
874            }
875            if (!ObjectUtilities.equal(this.baseToolTipGenerator,
876                    that.baseToolTipGenerator)) {
877                return false;
878            }
879            if (!ObjectUtilities.equal(this.urlGenerator, that.urlGenerator)) {
880                return false;
881            }
882            if (!ObjectUtilities.equal(this.legendItemToolTipGenerator,
883                    that.legendItemToolTipGenerator)) {
884                return false;
885            }
886            if (!ObjectUtilities.equal(this.legendItemURLGenerator,
887                    that.legendItemURLGenerator)) {
888                return false;
889            }
890            return super.equals(obj);
891        }
892    
893        /**
894         * Returns a clone of the renderer.
895         *
896         * @return A clone.
897         *
898         * @throws CloneNotSupportedException if the renderer cannot be cloned.
899         */
900        public Object clone() throws CloneNotSupportedException {
901            DefaultPolarItemRenderer clone
902                    = (DefaultPolarItemRenderer) super.clone();
903            if (this.legendLine != null) {
904                clone.legendLine = ShapeUtilities.clone(this.legendLine);
905            }
906            clone.seriesFilled = (BooleanList) this.seriesFilled.clone();
907            clone.toolTipGeneratorList
908                    = (ObjectList) this.toolTipGeneratorList.clone();
909            if (clone.baseToolTipGenerator instanceof PublicCloneable) {
910                clone.baseToolTipGenerator = (XYToolTipGenerator)
911                        ObjectUtilities.clone(this.baseToolTipGenerator);
912            }
913            if (clone.urlGenerator instanceof PublicCloneable) {
914                clone.urlGenerator = (XYURLGenerator)
915                        ObjectUtilities.clone(this.urlGenerator);
916            }
917            if (clone.legendItemToolTipGenerator instanceof PublicCloneable) {
918                clone.legendItemToolTipGenerator = (XYSeriesLabelGenerator)
919                        ObjectUtilities.clone(this.legendItemToolTipGenerator);
920            }
921            if (clone.legendItemURLGenerator instanceof PublicCloneable) {
922                clone.legendItemURLGenerator = (XYSeriesLabelGenerator)
923                        ObjectUtilities.clone(this.legendItemURLGenerator);
924            }
925            return clone;
926        }
927    
928        /**
929         * Provides serialization support.
930         *
931         * @param stream  the input stream.
932         *
933         * @throws IOException  if there is an I/O error.
934         * @throws ClassNotFoundException  if there is a classpath problem.
935         */
936        private void readObject(ObjectInputStream stream)
937                throws IOException, ClassNotFoundException {
938            stream.defaultReadObject();
939            this.legendLine = SerialUtilities.readShape(stream);
940            this.fillComposite = SerialUtilities.readComposite(stream);
941        }
942    
943        /**
944         * Provides serialization support.
945         *
946         * @param stream  the output stream.
947         *
948         * @throws IOException  if there is an I/O error.
949         */
950        private void writeObject(ObjectOutputStream stream) throws IOException {
951            stream.defaultWriteObject();
952            SerialUtilities.writeShape(this.legendLine, stream);
953            SerialUtilities.writeComposite(this.fillComposite, stream);
954        }
955    }