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