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