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