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