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