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    
065    package org.jfree.chart.plot;
066    
067    import java.awt.BasicStroke;
068    import java.awt.Color;
069    import java.awt.Font;
070    import java.awt.Graphics2D;
071    import java.awt.Paint;
072    import java.awt.Polygon;
073    import java.awt.Stroke;
074    import java.awt.geom.Area;
075    import java.awt.geom.Ellipse2D;
076    import java.awt.geom.Point2D;
077    import java.awt.geom.Rectangle2D;
078    import java.io.IOException;
079    import java.io.ObjectInputStream;
080    import java.io.ObjectOutputStream;
081    import java.io.Serializable;
082    import java.util.Arrays;
083    import java.util.ResourceBundle;
084    
085    import org.jfree.chart.LegendItemCollection;
086    import org.jfree.chart.event.PlotChangeEvent;
087    import org.jfree.chart.needle.ArrowNeedle;
088    import org.jfree.chart.needle.LineNeedle;
089    import org.jfree.chart.needle.LongNeedle;
090    import org.jfree.chart.needle.MeterNeedle;
091    import org.jfree.chart.needle.MiddlePinNeedle;
092    import org.jfree.chart.needle.PinNeedle;
093    import org.jfree.chart.needle.PlumNeedle;
094    import org.jfree.chart.needle.PointerNeedle;
095    import org.jfree.chart.needle.ShipNeedle;
096    import org.jfree.chart.needle.WindNeedle;
097    import org.jfree.chart.util.ResourceBundleWrapper;
098    import org.jfree.data.general.DefaultValueDataset;
099    import org.jfree.data.general.ValueDataset;
100    import org.jfree.io.SerialUtilities;
101    import org.jfree.ui.RectangleInsets;
102    import org.jfree.util.ObjectUtilities;
103    import 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     */
109    public 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    }