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