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    
060    package org.jfree.chart.plot;
061    
062    import java.awt.AlphaComposite;
063    import java.awt.BasicStroke;
064    import java.awt.Color;
065    import java.awt.Composite;
066    import java.awt.Font;
067    import java.awt.FontMetrics;
068    import java.awt.Graphics2D;
069    import java.awt.Paint;
070    import java.awt.Point;
071    import java.awt.Shape;
072    import java.awt.Stroke;
073    import java.awt.geom.Point2D;
074    import java.awt.geom.Rectangle2D;
075    import java.io.IOException;
076    import java.io.ObjectInputStream;
077    import java.io.ObjectOutputStream;
078    import java.io.Serializable;
079    import java.util.ArrayList;
080    import java.util.HashSet;
081    import java.util.Iterator;
082    import java.util.List;
083    import java.util.Map;
084    import java.util.ResourceBundle;
085    import java.util.TreeMap;
086    
087    import org.jfree.chart.LegendItem;
088    import org.jfree.chart.LegendItemCollection;
089    import org.jfree.chart.axis.Axis;
090    import org.jfree.chart.axis.AxisState;
091    import org.jfree.chart.axis.NumberTick;
092    import org.jfree.chart.axis.NumberTickUnit;
093    import org.jfree.chart.axis.TickUnit;
094    import org.jfree.chart.axis.ValueAxis;
095    import org.jfree.chart.event.PlotChangeEvent;
096    import org.jfree.chart.event.RendererChangeEvent;
097    import org.jfree.chart.event.RendererChangeListener;
098    import org.jfree.chart.renderer.PolarItemRenderer;
099    import org.jfree.chart.util.ResourceBundleWrapper;
100    import org.jfree.data.Range;
101    import org.jfree.data.general.Dataset;
102    import org.jfree.data.general.DatasetChangeEvent;
103    import org.jfree.data.general.DatasetUtilities;
104    import org.jfree.data.xy.XYDataset;
105    import org.jfree.io.SerialUtilities;
106    import org.jfree.text.TextUtilities;
107    import org.jfree.ui.RectangleEdge;
108    import org.jfree.ui.RectangleInsets;
109    import org.jfree.ui.TextAnchor;
110    import org.jfree.util.ObjectList;
111    import org.jfree.util.ObjectUtilities;
112    import org.jfree.util.PaintUtilities;
113    import 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     */
119    public 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    }