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