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