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