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