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