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