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