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