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 * PolarPlot.java
029 * --------------
030 * (C) Copyright 2004-2021, by Solution Engineering, Inc. and Contributors.
031 *
032 * Original Author:  Daniel Bridenbecker, Solution Engineering, Inc.;
033 * Contributor(s):   David Gilbert;
034 *                   Martin Hoeller (patches 1871902 and 2850344);
035 * 
036 */
037
038package org.jfree.chart.plot;
039
040import java.awt.AlphaComposite;
041import java.awt.BasicStroke;
042import java.awt.Color;
043import java.awt.Composite;
044import java.awt.Font;
045import java.awt.FontMetrics;
046import java.awt.Graphics2D;
047import java.awt.Paint;
048import java.awt.Point;
049import java.awt.Shape;
050import java.awt.Stroke;
051import java.awt.geom.Point2D;
052import java.awt.geom.Rectangle2D;
053import java.io.IOException;
054import java.io.ObjectInputStream;
055import java.io.ObjectOutputStream;
056import java.io.Serializable;
057import java.util.ArrayList;
058import java.util.HashMap;
059import java.util.HashSet;
060import java.util.List;
061import java.util.Map;
062import java.util.Map.Entry;
063import java.util.Objects;
064import java.util.ResourceBundle;
065import java.util.Set;
066import java.util.TreeMap;
067import org.jfree.chart.ChartElementVisitor;
068
069import org.jfree.chart.legend.LegendItem;
070import org.jfree.chart.legend.LegendItemCollection;
071import org.jfree.chart.axis.Axis;
072import org.jfree.chart.axis.AxisState;
073import org.jfree.chart.axis.NumberTick;
074import org.jfree.chart.axis.NumberTickUnit;
075import org.jfree.chart.axis.TickType;
076import org.jfree.chart.axis.TickUnit;
077import org.jfree.chart.axis.ValueAxis;
078import org.jfree.chart.axis.ValueTick;
079import org.jfree.chart.event.PlotChangeEvent;
080import org.jfree.chart.event.RendererChangeEvent;
081import org.jfree.chart.event.RendererChangeListener;
082import org.jfree.chart.renderer.PolarItemRenderer;
083import org.jfree.chart.text.TextUtils;
084import org.jfree.chart.api.RectangleEdge;
085import org.jfree.chart.api.RectangleInsets;
086import org.jfree.chart.text.TextAnchor;
087import org.jfree.chart.internal.CloneUtils;
088import org.jfree.chart.internal.PaintUtils;
089import org.jfree.chart.internal.Args;
090import org.jfree.chart.api.PublicCloneable;
091import org.jfree.chart.internal.SerialUtils;
092import org.jfree.data.Range;
093import org.jfree.data.general.Dataset;
094import org.jfree.data.general.DatasetChangeEvent;
095import org.jfree.data.general.DatasetUtils;
096import org.jfree.data.xy.XYDataset;
097
098/**
099 * Plots data that is in (theta, radius) pairs where theta equal to zero is 
100 * due north and increases clockwise.
101 */
102public class PolarPlot extends Plot implements ValueAxisPlot, Zoomable,
103        RendererChangeListener, Cloneable, Serializable {
104
105    /** For serialization. */
106    private static final long serialVersionUID = 3794383185924179525L;
107
108    /** The default margin. */
109    private static final int DEFAULT_MARGIN = 20;
110
111    /** The annotation margin. */
112    private static final double ANNOTATION_MARGIN = 7.0;
113
114    /** The default angle tick unit size. */
115    public static final double DEFAULT_ANGLE_TICK_UNIT_SIZE = 45.0;
116
117    /** The default angle offset. */
118    public static final double DEFAULT_ANGLE_OFFSET = -90.0;
119
120    /** The default grid line stroke. */
121    public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(
122            0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
123            0.0f, new float[]{2.0f, 2.0f}, 0.0f);
124
125    /** The default grid line paint. */
126    public static final Paint DEFAULT_GRIDLINE_PAINT = Color.GRAY;
127
128    /** The resourceBundle for the localization. */
129    protected static ResourceBundle localizationResources
130            = ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");
131
132    /** The angles that are marked with gridlines. */
133    private List<ValueTick> angleTicks;
134
135    /** The range axis (used for the y-values). */
136    private Map<Integer, ValueAxis> axes;
137
138    /** The axis locations. */
139    private final Map<Integer, PolarAxisLocation> axisLocations;
140
141    /** Storage for the datasets. */
142    private Map<Integer, XYDataset> datasets;
143
144    /** Storage for the renderers. */
145    private Map<Integer, PolarItemRenderer> renderers;
146
147    /**
148     * The tick unit that controls the spacing between the angular grid lines.
149     */
150    private TickUnit angleTickUnit;
151
152    /**
153     * An offset for the angles, to start with 0 degrees at north, east, south
154     * or west.
155     */
156    private double angleOffset;
157
158    /**
159     * A flag indicating if the angles increase counterclockwise or clockwise.
160     */
161    private boolean counterClockwise;
162
163    /** A flag that controls whether or not the angle labels are visible. */
164    private boolean angleLabelsVisible = true;
165
166    /** The font used to display the angle labels - never null. */
167    private Font angleLabelFont = new Font("SansSerif", Font.PLAIN, 12);
168
169    /** The paint used to display the angle labels. */
170    private transient Paint angleLabelPaint = Color.BLACK;
171
172    /** A flag that controls whether the angular grid-lines are visible. */
173    private boolean angleGridlinesVisible;
174
175    /** The stroke used to draw the angular grid-lines. */
176    private transient Stroke angleGridlineStroke;
177
178    /** The paint used to draw the angular grid-lines. */
179    private transient Paint angleGridlinePaint;
180
181    /** A flag that controls whether the radius grid-lines are visible. */
182    private boolean radiusGridlinesVisible;
183
184    /** The stroke used to draw the radius grid-lines. */
185    private transient Stroke radiusGridlineStroke;
186
187    /** The paint used to draw the radius grid-lines. */
188    private transient Paint radiusGridlinePaint;
189
190    /**
191     * A flag that controls whether the radial minor grid-lines are visible.
192     */
193    private boolean radiusMinorGridlinesVisible;
194
195    /** The annotations for the plot. */
196    private List<String> cornerTextItems = new ArrayList<>();
197
198    /**
199     * The actual margin in pixels.
200     */
201    private int margin;
202
203    /**
204     * An optional collection of legend items that can be returned by the
205     * getLegendItems() method.
206     */
207    private LegendItemCollection fixedLegendItems;
208
209    /**
210     * Storage for the mapping between datasets/renderers and range axes.  The
211     * keys in the map are Integer objects, corresponding to the dataset
212     * index.  The values in the map are List<Integer> instances (corresponding 
213     * to the axis indices).  If the map contains no
214     * entry for a dataset, it is assumed to map to the primary domain axis
215     * (index = 0).
216     */
217    private final Map<Integer, List<Integer>> datasetToAxesMap;
218
219    /**
220     * Default constructor.
221     */
222    public PolarPlot() {
223        this(null, null, null);
224    }
225
226   /**
227     * Creates a new plot.
228     *
229     * @param dataset  the dataset ({@code null} permitted).
230     * @param radiusAxis  the radius axis ({@code null} permitted).
231     * @param renderer  the renderer ({@code null} permitted).
232     */
233    public PolarPlot(XYDataset dataset, ValueAxis radiusAxis, PolarItemRenderer renderer) {
234        super();
235        this.datasets = new HashMap<>();
236        this.datasets.put(0, dataset);
237        if (dataset != null) {
238            dataset.addChangeListener(this);
239        }
240        this.angleTickUnit = new NumberTickUnit(DEFAULT_ANGLE_TICK_UNIT_SIZE);
241
242        this.axes = new HashMap<>();
243        this.datasetToAxesMap = new TreeMap<>();
244        this.axes.put(0, radiusAxis);
245        if (radiusAxis != null) {
246            radiusAxis.setPlot(this);
247            radiusAxis.addChangeListener(this);
248        }
249
250        // define the default locations for up to 8 axes...
251        this.axisLocations = new HashMap<>();
252        this.axisLocations.put(0, PolarAxisLocation.EAST_ABOVE);
253        this.axisLocations.put(1, PolarAxisLocation.NORTH_LEFT);
254        this.axisLocations.put(2, PolarAxisLocation.WEST_BELOW);
255        this.axisLocations.put(3, PolarAxisLocation.SOUTH_RIGHT);
256        this.axisLocations.put(4, PolarAxisLocation.EAST_BELOW);
257        this.axisLocations.put(5, PolarAxisLocation.NORTH_RIGHT);
258        this.axisLocations.put(6, PolarAxisLocation.WEST_ABOVE);
259        this.axisLocations.put(7, PolarAxisLocation.SOUTH_LEFT);
260
261        this.renderers = new HashMap<>();
262        this.renderers.put(0, renderer);
263        if (renderer != null) {
264            renderer.setPlot(this);
265            renderer.addChangeListener(this);
266        }
267
268        this.angleOffset = DEFAULT_ANGLE_OFFSET;
269        this.counterClockwise = false;
270        this.angleGridlinesVisible = true;
271        this.angleGridlineStroke = DEFAULT_GRIDLINE_STROKE;
272        this.angleGridlinePaint = DEFAULT_GRIDLINE_PAINT;
273
274        this.radiusGridlinesVisible = true;
275        this.radiusMinorGridlinesVisible = true;
276        this.radiusGridlineStroke = DEFAULT_GRIDLINE_STROKE;
277        this.radiusGridlinePaint = DEFAULT_GRIDLINE_PAINT;
278        this.margin = DEFAULT_MARGIN;
279    }
280
281    /**
282     * Returns the plot type as a string.
283     *
284     * @return A short string describing the type of plot.
285     */
286    @Override
287    public String getPlotType() {
288       return PolarPlot.localizationResources.getString("Polar_Plot");
289    }
290
291    /**
292     * Returns the primary axis for the plot.
293     *
294     * @return The primary axis (possibly {@code null}).
295     *
296     * @see #setAxis(ValueAxis)
297     */
298    public ValueAxis getAxis() {
299        return getAxis(0);
300    }
301
302    /**
303     * Returns an axis for the plot.
304     *
305     * @param index  the axis index.
306     *
307     * @return The axis ({@code null} possible).
308     *
309     * @see #setAxis(int, ValueAxis)
310     */
311    public ValueAxis getAxis(int index) {
312        return this.axes.get(index);
313    }
314
315    /**
316     * Sets the primary axis for the plot and sends a {@link PlotChangeEvent}
317     * to all registered listeners.
318     *
319     * @param axis  the new primary axis ({@code null} permitted).
320     */
321    public void setAxis(ValueAxis axis) {
322        setAxis(0, axis);
323    }
324
325    /**
326     * Sets an axis for the plot and sends a {@link PlotChangeEvent} to all
327     * registered listeners.
328     *
329     * @param index  the axis index.
330     * @param axis  the axis ({@code null} permitted).
331     *
332     * @see #getAxis(int)
333     */
334    public void setAxis(int index, ValueAxis axis) {
335        setAxis(index, axis, true);
336    }
337
338    /**
339     * Sets an axis for the plot and, if requested, sends a
340     * {@link PlotChangeEvent} to all registered listeners.
341     *
342     * @param index  the axis index.
343     * @param axis  the axis ({@code null} permitted).
344     * @param notify  notify listeners?
345     *
346     * @see #getAxis(int)
347     */
348    public void setAxis(int index, ValueAxis axis, boolean notify) {
349        ValueAxis existing = getAxis(index);
350        if (existing != null) {
351            existing.removeChangeListener(this);
352        }
353        if (axis != null) {
354            axis.setPlot(this);
355        }
356        this.axes.put(index, axis);
357        if (axis != null) {
358            axis.configure();
359            axis.addChangeListener(this);
360        }
361        if (notify) {
362            fireChangeEvent();
363        }
364    }
365
366    /**
367     * Returns the location of the primary axis.
368     *
369     * @return The location (never {@code null}).
370     *
371     * @see #setAxisLocation(PolarAxisLocation)
372     */
373    public PolarAxisLocation getAxisLocation() {
374        return getAxisLocation(0);
375    }
376
377    /**
378     * Returns the location for an axis.
379     *
380     * @param index  the axis index.
381     *
382     * @return The location (possibly {@code null}).
383     *
384     * @see #setAxisLocation(int, PolarAxisLocation)
385     */
386    public PolarAxisLocation getAxisLocation(int index) {
387        return this.axisLocations.get(index);
388    }
389
390    /**
391     * Sets the location of the primary axis and sends a
392     * {@link PlotChangeEvent} to all registered listeners.
393     *
394     * @param location  the location ({@code null} not permitted).
395     *
396     * @see #getAxisLocation()
397     */
398    public void setAxisLocation(PolarAxisLocation location) {
399        // delegate argument checks...
400        setAxisLocation(0, location, true);
401    }
402
403    /**
404     * Sets the location of the primary axis and, if requested, sends a
405     * {@link PlotChangeEvent} to all registered listeners.
406     *
407     * @param location  the location ({@code null} not permitted).
408     * @param notify  notify listeners?
409     *
410     * @see #getAxisLocation()
411     */
412    public void setAxisLocation(PolarAxisLocation location, boolean notify) {
413        // delegate...
414        setAxisLocation(0, location, notify);
415    }
416
417    /**
418     * Sets the location for an axis and sends a {@link PlotChangeEvent}
419     * to all registered listeners.
420     *
421     * @param index  the axis index.
422     * @param location  the location ({@code null} not permitted).
423     *
424     * @see #getAxisLocation(int)
425     */
426    public void setAxisLocation(int index, PolarAxisLocation location) {
427        // delegate...
428        setAxisLocation(index, location, true);
429    }
430
431    /**
432     * Sets the axis location for an axis and, if requested, sends a
433     * {@link PlotChangeEvent} to all registered listeners.
434     *
435     * @param index  the axis index.
436     * @param location  the location ({@code null} not permitted).
437     * @param notify  notify listeners?
438     */
439    public void setAxisLocation(int index, PolarAxisLocation location,
440            boolean notify) {
441        Args.nullNotPermitted(location, "location");
442        this.axisLocations.put(index, location);
443        if (notify) {
444            fireChangeEvent();
445        }
446    }
447
448    /**
449     * Returns the number of domain axes.
450     *
451     * @return The axis count.
452     **/
453    public int getAxisCount() {
454        return this.axes.size();
455    }
456
457    /**
458     * Returns the primary dataset for the plot.
459     *
460     * @return The primary dataset (possibly {@code null}).
461     *
462     * @see #setDataset(XYDataset)
463     */
464    public XYDataset getDataset() {
465        return getDataset(0);
466    }
467
468    /**
469     * Returns the dataset with the specified index, if any.
470     *
471     * @param index  the dataset index.
472     *
473     * @return The dataset (possibly {@code null}).
474     *
475     * @see #setDataset(int, XYDataset)
476     */
477    public XYDataset getDataset(int index) {
478        return this.datasets.get(index);
479    }
480
481    /**
482     * Sets the primary dataset for the plot, replacing the existing dataset
483     * if there is one, and sends a {@code link PlotChangeEvent} to all
484     * registered listeners.
485     *
486     * @param dataset  the dataset ({@code null} permitted).
487     *
488     * @see #getDataset()
489     */
490    public void setDataset(XYDataset dataset) {
491        setDataset(0, dataset);
492    }
493
494    /**
495     * Sets a dataset for the plot, replacing the existing dataset at the same
496     * index if there is one, and sends a {@code link PlotChangeEvent} to all
497     * registered listeners.
498     *
499     * @param index  the dataset index.
500     * @param dataset  the dataset ({@code null} permitted).
501     *
502     * @see #getDataset(int)
503     */
504    public void setDataset(int index, XYDataset dataset) {
505        XYDataset existing = getDataset(index);
506        if (existing != null) {
507            existing.removeChangeListener(this);
508        }
509        this.datasets.put(index, dataset);
510        if (dataset != null) {
511            dataset.addChangeListener(this);
512        }
513
514        // send a dataset change event to self...
515        DatasetChangeEvent event = new DatasetChangeEvent(this, dataset);
516        datasetChanged(event);
517    }
518
519    /**
520     * Returns the number of datasets.
521     *
522     * @return The number of datasets.
523     */
524    public int getDatasetCount() {
525        return this.datasets.size();
526    }
527
528    /**
529     * Returns the index of the specified dataset, or {@code -1} if the
530     * dataset does not belong to the plot.
531     *
532     * @param dataset  the dataset ({@code null} not permitted).
533     *
534     * @return The index.
535     */
536    public int indexOf(XYDataset dataset) {
537        for (Entry<Integer, XYDataset> entry : this.datasets.entrySet()) {
538            if (entry.getValue() == dataset) {
539                return entry.getKey();
540            }
541        }
542        return -1;
543    }
544
545    /**
546     * Returns the primary renderer.
547     *
548     * @return The renderer (possibly {@code null}).
549     *
550     * @see #setRenderer(PolarItemRenderer)
551     */
552    public PolarItemRenderer getRenderer() {
553        return getRenderer(0);
554    }
555
556    /**
557     * Returns the renderer at the specified index, if there is one.
558     *
559     * @param index  the renderer index.
560     *
561     * @return The renderer (possibly {@code null}).
562     *
563     * @see #setRenderer(int, PolarItemRenderer)
564     */
565    public PolarItemRenderer getRenderer(int index) {
566        return this.renderers.get(index);
567    }
568
569    /**
570     * Sets the primary renderer, and notifies all listeners of a change to the
571     * plot.  If the renderer is set to {@code null}, no data items will
572     * be drawn for the corresponding dataset.
573     *
574     * @param renderer  the new renderer ({@code null} permitted).
575     *
576     * @see #getRenderer()
577     */
578    public void setRenderer(PolarItemRenderer renderer) {
579        setRenderer(0, renderer);
580    }
581
582    /**
583     * Sets a renderer and sends a {@link PlotChangeEvent} to all
584     * registered listeners.
585     *
586     * @param index  the index.
587     * @param renderer  the renderer.
588     *
589     * @see #getRenderer(int)
590     */
591    public void setRenderer(int index, PolarItemRenderer renderer) {
592        setRenderer(index, renderer, true);
593    }
594
595    /**
596     * Sets a renderer and, if requested, sends a {@link PlotChangeEvent} to
597     * all registered listeners.
598     *
599     * @param index  the index.
600     * @param renderer  the renderer.
601     * @param notify  notify listeners?
602     *
603     * @see #getRenderer(int)
604     */
605    public void setRenderer(int index, PolarItemRenderer renderer,
606                            boolean notify) {
607        PolarItemRenderer existing = getRenderer(index);
608        if (existing != null) {
609            existing.removeChangeListener(this);
610        }
611        this.renderers.put(index, renderer);
612        if (renderer != null) {
613            renderer.setPlot(this);
614            renderer.addChangeListener(this);
615        }
616        if (notify) {
617            fireChangeEvent();
618        }
619    }
620
621    /**
622     * Returns the tick unit that controls the spacing of the angular grid
623     * lines.
624     *
625     * @return The tick unit (never {@code null}).
626     */
627    public TickUnit getAngleTickUnit() {
628        return this.angleTickUnit;
629    }
630
631    /**
632     * Sets the tick unit that controls the spacing of the angular grid
633     * lines, and sends a {@link PlotChangeEvent} to all registered listeners.
634     *
635     * @param unit  the tick unit ({@code null} not permitted).
636     */
637    public void setAngleTickUnit(TickUnit unit) {
638        Args.nullNotPermitted(unit, "unit");
639        this.angleTickUnit = unit;
640        fireChangeEvent();
641    }
642
643    /**
644     * Returns the offset that is used for all angles.
645     *
646     * @return The offset for the angles.
647     */
648    public double getAngleOffset() {
649        return this.angleOffset;
650    }
651
652    /**
653     * Sets the offset that is used for all angles and sends a
654     * {@link PlotChangeEvent} to all registered listeners.
655     *
656     * This is useful to let 0 degrees be at the north, east, south or west
657     * side of the chart.
658     *
659     * @param offset The offset
660     */
661    public void setAngleOffset(double offset) {
662        this.angleOffset = offset;
663        fireChangeEvent();
664    }
665
666    /**
667     * Get the direction for growing angle degrees.
668     *
669     * @return {@code true} if angle increases counterclockwise,
670     *         {@code false} otherwise.
671     */
672    public boolean isCounterClockwise() {
673        return this.counterClockwise;
674    }
675
676    /**
677     * Sets the flag for increasing angle degrees direction.
678     *
679     * {@code true} for counterclockwise, {@code false} for
680     * clockwise.
681     *
682     * @param counterClockwise The flag.
683     */
684    public void setCounterClockwise(boolean counterClockwise)
685    {
686        this.counterClockwise = counterClockwise;
687    }
688
689    /**
690     * Returns a flag that controls whether or not the angle labels are visible.
691     *
692     * @return A boolean.
693     *
694     * @see #setAngleLabelsVisible(boolean)
695     */
696    public boolean isAngleLabelsVisible() {
697        return this.angleLabelsVisible;
698    }
699
700    /**
701     * Sets the flag that controls whether or not the angle labels are visible,
702     * and sends a {@link PlotChangeEvent} to all registered listeners.
703     *
704     * @param visible  the flag.
705     *
706     * @see #isAngleLabelsVisible()
707     */
708    public void setAngleLabelsVisible(boolean visible) {
709        if (this.angleLabelsVisible != visible) {
710            this.angleLabelsVisible = visible;
711            fireChangeEvent();
712        }
713    }
714
715    /**
716     * Returns the font used to display the angle labels.
717     *
718     * @return A font (never {@code null}).
719     *
720     * @see #setAngleLabelFont(Font)
721     */
722    public Font getAngleLabelFont() {
723        return this.angleLabelFont;
724    }
725
726    /**
727     * Sets the font used to display the angle labels and sends a
728     * {@link PlotChangeEvent} to all registered listeners.
729     *
730     * @param font  the font ({@code null} not permitted).
731     *
732     * @see #getAngleLabelFont()
733     */
734    public void setAngleLabelFont(Font font) {
735        Args.nullNotPermitted(font, "font");
736        this.angleLabelFont = font;
737        fireChangeEvent();
738    }
739
740    /**
741     * Returns the paint used to display the angle labels.
742     *
743     * @return A paint (never {@code null}).
744     *
745     * @see #setAngleLabelPaint(Paint)
746     */
747    public Paint getAngleLabelPaint() {
748        return this.angleLabelPaint;
749    }
750
751    /**
752     * Sets the paint used to display the angle labels and sends a
753     * {@link PlotChangeEvent} to all registered listeners.
754     *
755     * @param paint  the paint ({@code null} not permitted).
756     */
757    public void setAngleLabelPaint(Paint paint) {
758        Args.nullNotPermitted(paint, "paint");
759        this.angleLabelPaint = paint;
760        fireChangeEvent();
761    }
762
763    /**
764     * Returns {@code true} if the angular gridlines are visible, and
765     * {@code false} otherwise.
766     *
767     * @return {@code true} or {@code false}.
768     *
769     * @see #setAngleGridlinesVisible(boolean)
770     */
771    public boolean isAngleGridlinesVisible() {
772        return this.angleGridlinesVisible;
773    }
774
775    /**
776     * Sets the flag that controls whether or not the angular grid-lines are
777     * visible.
778     * <p>
779     * If the flag value is changed, a {@link PlotChangeEvent} is sent to all
780     * registered listeners.
781     *
782     * @param visible  the new value of the flag.
783     *
784     * @see #isAngleGridlinesVisible()
785     */
786    public void setAngleGridlinesVisible(boolean visible) {
787        if (this.angleGridlinesVisible != visible) {
788            this.angleGridlinesVisible = visible;
789            fireChangeEvent();
790        }
791    }
792
793    /**
794     * Returns the stroke for the grid-lines (if any) plotted against the
795     * angular axis.
796     *
797     * @return The stroke (possibly {@code null}).
798     *
799     * @see #setAngleGridlineStroke(Stroke)
800     */
801    public Stroke getAngleGridlineStroke() {
802        return this.angleGridlineStroke;
803    }
804
805    /**
806     * Sets the stroke for the grid lines plotted against the angular axis and
807     * sends a {@link PlotChangeEvent} to all registered listeners.
808     * <p>
809     * If you set this to {@code null}, no grid lines will be drawn.
810     *
811     * @param stroke  the stroke ({@code null} permitted).
812     *
813     * @see #getAngleGridlineStroke()
814     */
815    public void setAngleGridlineStroke(Stroke stroke) {
816        this.angleGridlineStroke = stroke;
817        fireChangeEvent();
818    }
819
820    /**
821     * Returns the paint for the grid lines (if any) plotted against the
822     * angular axis.
823     *
824     * @return The paint (possibly {@code null}).
825     *
826     * @see #setAngleGridlinePaint(Paint)
827     */
828    public Paint getAngleGridlinePaint() {
829        return this.angleGridlinePaint;
830    }
831
832    /**
833     * Sets the paint for the grid lines plotted against the angular axis.
834     * <p>
835     * If you set this to {@code null}, no grid lines will be drawn.
836     *
837     * @param paint  the paint ({@code null} permitted).
838     *
839     * @see #getAngleGridlinePaint()
840     */
841    public void setAngleGridlinePaint(Paint paint) {
842        this.angleGridlinePaint = paint;
843        fireChangeEvent();
844    }
845
846    /**
847     * Returns {@code true} if the radius axis grid is visible, and
848     * {@code false} otherwise.
849     *
850     * @return {@code true} or {@code false}.
851     *
852     * @see #setRadiusGridlinesVisible(boolean)
853     */
854    public boolean isRadiusGridlinesVisible() {
855        return this.radiusGridlinesVisible;
856    }
857
858    /**
859     * Sets the flag that controls whether or not the radius axis grid lines
860     * are visible.
861     * <p>
862     * If the flag value is changed, a {@link PlotChangeEvent} is sent to all
863     * registered listeners.
864     *
865     * @param visible  the new value of the flag.
866     *
867     * @see #isRadiusGridlinesVisible()
868     */
869    public void setRadiusGridlinesVisible(boolean visible) {
870        if (this.radiusGridlinesVisible != visible) {
871            this.radiusGridlinesVisible = visible;
872            fireChangeEvent();
873        }
874    }
875
876    /**
877     * Returns the stroke for the grid lines (if any) plotted against the
878     * radius axis.
879     *
880     * @return The stroke (possibly {@code null}).
881     *
882     * @see #setRadiusGridlineStroke(Stroke)
883     */
884    public Stroke getRadiusGridlineStroke() {
885        return this.radiusGridlineStroke;
886    }
887
888    /**
889     * Sets the stroke for the grid lines plotted against the radius axis and
890     * sends a {@link PlotChangeEvent} to all registered listeners.
891     * <p>
892     * If you set this to {@code null}, no grid lines will be drawn.
893     *
894     * @param stroke  the stroke ({@code null} permitted).
895     *
896     * @see #getRadiusGridlineStroke()
897     */
898    public void setRadiusGridlineStroke(Stroke stroke) {
899        this.radiusGridlineStroke = stroke;
900        fireChangeEvent();
901    }
902
903    /**
904     * Returns the paint for the grid lines (if any) plotted against the radius
905     * axis.
906     *
907     * @return The paint (possibly {@code null}).
908     *
909     * @see #setRadiusGridlinePaint(Paint)
910     */
911    public Paint getRadiusGridlinePaint() {
912        return this.radiusGridlinePaint;
913    }
914
915    /**
916     * Sets the paint for the grid lines plotted against the radius axis and
917     * sends a {@link PlotChangeEvent} to all registered listeners.
918     * <p>
919     * If you set this to {@code null}, no grid lines will be drawn.
920     *
921     * @param paint  the paint ({@code null} permitted).
922     *
923     * @see #getRadiusGridlinePaint()
924     */
925    public void setRadiusGridlinePaint(Paint paint) {
926        this.radiusGridlinePaint = paint;
927        fireChangeEvent();
928    }
929
930    /**
931     * Return the current value of the flag indicating if radial minor
932     * grid-lines will be drawn or not.
933     *
934     * @return Returns {@code true} if radial minor grid-lines are drawn.
935     */
936    public boolean isRadiusMinorGridlinesVisible() {
937        return this.radiusMinorGridlinesVisible;
938    }
939
940    /**
941     * Set the flag that determines if radial minor grid-lines will be drawn,
942     * and sends a {@link PlotChangeEvent} to all registered listeners.
943     *
944     * @param flag {@code true} to draw the radial minor grid-lines,
945     *             {@code false} to hide them.
946     */
947    public void setRadiusMinorGridlinesVisible(boolean flag) {
948        this.radiusMinorGridlinesVisible = flag;
949        fireChangeEvent();
950    }
951
952    /**
953     * Returns the margin around the plot area.
954     *
955     * @return The actual margin in pixels.
956     */
957    public int getMargin() {
958        return this.margin;
959    }
960
961    /**
962     * Set the margin around the plot area and sends a
963     * {@link PlotChangeEvent} to all registered listeners.
964     *
965     * @param margin The new margin in pixels.
966     */
967    public void setMargin(int margin) {
968        this.margin = margin;
969        fireChangeEvent();
970    }
971
972    /**
973     * Returns the fixed legend items, if any.
974     *
975     * @return The legend items (possibly {@code null}).
976     *
977     * @see #setFixedLegendItems(LegendItemCollection)
978     */
979    public LegendItemCollection getFixedLegendItems() {
980        return this.fixedLegendItems;
981    }
982
983    /**
984     * Sets the fixed legend items for the plot.  Leave this set to
985     * {@code null} if you prefer the legend items to be created
986     * automatically.
987     *
988     * @param items  the legend items ({@code null} permitted).
989     *
990     * @see #getFixedLegendItems()
991     */
992    public void setFixedLegendItems(LegendItemCollection items) {
993        this.fixedLegendItems = items;
994        fireChangeEvent();
995    }
996
997    /**
998     * Add text to be displayed in the lower right hand corner and sends a
999     * {@link PlotChangeEvent} to all registered listeners.
1000     *
1001     * @param text  the text to display ({@code null} not permitted).
1002     *
1003     * @see #removeCornerTextItem(String)
1004     */
1005    public void addCornerTextItem(String text) {
1006        Args.nullNotPermitted(text, "text");
1007        this.cornerTextItems.add(text);
1008        fireChangeEvent();
1009    }
1010
1011    /**
1012     * Remove the given text from the list of corner text items and
1013     * sends a {@link PlotChangeEvent} to all registered listeners.
1014     *
1015     * @param text  the text to remove ({@code null} ignored).
1016     *
1017     * @see #addCornerTextItem(String)
1018     */
1019    public void removeCornerTextItem(String text) {
1020        boolean removed = this.cornerTextItems.remove(text);
1021        if (removed) {
1022            fireChangeEvent();
1023        }
1024    }
1025
1026    /**
1027     * Clear the list of corner text items and sends a {@link PlotChangeEvent}
1028     * to all registered listeners.
1029     *
1030     * @see #addCornerTextItem(String)
1031     * @see #removeCornerTextItem(String)
1032     */
1033    public void clearCornerTextItems() {
1034        if (!this.cornerTextItems.isEmpty()) {
1035            this.cornerTextItems.clear();
1036            fireChangeEvent();
1037        }
1038    }
1039
1040    /**
1041     * Generates a list of tick values for the angular tick marks.
1042     *
1043     * @return A list of {@link NumberTick} instances.
1044     */
1045    protected List<ValueTick> refreshAngleTicks() {
1046        List<ValueTick> ticks = new ArrayList<>();
1047        for (double currentTickVal = 0.0; currentTickVal < 360.0;
1048                currentTickVal += this.angleTickUnit.getSize()) {
1049            TextAnchor ta = calculateTextAnchor(currentTickVal);
1050            NumberTick tick = new NumberTick(currentTickVal,
1051                this.angleTickUnit.valueToString(currentTickVal),
1052                ta, TextAnchor.CENTER, 0.0);
1053            ticks.add(tick);
1054        }
1055        return ticks;
1056    }
1057
1058    /**
1059     * Calculate the text position for the given degrees.
1060     *
1061     * @param angleDegrees  the angle in degrees.
1062     * 
1063     * @return The optimal text anchor.
1064     */
1065    protected TextAnchor calculateTextAnchor(double angleDegrees) {
1066        TextAnchor ta = TextAnchor.CENTER;
1067
1068        // normalize angle
1069        double offset = this.angleOffset;
1070        while (offset < 0.0) {
1071            offset += 360.0;
1072        }
1073        double normalizedAngle = (((this.counterClockwise ? -1 : 1)
1074                * angleDegrees) + offset) % 360;
1075        while (this.counterClockwise && (normalizedAngle < 0.0)) {
1076            normalizedAngle += 360.0;
1077        }
1078
1079        if (normalizedAngle == 0.0) {
1080            ta = TextAnchor.CENTER_LEFT;
1081        } else if (normalizedAngle > 0.0 && normalizedAngle < 90.0) {
1082            ta = TextAnchor.TOP_LEFT;
1083        } else if (normalizedAngle == 90.0) {
1084            ta = TextAnchor.TOP_CENTER;
1085        } else if (normalizedAngle > 90.0 && normalizedAngle < 180.0) {
1086            ta = TextAnchor.TOP_RIGHT;
1087        } else if (normalizedAngle == 180) {
1088            ta = TextAnchor.CENTER_RIGHT;
1089        } else if (normalizedAngle > 180.0 && normalizedAngle < 270.0) {
1090            ta = TextAnchor.BOTTOM_RIGHT;
1091        } else if (normalizedAngle == 270) {
1092            ta = TextAnchor.BOTTOM_CENTER;
1093        } else if (normalizedAngle > 270.0 && normalizedAngle < 360.0) {
1094            ta = TextAnchor.BOTTOM_LEFT;
1095        }
1096        return ta;
1097    }
1098
1099    /**
1100     * Maps a dataset to a particular axis.  All data will be plotted
1101     * against axis zero by default, no mapping is required for this case.
1102     *
1103     * @param index  the dataset index (zero-based).
1104     * @param axisIndex  the axis index.
1105     */
1106    public void mapDatasetToAxis(int index, int axisIndex) {
1107        List<Integer> axisIndices = new ArrayList<>(1);
1108        axisIndices.add(axisIndex);
1109        mapDatasetToAxes(index, axisIndices);
1110    }
1111
1112    /**
1113     * Maps the specified dataset to the axes in the list.  Note that the
1114     * conversion of data values into Java2D space is always performed using
1115     * the first axis in the list.
1116     *
1117     * @param index  the dataset index (zero-based).
1118     * @param axisIndices  the axis indices ({@code null} permitted).
1119     */
1120    public void mapDatasetToAxes(int index, List<Integer> axisIndices) {
1121        if (index < 0) {
1122            throw new IllegalArgumentException("Requires 'index' >= 0.");
1123        }
1124        checkAxisIndices(axisIndices);
1125        Integer key = index;
1126        this.datasetToAxesMap.put(key, new ArrayList<>(axisIndices));
1127        // fake a dataset change event to update axes...
1128        datasetChanged(new DatasetChangeEvent(this, getDataset(index)));
1129    }
1130
1131    /**
1132     * This method is used to perform argument checking on the list of
1133     * axis indices passed to mapDatasetToAxes().
1134     *
1135     * @param indices  the list of indices ({@code null} permitted).
1136     */
1137    private void checkAxisIndices(List<Integer> indices) {
1138        // axisIndices can be:
1139        // 1.  null;
1140        // 2.  non-empty, containing only Integer objects that are unique.
1141        if (indices == null) {
1142            return;  // OK
1143        }
1144        if (indices.isEmpty()) {
1145            throw new IllegalArgumentException("Empty list not permitted.");
1146        }
1147        Set<Integer> set = new HashSet<>();
1148        for (Integer i : indices) {
1149            if (set.contains(i)) {
1150                throw new IllegalArgumentException("Indices must be unique.");
1151            }
1152            set.add(i);
1153        }
1154    }
1155
1156    /**
1157     * Returns the axis for a dataset.
1158     *
1159     * @param index  the dataset index.
1160     *
1161     * @return The axis.
1162     */
1163    public ValueAxis getAxisForDataset(int index) {
1164        ValueAxis valueAxis;
1165        List<Integer> axisIndices = this.datasetToAxesMap.get(index);
1166        if (axisIndices != null) {
1167            // the first axis in the list is used for data <--> Java2D
1168            Integer axisIndex = axisIndices.get(0);
1169            valueAxis = getAxis(axisIndex);
1170        }
1171        else {
1172            valueAxis = getAxis(0);
1173        }
1174        return valueAxis;
1175    }
1176
1177    /**
1178     * Returns the index of the given axis.
1179     *
1180     * @param axis  the axis.
1181     *
1182     * @return The axis index or -1 if axis is not used in this plot.
1183     */
1184    public int getAxisIndex(ValueAxis axis) {
1185        for (Entry<Integer, ValueAxis> entry : this.axes.entrySet()) {
1186            if (axis.equals(entry.getValue())) {
1187                return entry.getKey();
1188            }
1189        }
1190        // try the parent plot
1191        Plot parent = getParent();
1192        if (parent instanceof PolarPlot) {
1193            PolarPlot p = (PolarPlot) parent;
1194            return p.getAxisIndex(axis);
1195        }
1196        return -1;
1197    }
1198
1199    /**
1200     * Returns the index of the specified renderer, or {@code -1} if the
1201     * renderer is not assigned to this plot.
1202     *
1203     * @param renderer  the renderer ({@code null} not permitted).
1204     *
1205     * @return The renderer index.
1206     */
1207    public int getIndexOf(PolarItemRenderer renderer) {
1208        Args.nullNotPermitted(renderer, "renderer");
1209        for (Entry<Integer, PolarItemRenderer> entry : this.renderers.entrySet()) {
1210            if (renderer.equals(entry.getValue())) {
1211                return entry.getKey();
1212            }
1213        }
1214        return -1;
1215    }
1216
1217    /**
1218     * Receives a chart element visitor.  Many plot subclasses will override
1219     * this method to handle their subcomponents.
1220     * 
1221     * @param visitor  the visitor ({@code null} not permitted).
1222     */
1223    @Override
1224    public void receive(ChartElementVisitor visitor) {
1225        // FIXME: handle axes and renderers
1226        visitor.visit(this);
1227    }
1228
1229    /**
1230     * Draws the plot on a Java 2D graphics device (such as the screen or a
1231     * printer).
1232     * <P>
1233     * This plot relies on a {@link PolarItemRenderer} to draw each
1234     * item in the plot.  This allows the visual representation of the data to
1235     * be changed easily.
1236     * <P>
1237     * The optional info argument collects information about the rendering of
1238     * the plot (dimensions, tooltip information etc).  Just pass in
1239     * {@code null} if you do not need this information.
1240     *
1241     * @param g2  the graphics device.
1242     * @param area  the area within which the plot (including axes and
1243     *              labels) should be drawn.
1244     * @param anchor  the anchor point ({@code null} permitted).
1245     * @param parentState  ignored.
1246     * @param info  collects chart drawing information ({@code null}
1247     *              permitted).
1248     */
1249    @Override
1250    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
1251            PlotState parentState, PlotRenderingInfo info) {
1252
1253        // if the plot area is too small, just return...
1254        boolean b1 = (area.getWidth() <= MINIMUM_WIDTH_TO_DRAW);
1255        boolean b2 = (area.getHeight() <= MINIMUM_HEIGHT_TO_DRAW);
1256        if (b1 || b2) {
1257            return;
1258        }
1259
1260        // record the plot area...
1261        if (info != null) {
1262            info.setPlotArea(area);
1263        }
1264
1265        // adjust the drawing area for the plot insets (if any)...
1266        RectangleInsets insets = getInsets();
1267        insets.trim(area);
1268
1269        Rectangle2D dataArea = area;
1270        if (info != null) {
1271            info.setDataArea(dataArea);
1272        }
1273
1274        // draw the plot background and axes...
1275        drawBackground(g2, dataArea);
1276        int axisCount = this.axes.size();
1277        AxisState state = null;
1278        for (int i = 0; i < axisCount; i++) {
1279            ValueAxis axis = getAxis(i);
1280            if (axis != null) {
1281                PolarAxisLocation location = this.axisLocations.get(i);
1282                AxisState s = drawAxis(axis, location, g2, dataArea);
1283                if (i == 0) {
1284                    state = s;
1285                }
1286            }
1287        }
1288
1289        // now for each dataset, get the renderer and the appropriate axis
1290        // and render the dataset...
1291        Shape originalClip = g2.getClip();
1292        Composite originalComposite = g2.getComposite();
1293
1294        g2.clip(dataArea);
1295        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1296                getForegroundAlpha()));
1297        this.angleTicks = refreshAngleTicks();
1298        drawGridlines(g2, dataArea, this.angleTicks, state.getTicks());
1299        render(g2, dataArea, info);
1300        g2.setClip(originalClip);
1301        g2.setComposite(originalComposite);
1302        drawOutline(g2, dataArea);
1303        drawCornerTextItems(g2, dataArea);
1304    }
1305
1306    /**
1307     * Draws the corner text items.
1308     *
1309     * @param g2  the drawing surface.
1310     * @param area  the area.
1311     */
1312    protected void drawCornerTextItems(Graphics2D g2, Rectangle2D area) {
1313        if (this.cornerTextItems.isEmpty()) {
1314            return;
1315        }
1316
1317        g2.setColor(Color.BLACK);
1318        double width = 0.0;
1319        double height = 0.0;
1320        for (String msg : this.cornerTextItems) {
1321            FontMetrics fm = g2.getFontMetrics();
1322            Rectangle2D bounds = TextUtils.getTextBounds(msg, g2, fm);
1323            width = Math.max(width, bounds.getWidth());
1324            height += bounds.getHeight();
1325        }
1326
1327        double xadj = ANNOTATION_MARGIN * 2.0;
1328        double yadj = ANNOTATION_MARGIN;
1329        width += xadj;
1330        height += yadj;
1331
1332        double x = area.getMaxX() - width;
1333        double y = area.getMaxY() - height;
1334        g2.drawRect((int) x, (int) y, (int) width, (int) height);
1335        x += ANNOTATION_MARGIN;
1336        for (String msg : this.cornerTextItems) {
1337            Rectangle2D bounds = TextUtils.getTextBounds(msg, g2,
1338                    g2.getFontMetrics());
1339            y += bounds.getHeight();
1340            g2.drawString(msg, (int) x, (int) y);
1341        }
1342    }
1343
1344    /**
1345     * Draws the axis with the specified index.
1346     *
1347     * @param axis  the axis.
1348     * @param location  the axis location.
1349     * @param g2  the graphics target.
1350     * @param plotArea  the plot area.
1351     *
1352     * @return The axis state.
1353     */
1354    protected AxisState drawAxis(ValueAxis axis, PolarAxisLocation location,
1355            Graphics2D g2, Rectangle2D plotArea) {
1356
1357        double centerX = plotArea.getCenterX();
1358        double centerY = plotArea.getCenterY();
1359        double r = Math.min(plotArea.getWidth() / 2.0,
1360                plotArea.getHeight() / 2.0) - this.margin;
1361        double x = centerX - r;
1362        double y = centerY - r;
1363
1364        Rectangle2D dataArea = null;
1365        AxisState result = null;
1366        if (location == PolarAxisLocation.NORTH_RIGHT) {
1367            dataArea = new Rectangle2D.Double(x, y, r, r);
1368            result = axis.draw(g2, centerX, plotArea, dataArea,
1369                    RectangleEdge.RIGHT, null);
1370        }
1371        else if (location == PolarAxisLocation.NORTH_LEFT) {
1372            dataArea = new Rectangle2D.Double(centerX, y, r, r);
1373            result = axis.draw(g2, centerX, plotArea, dataArea,
1374                    RectangleEdge.LEFT, null);
1375        }
1376        else if (location == PolarAxisLocation.SOUTH_LEFT) {
1377            dataArea = new Rectangle2D.Double(centerX, centerY, r, r);
1378            result = axis.draw(g2, centerX, plotArea, dataArea,
1379                    RectangleEdge.LEFT, null);
1380        }
1381        else if (location == PolarAxisLocation.SOUTH_RIGHT) {
1382            dataArea = new Rectangle2D.Double(x, centerY, r, r);
1383            result = axis.draw(g2, centerX, plotArea, dataArea,
1384                    RectangleEdge.RIGHT, null);
1385        }
1386        else if (location == PolarAxisLocation.EAST_ABOVE) {
1387            dataArea = new Rectangle2D.Double(centerX, centerY, r, r);
1388            result = axis.draw(g2, centerY, plotArea, dataArea,
1389                    RectangleEdge.TOP, null);
1390        }
1391        else if (location == PolarAxisLocation.EAST_BELOW) {
1392            dataArea = new Rectangle2D.Double(centerX, y, r, r);
1393            result = axis.draw(g2, centerY, plotArea, dataArea,
1394                    RectangleEdge.BOTTOM, null);
1395        }
1396        else if (location == PolarAxisLocation.WEST_ABOVE) {
1397            dataArea = new Rectangle2D.Double(x, centerY, r, r);
1398            result = axis.draw(g2, centerY, plotArea, dataArea,
1399                    RectangleEdge.TOP, null);
1400        }
1401        else if (location == PolarAxisLocation.WEST_BELOW) {
1402            dataArea = new Rectangle2D.Double(x, y, r, r);
1403            result = axis.draw(g2, centerY, plotArea, dataArea,
1404                    RectangleEdge.BOTTOM, null);
1405        }
1406
1407        return result;
1408    }
1409
1410    /**
1411     * Draws a representation of the data within the dataArea region, using the
1412     * current m_Renderer.
1413     *
1414     * @param g2  the graphics device.
1415     * @param dataArea  the region in which the data is to be drawn.
1416     * @param info  an optional object for collection dimension
1417     *              information ({@code null} permitted).
1418     */
1419    protected void render(Graphics2D g2, Rectangle2D dataArea,
1420            PlotRenderingInfo info) {
1421
1422        // now get the data and plot it (the visual representation will depend
1423        // on the m_Renderer that has been set)...
1424        boolean hasData = false;
1425        int datasetCount = this.datasets.size();
1426        for (int i = datasetCount - 1; i >= 0; i--) {
1427            XYDataset dataset = getDataset(i);
1428            if (dataset == null) {
1429                continue;
1430            }
1431            PolarItemRenderer renderer = getRenderer(i);
1432            if (renderer == null) {
1433                continue;
1434            }
1435            if (!DatasetUtils.isEmptyOrNull(dataset)) {
1436                hasData = true;
1437                int seriesCount = dataset.getSeriesCount();
1438                for (int series = 0; series < seriesCount; series++) {
1439                    renderer.drawSeries(g2, dataArea, info, this, dataset,
1440                            series);
1441                }
1442            }
1443        }
1444        if (!hasData) {
1445            drawNoDataMessage(g2, dataArea);
1446        }
1447    }
1448
1449    /**
1450     * Draws the gridlines for the plot, if they are visible.
1451     *
1452     * @param g2  the graphics device.
1453     * @param dataArea  the data area.
1454     * @param angularTicks  the ticks for the angular axis.
1455     * @param radialTicks  the ticks for the radial axis.
1456     */
1457    protected void drawGridlines(Graphics2D g2, Rectangle2D dataArea,
1458            List<ValueTick> angularTicks, List<ValueTick> radialTicks) {
1459
1460        PolarItemRenderer renderer = getRenderer();
1461        // no renderer, no gridlines...
1462        if (renderer == null) {
1463            return;
1464        }
1465
1466        // draw the domain grid lines, if any...
1467        if (isAngleGridlinesVisible()) {
1468            Stroke gridStroke = getAngleGridlineStroke();
1469            Paint gridPaint = getAngleGridlinePaint();
1470            if ((gridStroke != null) && (gridPaint != null)) {
1471                renderer.drawAngularGridLines(g2, this, angularTicks,
1472                        dataArea);
1473            }
1474        }
1475
1476        // draw the radius grid lines, if any...
1477        if (isRadiusGridlinesVisible()) {
1478            Stroke gridStroke = getRadiusGridlineStroke();
1479            Paint gridPaint = getRadiusGridlinePaint();
1480            if ((gridStroke != null) && (gridPaint != null)) {
1481                List<ValueTick> ticks = buildRadialTicks(radialTicks);
1482                renderer.drawRadialGridLines(g2, this, getAxis(), ticks, dataArea);
1483            }
1484        }
1485    }
1486
1487    /**
1488     * Create a list of ticks based on the given list and plot properties.
1489     * Only ticks of a specific type may be in the result list.
1490     *
1491     * @param allTicks A list of all available ticks for the primary axis.
1492     *        {@code null} not permitted.
1493     * @return Ticks to use for radial gridlines.
1494     */
1495    protected List<ValueTick> buildRadialTicks(List<ValueTick> allTicks) {
1496        List<ValueTick> ticks = new ArrayList<>();
1497        for (ValueTick tick : allTicks) {
1498            if (isRadiusMinorGridlinesVisible() || TickType.MAJOR.equals(tick.getTickType())) {
1499                ticks.add(tick);
1500            }
1501        }
1502        return ticks;
1503    }
1504
1505    /**
1506     * Zooms the axis ranges by the specified percentage about the anchor point.
1507     *
1508     * @param percent  the amount of the zoom.
1509     */
1510    @Override
1511    public void zoom(double percent) {
1512        for (int axisIdx = 0; axisIdx < getAxisCount(); axisIdx++) {
1513            final ValueAxis axis = getAxis(axisIdx);
1514            if (axis != null) {
1515                if (percent > 0.0) {
1516                    double radius = axis.getUpperBound();
1517                    double scaledRadius = radius * percent;
1518                    axis.setUpperBound(scaledRadius);
1519                    axis.setAutoRange(false);
1520                } else {
1521                    axis.setAutoRange(true);
1522                }
1523            }
1524        }
1525    }
1526
1527    /**
1528     * A utility method that returns a list of datasets that are mapped to a
1529     * particular axis.
1530     *
1531     * @param axisIndex  the axis index ({@code null} not permitted).
1532     *
1533     * @return A list of datasets.
1534     */
1535    private List<XYDataset> getDatasetsMappedToAxis(Integer axisIndex) { 
1536        Args.nullNotPermitted(axisIndex, "axisIndex");
1537        List<XYDataset> result = new ArrayList<>();
1538       for (Entry<Integer, XYDataset> entry : this.datasets.entrySet()) {
1539            List<Integer> mappedAxes = this.datasetToAxesMap.get(entry.getKey());
1540            if (mappedAxes == null) {
1541                if (axisIndex.equals(ZERO)) {
1542                    result.add(getDataset(entry.getKey()));
1543                }
1544            } else {
1545                if (mappedAxes.contains(axisIndex)) {
1546                    result.add(getDataset(entry.getKey()));
1547                }
1548            }
1549        }
1550        return result;
1551    }
1552
1553    /**
1554     * Returns the range for the specified axis.
1555     *
1556     * @param axis  the axis.
1557     *
1558     * @return The range.
1559     */
1560    @Override
1561    public Range getDataRange(ValueAxis axis) {
1562        Range result = null;
1563        List<XYDataset> mappedDatasets = new ArrayList<>();
1564        int axisIndex = getAxisIndex(axis);
1565        if (axisIndex >= 0) {
1566            mappedDatasets = getDatasetsMappedToAxis(axisIndex);
1567        }
1568
1569        // iterate through the datasets that map to the axis and get the union
1570        // of the ranges.
1571        for (XYDataset dataset : mappedDatasets) {
1572            if (dataset != null) {
1573                // FIXME better ask the renderer instead of DatasetUtilities
1574                result = Range.combine(result, DatasetUtils.findRangeBounds(dataset));
1575            }
1576        }
1577
1578        return result;
1579    }
1580
1581    /**
1582     * Receives notification of a change to the plot's m_Dataset.
1583     * <P>
1584     * The axis ranges are updated if necessary.
1585     *
1586     * @param event  information about the event (not used here).
1587     */
1588    @Override
1589    public void datasetChanged(DatasetChangeEvent event) {
1590        for (int i = 0; i < this.axes.size(); i++) {
1591            final ValueAxis axis = (ValueAxis) this.axes.get(i);
1592            if (axis != null) {
1593                axis.configure();
1594            }
1595        }
1596        if (getParent() != null) {
1597            getParent().datasetChanged(event);
1598        }
1599        else {
1600            super.datasetChanged(event);
1601        }
1602    }
1603
1604    /**
1605     * Notifies all registered listeners of a property change.
1606     * <P>
1607     * One source of property change events is the plot's m_Renderer.
1608     *
1609     * @param event  information about the property change.
1610     */
1611    @Override
1612    public void rendererChanged(RendererChangeEvent event) {
1613        fireChangeEvent();
1614    }
1615
1616    /**
1617     * Returns the legend items for the plot.  Each legend item is generated by
1618     * the plot's m_Renderer, since the m_Renderer is responsible for the visual
1619     * representation of the data.
1620     *
1621     * @return The legend items.
1622     */
1623    @Override
1624    public LegendItemCollection getLegendItems() {
1625        if (this.fixedLegendItems != null) {
1626            return this.fixedLegendItems;
1627        }
1628        LegendItemCollection result = new LegendItemCollection();
1629        int count = this.datasets.size();
1630        for (int datasetIndex = 0; datasetIndex < count; datasetIndex++) {
1631            XYDataset dataset = getDataset(datasetIndex);
1632            PolarItemRenderer renderer = getRenderer(datasetIndex);
1633            if (dataset != null && renderer != null) {
1634                int seriesCount = dataset.getSeriesCount();
1635                for (int i = 0; i < seriesCount; i++) {
1636                    LegendItem item = renderer.getLegendItem(i);
1637                    result.add(item);
1638                }
1639            }
1640        }
1641        return result;
1642    }
1643
1644    /**
1645     * Tests this plot for equality with another object.
1646     *
1647     * @param obj  the object ({@code null} permitted).
1648     *
1649     * @return {@code true} or {@code false}.
1650     */
1651    @Override
1652    public boolean equals(Object obj) {
1653        if (obj == this) {
1654            return true;
1655        }
1656        if (!(obj instanceof PolarPlot)) {
1657            return false;
1658        }
1659        PolarPlot that = (PolarPlot) obj;
1660        if (!this.axes.equals(that.axes)) {
1661            return false;
1662        }
1663        if (!this.axisLocations.equals(that.axisLocations)) {
1664            return false;
1665        }
1666        if (!this.renderers.equals(that.renderers)) {
1667            return false;
1668        }
1669        if (!this.angleTickUnit.equals(that.angleTickUnit)) {
1670            return false;
1671        }
1672        if (this.angleGridlinesVisible != that.angleGridlinesVisible) {
1673            return false;
1674        }
1675        if (this.angleOffset != that.angleOffset)
1676        {
1677            return false;
1678        }
1679        if (this.counterClockwise != that.counterClockwise)
1680        {
1681            return false;
1682        }
1683        if (this.angleLabelsVisible != that.angleLabelsVisible) {
1684            return false;
1685        }
1686        if (!this.angleLabelFont.equals(that.angleLabelFont)) {
1687            return false;
1688        }
1689        if (!PaintUtils.equal(this.angleLabelPaint, that.angleLabelPaint)) {
1690            return false;
1691        }
1692        if (!Objects.equals(this.angleGridlineStroke, that.angleGridlineStroke)) {
1693            return false;
1694        }
1695        if (!PaintUtils.equal(
1696            this.angleGridlinePaint, that.angleGridlinePaint
1697        )) {
1698            return false;
1699        }
1700        if (this.radiusGridlinesVisible != that.radiusGridlinesVisible) {
1701            return false;
1702        }
1703        if (!Objects.equals(this.radiusGridlineStroke, that.radiusGridlineStroke)) {
1704            return false;
1705        }
1706        if (!PaintUtils.equal(this.radiusGridlinePaint,
1707                that.radiusGridlinePaint)) {
1708            return false;
1709        }
1710        if (this.radiusMinorGridlinesVisible !=
1711            that.radiusMinorGridlinesVisible) {
1712            return false;
1713        }
1714        if (!this.cornerTextItems.equals(that.cornerTextItems)) {
1715            return false;
1716        }
1717        if (this.margin != that.margin) {
1718            return false;
1719        }
1720        if (!Objects.equals(this.fixedLegendItems, that.fixedLegendItems)) {
1721            return false;
1722        }
1723        return super.equals(obj);
1724    }
1725
1726    /**
1727     * Returns a clone of the plot.
1728     *
1729     * @return A clone.
1730     *
1731     * @throws CloneNotSupportedException  this can occur if some component of
1732     *         the plot cannot be cloned.
1733     */
1734    @Override
1735    public Object clone() throws CloneNotSupportedException {
1736        PolarPlot clone = (PolarPlot) super.clone();
1737        clone.axes = CloneUtils.clone(this.axes);
1738        for (int i = 0; i < this.axes.size(); i++) {
1739            ValueAxis axis = (ValueAxis) this.axes.get(i);
1740            if (axis != null) {
1741                ValueAxis clonedAxis = (ValueAxis) axis.clone();
1742                clone.axes.put(i, clonedAxis);
1743                clonedAxis.setPlot(clone);
1744                clonedAxis.addChangeListener(clone);
1745            }
1746        }
1747
1748        // the datasets are not cloned, but listeners need to be added...
1749        clone.datasets = CloneUtils.clone(this.datasets);
1750        for (int i = 0; i < clone.datasets.size(); ++i) {
1751            XYDataset d = getDataset(i);
1752            if (d != null) {
1753                d.addChangeListener(clone);
1754            }
1755        }
1756
1757        clone.renderers = CloneUtils.clone(this.renderers);
1758        for (int i = 0; i < this.renderers.size(); i++) {
1759            PolarItemRenderer renderer2 = (PolarItemRenderer) this.renderers.get(i);
1760            if (renderer2 instanceof PublicCloneable) {
1761                PublicCloneable pc = (PublicCloneable) renderer2;
1762                PolarItemRenderer rc = (PolarItemRenderer) pc.clone();
1763                clone.renderers.put(i, rc);
1764                rc.setPlot(clone);
1765                rc.addChangeListener(clone);
1766            }
1767        }
1768
1769        clone.cornerTextItems = new ArrayList<>(this.cornerTextItems);
1770
1771        return clone;
1772    }
1773
1774    /**
1775     * Provides serialization support.
1776     *
1777     * @param stream  the output stream.
1778     *
1779     * @throws IOException  if there is an I/O error.
1780     */
1781    private void writeObject(ObjectOutputStream stream) throws IOException {
1782        stream.defaultWriteObject();
1783        SerialUtils.writeStroke(this.angleGridlineStroke, stream);
1784        SerialUtils.writePaint(this.angleGridlinePaint, stream);
1785        SerialUtils.writeStroke(this.radiusGridlineStroke, stream);
1786        SerialUtils.writePaint(this.radiusGridlinePaint, stream);
1787        SerialUtils.writePaint(this.angleLabelPaint, stream);
1788    }
1789
1790    /**
1791     * Provides serialization support.
1792     *
1793     * @param stream  the input stream.
1794     *
1795     * @throws IOException  if there is an I/O error.
1796     * @throws ClassNotFoundException  if there is a classpath problem.
1797     */
1798    private void readObject(ObjectInputStream stream)
1799        throws IOException, ClassNotFoundException {
1800
1801        stream.defaultReadObject();
1802        this.angleGridlineStroke = SerialUtils.readStroke(stream);
1803        this.angleGridlinePaint = SerialUtils.readPaint(stream);
1804        this.radiusGridlineStroke = SerialUtils.readStroke(stream);
1805        this.radiusGridlinePaint = SerialUtils.readPaint(stream);
1806        this.angleLabelPaint = SerialUtils.readPaint(stream);
1807
1808        int rangeAxisCount = this.axes.size();
1809        for (int i = 0; i < rangeAxisCount; i++) {
1810            Axis axis = (Axis) this.axes.get(i);
1811            if (axis != null) {
1812                axis.setPlot(this);
1813                axis.addChangeListener(this);
1814            }
1815        }
1816        int datasetCount = this.datasets.size();
1817        for (int i = 0; i < datasetCount; i++) {
1818            Dataset dataset = (Dataset) this.datasets.get(i);
1819            if (dataset != null) {
1820                dataset.addChangeListener(this);
1821            }
1822        }
1823        int rendererCount = this.renderers.size();
1824        for (int i = 0; i < rendererCount; i++) {
1825            PolarItemRenderer renderer = (PolarItemRenderer) this.renderers.get(i);
1826            if (renderer != null) {
1827                renderer.addChangeListener(this);
1828            }
1829        }
1830    }
1831
1832    /**
1833     * This method is required by the {@link Zoomable} interface, but since
1834     * the plot does not have any domain axes, it does nothing.
1835     *
1836     * @param factor  the zoom factor.
1837     * @param state  the plot state.
1838     * @param source  the source point (in Java2D coordinates).
1839     */
1840    @Override
1841    public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1842                               Point2D source) {
1843        // do nothing
1844    }
1845
1846    /**
1847     * This method is required by the {@link Zoomable} interface, but since
1848     * the plot does not have any domain axes, it does nothing.
1849     *
1850     * @param factor  the zoom factor.
1851     * @param state  the plot state.
1852     * @param source  the source point (in Java2D coordinates).
1853     * @param useAnchor  use source point as zoom anchor?
1854     */
1855    @Override
1856    public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1857                               Point2D source, boolean useAnchor) {
1858        // do nothing
1859    }
1860
1861    /**
1862     * This method is required by the {@link Zoomable} interface, but since
1863     * the plot does not have any domain axes, it does nothing.
1864     *
1865     * @param lowerPercent  the new lower bound.
1866     * @param upperPercent  the new upper bound.
1867     * @param state  the plot state.
1868     * @param source  the source point (in Java2D coordinates).
1869     */
1870    @Override
1871    public void zoomDomainAxes(double lowerPercent, double upperPercent,
1872                               PlotRenderingInfo state, Point2D source) {
1873        // do nothing
1874    }
1875
1876    /**
1877     * Multiplies the range on the range axis/axes by the specified factor.
1878     *
1879     * @param factor  the zoom factor.
1880     * @param state  the plot state.
1881     * @param source  the source point (in Java2D coordinates).
1882     */
1883    @Override
1884    public void zoomRangeAxes(double factor, PlotRenderingInfo state,
1885                              Point2D source) {
1886        zoom(factor);
1887    }
1888
1889    /**
1890     * Multiplies the range on the range axis by the specified factor.
1891     *
1892     * @param factor  the zoom factor.
1893     * @param info  the plot rendering info.
1894     * @param source  the source point (in Java2D space).
1895     * @param useAnchor  use source point as zoom anchor?
1896     *
1897     * @see #zoomDomainAxes(double, PlotRenderingInfo, Point2D, boolean)
1898     */
1899    @Override
1900    public void zoomRangeAxes(double factor, PlotRenderingInfo info,
1901                              Point2D source, boolean useAnchor) {
1902        // get the source coordinate - this plot has always a VERTICAL
1903        // orientation
1904        final double sourceX = source.getX();
1905
1906        for (int axisIdx = 0; axisIdx < getAxisCount(); axisIdx++) {
1907            final ValueAxis axis = getAxis(axisIdx);
1908            if (axis != null) {
1909                if (useAnchor) {
1910                    double anchorX = axis.java2DToValue(sourceX,
1911                            info.getDataArea(), RectangleEdge.BOTTOM);
1912                    axis.resizeRange(factor, anchorX);
1913                }
1914                else {
1915                    axis.resizeRange(factor);
1916                }
1917            }
1918        }
1919    }
1920
1921    /**
1922     * Zooms in on the range axes.
1923     *
1924     * @param lowerPercent  the new lower bound.
1925     * @param upperPercent  the new upper bound.
1926     * @param state  the plot state.
1927     * @param source  the source point (in Java2D coordinates).
1928     */
1929    @Override
1930    public void zoomRangeAxes(double lowerPercent, double upperPercent,
1931                              PlotRenderingInfo state, Point2D source) {
1932        zoom((upperPercent + lowerPercent) / 2.0);
1933    }
1934
1935    /**
1936     * Returns {@code false} always.
1937     *
1938     * @return {@code false} always.
1939     */
1940    @Override
1941    public boolean isDomainZoomable() {
1942        return false;
1943    }
1944
1945    /**
1946     * Returns {@code true} to indicate that the range axis is zoomable.
1947     *
1948     * @return {@code true}.
1949     */
1950    @Override
1951    public boolean isRangeZoomable() {
1952        return true;
1953    }
1954
1955    /**
1956     * Returns the orientation of the plot.
1957     *
1958     * @return The orientation.
1959     */
1960    @Override
1961    public PlotOrientation getOrientation() {
1962        return PlotOrientation.HORIZONTAL;
1963    }
1964
1965    /**
1966     * Translates a (theta, radius) pair into Java2D coordinates.  If
1967     * {@code radius} is less than the lower bound of the axis, then
1968     * this method returns the centre point.
1969     *
1970     * @param angleDegrees  the angle in degrees.
1971     * @param radius  the radius.
1972     * @param axis  the axis.
1973     * @param dataArea  the data area.
1974     *
1975     * @return A point in Java2D space.
1976     */
1977    public Point translateToJava2D(double angleDegrees, double radius,
1978            ValueAxis axis, Rectangle2D dataArea) {
1979
1980        if (counterClockwise) {
1981            angleDegrees = -angleDegrees;
1982        }
1983        double radians = Math.toRadians(angleDegrees + this.angleOffset);
1984
1985        double minx = dataArea.getMinX() + this.margin;
1986        double maxx = dataArea.getMaxX() - this.margin;
1987        double miny = dataArea.getMinY() + this.margin;
1988        double maxy = dataArea.getMaxY() - this.margin;
1989
1990        double halfWidth = (maxx - minx) / 2.0;
1991        double halfHeight = (maxy - miny) / 2.0;
1992
1993        double midX = minx + halfWidth;
1994        double midY = miny + halfHeight;
1995
1996        double l = Math.min(halfWidth, halfHeight);
1997        Rectangle2D quadrant = new Rectangle2D.Double(midX, midY, l, l);
1998
1999        double axisMin = axis.getLowerBound();
2000        double adjustedRadius = Math.max(radius, axisMin);
2001
2002        double length = axis.valueToJava2D(adjustedRadius, quadrant, RectangleEdge.BOTTOM) - midX;
2003        float x = (float) (midX + Math.cos(radians) * length);
2004        float y = (float) (midY + Math.sin(radians) * length);
2005
2006        int ix = Math.round(x);
2007        int iy = Math.round(y);
2008
2009        Point p = new Point(ix, iy);
2010        return p;
2011
2012    }
2013
2014}