    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
    067package org.jfree.chart.plot;
    068
    069import java.awt.BasicStroke;
    070import java.awt.Color;
    071import java.awt.Font;
    072import java.awt.Graphics2D;
    073import java.awt.Paint;
    074import java.awt.Polygon;
    075import java.awt.Stroke;
    076import java.awt.geom.Area;
    077import java.awt.geom.Ellipse2D;
    078import java.awt.geom.Point2D;
    079import java.awt.geom.Rectangle2D;
    080import java.io.IOException;
    081import java.io.ObjectInputStream;
    082import java.io.ObjectOutputStream;
    083import java.io.Serializable;
    084import java.util.Arrays;
    085import java.util.ResourceBundle;
    086
    087import org.jfree.chart.LegendItemCollection;
    088import org.jfree.chart.event.PlotChangeEvent;
    089import org.jfree.chart.needle.ArrowNeedle;
    090import org.jfree.chart.needle.LineNeedle;
    091import org.jfree.chart.needle.LongNeedle;
    092import org.jfree.chart.needle.MeterNeedle;
    093import org.jfree.chart.needle.MiddlePinNeedle;
    094import org.jfree.chart.needle.PinNeedle;
    095import org.jfree.chart.needle.PlumNeedle;
    096import org.jfree.chart.needle.PointerNeedle;
    097import org.jfree.chart.needle.ShipNeedle;
    098import org.jfree.chart.needle.WindNeedle;
    099import org.jfree.chart.util.ResourceBundleWrapper;
    100import org.jfree.data.general.DefaultValueDataset;
    101import org.jfree.data.general.ValueDataset;
    102import org.jfree.io.SerialUtilities;
    103import org.jfree.ui.RectangleInsets;
    104import org.jfree.util.ObjectUtilities;
    105import 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 */
    111public 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}