# PolarPlot.java

    /* ===========================================================
     * JFreeChart : a free chart library for the Java(tm) platform
     * ===========================================================
     *
     * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
     *
     * Project Info:  http://www.jfree.org/jfreechart/index.html
     *
     * This library is free software; you can redistribute it and/or modify it
     * under the terms of the GNU Lesser General Public License as published by
     * the Free Software Foundation; either version 2.1 of the License, or
     * (at your option) any later version.
     *
     * This library is distributed in the hope that it will be useful, but
     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
     * License for more details.
     *
     * You should have received a copy of the GNU Lesser General Public
     * License along with this library; if not, write to the Free Software
     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
     * USA.
     *
     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
     * in the United States and other countries.]
     *
     * --------------
     * PolarPlot.java
     * --------------
     * (C) Copyright 2004-2008, by Solution Engineering, Inc. and Contributors.
     *
     * Original Author:  Daniel Bridenbecker, Solution Engineering, Inc.;
     * Contributor(s):   David Gilbert (for Object Refinery Limited);
     *                   Martin Hoeller (patch 1871902);
     *
     * Changes
     * -------
     * 19-Jan-2004 : Version 1, contributed by DB with minor changes by DG (DG);
     * 07-Apr-2004 : Changed text bounds calculation (DG);
     * 05-May-2005 : Updated draw() method parameters (DG);
     * 09-Jun-2005 : Fixed getDataRange() and equals() methods (DG);
     * 25-Oct-2005 : Implemented Zoomable (DG);
     * ------------- JFREECHART 1.0.x ---------------------------------------------
     * 07-Feb-2007 : Fixed bug 1599761, data value less than axis minimum (DG);
     * 21-Mar-2007 : Fixed serialization bug (DG);
     * 24-Sep-2007 : Implemented new zooming methods (DG);
     * 17-Feb-2007 : Added angle tick unit attribute (see patch 1871902 by
     *               Martin Hoeller) (DG);
     *
     */

    package org.jfree.chart.plot;

    import java.awt.AlphaComposite;
    import java.awt.BasicStroke;
    import java.awt.Color;
    import java.awt.Composite;
    import java.awt.Font;
    import java.awt.FontMetrics;
    import java.awt.Graphics2D;
    import java.awt.Paint;
    import java.awt.Point;
    import java.awt.Shape;
    import java.awt.Stroke;
    import java.awt.geom.Point2D;
    import java.awt.geom.Rectangle2D;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;
    import java.util.ResourceBundle;

    import org.jfree.chart.LegendItem;
    import org.jfree.chart.LegendItemCollection;
    import org.jfree.chart.axis.AxisState;
    import org.jfree.chart.axis.NumberTick;
    import org.jfree.chart.axis.NumberTickUnit;
    import org.jfree.chart.axis.TickUnit;
    import org.jfree.chart.axis.ValueAxis;
    import org.jfree.chart.event.PlotChangeEvent;
    import org.jfree.chart.event.RendererChangeEvent;
    import org.jfree.chart.event.RendererChangeListener;
    import org.jfree.chart.renderer.PolarItemRenderer;
    import org.jfree.data.Range;
    import org.jfree.data.general.DatasetChangeEvent;
    import org.jfree.data.general.DatasetUtilities;
    import org.jfree.data.xy.XYDataset;
    import org.jfree.io.SerialUtilities;
    import org.jfree.text.TextUtilities;
    import org.jfree.ui.RectangleEdge;
    import org.jfree.ui.RectangleInsets;
    import org.jfree.ui.TextAnchor;
    import org.jfree.util.ObjectUtilities;
    import org.jfree.util.PaintUtilities;

    /**
     * Plots data that is in (theta, radius) pairs where
     * theta equal to zero is due north and increases clockwise.
     */
    public class PolarPlot extends Plot implements ValueAxisPlot, Zoomable,
            RendererChangeListener, Cloneable, Serializable {

        /** For serialization. */
        private static final long serialVersionUID = 3794383185924179525L;

        /** The default margin. */
        private static final int MARGIN = 20;

        /** The annotation margin. */
        private static final double ANNOTATION_MARGIN = 7.0;

        /**
         * The default angle tick unit size.
         *
         * @since 1.0.10
         */
        public static final double DEFAULT_ANGLE_TICK_UNIT_SIZE = 45.0;

        /** The default grid line stroke. */
        public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(
                0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
                0.0f, new float[]{2.0f, 2.0f}, 0.0f);

        /** The default grid line paint. */
        public static final Paint DEFAULT_GRIDLINE_PAINT = Color.gray;

        /** The resourceBundle for the localization. */
        protected static ResourceBundle localizationResources
            = ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");

        /** The angles that are marked with gridlines. */
        private List angleTicks;

        /** The axis (used for the y-values). */
        private ValueAxis axis;

        /** The dataset. */
        private XYDataset dataset;

        /**
         * Object responsible for drawing the visual representation of each point
         * on the plot.
         */
        private PolarItemRenderer renderer;

        /**
         * The tick unit that controls the spacing between the angular grid lines.
         *
         * @since 1.0.10
         */
        private TickUnit angleTickUnit;

        /** A flag that controls whether or not the angle labels are visible. */
        private boolean angleLabelsVisible = true;

        /** The font used to display the angle labels - never null. */
        private Font angleLabelFont = new Font("SansSerif", Font.PLAIN, 12);

        /** The paint used to display the angle labels. */
        private transient Paint angleLabelPaint = Color.black;

        /** A flag that controls whether the angular grid-lines are visible. */
        private boolean angleGridlinesVisible;

        /** The stroke used to draw the angular grid-lines. */
        private transient Stroke angleGridlineStroke;

        /** The paint used to draw the angular grid-lines. */
        private transient Paint angleGridlinePaint;

        /** A flag that controls whether the radius grid-lines are visible. */
        private boolean radiusGridlinesVisible;

        /** The stroke used to draw the radius grid-lines. */
        private transient Stroke radiusGridlineStroke;

        /** The paint used to draw the radius grid-lines. */
        private transient Paint radiusGridlinePaint;

        /** The annotations for the plot. */
        private List cornerTextItems = new ArrayList();

        /**
         * Default constructor.
         */
        public PolarPlot() {
            this(null, null, null);
        }

       /**
         * Creates a new plot.
         *
         * @param dataset  the dataset (<code>null</code> permitted).
         * @param radiusAxis  the radius axis (<code>null</code> permitted).
         * @param renderer  the renderer (<code>null</code> permitted).
         */
        public PolarPlot(XYDataset dataset,
                         ValueAxis radiusAxis,
                         PolarItemRenderer renderer) {

            super();

            this.dataset = dataset;
            if (this.dataset != null) {
                this.dataset.addChangeListener(this);
            }
            this.angleTickUnit = new NumberTickUnit(DEFAULT_ANGLE_TICK_UNIT_SIZE);

            this.axis = radiusAxis;
            if (this.axis != null) {
                this.axis.setPlot(this);
                this.axis.addChangeListener(this);
            }

            this.renderer = renderer;
            if (this.renderer != null) {
                this.renderer.setPlot(this);
                this.renderer.addChangeListener(this);
            }

            this.angleGridlinesVisible = true;
            this.angleGridlineStroke = DEFAULT_GRIDLINE_STROKE;
            this.angleGridlinePaint = DEFAULT_GRIDLINE_PAINT;

            this.radiusGridlinesVisible = true;
            this.radiusGridlineStroke = DEFAULT_GRIDLINE_STROKE;
            this.radiusGridlinePaint = DEFAULT_GRIDLINE_PAINT;
        }

        /**
         * Add text to be displayed in the lower right hand corner and sends a
         * {@link PlotChangeEvent} to all registered listeners.
         *
         * @param text  the text to display (<code>null</code> not permitted).
         *
         * @see #removeCornerTextItem(String)
         */
        public void addCornerTextItem(String text) {
            if (text == null) {
                throw new IllegalArgumentException("Null 'text' argument.");
            }
            this.cornerTextItems.add(text);
            fireChangeEvent();
        }

        /**
         * Remove the given text from the list of corner text items and
         * sends a {@link PlotChangeEvent} to all registered listeners.
         *
         * @param text  the text to remove (<code>null</code> ignored).
         *
         * @see #addCornerTextItem(String)
         */
        public void removeCornerTextItem(String text) {
            boolean removed = this.cornerTextItems.remove(text);
            if (removed) {
                fireChangeEvent();
            }
        }

        /**
         * Clear the list of corner text items and sends a {@link PlotChangeEvent}
         * to all registered listeners.
         *
         * @see #addCornerTextItem(String)
         * @see #removeCornerTextItem(String)
         */
        public void clearCornerTextItems() {
            if (this.cornerTextItems.size() > 0) {
                this.cornerTextItems.clear();
                fireChangeEvent();
            }
        }

        /**
         * Returns the plot type as a string.
         *
         * @return A short string describing the type of plot.
         */
        public String getPlotType() {
           return PolarPlot.localizationResources.getString("Polar_Plot");
        }

        /**
         * Returns the axis for the plot.
         *
         * @return The radius axis (possibly <code>null</code>).
         *
         * @see #setAxis(ValueAxis)
         */
        public ValueAxis getAxis() {
            return this.axis;
        }

        /**
         * Sets the axis for the plot and sends a {@link PlotChangeEvent} to all
         * registered listeners.
         *
         * @param axis  the new axis (<code>null</code> permitted).
         */
        public void setAxis(ValueAxis axis) {
            if (axis != null) {
                axis.setPlot(this);
            }

            // plot is likely registered as a listener with the existing axis...
            if (this.axis != null) {
                this.axis.removeChangeListener(this);
            }

            this.axis = axis;
            if (this.axis != null) {
                this.axis.configure();
                this.axis.addChangeListener(this);
            }
            fireChangeEvent();
        }

        /**
         * Returns the primary dataset for the plot.
         *
         * @return The primary dataset (possibly <code>null</code>).
         *
         * @see #setDataset(XYDataset)
         */
        public XYDataset getDataset() {
            return this.dataset;
        }

        /**
         * Sets the dataset for the plot, replacing the existing dataset if there
         * is one.
         *
         * @param dataset  the dataset (<code>null</code> permitted).
         *
         * @see #getDataset()
         */
        public void setDataset(XYDataset dataset) {
            // if there is an existing dataset, remove the plot from the list of
            // change listeners...
            XYDataset existing = this.dataset;
            if (existing != null) {
                existing.removeChangeListener(this);
            }

            // set the new m_Dataset, and register the chart as a change listener...
            this.dataset = dataset;
            if (this.dataset != null) {
                setDatasetGroup(this.dataset.getGroup());
                this.dataset.addChangeListener(this);
            }

            // send a m_Dataset change event to self...
            DatasetChangeEvent event = new DatasetChangeEvent(this, this.dataset);
            datasetChanged(event);
        }

        /**
         * Returns the item renderer.
         *
         * @return The renderer (possibly <code>null</code>).
         *
         * @see #setRenderer(PolarItemRenderer)
         */
        public PolarItemRenderer getRenderer() {
            return this.renderer;
        }

        /**
         * Sets the item renderer, and notifies all listeners of a change to the
         * plot.
         * <P>
         * If the renderer is set to <code>null</code>, no chart will be drawn.
         *
         * @param renderer  the new renderer (<code>null</code> permitted).
         *
         * @see #getRenderer()
         */
        public void setRenderer(PolarItemRenderer renderer) {
            if (this.renderer != null) {
                this.renderer.removeChangeListener(this);
            }

            this.renderer = renderer;
            if (this.renderer != null) {
                this.renderer.setPlot(this);
            }
            fireChangeEvent();
        }

        /**
         * Returns the tick unit that controls the spacing of the angular grid
         * lines.
         *
         * @return The tick unit (never <code>null</code>).
         *
         * @since 1.0.10
         */
        public TickUnit getAngleTickUnit() {
            return this.angleTickUnit;
        }

        /**
         * Sets the tick unit that controls the spacing of the angular grid
         * lines, and sends a {@link PlotChangeEvent} to all registered listeners.
         *
         * @param unit  the tick unit (<code>null</code> not permitted).
         *
         * @since 1.0.10
         */
        public void setAngleTickUnit(TickUnit unit) {
            if (unit == null) {
                throw new IllegalArgumentException("Null 'unit' argument.");
            }
            this.angleTickUnit = unit;
            fireChangeEvent();
        }

        /**
         * Returns a flag that controls whether or not the angle labels are visible.
         *
         * @return A boolean.
         *
         * @see #setAngleLabelsVisible(boolean)
         */
        public boolean isAngleLabelsVisible() {
            return this.angleLabelsVisible;
        }

        /**
         * Sets the flag that controls whether or not the angle labels are visible,
         * and sends a {@link PlotChangeEvent} to all registered listeners.
         *
         * @param visible  the flag.
         *
         * @see #isAngleLabelsVisible()
         */
        public void setAngleLabelsVisible(boolean visible) {
            if (this.angleLabelsVisible != visible) {
                this.angleLabelsVisible = visible;
                fireChangeEvent();
            }
        }

        /**
         * Returns the font used to display the angle labels.
         *
         * @return A font (never <code>null</code>).
         *
         * @see #setAngleLabelFont(Font)
         */
        public Font getAngleLabelFont() {
            return this.angleLabelFont;
        }

        /**
         * Sets the font used to display the angle labels and sends a
         * {@link PlotChangeEvent} to all registered listeners.
         *
         * @param font  the font (<code>null</code> not permitted).
         *
         * @see #getAngleLabelFont()
         */
        public void setAngleLabelFont(Font font) {
            if (font == null) {
                throw new IllegalArgumentException("Null 'font' argument.");
            }
            this.angleLabelFont = font;
            fireChangeEvent();
        }

        /**
         * Returns the paint used to display the angle labels.
         *
         * @return A paint (never <code>null</code>).
         *
         * @see #setAngleLabelPaint(Paint)
         */
        public Paint getAngleLabelPaint() {
            return this.angleLabelPaint;
        }

        /**
         * Sets the paint used to display the angle labels and sends a
         * {@link PlotChangeEvent} to all registered listeners.
         *
         * @param paint  the paint (<code>null</code> not permitted).
         */
        public void setAngleLabelPaint(Paint paint) {
            if (paint == null) {
                throw new IllegalArgumentException("Null 'paint' argument.");
            }
            this.angleLabelPaint = paint;
            fireChangeEvent();
        }

        /**
         * Returns <code>true</code> if the angular gridlines are visible, and
         * <code>false<code> otherwise.
         *
         * @return <code>true</code> or <code>false</code>.
         *
         * @see #setAngleGridlinesVisible(boolean)
         */
        public boolean isAngleGridlinesVisible() {
            return this.angleGridlinesVisible;
        }

        /**
         * Sets the flag that controls whether or not the angular grid-lines are
         * visible.
         * <p>
         * If the flag value is changed, a {@link PlotChangeEvent} is sent to all
         * registered listeners.
         *
         * @param visible  the new value of the flag.
         *
         * @see #isAngleGridlinesVisible()
         */
        public void setAngleGridlinesVisible(boolean visible) {
            if (this.angleGridlinesVisible != visible) {
                this.angleGridlinesVisible = visible;
                fireChangeEvent();
            }
        }

        /**
         * Returns the stroke for the grid-lines (if any) plotted against the
         * angular axis.
         *
         * @return The stroke (possibly <code>null</code>).
         *
         * @see #setAngleGridlineStroke(Stroke)
         */
        public Stroke getAngleGridlineStroke() {
            return this.angleGridlineStroke;
        }

        /**
         * Sets the stroke for the grid lines plotted against the angular axis and
         * sends a {@link PlotChangeEvent} to all registered listeners.
         * <p>
         * If you set this to <code>null</code>, no grid lines will be drawn.
         *
         * @param stroke  the stroke (<code>null</code> permitted).
         *
         * @see #getAngleGridlineStroke()
         */
        public void setAngleGridlineStroke(Stroke stroke) {
            this.angleGridlineStroke = stroke;
            fireChangeEvent();
        }

        /**
         * Returns the paint for the grid lines (if any) plotted against the
         * angular axis.
         *
         * @return The paint (possibly <code>null</code>).
         *
         * @see #setAngleGridlinePaint(Paint)
         */
        public Paint getAngleGridlinePaint() {
            return this.angleGridlinePaint;
        }

        /**
         * Sets the paint for the grid lines plotted against the angular axis.
         * <p>
         * If you set this to <code>null</code>, no grid lines will be drawn.
         *
         * @param paint  the paint (<code>null</code> permitted).
         *
         * @see #getAngleGridlinePaint()
         */
        public void setAngleGridlinePaint(Paint paint) {
            this.angleGridlinePaint = paint;
            fireChangeEvent();
        }

        /**
         * Returns <code>true</code> if the radius axis grid is visible, and
         * <code>false<code> otherwise.
         *
         * @return <code>true</code> or <code>false</code>.
         *
         * @see #setRadiusGridlinesVisible(boolean)
         */
        public boolean isRadiusGridlinesVisible() {
            return this.radiusGridlinesVisible;
        }

        /**
         * Sets the flag that controls whether or not the radius axis grid lines
         * are visible.
         * <p>
         * If the flag value is changed, a {@link PlotChangeEvent} is sent to all
         * registered listeners.
         *
         * @param visible  the new value of the flag.
         *
         * @see #isRadiusGridlinesVisible()
         */
        public void setRadiusGridlinesVisible(boolean visible) {
            if (this.radiusGridlinesVisible != visible) {
                this.radiusGridlinesVisible = visible;
                fireChangeEvent();
            }
        }

        /**
         * Returns the stroke for the grid lines (if any) plotted against the
         * radius axis.
         *
         * @return The stroke (possibly <code>null</code>).
         *
         * @see #setRadiusGridlineStroke(Stroke)
         */
        public Stroke getRadiusGridlineStroke() {
            return this.radiusGridlineStroke;
        }

        /**
         * Sets the stroke for the grid lines plotted against the radius axis and
         * sends a {@link PlotChangeEvent} to all registered listeners.
         * <p>
         * If you set this to <code>null</code>, no grid lines will be drawn.
         *
         * @param stroke  the stroke (<code>null</code> permitted).
         *
         * @see #getRadiusGridlineStroke()
         */
        public void setRadiusGridlineStroke(Stroke stroke) {
            this.radiusGridlineStroke = stroke;
            fireChangeEvent();
        }

        /**
         * Returns the paint for the grid lines (if any) plotted against the radius
         * axis.
         *
         * @return The paint (possibly <code>null</code>).
         *
         * @see #setRadiusGridlinePaint(Paint)
         */
        public Paint getRadiusGridlinePaint() {
            return this.radiusGridlinePaint;
        }

        /**
         * Sets the paint for the grid lines plotted against the radius axis and
         * sends a {@link PlotChangeEvent} to all registered listeners.
         * <p>
         * If you set this to <code>null</code>, no grid lines will be drawn.
         *
         * @param paint  the paint (<code>null</code> permitted).
         *
         * @see #getRadiusGridlinePaint()
         */
        public void setRadiusGridlinePaint(Paint paint) {
            this.radiusGridlinePaint = paint;
            fireChangeEvent();
        }

        /**
         * Generates a list of tick values for the angular tick marks.
         *
         * @return A list of {@link NumberTick} instances.
         *
         * @since 1.0.10
         */
        protected List refreshAngleTicks() {
            List ticks = new ArrayList();
            for (double currentTickVal = 0.0; currentTickVal < 360.0;
                    currentTickVal += this.angleTickUnit.getSize()) {
                NumberTick tick = new NumberTick(new Double(currentTickVal),
                    this.angleTickUnit.valueToString(currentTickVal),
                    TextAnchor.CENTER, TextAnchor.CENTER, 0.0);
                ticks.add(tick);
            }
            return ticks;
        }

        /**
         * Draws the plot on a Java 2D graphics device (such as the screen or a
         * printer).
         * <P>
         * This plot relies on a {@link PolarItemRenderer} to draw each
         * item in the plot.  This allows the visual representation of the data to
         * be changed easily.
         * <P>
         * The optional info argument collects information about the rendering of
         * the plot (dimensions, tooltip information etc).  Just pass in
         * <code>null</code> if you do not need this information.
         *
         * @param g2  the graphics device.
         * @param area  the area within which the plot (including axes and
         *              labels) should be drawn.
         * @param anchor  the anchor point (<code>null</code> permitted).
         * @param parentState  ignored.
         * @param info  collects chart drawing information (<code>null</code>
         *              permitted).
         */
        public void draw(Graphics2D g2,
                         Rectangle2D area,
                         Point2D anchor,
                         PlotState parentState,
                         PlotRenderingInfo info) {

            // if the plot area is too small, just return...
            boolean b1 = (area.getWidth() <= MINIMUM_WIDTH_TO_DRAW);
            boolean b2 = (area.getHeight() <= MINIMUM_HEIGHT_TO_DRAW);
            if (b1 || b2) {
                return;
            }

            // record the plot area...
            if (info != null) {
                info.setPlotArea(area);
            }

            // adjust the drawing area for the plot insets (if any)...
            RectangleInsets insets = getInsets();
            insets.trim(area);

            Rectangle2D dataArea = area;
            if (info != null) {
                info.setDataArea(dataArea);
            }

            // draw the plot background and axes...
            drawBackground(g2, dataArea);
            double h = Math.min(dataArea.getWidth() / 2.0,
                    dataArea.getHeight() / 2.0) - MARGIN;
            Rectangle2D quadrant = new Rectangle2D.Double(dataArea.getCenterX(),
                    dataArea.getCenterY(), h, h);
            AxisState state = drawAxis(g2, area, quadrant);
            if (this.renderer != null) {
                Shape originalClip = g2.getClip();
                Composite originalComposite = g2.getComposite();

                g2.clip(dataArea);
                g2.setComposite(AlphaComposite.getInstance(
                        AlphaComposite.SRC_OVER, getForegroundAlpha()));

                this.angleTicks = refreshAngleTicks();
                drawGridlines(g2, dataArea, this.angleTicks, state.getTicks());

                // draw...
                render(g2, dataArea, info);

                g2.setClip(originalClip);
                g2.setComposite(originalComposite);
            }
            drawOutline(g2, dataArea);
            drawCornerTextItems(g2, dataArea);
        }

        /**
         * Draws the corner text items.
         *
         * @param g2  the drawing surface.
         * @param area  the area.
         */
        protected void drawCornerTextItems(Graphics2D g2, Rectangle2D area) {
            if (this.cornerTextItems.isEmpty()) {
                return;
            }

            g2.setColor(Color.black);
            double width = 0.0;
            double height = 0.0;
            for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
                String msg = (String) it.next();
                FontMetrics fm = g2.getFontMetrics();
                Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2, fm);
                width = Math.max(width, bounds.getWidth());
                height += bounds.getHeight();
            }

            double xadj = ANNOTATION_MARGIN * 2.0;
            double yadj = ANNOTATION_MARGIN;
            width += xadj;
            height += yadj;

            double x = area.getMaxX() - width;
            double y = area.getMaxY() - height;
            g2.drawRect((int) x, (int) y, (int) width, (int) height);
            x += ANNOTATION_MARGIN;
            for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
                String msg = (String) it.next();
                Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2,
                        g2.getFontMetrics());
                y += bounds.getHeight();
                g2.drawString(msg, (int) x, (int) y);
            }
        }

        /**
         * A utility method for drawing the axes.
         *
         * @param g2  the graphics device.
         * @param plotArea  the plot area.
         * @param dataArea  the data area.
         *
         * @return A map containing the axis states.
         */
        protected AxisState drawAxis(Graphics2D g2, Rectangle2D plotArea,
                                     Rectangle2D dataArea) {
            return this.axis.draw(g2, dataArea.getMinY(), plotArea, dataArea,
                    RectangleEdge.TOP, null);
        }

        /**
         * Draws a representation of the data within the dataArea region, using the
         * current m_Renderer.
         *
         * @param g2  the graphics device.
         * @param dataArea  the region in which the data is to be drawn.
         * @param info  an optional object for collection dimension
         *              information (<code>null</code> permitted).
         */
        protected void render(Graphics2D g2,
                           Rectangle2D dataArea,
                           PlotRenderingInfo info) {

            // now get the data and plot it (the visual representation will depend
            // on the m_Renderer that has been set)...
            if (!DatasetUtilities.isEmptyOrNull(this.dataset)) {
                int seriesCount = this.dataset.getSeriesCount();
                for (int series = 0; series < seriesCount; series++) {
                    this.renderer.drawSeries(g2, dataArea, info, this,
                            this.dataset, series);
                }
            }
            else {
                drawNoDataMessage(g2, dataArea);
            }
        }

        /**
         * Draws the gridlines for the plot, if they are visible.
         *
         * @param g2  the graphics device.
         * @param dataArea  the data area.
         * @param angularTicks  the ticks for the angular axis.
         * @param radialTicks  the ticks for the radial axis.
         */
        protected void drawGridlines(Graphics2D g2, Rectangle2D dataArea,
                                     List angularTicks, List radialTicks) {

            // no renderer, no gridlines...
            if (this.renderer == null) {
                return;
            }

            // draw the domain grid lines, if any...
            if (isAngleGridlinesVisible()) {
                Stroke gridStroke = getAngleGridlineStroke();
                Paint gridPaint = getAngleGridlinePaint();
                if ((gridStroke != null) && (gridPaint != null)) {
                    this.renderer.drawAngularGridLines(g2, this, angularTicks,
                            dataArea);
                }
            }

            // draw the radius grid lines, if any...
            if (isRadiusGridlinesVisible()) {
                Stroke gridStroke = getRadiusGridlineStroke();
                Paint gridPaint = getRadiusGridlinePaint();
                if ((gridStroke != null) && (gridPaint != null)) {
                    this.renderer.drawRadialGridLines(g2, this, this.axis,
                            radialTicks, dataArea);
                }
            }
        }

        /**
         * Zooms the axis ranges by the specified percentage about the anchor point.
         *
         * @param percent  the amount of the zoom.
         */
        public void zoom(double percent) {
            if (percent > 0.0) {
                double radius = getMaxRadius();
                double scaledRadius = radius * percent;
                this.axis.setUpperBound(scaledRadius);
                getAxis().setAutoRange(false);
            }
            else {
                getAxis().setAutoRange(true);
            }
        }

        /**
         * Returns the range for the specified axis.
         *
         * @param axis  the axis.
         *
         * @return The range.
         */
        public Range getDataRange(ValueAxis axis) {
            Range result = null;
            if (this.dataset != null) {
                result = Range.combine(result,
                        DatasetUtilities.findRangeBounds(this.dataset));
            }
            return result;
        }

        /**
         * Receives notification of a change to the plot's m_Dataset.
         * <P>
         * The axis ranges are updated if necessary.
         *
         * @param event  information about the event (not used here).
         */
        public void datasetChanged(DatasetChangeEvent event) {

            if (this.axis != null) {
                this.axis.configure();
            }

            if (getParent() != null) {
                getParent().datasetChanged(event);
            }
            else {
                super.datasetChanged(event);
            }
        }

        /**
         * Notifies all registered listeners of a property change.
         * <P>
         * One source of property change events is the plot's m_Renderer.
         *
         * @param event  information about the property change.
         */
        public void rendererChanged(RendererChangeEvent event) {
            fireChangeEvent();
        }

        /**
         * Returns the number of series in the dataset for this plot.  If the
         * dataset is <code>null</code>, the method returns 0.
         *
         * @return The series count.
         */
        public int getSeriesCount() {
            int result = 0;

            if (this.dataset != null) {
                result = this.dataset.getSeriesCount();
            }
            return result;
        }

        /**
         * Returns the legend items for the plot.  Each legend item is generated by
         * the plot's m_Renderer, since the m_Renderer is responsible for the visual
         * representation of the data.
         *
         * @return The legend items.
         */
        public LegendItemCollection getLegendItems() {
            LegendItemCollection result = new LegendItemCollection();

            // get the legend items for the main m_Dataset...
            if (this.dataset != null) {
                if (this.renderer != null) {
                    int seriesCount = this.dataset.getSeriesCount();
                    for (int i = 0; i < seriesCount; i++) {
                        LegendItem item = this.renderer.getLegendItem(i);
                        result.add(item);
                    }
                }
            }
            return result;
        }

        /**
         * Tests this plot for equality with another object.
         *
         * @param obj  the object (<code>null</code> permitted).
         *
         * @return <code>true</code> or <code>false</code>.
         */
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            }
            if (!(obj instanceof PolarPlot)) {
                return false;
            }
            PolarPlot that = (PolarPlot) obj;
            if (!ObjectUtilities.equal(this.axis, that.axis)) {
                return false;
            }
            if (!ObjectUtilities.equal(this.renderer, that.renderer)) {
                return false;
            }
            if (!this.angleTickUnit.equals(that.angleTickUnit)) {
                return false;
            }
            if (this.angleGridlinesVisible != that.angleGridlinesVisible) {
                return false;
            }
            if (this.angleLabelsVisible != that.angleLabelsVisible) {
                return false;
            }
            if (!this.angleLabelFont.equals(that.angleLabelFont)) {
                return false;
            }
            if (!PaintUtilities.equal(this.angleLabelPaint, that.angleLabelPaint)) {
                return false;
            }
            if (!ObjectUtilities.equal(this.angleGridlineStroke,
                    that.angleGridlineStroke)) {
                return false;
            }
            if (!PaintUtilities.equal(
                this.angleGridlinePaint, that.angleGridlinePaint
            )) {
                return false;
            }
            if (this.radiusGridlinesVisible != that.radiusGridlinesVisible) {
                return false;
            }
            if (!ObjectUtilities.equal(this.radiusGridlineStroke,
                    that.radiusGridlineStroke)) {
                return false;
            }
            if (!PaintUtilities.equal(this.radiusGridlinePaint,
                    that.radiusGridlinePaint)) {
                return false;
            }
            if (!this.cornerTextItems.equals(that.cornerTextItems)) {
                return false;
            }
            return super.equals(obj);
        }

        /**
         * Returns a clone of the plot.
         *
         * @return A clone.
         *
         * @throws CloneNotSupportedException  this can occur if some component of
         *         the plot cannot be cloned.
         */
        public Object clone() throws CloneNotSupportedException {

            PolarPlot clone = (PolarPlot) super.clone();
            if (this.axis != null) {
                clone.axis = (ValueAxis) ObjectUtilities.clone(this.axis);
                clone.axis.setPlot(clone);
                clone.axis.addChangeListener(clone);
            }

            if (clone.dataset != null) {
                clone.dataset.addChangeListener(clone);
            }

            if (this.renderer != null) {
                clone.renderer
                    = (PolarItemRenderer) ObjectUtilities.clone(this.renderer);
            }

            clone.cornerTextItems = new ArrayList(this.cornerTextItems);

            return clone;
        }

        /**
         * Provides serialization support.
         *
         * @param stream  the output stream.
         *
         * @throws IOException  if there is an I/O error.
         */
        private void writeObject(ObjectOutputStream stream) throws IOException {
            stream.defaultWriteObject();
            SerialUtilities.writeStroke(this.angleGridlineStroke, stream);
            SerialUtilities.writePaint(this.angleGridlinePaint, stream);
            SerialUtilities.writeStroke(this.radiusGridlineStroke, stream);
            SerialUtilities.writePaint(this.radiusGridlinePaint, stream);
            SerialUtilities.writePaint(this.angleLabelPaint, stream);
        }

        /**
         * Provides serialization support.
         *
         * @param stream  the input stream.
         *
         * @throws IOException  if there is an I/O error.
         * @throws ClassNotFoundException  if there is a classpath problem.
         */
        private void readObject(ObjectInputStream stream)
            throws IOException, ClassNotFoundException {

            stream.defaultReadObject();
            this.angleGridlineStroke = SerialUtilities.readStroke(stream);
            this.angleGridlinePaint = SerialUtilities.readPaint(stream);
            this.radiusGridlineStroke = SerialUtilities.readStroke(stream);
            this.radiusGridlinePaint = SerialUtilities.readPaint(stream);
            this.angleLabelPaint = SerialUtilities.readPaint(stream);

            if (this.axis != null) {
                this.axis.setPlot(this);
                this.axis.addChangeListener(this);
            }

            if (this.dataset != null) {
                this.dataset.addChangeListener(this);
            }
        }

        /**
         * This method is required by the {@link Zoomable} interface, but since
         * the plot does not have any domain axes, it does nothing.
         *
         * @param factor  the zoom factor.
         * @param state  the plot state.
         * @param source  the source point (in Java2D coordinates).
         */
        public void zoomDomainAxes(double factor, PlotRenderingInfo state,
                                   Point2D source) {
            // do nothing
        }

        /**
         * This method is required by the {@link Zoomable} interface, but since
         * the plot does not have any domain axes, it does nothing.
         *
         * @param factor  the zoom factor.
         * @param state  the plot state.
         * @param source  the source point (in Java2D coordinates).
         * @param useAnchor  use source point as zoom anchor?
         *
         * @since 1.0.7
         */
        public void zoomDomainAxes(double factor, PlotRenderingInfo state,
                                   Point2D source, boolean useAnchor) {
            // do nothing
        }

        /**
         * This method is required by the {@link Zoomable} interface, but since
         * the plot does not have any domain axes, it does nothing.
         *
         * @param lowerPercent  the new lower bound.
         * @param upperPercent  the new upper bound.
         * @param state  the plot state.
         * @param source  the source point (in Java2D coordinates).
         */
        public void zoomDomainAxes(double lowerPercent, double upperPercent,
                                   PlotRenderingInfo state, Point2D source) {
            // do nothing
        }

        /**
         * Multiplies the range on the range axis/axes by the specified factor.
         *
         * @param factor  the zoom factor.
         * @param state  the plot state.
         * @param source  the source point (in Java2D coordinates).
         */
        public void zoomRangeAxes(double factor, PlotRenderingInfo state,
                                  Point2D source) {
            zoom(factor);
        }

        /**
         * Multiplies the range on the range axis by the specified factor.
         *
         * @param factor  the zoom factor.
         * @param info  the plot rendering info.
         * @param source  the source point (in Java2D space).
         * @param useAnchor  use source point as zoom anchor?
         *
         * @see #zoomDomainAxes(double, PlotRenderingInfo, Point2D, boolean)
         *
         * @since 1.0.7
         */
        public void zoomRangeAxes(double factor, PlotRenderingInfo info,
                                  Point2D source, boolean useAnchor) {

            if (useAnchor) {
                // get the source coordinate - this plot has always a VERTICAL
                // orientation
                double sourceX = source.getX();
                double anchorX = this.axis.java2DToValue(sourceX,
                        info.getDataArea(), RectangleEdge.BOTTOM);
                this.axis.resizeRange(factor, anchorX);
            }
            else {
                this.axis.resizeRange(factor);
            }

        }

        /**
         * Zooms in on the range axes.
         *
         * @param lowerPercent  the new lower bound.
         * @param upperPercent  the new upper bound.
         * @param state  the plot state.
         * @param source  the source point (in Java2D coordinates).
         */
        public void zoomRangeAxes(double lowerPercent, double upperPercent,
                                  PlotRenderingInfo state, Point2D source) {
            zoom((upperPercent + lowerPercent) / 2.0);
        }

        /**
         * Returns <code>false</code> always.
         *
         * @return <code>false</code> always.
         */
        public boolean isDomainZoomable() {
            return false;
        }

        /**
         * Returns <code>true</code> to indicate that the range axis is zoomable.
         *
         * @return <code>true</code>.
         */
        public boolean isRangeZoomable() {
            return true;
        }

        /**
         * Returns the orientation of the plot.
         *
         * @return The orientation.
         */
        public PlotOrientation getOrientation() {
            return PlotOrientation.HORIZONTAL;
        }

        /**
         * Returns the upper bound of the radius axis.
         *
         * @return The upper bound.
         */
        public double getMaxRadius() {
            return this.axis.getUpperBound();
        }

        /**
         * Translates a (theta, radius) pair into Java2D coordinates.  If
         * <code>radius</code> is less than the lower bound of the axis, then
         * this method returns the centre point.
         *
         * @param angleDegrees  the angle in degrees.
         * @param radius  the radius.
         * @param dataArea  the data area.
         *
         * @return A point in Java2D space.
         */
        public Point translateValueThetaRadiusToJava2D(double angleDegrees,
                                                       double radius,
                                                       Rectangle2D dataArea) {

            double radians = Math.toRadians(angleDegrees - 90.0);

            double minx = dataArea.getMinX() + MARGIN;
            double maxx = dataArea.getMaxX() - MARGIN;
            double miny = dataArea.getMinY() + MARGIN;
            double maxy = dataArea.getMaxY() - MARGIN;

            double lengthX = maxx - minx;
            double lengthY = maxy - miny;
            double length = Math.min(lengthX, lengthY);

            double midX = minx + lengthX / 2.0;
            double midY = miny + lengthY / 2.0;

            double axisMin = this.axis.getLowerBound();
            double axisMax =  getMaxRadius();
            double adjustedRadius = Math.max(radius, axisMin);

            double xv = length / 2.0 * Math.cos(radians);
            double yv = length / 2.0 * Math.sin(radians);

            float x = (float) (midX + (xv * (adjustedRadius - axisMin)
                    / (axisMax - axisMin)));
            float y = (float) (midY + (yv * (adjustedRadius - axisMin)
                    / (axisMax - axisMin)));

            int ix = Math.round(x);
            int iy = Math.round(y);

            Point p = new Point(ix, iy);
            return p;

        }

    }