001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2011, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ----------------
028 * CompassPlot.java
029 * ----------------
030 * (C) Copyright 2002-2008, by the Australian Antarctic Division and
031 * Contributors.
032 *
033 * Original Author:  Bryan Scott (for the Australian Antarctic Division);
034 * Contributor(s):   David Gilbert (for Object Refinery Limited);
035 *                   Arnaud Lelievre;
036 *                   Martin Hoeller;
037 *
038 * Changes:
039 * --------
040 * 25-Sep-2002 : Version 1, contributed by Bryan Scott (DG);
041 * 23-Jan-2003 : Removed one constructor (DG);
042 * 26-Mar-2003 : Implemented Serializable (DG);
043 * 27-Mar-2003 : Changed MeterDataset to ValueDataset (DG);
044 * 21-Aug-2003 : Implemented Cloneable (DG);
045 * 08-Sep-2003 : Added internationalization via use of properties
046 *               resourceBundle (RFE 690236) (AL);
047 * 09-Sep-2003 : Changed Color --> Paint (DG);
048 * 15-Sep-2003 : Added null data value check (bug report 805009) (DG);
049 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
050 * 16-Mar-2004 : Added support for revolutionDistance to enable support for
051 *               other units than degrees.
052 * 16-Mar-2004 : Enabled LongNeedle to rotate about center.
053 * 11-Jan-2005 : Removed deprecated code in preparation for 1.0.0 release (DG);
054 * 17-Apr-2005 : Fixed bug in clone() method (DG);
055 * 05-May-2005 : Updated draw() method parameters (DG);
056 * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG);
057 * 16-Jun-2005 : Renamed getData() --> getDatasets() and
058 *               addData() --> addDataset() (DG);
059 * ------------- JFREECHART 1.0.x ---------------------------------------------
060 * 20-Mar-2007 : Fixed serialization (DG);
061 * 18-Dec-2008 : Use ResourceBundleWrapper - see patch 1607918 by
062 *               Jess Thrysoee (DG);
063 * 10-Oct-2011 : localization fix: bug #3353913 (MH);
064 *
065 */
066
067package org.jfree.chart.plot;
068
069import java.awt.BasicStroke;
070import java.awt.Color;
071import java.awt.Font;
072import java.awt.Graphics2D;
073import java.awt.Paint;
074import java.awt.Polygon;
075import java.awt.Stroke;
076import java.awt.geom.Area;
077import java.awt.geom.Ellipse2D;
078import java.awt.geom.Point2D;
079import java.awt.geom.Rectangle2D;
080import java.io.IOException;
081import java.io.ObjectInputStream;
082import java.io.ObjectOutputStream;
083import java.io.Serializable;
084import java.util.Arrays;
085import java.util.ResourceBundle;
086
087import org.jfree.chart.LegendItemCollection;
088import org.jfree.chart.event.PlotChangeEvent;
089import org.jfree.chart.needle.ArrowNeedle;
090import org.jfree.chart.needle.LineNeedle;
091import org.jfree.chart.needle.LongNeedle;
092import org.jfree.chart.needle.MeterNeedle;
093import org.jfree.chart.needle.MiddlePinNeedle;
094import org.jfree.chart.needle.PinNeedle;
095import org.jfree.chart.needle.PlumNeedle;
096import org.jfree.chart.needle.PointerNeedle;
097import org.jfree.chart.needle.ShipNeedle;
098import org.jfree.chart.needle.WindNeedle;
099import org.jfree.chart.util.ResourceBundleWrapper;
100import org.jfree.data.general.DefaultValueDataset;
101import org.jfree.data.general.ValueDataset;
102import org.jfree.io.SerialUtilities;
103import org.jfree.ui.RectangleInsets;
104import org.jfree.util.ObjectUtilities;
105import org.jfree.util.PaintUtilities;
106
107/**
108 * A specialised plot that draws a compass to indicate a direction based on the
109 * value from a {@link ValueDataset}.
110 */
111public class CompassPlot extends Plot implements Cloneable, Serializable {
112
113    /** For serialization. */
114    private static final long serialVersionUID = 6924382802125527395L;
115
116    /** The default label font. */
117    public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif",
118            Font.BOLD, 10);
119
120    /** A constant for the label type. */
121    public static final int NO_LABELS = 0;
122
123    /** A constant for the label type. */
124    public static final int VALUE_LABELS = 1;
125
126    /** The label type (NO_LABELS, VALUE_LABELS). */
127    private int labelType;
128
129    /** The label font. */
130    private Font labelFont;
131
132    /** A flag that controls whether or not a border is drawn. */
133    private boolean drawBorder = false;
134
135    /** The rose highlight paint. */
136    private transient Paint roseHighlightPaint = Color.black;
137
138    /** The rose paint. */
139    private transient Paint rosePaint = Color.yellow;
140
141    /** The rose center paint. */
142    private transient Paint roseCenterPaint = Color.white;
143
144    /** The compass font. */
145    private Font compassFont = new Font("Arial", Font.PLAIN, 10);
146
147    /** A working shape. */
148    private transient Ellipse2D circle1;
149
150    /** A working shape. */
151    private transient Ellipse2D circle2;
152
153    /** A working area. */
154    private transient Area a1;
155
156    /** A working area. */
157    private transient Area a2;
158
159    /** A working shape. */
160    private transient Rectangle2D rect1;
161
162    /** An array of value datasets. */
163    private ValueDataset[] datasets = new ValueDataset[1];
164
165    /** An array of needles. */
166    private MeterNeedle[] seriesNeedle = new MeterNeedle[1];
167
168    /** The resourceBundle for the localization. */
169    protected static ResourceBundle localizationResources
170            = ResourceBundleWrapper.getBundle(
171                    "org.jfree.chart.plot.LocalizationBundle");
172
173    /**
174     * The count to complete one revolution.  Can be arbitrarily set
175     * For degrees (the default) it is 360, for radians this is 2*Pi, etc
176     */
177    protected double revolutionDistance = 360;
178
179    /**
180     * Default constructor.
181     */
182    public CompassPlot() {
183        this(new DefaultValueDataset());
184    }
185
186    /**
187     * Constructs a new compass plot.
188     *
189     * @param dataset  the dataset for the plot (<code>null</code> permitted).
190     */
191    public CompassPlot(ValueDataset dataset) {
192        super();
193        if (dataset != null) {
194            this.datasets[0] = dataset;
195            dataset.addChangeListener(this);
196        }
197        this.circle1 = new Ellipse2D.Double();
198        this.circle2 = new Ellipse2D.Double();
199        this.rect1   = new Rectangle2D.Double();
200        setSeriesNeedle(0);
201    }
202
203    /**
204     * Returns the label type.  Defined by the constants: {@link #NO_LABELS}
205     * and {@link #VALUE_LABELS}.
206     *
207     * @return The label type.
208     *
209     * @see #setLabelType(int)
210     */
211    public int getLabelType() {
212        // FIXME: this attribute is never used - deprecate?
213        return this.labelType;
214    }
215
216    /**
217     * Sets the label type (either {@link #NO_LABELS} or {@link #VALUE_LABELS}.
218     *
219     * @param type  the type.
220     *
221     * @see #getLabelType()
222     */
223    public void setLabelType(int type) {
224        // FIXME: this attribute is never used - deprecate?
225        if ((type != NO_LABELS) && (type != VALUE_LABELS)) {
226            throw new IllegalArgumentException(
227                    "MeterPlot.setLabelType(int): unrecognised type.");
228        }
229        if (this.labelType != type) {
230            this.labelType = type;
231            fireChangeEvent();
232        }
233    }
234
235    /**
236     * Returns the label font.
237     *
238     * @return The label font.
239     *
240     * @see #setLabelFont(Font)
241     */
242    public Font getLabelFont() {
243        // FIXME: this attribute is not used - deprecate?
244        return this.labelFont;
245    }
246
247    /**
248     * Sets the label font and sends a {@link PlotChangeEvent} to all
249     * registered listeners.
250     *
251     * @param font  the new label font.
252     *
253     * @see #getLabelFont()
254     */
255    public void setLabelFont(Font font) {
256        // FIXME: this attribute is not used - deprecate?
257        if (font == null) {
258            throw new IllegalArgumentException("Null 'font' not allowed.");
259        }
260        this.labelFont = font;
261        fireChangeEvent();
262    }
263
264    /**
265     * Returns the paint used to fill the outer circle of the compass.
266     *
267     * @return The paint (never <code>null</code>).
268     *
269     * @see #setRosePaint(Paint)
270     */
271    public Paint getRosePaint() {
272        return this.rosePaint;
273    }
274
275    /**
276     * Sets the paint used to fill the outer circle of the compass,
277     * and sends a {@link PlotChangeEvent} to all registered listeners.
278     *
279     * @param paint  the paint (<code>null</code> not permitted).
280     *
281     * @see #getRosePaint()
282     */
283    public void setRosePaint(Paint paint) {
284        if (paint == null) {
285            throw new IllegalArgumentException("Null 'paint' argument.");
286        }
287        this.rosePaint = paint;
288        fireChangeEvent();
289    }
290
291    /**
292     * Returns the paint used to fill the inner background area of the
293     * compass.
294     *
295     * @return The paint (never <code>null</code>).
296     *
297     * @see #setRoseCenterPaint(Paint)
298     */
299    public Paint getRoseCenterPaint() {
300        return this.roseCenterPaint;
301    }
302
303    /**
304     * Sets the paint used to fill the inner background area of the compass,
305     * and sends a {@link PlotChangeEvent} to all registered listeners.
306     *
307     * @param paint  the paint (<code>null</code> not permitted).
308     *
309     * @see #getRoseCenterPaint()
310     */
311    public void setRoseCenterPaint(Paint paint) {
312        if (paint == null) {
313            throw new IllegalArgumentException("Null 'paint' argument.");
314        }
315        this.roseCenterPaint = paint;
316        fireChangeEvent();
317    }
318
319    /**
320     * Returns the paint used to draw the circles, symbols and labels on the
321     * compass.
322     *
323     * @return The paint (never <code>null</code>).
324     *
325     * @see #setRoseHighlightPaint(Paint)
326     */
327    public Paint getRoseHighlightPaint() {
328        return this.roseHighlightPaint;
329    }
330
331    /**
332     * Sets the paint used to draw the circles, symbols and labels of the
333     * compass, and sends a {@link PlotChangeEvent} to all registered listeners.
334     *
335     * @param paint  the paint (<code>null</code> not permitted).
336     *
337     * @see #getRoseHighlightPaint()
338     */
339    public void setRoseHighlightPaint(Paint paint) {
340        if (paint == null) {
341            throw new IllegalArgumentException("Null 'paint' argument.");
342        }
343        this.roseHighlightPaint = paint;
344        fireChangeEvent();
345    }
346
347    /**
348     * Returns a flag that controls whether or not a border is drawn.
349     *
350     * @return The flag.
351     *
352     * @see #setDrawBorder(boolean)
353     */
354    public boolean getDrawBorder() {
355        return this.drawBorder;
356    }
357
358    /**
359     * Sets a flag that controls whether or not a border is drawn.
360     *
361     * @param status  the flag status.
362     *
363     * @see #getDrawBorder()
364     */
365    public void setDrawBorder(boolean status) {
366        this.drawBorder = status;
367        fireChangeEvent();
368    }
369
370    /**
371     * Sets the series paint.
372     *
373     * @param series  the series index.
374     * @param paint  the paint.
375     *
376     * @see #setSeriesOutlinePaint(int, Paint)
377     */
378    public void setSeriesPaint(int series, Paint paint) {
379       // super.setSeriesPaint(series, paint);
380        if ((series >= 0) && (series < this.seriesNeedle.length)) {
381            this.seriesNeedle[series].setFillPaint(paint);
382        }
383    }
384
385    /**
386     * Sets the series outline paint.
387     *
388     * @param series  the series index.
389     * @param p  the paint.
390     *
391     * @see #setSeriesPaint(int, Paint)
392     */
393    public void setSeriesOutlinePaint(int series, Paint p) {
394
395        if ((series >= 0) && (series < this.seriesNeedle.length)) {
396            this.seriesNeedle[series].setOutlinePaint(p);
397        }
398
399    }
400
401    /**
402     * Sets the series outline stroke.
403     *
404     * @param series  the series index.
405     * @param stroke  the stroke.
406     *
407     * @see #setSeriesOutlinePaint(int, Paint)
408     */
409    public void setSeriesOutlineStroke(int series, Stroke stroke) {
410
411        if ((series >= 0) && (series < this.seriesNeedle.length)) {
412            this.seriesNeedle[series].setOutlineStroke(stroke);
413        }
414
415    }
416
417    /**
418     * Sets the needle type.
419     *
420     * @param type  the type.
421     *
422     * @see #setSeriesNeedle(int, int)
423     */
424    public void setSeriesNeedle(int type) {
425        setSeriesNeedle(0, type);
426    }
427
428    /**
429     * Sets the needle for a series.  The needle type is one of the following:
430     * <ul>
431     * <li>0 = {@link ArrowNeedle};</li>
432     * <li>1 = {@link LineNeedle};</li>
433     * <li>2 = {@link LongNeedle};</li>
434     * <li>3 = {@link PinNeedle};</li>
435     * <li>4 = {@link PlumNeedle};</li>
436     * <li>5 = {@link PointerNeedle};</li>
437     * <li>6 = {@link ShipNeedle};</li>
438     * <li>7 = {@link WindNeedle};</li>
439     * <li>8 = {@link ArrowNeedle};</li>
440     * <li>9 = {@link MiddlePinNeedle};</li>
441     * </ul>
442     * @param index  the series index.
443     * @param type  the needle type.
444     *
445     * @see #setSeriesNeedle(int)
446     */
447    public void setSeriesNeedle(int index, int type) {
448        switch (type) {
449            case 0:
450                setSeriesNeedle(index, new ArrowNeedle(true));
451                setSeriesPaint(index, Color.red);
452                this.seriesNeedle[index].setHighlightPaint(Color.white);
453                break;
454            case 1:
455                setSeriesNeedle(index, new LineNeedle());
456                break;
457            case 2:
458                MeterNeedle longNeedle = new LongNeedle();
459                longNeedle.setRotateY(0.5);
460                setSeriesNeedle(index, longNeedle);
461                break;
462            case 3:
463                setSeriesNeedle(index, new PinNeedle());
464                break;
465            case 4:
466                setSeriesNeedle(index, new PlumNeedle());
467                break;
468            case 5:
469                setSeriesNeedle(index, new PointerNeedle());
470                break;
471            case 6:
472                setSeriesPaint(index, null);
473                setSeriesOutlineStroke(index, new BasicStroke(3));
474                setSeriesNeedle(index, new ShipNeedle());
475                break;
476            case 7:
477                setSeriesPaint(index, Color.blue);
478                setSeriesNeedle(index, new WindNeedle());
479                break;
480            case 8:
481                setSeriesNeedle(index, new ArrowNeedle(true));
482                break;
483            case 9:
484                setSeriesNeedle(index, new MiddlePinNeedle());
485                break;
486
487            default:
488                throw new IllegalArgumentException("Unrecognised type.");
489        }
490
491    }
492
493    /**
494     * Sets the needle for a series and sends a {@link PlotChangeEvent} to all
495     * registered listeners.
496     *
497     * @param index  the series index.
498     * @param needle  the needle.
499     */
500    public void setSeriesNeedle(int index, MeterNeedle needle) {
501        if ((needle != null) && (index < this.seriesNeedle.length)) {
502            this.seriesNeedle[index] = needle;
503        }
504        fireChangeEvent();
505    }
506
507    /**
508     * Returns an array of dataset references for the plot.
509     *
510     * @return The dataset for the plot, cast as a ValueDataset.
511     *
512     * @see #addDataset(ValueDataset)
513     */
514    public ValueDataset[] getDatasets() {
515        return this.datasets;
516    }
517
518    /**
519     * Adds a dataset to the compass.
520     *
521     * @param dataset  the new dataset (<code>null</code> ignored).
522     *
523     * @see #addDataset(ValueDataset, MeterNeedle)
524     */
525    public void addDataset(ValueDataset dataset) {
526        addDataset(dataset, null);
527    }
528
529    /**
530     * Adds a dataset to the compass.
531     *
532     * @param dataset  the new dataset (<code>null</code> ignored).
533     * @param needle  the needle (<code>null</code> permitted).
534     */
535    public void addDataset(ValueDataset dataset, MeterNeedle needle) {
536
537        if (dataset != null) {
538            int i = this.datasets.length + 1;
539            ValueDataset[] t = new ValueDataset[i];
540            MeterNeedle[] p = new MeterNeedle[i];
541            i = i - 2;
542            for (; i >= 0; --i) {
543                t[i] = this.datasets[i];
544                p[i] = this.seriesNeedle[i];
545            }
546            i = this.datasets.length;
547            t[i] = dataset;
548            p[i] = ((needle != null) ? needle : p[i - 1]);
549
550            ValueDataset[] a = this.datasets;
551            MeterNeedle[] b = this.seriesNeedle;
552            this.datasets = t;
553            this.seriesNeedle = p;
554
555            for (--i; i >= 0; --i) {
556                a[i] = null;
557                b[i] = null;
558            }
559            dataset.addChangeListener(this);
560        }
561    }
562
563    /**
564     * Draws the plot on a Java 2D graphics device (such as the screen or a
565     * printer).
566     *
567     * @param g2  the graphics device.
568     * @param area  the area within which the plot should be drawn.
569     * @param anchor  the anchor point (<code>null</code> permitted).
570     * @param parentState  the state from the parent plot, if there is one.
571     * @param info  collects info about the drawing.
572     */
573    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
574                     PlotState parentState,
575                     PlotRenderingInfo info) {
576
577        int outerRadius = 0;
578        int innerRadius = 0;
579        int x1, y1, x2, y2;
580        double a;
581
582        if (info != null) {
583            info.setPlotArea(area);
584        }
585
586        // adjust for insets...
587        RectangleInsets insets = getInsets();
588        insets.trim(area);
589
590        // draw the background
591        if (this.drawBorder) {
592            drawBackground(g2, area);
593        }
594
595        int midX = (int) (area.getWidth() / 2);
596        int midY = (int) (area.getHeight() / 2);
597        int radius = midX;
598        if (midY < midX) {
599            radius = midY;
600        }
601        --radius;
602        int diameter = 2 * radius;
603
604        midX += (int) area.getMinX();
605        midY += (int) area.getMinY();
606
607        this.circle1.setFrame(midX - radius, midY - radius, diameter, diameter);
608        this.circle2.setFrame(
609            midX - radius + 15, midY - radius + 15,
610            diameter - 30, diameter - 30
611        );
612        g2.setPaint(this.rosePaint);
613        this.a1 = new Area(this.circle1);
614        this.a2 = new Area(this.circle2);
615        this.a1.subtract(this.a2);
616        g2.fill(this.a1);
617
618        g2.setPaint(this.roseCenterPaint);
619        x1 = diameter - 30;
620        g2.fillOval(midX - radius + 15, midY - radius + 15, x1, x1);
621        g2.setPaint(this.roseHighlightPaint);
622        g2.drawOval(midX - radius, midY - radius, diameter, diameter);
623        x1 = diameter - 20;
624        g2.drawOval(midX - radius + 10, midY - radius + 10, x1, x1);
625        x1 = diameter - 30;
626        g2.drawOval(midX - radius + 15, midY - radius + 15, x1, x1);
627        x1 = diameter - 80;
628        g2.drawOval(midX - radius + 40, midY - radius + 40, x1, x1);
629
630        outerRadius = radius - 20;
631        innerRadius = radius - 32;
632        for (int w = 0; w < 360; w += 15) {
633            a = Math.toRadians(w);
634            x1 = midX - ((int) (Math.sin(a) * innerRadius));
635            x2 = midX - ((int) (Math.sin(a) * outerRadius));
636            y1 = midY - ((int) (Math.cos(a) * innerRadius));
637            y2 = midY - ((int) (Math.cos(a) * outerRadius));
638            g2.drawLine(x1, y1, x2, y2);
639        }
640
641        g2.setPaint(this.roseHighlightPaint);
642        innerRadius = radius - 26;
643        outerRadius = 7;
644        for (int w = 45; w < 360; w += 90) {
645            a = Math.toRadians(w);
646            x1 = midX - ((int) (Math.sin(a) * innerRadius));
647            y1 = midY - ((int) (Math.cos(a) * innerRadius));
648            g2.fillOval(x1 - outerRadius, y1 - outerRadius, 2 * outerRadius,
649                    2 * outerRadius);
650        }
651
652        /// Squares
653        for (int w = 0; w < 360; w += 90) {
654            a = Math.toRadians(w);
655            x1 = midX - ((int) (Math.sin(a) * innerRadius));
656            y1 = midY - ((int) (Math.cos(a) * innerRadius));
657
658            Polygon p = new Polygon();
659            p.addPoint(x1 - outerRadius, y1);
660            p.addPoint(x1, y1 + outerRadius);
661            p.addPoint(x1 + outerRadius, y1);
662            p.addPoint(x1, y1 - outerRadius);
663            g2.fillPolygon(p);
664        }
665
666        /// Draw N, S, E, W
667        innerRadius = radius - 42;
668        Font f = getCompassFont(radius);
669        g2.setFont(f);
670        g2.drawString(localizationResources.getString("N"), midX - 5, midY - innerRadius + f.getSize());
671        g2.drawString(localizationResources.getString("S"), midX - 5, midY + innerRadius - 5);
672        g2.drawString(localizationResources.getString("W"), midX - innerRadius + 5, midY + 5);
673        g2.drawString(localizationResources.getString("E"), midX + innerRadius - f.getSize(), midY + 5);
674
675        // plot the data (unless the dataset is null)...
676        y1 = radius / 2;
677        x1 = radius / 6;
678        Rectangle2D needleArea = new Rectangle2D.Double(
679            (midX - x1), (midY - y1), (2 * x1), (2 * y1)
680        );
681        int x = this.seriesNeedle.length;
682        int current = 0;
683        double value = 0;
684        int i = (this.datasets.length - 1);
685        for (; i >= 0; --i) {
686            ValueDataset data = this.datasets[i];
687
688            if (data != null && data.getValue() != null) {
689                value = (data.getValue().doubleValue())
690                    % this.revolutionDistance;
691                value = value / this.revolutionDistance * 360;
692                current = i % x;
693                this.seriesNeedle[current].draw(g2, needleArea, value);
694            }
695        }
696
697        if (this.drawBorder) {
698            drawOutline(g2, area);
699        }
700
701    }
702
703    /**
704     * Returns a short string describing the type of plot.
705     *
706     * @return A string describing the plot.
707     */
708    public String getPlotType() {
709        return localizationResources.getString("Compass_Plot");
710    }
711
712    /**
713     * Returns the legend items for the plot.  For now, no legend is available
714     * - this method returns null.
715     *
716     * @return The legend items.
717     */
718    public LegendItemCollection getLegendItems() {
719        return null;
720    }
721
722    /**
723     * No zooming is implemented for compass plot, so this method is empty.
724     *
725     * @param percent  the zoom amount.
726     */
727    public void zoom(double percent) {
728        // no zooming possible
729    }
730
731    /**
732     * Returns the font for the compass, adjusted for the size of the plot.
733     *
734     * @param radius the radius.
735     *
736     * @return The font.
737     */
738    protected Font getCompassFont(int radius) {
739        float fontSize = radius / 10.0f;
740        if (fontSize < 8) {
741            fontSize = 8;
742        }
743        Font newFont = this.compassFont.deriveFont(fontSize);
744        return newFont;
745    }
746
747    /**
748     * Tests an object for equality with this plot.
749     *
750     * @param obj  the object (<code>null</code> permitted).
751     *
752     * @return A boolean.
753     */
754    public boolean equals(Object obj) {
755        if (obj == this) {
756            return true;
757        }
758        if (!(obj instanceof CompassPlot)) {
759            return false;
760        }
761        if (!super.equals(obj)) {
762            return false;
763        }
764        CompassPlot that = (CompassPlot) obj;
765        if (this.labelType != that.labelType) {
766            return false;
767        }
768        if (!ObjectUtilities.equal(this.labelFont, that.labelFont)) {
769            return false;
770        }
771        if (this.drawBorder != that.drawBorder) {
772            return false;
773        }
774        if (!PaintUtilities.equal(this.roseHighlightPaint,
775                that.roseHighlightPaint)) {
776            return false;
777        }
778        if (!PaintUtilities.equal(this.rosePaint, that.rosePaint)) {
779            return false;
780        }
781        if (!PaintUtilities.equal(this.roseCenterPaint,
782                that.roseCenterPaint)) {
783            return false;
784        }
785        if (!ObjectUtilities.equal(this.compassFont, that.compassFont)) {
786            return false;
787        }
788        if (!Arrays.equals(this.seriesNeedle, that.seriesNeedle)) {
789            return false;
790        }
791        if (getRevolutionDistance() != that.getRevolutionDistance()) {
792            return false;
793        }
794        return true;
795
796    }
797
798    /**
799     * Returns a clone of the plot.
800     *
801     * @return A clone.
802     *
803     * @throws CloneNotSupportedException  this class will not throw this
804     *         exception, but subclasses (if any) might.
805     */
806    public Object clone() throws CloneNotSupportedException {
807
808        CompassPlot clone = (CompassPlot) super.clone();
809        if (this.circle1 != null) {
810            clone.circle1 = (Ellipse2D) this.circle1.clone();
811        }
812        if (this.circle2 != null) {
813            clone.circle2 = (Ellipse2D) this.circle2.clone();
814        }
815        if (this.a1 != null) {
816            clone.a1 = (Area) this.a1.clone();
817        }
818        if (this.a2 != null) {
819            clone.a2 = (Area) this.a2.clone();
820        }
821        if (this.rect1 != null) {
822            clone.rect1 = (Rectangle2D) this.rect1.clone();
823        }
824        clone.datasets = (ValueDataset[]) this.datasets.clone();
825        clone.seriesNeedle = (MeterNeedle[]) this.seriesNeedle.clone();
826
827        // clone share data sets => add the clone as listener to the dataset
828        for (int i = 0; i < this.datasets.length; ++i) {
829            if (clone.datasets[i] != null) {
830                clone.datasets[i].addChangeListener(clone);
831            }
832        }
833        return clone;
834
835    }
836
837    /**
838     * Sets the count to complete one revolution.  Can be arbitrarily set
839     * For degrees (the default) it is 360, for radians this is 2*Pi, etc
840     *
841     * @param size the count to complete one revolution.
842     *
843     * @see #getRevolutionDistance()
844     */
845    public void setRevolutionDistance(double size) {
846        if (size > 0) {
847            this.revolutionDistance = size;
848        }
849    }
850
851    /**
852     * Gets the count to complete one revolution.
853     *
854     * @return The count to complete one revolution.
855     *
856     * @see #setRevolutionDistance(double)
857     */
858    public double getRevolutionDistance() {
859        return this.revolutionDistance;
860    }
861
862    /**
863     * Provides serialization support.
864     *
865     * @param stream  the output stream.
866     *
867     * @throws IOException  if there is an I/O error.
868     */
869    private void writeObject(ObjectOutputStream stream) throws IOException {
870        stream.defaultWriteObject();
871        SerialUtilities.writePaint(this.rosePaint, stream);
872        SerialUtilities.writePaint(this.roseCenterPaint, stream);
873        SerialUtilities.writePaint(this.roseHighlightPaint, stream);
874    }
875
876    /**
877     * Provides serialization support.
878     *
879     * @param stream  the input stream.
880     *
881     * @throws IOException  if there is an I/O error.
882     * @throws ClassNotFoundException  if there is a classpath problem.
883     */
884    private void readObject(ObjectInputStream stream)
885        throws IOException, ClassNotFoundException {
886        stream.defaultReadObject();
887        this.rosePaint = SerialUtilities.readPaint(stream);
888        this.roseCenterPaint = SerialUtilities.readPaint(stream);
889        this.roseHighlightPaint = SerialUtilities.readPaint(stream);
890    }
891
892}