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                fireChangeEvent();
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            fireChangeEvent();
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            fireChangeEvent();       
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            fireChangeEvent();     
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            fireChangeEvent();     
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            fireChangeEvent();
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            if ((needle != null) && (index < this.seriesNeedle.length)) {
497                this.seriesNeedle[index] = needle;
498            }
499            fireChangeEvent();
500        }
501    
502        /**
503         * Returns an array of dataset references for the plot.
504         *
505         * @return The dataset for the plot, cast as a ValueDataset.
506         * 
507         * @see #addDataset(ValueDataset)
508         */
509        public ValueDataset[] getDatasets() {
510            return this.datasets;
511        }
512    
513        /**
514         * Adds a dataset to the compass.
515         *
516         * @param dataset  the new dataset (<code>null</code> ignored).
517         * 
518         * @see #addDataset(ValueDataset, MeterNeedle)
519         */
520        public void addDataset(ValueDataset dataset) {
521            addDataset(dataset, null);
522        }
523    
524        /**
525         * Adds a dataset to the compass.
526         *
527         * @param dataset  the new dataset (<code>null</code> ignored).
528         * @param needle  the needle (<code>null</code> permitted).
529         */
530        public void addDataset(ValueDataset dataset, MeterNeedle needle) {
531    
532            if (dataset != null) {
533                int i = this.datasets.length + 1;
534                ValueDataset[] t = new ValueDataset[i];
535                MeterNeedle[] p = new MeterNeedle[i];
536                i = i - 2;
537                for (; i >= 0; --i) {
538                    t[i] = this.datasets[i];
539                    p[i] = this.seriesNeedle[i];
540                }
541                i = this.datasets.length;
542                t[i] = dataset;
543                p[i] = ((needle != null) ? needle : p[i - 1]);
544    
545                ValueDataset[] a = this.datasets;
546                MeterNeedle[] b = this.seriesNeedle;
547                this.datasets = t;
548                this.seriesNeedle = p;
549    
550                for (--i; i >= 0; --i) {
551                    a[i] = null;
552                    b[i] = null;
553                }
554                dataset.addChangeListener(this);
555            }
556        }
557    
558        /**
559         * Draws the plot on a Java 2D graphics device (such as the screen or a 
560         * printer).
561         *
562         * @param g2  the graphics device.
563         * @param area  the area within which the plot should be drawn.
564         * @param anchor  the anchor point (<code>null</code> permitted).
565         * @param parentState  the state from the parent plot, if there is one.
566         * @param info  collects info about the drawing.
567         */
568        public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
569                         PlotState parentState,
570                         PlotRenderingInfo info) {
571    
572            int outerRadius = 0;
573            int innerRadius = 0;
574            int x1, y1, x2, y2;
575            double a;
576    
577            if (info != null) {
578                info.setPlotArea(area);
579            }
580    
581            // adjust for insets...
582            RectangleInsets insets = getInsets();
583            insets.trim(area);
584    
585            // draw the background
586            if (this.drawBorder) {
587                drawBackground(g2, area);
588            }
589    
590            int midX = (int) (area.getWidth() / 2);
591            int midY = (int) (area.getHeight() / 2);
592            int radius = midX;
593            if (midY < midX) {
594                radius = midY;
595            }
596            --radius;
597            int diameter = 2 * radius;
598    
599            midX += (int) area.getMinX();
600            midY += (int) area.getMinY();
601    
602            this.circle1.setFrame(midX - radius, midY - radius, diameter, diameter);
603            this.circle2.setFrame(
604                midX - radius + 15, midY - radius + 15, 
605                diameter - 30, diameter - 30
606            );
607            g2.setPaint(this.rosePaint);
608            this.a1 = new Area(this.circle1);
609            this.a2 = new Area(this.circle2);
610            this.a1.subtract(this.a2);
611            g2.fill(this.a1);
612    
613            g2.setPaint(this.roseCenterPaint);
614            x1 = diameter - 30;
615            g2.fillOval(midX - radius + 15, midY - radius + 15, x1, x1);
616            g2.setPaint(this.roseHighlightPaint);
617            g2.drawOval(midX - radius, midY - radius, diameter, diameter);
618            x1 = diameter - 20;
619            g2.drawOval(midX - radius + 10, midY - radius + 10, x1, x1);
620            x1 = diameter - 30;
621            g2.drawOval(midX - radius + 15, midY - radius + 15, x1, x1);
622            x1 = diameter - 80;
623            g2.drawOval(midX - radius + 40, midY - radius + 40, x1, x1);
624    
625            outerRadius = radius - 20;
626            innerRadius = radius - 32;
627            for (int w = 0; w < 360; w += 15) {
628                a = Math.toRadians(w);
629                x1 = midX - ((int) (Math.sin(a) * innerRadius));
630                x2 = midX - ((int) (Math.sin(a) * outerRadius));
631                y1 = midY - ((int) (Math.cos(a) * innerRadius));
632                y2 = midY - ((int) (Math.cos(a) * outerRadius));
633                g2.drawLine(x1, y1, x2, y2);
634            }
635    
636            g2.setPaint(this.roseHighlightPaint);
637            innerRadius = radius - 26;
638            outerRadius = 7;
639            for (int w = 45; w < 360; w += 90) {
640                a = Math.toRadians(w);
641                x1 = midX - ((int) (Math.sin(a) * innerRadius));
642                y1 = midY - ((int) (Math.cos(a) * innerRadius));
643                g2.fillOval(x1 - outerRadius, y1 - outerRadius, 2 * outerRadius, 
644                        2 * outerRadius);
645            }
646    
647            /// Squares
648            for (int w = 0; w < 360; w += 90) {
649                a = Math.toRadians(w);
650                x1 = midX - ((int) (Math.sin(a) * innerRadius));
651                y1 = midY - ((int) (Math.cos(a) * innerRadius));
652    
653                Polygon p = new Polygon();
654                p.addPoint(x1 - outerRadius, y1);
655                p.addPoint(x1, y1 + outerRadius);
656                p.addPoint(x1 + outerRadius, y1);
657                p.addPoint(x1, y1 - outerRadius);
658                g2.fillPolygon(p);
659            }
660    
661            /// Draw N, S, E, W
662            innerRadius = radius - 42;
663            Font f = getCompassFont(radius);
664            g2.setFont(f);
665            g2.drawString("N", midX - 5, midY - innerRadius + f.getSize());
666            g2.drawString("S", midX - 5, midY + innerRadius - 5);
667            g2.drawString("W", midX - innerRadius + 5, midY + 5);
668            g2.drawString("E", midX + innerRadius - f.getSize(), midY + 5);
669    
670            // plot the data (unless the dataset is null)...
671            y1 = radius / 2;
672            x1 = radius / 6;
673            Rectangle2D needleArea = new Rectangle2D.Double(
674                (midX - x1), (midY - y1), (2 * x1), (2 * y1)
675            );
676            int x = this.seriesNeedle.length;
677            int current = 0;
678            double value = 0;
679            int i = (this.datasets.length - 1);
680            for (; i >= 0; --i) {
681                ValueDataset data = this.datasets[i];
682    
683                if (data != null && data.getValue() != null) {
684                    value = (data.getValue().doubleValue()) 
685                        % this.revolutionDistance;
686                    value = value / this.revolutionDistance * 360;
687                    current = i % x;
688                    this.seriesNeedle[current].draw(g2, needleArea, value);
689                }
690            }
691    
692            if (this.drawBorder) {
693                drawOutline(g2, area);
694            }
695    
696        }
697    
698        /**
699         * Returns a short string describing the type of plot.
700         *
701         * @return A string describing the plot.
702         */
703        public String getPlotType() {
704            return localizationResources.getString("Compass_Plot");
705        }
706    
707        /**
708         * Returns the legend items for the plot.  For now, no legend is available 
709         * - this method returns null.
710         *
711         * @return The legend items.
712         */
713        public LegendItemCollection getLegendItems() {
714            return null;
715        }
716    
717        /**
718         * No zooming is implemented for compass plot, so this method is empty.
719         *
720         * @param percent  the zoom amount.
721         */
722        public void zoom(double percent) {
723            // no zooming possible
724        }
725    
726        /**
727         * Returns the font for the compass, adjusted for the size of the plot.
728         *
729         * @param radius the radius.
730         *
731         * @return The font.
732         */
733        protected Font getCompassFont(int radius) {
734            float fontSize = radius / 10.0f;
735            if (fontSize < 8) {
736                fontSize = 8;
737            }
738            Font newFont = this.compassFont.deriveFont(fontSize);
739            return newFont;
740        }
741    
742        /**
743         * Tests an object for equality with this plot.
744         *
745         * @param obj  the object (<code>null</code> permitted).
746         *
747         * @return A boolean.
748         */
749        public boolean equals(Object obj) {
750            if (obj == this) {
751                return true;
752            }
753            if (!(obj instanceof CompassPlot)) {
754                return false;
755            }
756            if (!super.equals(obj)) {
757                return false;
758            }
759            CompassPlot that = (CompassPlot) obj;
760            if (this.labelType != that.labelType) {
761                return false;
762            }
763            if (!ObjectUtilities.equal(this.labelFont, that.labelFont)) {
764                return false;
765            }
766            if (this.drawBorder != that.drawBorder) {
767                return false;
768            }
769            if (!PaintUtilities.equal(this.roseHighlightPaint, 
770                    that.roseHighlightPaint)) {
771                return false;
772            }
773            if (!PaintUtilities.equal(this.rosePaint, that.rosePaint)) {
774                return false;
775            }
776            if (!PaintUtilities.equal(this.roseCenterPaint, 
777                    that.roseCenterPaint)) {
778                return false;
779            }
780            if (!ObjectUtilities.equal(this.compassFont, that.compassFont)) {
781                return false;
782            }
783            if (!Arrays.equals(this.seriesNeedle, that.seriesNeedle)) {
784                return false;
785            }
786            if (getRevolutionDistance() != that.getRevolutionDistance()) {
787                return false;
788            }
789            return true;
790    
791        }
792    
793        /**
794         * Returns a clone of the plot.
795         *
796         * @return A clone.
797         *
798         * @throws CloneNotSupportedException  this class will not throw this 
799         *         exception, but subclasses (if any) might.
800         */
801        public Object clone() throws CloneNotSupportedException {
802    
803            CompassPlot clone = (CompassPlot) super.clone();
804            if (this.circle1 != null) {
805                clone.circle1 = (Ellipse2D) this.circle1.clone();
806            }
807            if (this.circle2 != null) {
808                clone.circle2 = (Ellipse2D) this.circle2.clone();
809            }
810            if (this.a1 != null) {
811                clone.a1 = (Area) this.a1.clone();
812            }
813            if (this.a2 != null) {
814                clone.a2 = (Area) this.a2.clone();
815            }
816            if (this.rect1 != null) {
817                clone.rect1 = (Rectangle2D) this.rect1.clone();            
818            }
819            clone.datasets = (ValueDataset[]) this.datasets.clone();
820            clone.seriesNeedle = (MeterNeedle[]) this.seriesNeedle.clone();
821    
822            // clone share data sets => add the clone as listener to the dataset
823            for (int i = 0; i < this.datasets.length; ++i) {
824                if (clone.datasets[i] != null) {
825                    clone.datasets[i].addChangeListener(clone);
826                }
827            }
828            return clone;
829    
830        }
831    
832        /**
833         * Sets the count to complete one revolution.  Can be arbitrarily set
834         * For degrees (the default) it is 360, for radians this is 2*Pi, etc
835         *
836         * @param size the count to complete one revolution.
837         * 
838         * @see #getRevolutionDistance()
839         */
840        public void setRevolutionDistance(double size) {
841            if (size > 0) {
842                this.revolutionDistance = size;
843            }
844        }
845    
846        /**
847         * Gets the count to complete one revolution.
848         *
849         * @return The count to complete one revolution.
850         * 
851         * @see #setRevolutionDistance(double)
852         */
853        public double getRevolutionDistance() {
854            return this.revolutionDistance;
855        }
856        
857        /**
858         * Provides serialization support.
859         *
860         * @param stream  the output stream.
861         *
862         * @throws IOException  if there is an I/O error.
863         */
864        private void writeObject(ObjectOutputStream stream) throws IOException {
865            stream.defaultWriteObject();
866            SerialUtilities.writePaint(this.rosePaint, stream);
867            SerialUtilities.writePaint(this.roseCenterPaint, stream);
868            SerialUtilities.writePaint(this.roseHighlightPaint, stream);
869        }
870    
871        /**
872         * Provides serialization support.
873         *
874         * @param stream  the input stream.
875         *
876         * @throws IOException  if there is an I/O error.
877         * @throws ClassNotFoundException  if there is a classpath problem.
878         */
879        private void readObject(ObjectInputStream stream) 
880            throws IOException, ClassNotFoundException {
881            stream.defaultReadObject();
882            this.rosePaint = SerialUtilities.readPaint(stream);
883            this.roseCenterPaint = SerialUtilities.readPaint(stream);
884            this.roseHighlightPaint = SerialUtilities.readPaint(stream);
885        }
886    
887    }