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