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