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