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