001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2007, 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     * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025     * in the United States and other countries.]
026     *
027     * --------------
028     * PolarPlot.java
029     * --------------
030     * (C) Copyright 2004-2007, 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     *
035     * $Id: PolarPlot.java,v 1.13.2.8 2007/03/21 10:37:20 mungady Exp $
036     *
037     * Changes
038     * -------
039     * 19-Jan-2004 : Version 1, contributed by DB with minor changes by DG (DG);
040     * 07-Apr-2004 : Changed text bounds calculation (DG);
041     * 05-May-2005 : Updated draw() method parameters (DG);
042     * 09-Jun-2005 : Fixed getDataRange() and equals() methods (DG);
043     * 25-Oct-2005 : Implemented Zoomable (DG);
044     * ------------- JFREECHART 1.0.x ---------------------------------------------
045     * 07-Feb-2007 : Fixed bug 1599761, data value less than axis minimum (DG);
046     * 21-Mar-2007 : Fixed serialization bug (DG);
047     *
048     */
049    
050    package org.jfree.chart.plot;
051    
052    
053    import java.awt.AlphaComposite;
054    import java.awt.BasicStroke;
055    import java.awt.Color;
056    import java.awt.Composite;
057    import java.awt.Font;
058    import java.awt.FontMetrics;
059    import java.awt.Graphics2D;
060    import java.awt.Paint;
061    import java.awt.Point;
062    import java.awt.Shape;
063    import java.awt.Stroke;
064    import java.awt.geom.Point2D;
065    import java.awt.geom.Rectangle2D;
066    import java.io.IOException;
067    import java.io.ObjectInputStream;
068    import java.io.ObjectOutputStream;
069    import java.io.Serializable;
070    import java.util.ArrayList;
071    import java.util.Iterator;
072    import java.util.List;
073    import java.util.ResourceBundle;
074    
075    import org.jfree.chart.LegendItem;
076    import org.jfree.chart.LegendItemCollection;
077    import org.jfree.chart.axis.AxisState;
078    import org.jfree.chart.axis.NumberTick;
079    import org.jfree.chart.axis.ValueAxis;
080    import org.jfree.chart.event.PlotChangeEvent;
081    import org.jfree.chart.event.RendererChangeEvent;
082    import org.jfree.chart.event.RendererChangeListener;
083    import org.jfree.chart.renderer.PolarItemRenderer;
084    import org.jfree.data.Range;
085    import org.jfree.data.general.DatasetChangeEvent;
086    import org.jfree.data.general.DatasetUtilities;
087    import org.jfree.data.xy.XYDataset;
088    import org.jfree.io.SerialUtilities;
089    import org.jfree.text.TextUtilities;
090    import org.jfree.ui.RectangleEdge;
091    import org.jfree.ui.RectangleInsets;
092    import org.jfree.ui.TextAnchor;
093    import org.jfree.util.ObjectUtilities;
094    import org.jfree.util.PaintUtilities;
095    
096    
097    /**
098     * Plots data that is in (theta, radius) pairs where
099     * theta equal to zero is due north and increases clockwise.
100     */
101    public class PolarPlot extends Plot implements ValueAxisPlot, 
102                                                   Zoomable,
103                                                   RendererChangeListener, 
104                                                   Cloneable, 
105                                                   Serializable {
106       
107        /** For serialization. */
108        private static final long serialVersionUID = 3794383185924179525L;
109        
110        /** The default margin. */
111        private static final int MARGIN = 20;
112       
113        /** The annotation margin. */
114        private static final double ANNOTATION_MARGIN = 7.0;
115       
116        /** The default grid line stroke. */
117        public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(
118                0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 
119                0.0f, new float[]{2.0f, 2.0f}, 0.0f);
120       
121        /** The default grid line paint. */
122        public static final Paint DEFAULT_GRIDLINE_PAINT = Color.gray;
123       
124        /** The resourceBundle for the localization. */
125        protected static ResourceBundle localizationResources 
126            = ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");
127       
128        /** The angles that are marked with gridlines. */
129        private List angleTicks;
130        
131        /** The axis (used for the y-values). */
132        private ValueAxis axis;
133        
134        /** The dataset. */
135        private XYDataset dataset;
136       
137        /** 
138         * Object responsible for drawing the visual representation of each point 
139         * on the plot. 
140         */
141        private PolarItemRenderer renderer;
142       
143        /** A flag that controls whether or not the angle labels are visible. */
144        private boolean angleLabelsVisible = true;
145        
146        /** The font used to display the angle labels - never null. */
147        private Font angleLabelFont = new Font("SansSerif", Font.PLAIN, 12);
148        
149        /** The paint used to display the angle labels. */
150        private transient Paint angleLabelPaint = Color.black;
151        
152        /** A flag that controls whether the angular grid-lines are visible. */
153        private boolean angleGridlinesVisible;
154       
155        /** The stroke used to draw the angular grid-lines. */
156        private transient Stroke angleGridlineStroke;
157       
158        /** The paint used to draw the angular grid-lines. */
159        private transient Paint angleGridlinePaint;
160       
161        /** A flag that controls whether the radius grid-lines are visible. */
162        private boolean radiusGridlinesVisible;
163       
164        /** The stroke used to draw the radius grid-lines. */
165        private transient Stroke radiusGridlineStroke;
166       
167        /** The paint used to draw the radius grid-lines. */
168        private transient Paint radiusGridlinePaint;
169       
170        /** The annotations for the plot. */
171        private List cornerTextItems = new ArrayList();
172       
173        /**
174         * Default constructor.
175         */
176        public PolarPlot() {
177            this(null, null, null);
178        }
179       
180       /**
181         * Creates a new plot.
182         *
183         * @param dataset  the dataset (<code>null</code> permitted).
184         * @param radiusAxis  the radius axis (<code>null</code> permitted).
185         * @param renderer  the renderer (<code>null</code> permitted).
186         */
187        public PolarPlot(XYDataset dataset, 
188                         ValueAxis radiusAxis,
189                         PolarItemRenderer renderer) {
190          
191            super();
192                
193            this.dataset = dataset;
194            if (this.dataset != null) {
195                this.dataset.addChangeListener(this);
196            }
197          
198            this.angleTicks = new java.util.ArrayList();
199            this.angleTicks.add(new NumberTick(new Double(0.0), "0", 
200                    TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
201            this.angleTicks.add(new NumberTick(new Double(45.0), "45", 
202                    TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
203            this.angleTicks.add(new NumberTick(new Double(90.0), "90", 
204                    TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
205            this.angleTicks.add(new NumberTick(new Double(135.0), "135", 
206                    TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
207            this.angleTicks.add(new NumberTick(new Double(180.0), "180", 
208                    TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
209            this.angleTicks.add(new NumberTick(new Double(225.0), "225", 
210                    TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
211            this.angleTicks.add(new NumberTick(new Double(270.0), "270", 
212                    TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
213            this.angleTicks.add(new NumberTick(new Double(315.0), "315", 
214                    TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
215            
216            this.axis = radiusAxis;
217            if (this.axis != null) {
218                this.axis.setPlot(this);
219                this.axis.addChangeListener(this);
220            }
221          
222            this.renderer = renderer;
223            if (this.renderer != null) {
224                this.renderer.setPlot(this);
225                this.renderer.addChangeListener(this);
226            }
227          
228            this.angleGridlinesVisible = true;
229            this.angleGridlineStroke = DEFAULT_GRIDLINE_STROKE;
230            this.angleGridlinePaint = DEFAULT_GRIDLINE_PAINT;
231          
232            this.radiusGridlinesVisible = true;
233            this.radiusGridlineStroke = DEFAULT_GRIDLINE_STROKE;
234            this.radiusGridlinePaint = DEFAULT_GRIDLINE_PAINT;      
235        }
236       
237        /**
238         * Add text to be displayed in the lower right hand corner and sends a 
239         * {@link PlotChangeEvent} to all registered listeners.
240         * 
241         * @param text  the text to display (<code>null</code> not permitted).
242         * 
243         * @see #removeCornerTextItem(String)
244         */
245        public void addCornerTextItem(String text) {
246            if (text == null) {
247                throw new IllegalArgumentException("Null 'text' argument.");
248            }
249            this.cornerTextItems.add(text);
250            this.notifyListeners(new PlotChangeEvent(this));
251        }
252       
253        /**
254         * Remove the given text from the list of corner text items and
255         * sends a {@link PlotChangeEvent} to all registered listeners.
256         * 
257         * @param text  the text to remove (<code>null</code> ignored).
258         * 
259         * @see #addCornerTextItem(String)
260         */
261        public void removeCornerTextItem(String text) {
262            boolean removed = this.cornerTextItems.remove(text);
263            if (removed) {
264                this.notifyListeners(new PlotChangeEvent(this));        
265            }
266        }
267       
268        /**
269         * Clear the list of corner text items and sends a {@link PlotChangeEvent}
270         * to all registered listeners.
271         * 
272         * @see #addCornerTextItem(String)
273         * @see #removeCornerTextItem(String)
274         */
275        public void clearCornerTextItems() {
276            if (this.cornerTextItems.size() > 0) {
277                this.cornerTextItems.clear();
278                this.notifyListeners(new PlotChangeEvent(this));        
279            }
280        }
281       
282        /**
283         * Returns the plot type as a string.
284         *
285         * @return A short string describing the type of plot.
286         */
287        public String getPlotType() {
288           return PolarPlot.localizationResources.getString("Polar_Plot");
289        }
290        
291        /**
292         * Returns the axis for the plot.
293         *
294         * @return The radius axis (possibly <code>null</code>).
295         * 
296         * @see #setAxis(ValueAxis)
297         */
298        public ValueAxis getAxis() {
299            return this.axis;
300        }
301       
302        /**
303         * Sets the axis for the plot and sends a {@link PlotChangeEvent} to all
304         * registered listeners.
305         *
306         * @param axis  the new axis (<code>null</code> permitted).
307         */
308        public void setAxis(ValueAxis axis) {
309            if (axis != null) {
310                axis.setPlot(this);
311            }
312           
313            // plot is likely registered as a listener with the existing axis...
314            if (this.axis != null) {
315                this.axis.removeChangeListener(this);
316            }
317           
318            this.axis = axis;
319            if (this.axis != null) {
320                this.axis.configure();
321                this.axis.addChangeListener(this);
322            }
323            notifyListeners(new PlotChangeEvent(this));
324        }
325       
326        /**
327         * Returns the primary dataset for the plot.
328         *
329         * @return The primary dataset (possibly <code>null</code>).
330         * 
331         * @see #setDataset(XYDataset)
332         */
333        public XYDataset getDataset() {
334            return this.dataset;
335        }
336        
337        /**
338         * Sets the dataset for the plot, replacing the existing dataset if there 
339         * is one.
340         *
341         * @param dataset  the dataset (<code>null</code> permitted).
342         * 
343         * @see #getDataset()
344         */
345        public void setDataset(XYDataset dataset) {
346            // if there is an existing dataset, remove the plot from the list of 
347            // change listeners...
348            XYDataset existing = this.dataset;
349            if (existing != null) {
350                existing.removeChangeListener(this);
351            }
352           
353            // set the new m_Dataset, and register the chart as a change listener...
354            this.dataset = dataset;
355            if (this.dataset != null) {
356                setDatasetGroup(this.dataset.getGroup());
357                this.dataset.addChangeListener(this);
358            }
359           
360            // send a m_Dataset change event to self...
361            DatasetChangeEvent event = new DatasetChangeEvent(this, this.dataset);
362            datasetChanged(event);
363        }
364       
365        /**
366         * Returns the item renderer.
367         *
368         * @return The renderer (possibly <code>null</code>).
369         * 
370         * @see #setRenderer(PolarItemRenderer)
371         */
372        public PolarItemRenderer getRenderer() {
373            return this.renderer;
374        }
375       
376        /**
377         * Sets the item renderer, and notifies all listeners of a change to the 
378         * plot.
379         * <P>
380         * If the renderer is set to <code>null</code>, no chart will be drawn.
381         *
382         * @param renderer  the new renderer (<code>null</code> permitted).
383         * 
384         * @see #getRenderer()
385         */
386        public void setRenderer(PolarItemRenderer renderer) {
387            if (this.renderer != null) {
388                this.renderer.removeChangeListener(this);
389            }
390           
391            this.renderer = renderer;
392            if (this.renderer != null) {
393                this.renderer.setPlot(this);
394            }
395           
396            notifyListeners(new PlotChangeEvent(this));
397        }
398       
399        /**
400         * Returns a flag that controls whether or not the angle labels are visible.
401         * 
402         * @return A boolean.
403         * 
404         * @see #setAngleLabelsVisible(boolean)
405         */
406        public boolean isAngleLabelsVisible() {
407            return this.angleLabelsVisible;
408        }
409        
410        /**
411         * Sets the flag that controls whether or not the angle labels are visible,
412         * and sends a {@link PlotChangeEvent} to all registered listeners.
413         * 
414         * @param visible  the flag.
415         * 
416         * @see #isAngleLabelsVisible()
417         */
418        public void setAngleLabelsVisible(boolean visible) {
419            if (this.angleLabelsVisible != visible) {
420                this.angleLabelsVisible = visible;
421                notifyListeners(new PlotChangeEvent(this));
422            }
423        }
424        
425        /**
426         * Returns the font used to display the angle labels.
427         * 
428         * @return A font (never <code>null</code>).
429         * 
430         * @see #setAngleLabelFont(Font)
431         */
432        public Font getAngleLabelFont() {
433            return this.angleLabelFont;
434        }
435        
436        /**
437         * Sets the font used to display the angle labels and sends a 
438         * {@link PlotChangeEvent} to all registered listeners.
439         * 
440         * @param font  the font (<code>null</code> not permitted).
441         * 
442         * @see #getAngleLabelFont()
443         */
444        public void setAngleLabelFont(Font font) {
445            if (font == null) {
446                throw new IllegalArgumentException("Null 'font' argument.");   
447            }
448            this.angleLabelFont = font;
449            notifyListeners(new PlotChangeEvent(this));
450        }
451        
452        /**
453         * Returns the paint used to display the angle labels.
454         * 
455         * @return A paint (never <code>null</code>).
456         * 
457         * @see #setAngleLabelPaint(Paint)
458         */
459        public Paint getAngleLabelPaint() {
460            return this.angleLabelPaint;
461        }
462        
463        /**
464         * Sets the paint used to display the angle labels and sends a 
465         * {@link PlotChangeEvent} to all registered listeners.
466         * 
467         * @param paint  the paint (<code>null</code> not permitted).
468         */
469        public void setAngleLabelPaint(Paint paint) {
470            if (paint == null) {
471                throw new IllegalArgumentException("Null 'paint' argument.");
472            }
473            this.angleLabelPaint = paint;
474            notifyListeners(new PlotChangeEvent(this));
475        }
476        
477        /**
478         * Returns <code>true</code> if the angular gridlines are visible, and 
479         * <code>false<code> otherwise.
480         *
481         * @return <code>true</code> or <code>false</code>.
482         * 
483         * @see #setAngleGridlinesVisible(boolean)
484         */
485        public boolean isAngleGridlinesVisible() {
486            return this.angleGridlinesVisible;
487        }
488        
489        /**
490         * Sets the flag that controls whether or not the angular grid-lines are 
491         * visible.
492         * <p>
493         * If the flag value is changed, a {@link PlotChangeEvent} is sent to all 
494         * registered listeners.
495         *
496         * @param visible  the new value of the flag.
497         * 
498         * @see #isAngleGridlinesVisible()
499         */
500        public void setAngleGridlinesVisible(boolean visible) {
501            if (this.angleGridlinesVisible != visible) {
502                this.angleGridlinesVisible = visible;
503                notifyListeners(new PlotChangeEvent(this));
504            }
505        }
506       
507        /**
508         * Returns the stroke for the grid-lines (if any) plotted against the 
509         * angular axis.
510         *
511         * @return The stroke (possibly <code>null</code>).
512         * 
513         * @see #setAngleGridlineStroke(Stroke)
514         */
515        public Stroke getAngleGridlineStroke() {
516            return this.angleGridlineStroke;
517        }
518        
519        /**
520         * Sets the stroke for the grid lines plotted against the angular axis and
521         * sends a {@link PlotChangeEvent} to all registered listeners.
522         * <p>
523         * If you set this to <code>null</code>, no grid lines will be drawn.
524         *
525         * @param stroke  the stroke (<code>null</code> permitted).
526         * 
527         * @see #getAngleGridlineStroke()
528         */
529        public void setAngleGridlineStroke(Stroke stroke) {
530            this.angleGridlineStroke = stroke;
531            notifyListeners(new PlotChangeEvent(this));
532        }
533        
534        /**
535         * Returns the paint for the grid lines (if any) plotted against the 
536         * angular axis.
537         *
538         * @return The paint (possibly <code>null</code>).
539         * 
540         * @see #setAngleGridlinePaint(Paint)
541         */
542        public Paint getAngleGridlinePaint() {
543            return this.angleGridlinePaint;
544        }
545       
546        /**
547         * Sets the paint for the grid lines plotted against the angular axis.
548         * <p>
549         * If you set this to <code>null</code>, no grid lines will be drawn.
550         *
551         * @param paint  the paint (<code>null</code> permitted).
552         * 
553         * @see #getAngleGridlinePaint()
554         */
555        public void setAngleGridlinePaint(Paint paint) {
556            this.angleGridlinePaint = paint;
557            notifyListeners(new PlotChangeEvent(this));
558        }
559        
560        /**
561         * Returns <code>true</code> if the radius axis grid is visible, and 
562         * <code>false<code> otherwise.
563         *
564         * @return <code>true</code> or <code>false</code>.
565         * 
566         * @see #setRadiusGridlinesVisible(boolean)
567         */
568        public boolean isRadiusGridlinesVisible() {
569            return this.radiusGridlinesVisible;
570        }
571        
572        /**
573         * Sets the flag that controls whether or not the radius axis grid lines 
574         * are visible.
575         * <p>
576         * If the flag value is changed, a {@link PlotChangeEvent} is sent to all 
577         * registered listeners.
578         *
579         * @param visible  the new value of the flag.
580         * 
581         * @see #isRadiusGridlinesVisible()
582         */
583        public void setRadiusGridlinesVisible(boolean visible) {
584            if (this.radiusGridlinesVisible != visible) {
585                this.radiusGridlinesVisible = visible;
586                notifyListeners(new PlotChangeEvent(this));
587            }
588        }
589       
590        /**
591         * Returns the stroke for the grid lines (if any) plotted against the 
592         * radius axis.
593         *
594         * @return The stroke (possibly <code>null</code>).
595         * 
596         * @see #setRadiusGridlineStroke(Stroke)
597         */
598        public Stroke getRadiusGridlineStroke() {
599            return this.radiusGridlineStroke;
600        }
601        
602        /**
603         * Sets the stroke for the grid lines plotted against the radius axis and
604         * sends a {@link PlotChangeEvent} to all registered listeners.
605         * <p>
606         * If you set this to <code>null</code>, no grid lines will be drawn.
607         *
608         * @param stroke  the stroke (<code>null</code> permitted).
609         * 
610         * @see #getRadiusGridlineStroke()
611         */
612        public void setRadiusGridlineStroke(Stroke stroke) {
613            this.radiusGridlineStroke = stroke;
614            notifyListeners(new PlotChangeEvent(this));
615        }
616        
617        /**
618         * Returns the paint for the grid lines (if any) plotted against the radius
619         * axis.
620         *
621         * @return The paint (possibly <code>null</code>).
622         * 
623         * @see #setRadiusGridlinePaint(Paint)
624         */
625        public Paint getRadiusGridlinePaint() {
626            return this.radiusGridlinePaint;
627        }
628        
629        /**
630         * Sets the paint for the grid lines plotted against the radius axis and
631         * sends a {@link PlotChangeEvent} to all registered listeners.
632         * <p>
633         * If you set this to <code>null</code>, no grid lines will be drawn.
634         *
635         * @param paint  the paint (<code>null</code> permitted).
636         * 
637         * @see #getRadiusGridlinePaint()
638         */
639        public void setRadiusGridlinePaint(Paint paint) {
640            this.radiusGridlinePaint = paint;
641            notifyListeners(new PlotChangeEvent(this));
642        }
643        
644        /**
645         * Draws the plot on a Java 2D graphics device (such as the screen or a 
646         * printer).
647         * <P>
648         * This plot relies on a {@link PolarItemRenderer} to draw each 
649         * item in the plot.  This allows the visual representation of the data to 
650         * be changed easily.
651         * <P>
652         * The optional info argument collects information about the rendering of
653         * the plot (dimensions, tooltip information etc).  Just pass in 
654         * <code>null</code> if you do not need this information.
655         *
656         * @param g2  the graphics device.
657         * @param area  the area within which the plot (including axes and 
658         *              labels) should be drawn.
659         * @param anchor  the anchor point (<code>null</code> permitted).
660         * @param parentState  ignored.
661         * @param info  collects chart drawing information (<code>null</code> 
662         *              permitted).
663         */
664        public void draw(Graphics2D g2, 
665                         Rectangle2D area, 
666                         Point2D anchor,
667                         PlotState parentState,
668                         PlotRenderingInfo info) {
669           
670            // if the plot area is too small, just return...
671            boolean b1 = (area.getWidth() <= MINIMUM_WIDTH_TO_DRAW);
672            boolean b2 = (area.getHeight() <= MINIMUM_HEIGHT_TO_DRAW);
673            if (b1 || b2) {
674                return;
675            }
676           
677            // record the plot area...
678            if (info != null) {
679                info.setPlotArea(area);
680            }
681           
682            // adjust the drawing area for the plot insets (if any)...
683            RectangleInsets insets = getInsets();
684            insets.trim(area);
685          
686            Rectangle2D dataArea = area;
687            if (info != null) {
688                info.setDataArea(dataArea);
689            }
690           
691            // draw the plot background and axes...
692            drawBackground(g2, dataArea);
693            double h = Math.min(dataArea.getWidth() / 2.0, 
694                    dataArea.getHeight() / 2.0) - MARGIN;
695            Rectangle2D quadrant = new Rectangle2D.Double(dataArea.getCenterX(), 
696                    dataArea.getCenterY(), h, h);
697            AxisState state = drawAxis(g2, area, quadrant);
698            if (this.renderer != null) {
699                Shape originalClip = g2.getClip();
700                Composite originalComposite = g2.getComposite();
701              
702                g2.clip(dataArea);
703                g2.setComposite(AlphaComposite.getInstance(
704                        AlphaComposite.SRC_OVER, getForegroundAlpha()));
705              
706                drawGridlines(g2, dataArea, this.angleTicks, state.getTicks());
707              
708                // draw...
709                render(g2, dataArea, info);
710              
711                g2.setClip(originalClip);
712                g2.setComposite(originalComposite);
713            }
714            drawOutline(g2, dataArea);
715            drawCornerTextItems(g2, dataArea);
716        }
717       
718        /**
719         * Draws the corner text items.
720         * 
721         * @param g2  the drawing surface.
722         * @param area  the area.
723         */
724        protected void drawCornerTextItems(Graphics2D g2, Rectangle2D area) {
725            if (this.cornerTextItems.isEmpty()) {
726                return;
727            }
728           
729            g2.setColor(Color.black);
730            double width = 0.0;
731            double height = 0.0;
732            for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
733                String msg = (String) it.next();
734                FontMetrics fm = g2.getFontMetrics();
735                Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2, fm);
736                width = Math.max(width, bounds.getWidth());
737                height += bounds.getHeight();
738            }
739            
740            double xadj = ANNOTATION_MARGIN * 2.0;
741            double yadj = ANNOTATION_MARGIN;
742            width += xadj;
743            height += yadj;
744           
745            double x = area.getMaxX() - width;
746            double y = area.getMaxY() - height;
747            g2.drawRect((int) x, (int) y, (int) width, (int) height);
748            x += ANNOTATION_MARGIN;
749            for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
750                String msg = (String) it.next();
751                Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2, 
752                        g2.getFontMetrics());
753                y += bounds.getHeight();
754                g2.drawString(msg, (int) x, (int) y);
755            }
756        }
757       
758        /**
759         * A utility method for drawing the axes.
760         *
761         * @param g2  the graphics device.
762         * @param plotArea  the plot area.
763         * @param dataArea  the data area.
764         * 
765         * @return A map containing the axis states.
766         */
767        protected AxisState drawAxis(Graphics2D g2, Rectangle2D plotArea, 
768                                     Rectangle2D dataArea) {
769            return this.axis.draw(g2, dataArea.getMinY(), plotArea, dataArea, 
770                    RectangleEdge.TOP, null);
771        }
772       
773        /**
774         * Draws a representation of the data within the dataArea region, using the
775         * current m_Renderer.
776         *
777         * @param g2  the graphics device.
778         * @param dataArea  the region in which the data is to be drawn.
779         * @param info  an optional object for collection dimension 
780         *              information (<code>null</code> permitted).
781         */
782        protected void render(Graphics2D g2,
783                           Rectangle2D dataArea,
784                           PlotRenderingInfo info) {
785          
786            // now get the data and plot it (the visual representation will depend
787            // on the m_Renderer that has been set)...
788            if (!DatasetUtilities.isEmptyOrNull(this.dataset)) {
789                int seriesCount = this.dataset.getSeriesCount();
790                for (int series = 0; series < seriesCount; series++) {
791                    this.renderer.drawSeries(g2, dataArea, info, this, 
792                            this.dataset, series);
793                }
794            }
795            else {
796                drawNoDataMessage(g2, dataArea);
797            }
798        }
799       
800        /**
801         * Draws the gridlines for the plot, if they are visible.
802         *
803         * @param g2  the graphics device.
804         * @param dataArea  the data area.
805         * @param angularTicks  the ticks for the angular axis.
806         * @param radialTicks  the ticks for the radial axis.
807         */
808        protected void drawGridlines(Graphics2D g2, Rectangle2D dataArea, 
809                                     List angularTicks, List radialTicks) {
810    
811            // no renderer, no gridlines...
812            if (this.renderer == null) {
813                return;
814            }
815           
816            // draw the domain grid lines, if any...
817            if (isAngleGridlinesVisible()) {
818                Stroke gridStroke = getAngleGridlineStroke();
819                Paint gridPaint = getAngleGridlinePaint();
820                if ((gridStroke != null) && (gridPaint != null)) {
821                    this.renderer.drawAngularGridLines(g2, this, angularTicks, 
822                            dataArea);
823                }
824            }
825           
826            // draw the radius grid lines, if any...
827            if (isRadiusGridlinesVisible()) {
828                Stroke gridStroke = getRadiusGridlineStroke();
829                Paint gridPaint = getRadiusGridlinePaint();
830                if ((gridStroke != null) && (gridPaint != null)) {
831                    this.renderer.drawRadialGridLines(g2, this, this.axis, 
832                            radialTicks, dataArea);
833                }
834            }      
835        }
836       
837        /**
838         * Zooms the axis ranges by the specified percentage about the anchor point.
839         *
840         * @param percent  the amount of the zoom.
841         */
842        public void zoom(double percent) {
843            if (percent > 0.0) {
844                double radius = getMaxRadius();
845                double scaledRadius = radius * percent;
846                this.axis.setUpperBound(scaledRadius);
847                getAxis().setAutoRange(false);
848            } 
849            else {
850                getAxis().setAutoRange(true);
851            }
852        }
853       
854        /**
855         * Returns the range for the specified axis.
856         *
857         * @param axis  the axis.
858         *
859         * @return The range.
860         */
861        public Range getDataRange(ValueAxis axis) {
862            Range result = null;
863            if (this.dataset != null) {
864                result = Range.combine(result, 
865                        DatasetUtilities.findRangeBounds(this.dataset));
866            }
867            return result;
868        }
869       
870        /**
871         * Receives notification of a change to the plot's m_Dataset.
872         * <P>
873         * The axis ranges are updated if necessary.
874         *
875         * @param event  information about the event (not used here).
876         */
877        public void datasetChanged(DatasetChangeEvent event) {
878    
879            if (this.axis != null) {
880                this.axis.configure();
881            }
882           
883            if (getParent() != null) {
884                getParent().datasetChanged(event);
885            }
886            else {
887                super.datasetChanged(event);
888            }
889        }
890       
891        /**
892         * Notifies all registered listeners of a property change.
893         * <P>
894         * One source of property change events is the plot's m_Renderer.
895         *
896         * @param event  information about the property change.
897         */
898        public void rendererChanged(RendererChangeEvent event) {
899            notifyListeners(new PlotChangeEvent(this));
900        }
901       
902        /**
903         * Returns the number of series in the dataset for this plot.  If the 
904         * dataset is <code>null</code>, the method returns 0.
905         *
906         * @return The series count.
907         */
908        public int getSeriesCount() {
909            int result = 0;
910           
911            if (this.dataset != null) {
912                result = this.dataset.getSeriesCount();
913            }
914            return result;
915        }
916       
917        /**
918         * Returns the legend items for the plot.  Each legend item is generated by
919         * the plot's m_Renderer, since the m_Renderer is responsible for the visual
920         * representation of the data.
921         *
922         * @return The legend items.
923         */
924        public LegendItemCollection getLegendItems() {
925            LegendItemCollection result = new LegendItemCollection();
926           
927            // get the legend items for the main m_Dataset...
928            if (this.dataset != null) {
929                if (this.renderer != null) {
930                    int seriesCount = this.dataset.getSeriesCount();
931                    for (int i = 0; i < seriesCount; i++) {
932                        LegendItem item = this.renderer.getLegendItem(i);
933                        result.add(item);
934                    }
935                }
936            }      
937            return result;
938        }
939       
940        /**
941         * Tests this plot for equality with another object.
942         *
943         * @param obj  the object (<code>null</code> permitted).
944         *
945         * @return <code>true</code> or <code>false</code>.
946         */
947        public boolean equals(Object obj) {
948            if (obj == this) {
949                return true;
950            }
951            if (!(obj instanceof PolarPlot)) {
952                return false;
953            }
954            PolarPlot that = (PolarPlot) obj;
955            if (!ObjectUtilities.equal(this.axis, that.axis)) {
956                return false;
957            }
958            if (!ObjectUtilities.equal(this.renderer, that.renderer)) {
959                return false;
960            }
961            if (this.angleGridlinesVisible != that.angleGridlinesVisible) {
962                return false;
963            }
964            if (this.angleLabelsVisible != that.angleLabelsVisible) {
965                return false;   
966            }
967            if (!this.angleLabelFont.equals(that.angleLabelFont)) {
968                return false;   
969            }
970            if (!PaintUtilities.equal(this.angleLabelPaint, that.angleLabelPaint)) {
971                return false;   
972            }
973            if (!ObjectUtilities.equal(this.angleGridlineStroke, 
974                    that.angleGridlineStroke)) {
975                return false;
976            }
977            if (!PaintUtilities.equal(
978                this.angleGridlinePaint, that.angleGridlinePaint
979            )) {
980                return false;
981            }
982            if (this.radiusGridlinesVisible != that.radiusGridlinesVisible) {
983                return false;
984            }
985            if (!ObjectUtilities.equal(this.radiusGridlineStroke, 
986                    that.radiusGridlineStroke)) {
987                return false;
988            }
989            if (!PaintUtilities.equal(this.radiusGridlinePaint, 
990                    that.radiusGridlinePaint)) {
991                return false;
992            }
993            if (!this.cornerTextItems.equals(that.cornerTextItems)) {
994                return false;
995            }
996            return super.equals(obj);
997        }
998       
999        /**
1000         * Returns a clone of the plot.
1001         *
1002         * @return A clone.
1003         *
1004         * @throws CloneNotSupportedException  this can occur if some component of 
1005         *         the plot cannot be cloned.
1006         */
1007        public Object clone() throws CloneNotSupportedException {
1008          
1009            PolarPlot clone = (PolarPlot) super.clone();
1010            if (this.axis != null) {
1011                clone.axis = (ValueAxis) ObjectUtilities.clone(this.axis);
1012                clone.axis.setPlot(clone);
1013                clone.axis.addChangeListener(clone);
1014            }
1015          
1016            if (clone.dataset != null) {
1017                clone.dataset.addChangeListener(clone);
1018            }
1019          
1020            if (this.renderer != null) {
1021                clone.renderer 
1022                    = (PolarItemRenderer) ObjectUtilities.clone(this.renderer);
1023            }
1024            
1025            clone.cornerTextItems = new ArrayList(this.cornerTextItems);
1026           
1027            return clone;
1028        }
1029       
1030        /**
1031         * Provides serialization support.
1032         *
1033         * @param stream  the output stream.
1034         *
1035         * @throws IOException  if there is an I/O error.
1036         */
1037        private void writeObject(ObjectOutputStream stream) throws IOException {
1038            stream.defaultWriteObject();
1039            SerialUtilities.writeStroke(this.angleGridlineStroke, stream);
1040            SerialUtilities.writePaint(this.angleGridlinePaint, stream);
1041            SerialUtilities.writeStroke(this.radiusGridlineStroke, stream);
1042            SerialUtilities.writePaint(this.radiusGridlinePaint, stream);
1043            SerialUtilities.writePaint(this.angleLabelPaint, stream);
1044        }
1045       
1046        /**
1047         * Provides serialization support.
1048         *
1049         * @param stream  the input stream.
1050         *
1051         * @throws IOException  if there is an I/O error.
1052         * @throws ClassNotFoundException  if there is a classpath problem.
1053         */
1054        private void readObject(ObjectInputStream stream) 
1055            throws IOException, ClassNotFoundException {
1056          
1057            stream.defaultReadObject();
1058            this.angleGridlineStroke = SerialUtilities.readStroke(stream);
1059            this.angleGridlinePaint = SerialUtilities.readPaint(stream);
1060            this.radiusGridlineStroke = SerialUtilities.readStroke(stream);
1061            this.radiusGridlinePaint = SerialUtilities.readPaint(stream);
1062            this.angleLabelPaint = SerialUtilities.readPaint(stream);
1063          
1064            if (this.axis != null) {
1065                this.axis.setPlot(this);
1066                this.axis.addChangeListener(this);
1067            }
1068          
1069            if (this.dataset != null) {
1070                this.dataset.addChangeListener(this);
1071            }
1072        }
1073       
1074        /**
1075         * This method is required by the {@link Zoomable} interface, but since
1076         * the plot does not have any domain axes, it does nothing.
1077         *
1078         * @param factor  the zoom factor.
1079         * @param state  the plot state.
1080         * @param source  the source point (in Java2D coordinates).
1081         */
1082        public void zoomDomainAxes(double factor, PlotRenderingInfo state, 
1083                                   Point2D source) {
1084            // do nothing
1085        }
1086       
1087        /**
1088         * This method is required by the {@link Zoomable} interface, but since
1089         * the plot does not have any domain axes, it does nothing.
1090         * 
1091         * @param lowerPercent  the new lower bound.
1092         * @param upperPercent  the new upper bound.
1093         * @param state  the plot state.
1094         * @param source  the source point (in Java2D coordinates).
1095         */
1096        public void zoomDomainAxes(double lowerPercent, double upperPercent, 
1097                                   PlotRenderingInfo state, Point2D source) {
1098            // do nothing
1099        }
1100       
1101        /**
1102         * Multiplies the range on the range axis/axes by the specified factor.
1103         *
1104         * @param factor  the zoom factor.
1105         * @param state  the plot state.
1106         * @param source  the source point (in Java2D coordinates).
1107         */
1108        public void zoomRangeAxes(double factor, PlotRenderingInfo state, 
1109                                  Point2D source) {
1110            zoom(factor);
1111        }
1112       
1113        /**
1114         * Zooms in on the range axes.
1115         * 
1116         * @param lowerPercent  the new lower bound.
1117         * @param upperPercent  the new upper bound.
1118         * @param state  the plot state.
1119         * @param source  the source point (in Java2D coordinates).
1120         */
1121        public void zoomRangeAxes(double lowerPercent, double upperPercent, 
1122                                  PlotRenderingInfo state, Point2D source) {
1123            zoom((upperPercent + lowerPercent) / 2.0);
1124        }   
1125    
1126        /**
1127         * Returns <code>false</code> always.
1128         * 
1129         * @return <code>false</code> always.
1130         */
1131        public boolean isDomainZoomable() {
1132            return false;
1133        }
1134        
1135        /**
1136         * Returns <code>true</code> to indicate that the range axis is zoomable.
1137         * 
1138         * @return <code>true</code>.
1139         */
1140        public boolean isRangeZoomable() {
1141            return true;
1142        }
1143        
1144        /**
1145         * Returns the orientation of the plot.
1146         * 
1147         * @return The orientation.
1148         */
1149        public PlotOrientation getOrientation() {
1150            return PlotOrientation.HORIZONTAL;
1151        }
1152    
1153        /**
1154         * Returns the upper bound of the radius axis.
1155         * 
1156         * @return The upper bound.
1157         */
1158        public double getMaxRadius() {
1159            return this.axis.getUpperBound();
1160        }
1161    
1162        /**
1163         * Translates a (theta, radius) pair into Java2D coordinates.  If 
1164         * <code>radius</code> is less than the lower bound of the axis, then
1165         * this method returns the centre point.
1166         * 
1167         * @param angleDegrees  the angle in degrees.
1168         * @param radius  the radius.
1169         * @param dataArea  the data area.
1170         * 
1171         * @return A point in Java2D space.
1172         */   
1173        public Point translateValueThetaRadiusToJava2D(double angleDegrees, 
1174                                                       double radius,
1175                                                       Rectangle2D dataArea) {
1176           
1177            double radians = Math.toRadians(angleDegrees - 90.0);
1178          
1179            double minx = dataArea.getMinX() + MARGIN;
1180            double maxx = dataArea.getMaxX() - MARGIN;
1181            double miny = dataArea.getMinY() + MARGIN;
1182            double maxy = dataArea.getMaxY() - MARGIN;
1183          
1184            double lengthX = maxx - minx;
1185            double lengthY = maxy - miny;
1186            double length = Math.min(lengthX, lengthY);
1187          
1188            double midX = minx + lengthX / 2.0;
1189            double midY = miny + lengthY / 2.0;
1190          
1191            double axisMin = this.axis.getLowerBound();
1192            double axisMax =  getMaxRadius();
1193            double adjustedRadius = Math.max(radius, axisMin);
1194    
1195            double xv = length / 2.0 * Math.cos(radians);
1196            double yv = length / 2.0 * Math.sin(radians);
1197    
1198            float x = (float) (midX + (xv * (adjustedRadius - axisMin) 
1199                    / (axisMax - axisMin)));
1200            float y = (float) (midY + (yv * (adjustedRadius - axisMin) 
1201                    / (axisMax - axisMin)));
1202          
1203            int ix = Math.round(x);
1204            int iy = Math.round(y);
1205          
1206            Point p = new Point(ix, iy);
1207            return p;
1208            
1209        }
1210        
1211    }