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
054package org.jfree.chart.plot;
055
056import java.awt.AlphaComposite;
057import java.awt.BasicStroke;
058import java.awt.Color;
059import java.awt.Composite;
060import java.awt.Font;
061import java.awt.FontMetrics;
062import java.awt.Graphics2D;
063import java.awt.Paint;
064import java.awt.Point;
065import java.awt.Shape;
066import java.awt.Stroke;
067import java.awt.geom.Point2D;
068import java.awt.geom.Rectangle2D;
069import java.io.IOException;
070import java.io.ObjectInputStream;
071import java.io.ObjectOutputStream;
072import java.io.Serializable;
073import java.util.ArrayList;
074import java.util.Iterator;
075import java.util.List;
076import java.util.ResourceBundle;
077
078import org.jfree.chart.LegendItem;
079import org.jfree.chart.LegendItemCollection;
080import org.jfree.chart.axis.AxisState;
081import org.jfree.chart.axis.NumberTick;
082import org.jfree.chart.axis.NumberTickUnit;
083import org.jfree.chart.axis.TickUnit;
084import org.jfree.chart.axis.ValueAxis;
085import org.jfree.chart.event.PlotChangeEvent;
086import org.jfree.chart.event.RendererChangeEvent;
087import org.jfree.chart.event.RendererChangeListener;
088import org.jfree.chart.renderer.PolarItemRenderer;
089import org.jfree.chart.util.ResourceBundleWrapper;
090import org.jfree.data.Range;
091import org.jfree.data.general.DatasetChangeEvent;
092import org.jfree.data.general.DatasetUtilities;
093import org.jfree.data.xy.XYDataset;
094import org.jfree.io.SerialUtilities;
095import org.jfree.text.TextUtilities;
096import org.jfree.ui.RectangleEdge;
097import org.jfree.ui.RectangleInsets;
098import org.jfree.ui.TextAnchor;
099import org.jfree.util.ObjectUtilities;
100import 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 */
106public 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}