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