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