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