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