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