    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     * MeterPlot.java
    029     * --------------
    030     * (C) Copyright 2000-2007, by Hari and Contributors.
    031     *
    032     * Original Author:  Hari (ourhari@hotmail.com);
    033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
    034     *                   Bob Orchard;
    035     *                   Arnaud Lelievre;
    036     *                   Nicolas Brodu;
    037     *                   David Bastend;
    038     *
    039     * $Id: MeterPlot.java,v 1.13.2.9 2007/03/21 10:25:00 mungady Exp $
    040     *
    041     * Changes
    042     * -------
    043     * 01-Apr-2002 : Version 1, contributed by Hari (DG);
    044     * 23-Apr-2002 : Moved dataset from JFreeChart to Plot (DG);
    045     * 22-Aug-2002 : Added changes suggest by Bob Orchard, changed Color to Paint
    046     *               for consistency, plus added Javadoc comments (DG);
    047     * 01-Oct-2002 : Fixed errors reported by Checkstyle (DG);
    048     * 23-Jan-2003 : Removed one constructor (DG);
    049     * 26-Mar-2003 : Implemented Serializable (DG);
    050     * 20-Aug-2003 : Changed dataset from MeterDataset --> ValueDataset, added
    051     *               equals() method,
    052     * 08-Sep-2003 : Added internationalization via use of properties
    053     *               resourceBundle (RFE 690236) (AL);
    054     *               implemented Cloneable, and various other changes (DG);
    055     * 08-Sep-2003 : Added serialization methods (NB);
    056     * 11-Sep-2003 : Added cloning support (NB);
    057     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
    058     * 25-Sep-2003 : Fix useless cloning. Correct dataset listener registration in
    059     *               constructor. (NB)
    060     * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
    061     * 17-Jan-2004 : Changed to allow dialBackgroundPaint to be set to null - see
    062     *               bug 823628 (DG);
    063     * 07-Apr-2004 : Changed string bounds calculation (DG);
    064     * 12-May-2004 : Added tickLabelFormat attribute - see RFE 949566.  Also
    065     *               updated the equals() method (DG);
    066     * 02-Nov-2004 : Added sanity checks for range, and only draw the needle if the
    067     *               value is contained within the overall range - see bug report
    068     *               1056047 (DG);
    069     * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0
    070     *               release (DG);
    071     * 02-Feb-2005 : Added optional background paint for each region (DG);
    072     * 22-Mar-2005 : Removed 'normal', 'warning' and 'critical' regions and put in
    073     *               facility to define an arbitrary number of MeterIntervals,
    074     *               based on a contribution by David Bastend (DG);
    075     * 20-Apr-2005 : Small update for change to LegendItem constructors (DG);
    076     * 05-May-2005 : Updated draw() method parameters (DG);
    077     * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG);
    078     * 10-Nov-2005 : Added tickPaint, tickSize and valuePaint attributes, and
    079     *               put value label drawing code into a separate method (DG);
    080     * ------------- JFREECHART 1.0.x ---------------------------------------------
    081     * 05-Mar-2007 : Restore clip region correctly (see bug 1667750) (DG);
    082     *
    083     */
    084
    085    package org.jfree.chart.plot;
    086
    087    import java.awt.AlphaComposite;
    088    import java.awt.BasicStroke;
    089    import java.awt.Color;
    090    import java.awt.Composite;
    091    import java.awt.Font;
    092    import java.awt.FontMetrics;
    093    import java.awt.Graphics2D;
    094    import java.awt.Paint;
    095    import java.awt.Polygon;
    096    import java.awt.Shape;
    097    import java.awt.Stroke;
    098    import java.awt.geom.Arc2D;
    099    import java.awt.geom.Ellipse2D;
    100    import java.awt.geom.Line2D;
    101    import java.awt.geom.Point2D;
    102    import java.awt.geom.Rectangle2D;
    103    import java.io.IOException;
    104    import java.io.ObjectInputStream;
    105    import java.io.ObjectOutputStream;
    106    import java.io.Serializable;
    107    import java.text.NumberFormat;
    108    import java.util.Collections;
    109    import java.util.Iterator;
    110    import java.util.List;
    111    import java.util.ResourceBundle;
    112
    113    import org.jfree.chart.LegendItem;
    114    import org.jfree.chart.LegendItemCollection;
    115    import org.jfree.chart.event.PlotChangeEvent;
    116    import org.jfree.data.Range;
    117    import org.jfree.data.general.DatasetChangeEvent;
    118    import org.jfree.data.general.ValueDataset;
    119    import org.jfree.io.SerialUtilities;
    120    import org.jfree.text.TextUtilities;
    121    import org.jfree.ui.RectangleInsets;
    122    import org.jfree.ui.TextAnchor;
    123    import org.jfree.util.ObjectUtilities;
    124    import org.jfree.util.PaintUtilities;
    125
    126    /**
    127     * A plot that displays a single value in the form of a needle on a dial.
    128     * Defined ranges (for example, 'normal', 'warning' and 'critical') can be
    129     * highlighted on the dial.
    130     */
    131    public class MeterPlot extends Plot implements Serializable, Cloneable {
    132
    133        /** For serialization. */
    134        private static final long serialVersionUID = 2987472457734470962L;
    135
    136        /** The default background paint. */
    137        static final Paint DEFAULT_DIAL_BACKGROUND_PAINT = Color.black;
    138
    139        /** The default needle paint. */
    140        static final Paint DEFAULT_NEEDLE_PAINT = Color.green;
    141
    142        /** The default value font. */
    143        static final Font DEFAULT_VALUE_FONT = new Font("SansSerif", Font.BOLD, 12);
    144
    145        /** The default value paint. */
    146        static final Paint DEFAULT_VALUE_PAINT = Color.yellow;
    147
    148        /** The default meter angle. */
    149        public static final int DEFAULT_METER_ANGLE = 270;
    150
    151        /** The default border size. */
    152        public static final float DEFAULT_BORDER_SIZE = 3f;
    153
    154        /** The default circle size. */
    155        public static final float DEFAULT_CIRCLE_SIZE = 10f;
    156
    157        /** The default label font. */
    158        public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif",
    159                Font.BOLD, 10);
    160
    161        /** The dataset (contains a single value). */
    162        private ValueDataset dataset;
    163
    164        /** The dial shape (background shape). */
    165        private DialShape shape;
    166
    167        /** The dial extent (measured in degrees). */
    168        private int meterAngle;
    169
    170        /** The overall range of data values on the dial. */
    171        private Range range;
    172
    173        /** The tick size. */
    174        private double tickSize;
    175
    176        /** The paint used to draw the ticks. */
    177        private transient Paint tickPaint;
    178
    179        /** The units displayed on the dial. */
    180        private String units;
    181
    182        /** The font for the value displayed in the center of the dial. */
    183        private Font valueFont;
    184
    185        /** The paint for the value displayed in the center of the dial. */
    186        private transient Paint valuePaint;
    187
    188        /** A flag that controls whether or not the border is drawn. */
    189        private boolean drawBorder;
    190
    191        /** The outline paint. */
    192        private transient Paint dialOutlinePaint;
    193
    194        /** The paint for the dial background. */
    195        private transient Paint dialBackgroundPaint;
    196
    197        /** The paint for the needle. */
    198        private transient Paint needlePaint;
    199
    200        /** A flag that controls whether or not the tick labels are visible. */
    201        private boolean tickLabelsVisible;
    202
    203        /** The tick label font. */
    204        private Font tickLabelFont;
    205
    206        /** The tick label paint. */
    207        private transient Paint tickLabelPaint;
    208
    209        /** The tick label format. */
    210        private NumberFormat tickLabelFormat;
    211
    212        /** The resourceBundle for the localization. */
    213        protected static ResourceBundle localizationResources =
    214            ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");
    215
    216        /**
    217         * A (possibly empty) list of the {@link MeterInterval}s to be highlighted
    218         * on the dial.
    219         */
    220        private List intervals;
    221
    222        /**
    223         * Creates a new plot with a default range of <code>0</code> to
    224         * <code>100</code> and no value to display.
    225         */
    226        public MeterPlot() {
    227            this(null);
    228        }
    229
    230        /**
    231         * Creates a new plot that displays the value from the supplied dataset.
    232         *
    233         * @param dataset  the dataset (<code>null</code> permitted).
    234         */
    235        public MeterPlot(ValueDataset dataset) {
    236            super();
    237            this.shape = DialShape.CIRCLE;
    238            this.meterAngle = DEFAULT_METER_ANGLE;
    239            this.range = new Range(0.0, 100.0);
    240            this.tickSize = 10.0;
    241            this.tickPaint = Color.white;
    242            this.units = "Units";
    243            this.needlePaint = MeterPlot.DEFAULT_NEEDLE_PAINT;
    244            this.tickLabelsVisible = true;
    245            this.tickLabelFont = MeterPlot.DEFAULT_LABEL_FONT;
    246            this.tickLabelPaint = Color.black;
    247            this.tickLabelFormat = NumberFormat.getInstance();
    248            this.valueFont = MeterPlot.DEFAULT_VALUE_FONT;
    249            this.valuePaint = MeterPlot.DEFAULT_VALUE_PAINT;
    250            this.dialBackgroundPaint = MeterPlot.DEFAULT_DIAL_BACKGROUND_PAINT;
    251            this.intervals = new java.util.ArrayList();
    252            setDataset(dataset);
    253        }
    254
    255        /**
    256         * Returns the dial shape.  The default is {@link DialShape#CIRCLE}).
    257         *
    258         * @return The dial shape (never <code>null</code>).
    259         *
    260         * @see #setDialShape(DialShape)
    261         */
    262        public DialShape getDialShape() {
    263            return this.shape;
    264        }
    265
    266        /**
    267         * Sets the dial shape and sends a {@link PlotChangeEvent} to all
    268         * registered listeners.
    269         *
    270         * @param shape  the shape (<code>null</code> not permitted).
    271         *
    272         * @see #getDialShape()
    273         */
    274        public void setDialShape(DialShape shape) {
    275            if (shape == null) {
    276                throw new IllegalArgumentException("Null 'shape' argument.");
    277            }
    278            this.shape = shape;
    279            notifyListeners(new PlotChangeEvent(this));
    280        }
    281
    282        /**
    283         * Returns the meter angle in degrees.  This defines, in part, the shape
    284         * of the dial.  The default is 270 degrees.
    285         *
    286         * @return The meter angle (in degrees).
    287         *
    288         * @see #setMeterAngle(int)
    289         */
    290        public int getMeterAngle() {
    291            return this.meterAngle;
    292        }
    293
    294        /**
    295         * Sets the angle (in degrees) for the whole range of the dial and sends
    296         * a {@link PlotChangeEvent} to all registered listeners.
    297         *
    298         * @param angle  the angle (in degrees, in the range 1-360).
    299         *
    300         * @see #getMeterAngle()
    301         */
    302        public void setMeterAngle(int angle) {
    303            if (angle < 1 || angle > 360) {
    304                throw new IllegalArgumentException("Invalid 'angle' (" + angle
    305                        + ")");
    306            }
    307            this.meterAngle = angle;
    308            notifyListeners(new PlotChangeEvent(this));
    309        }
    310
    311        /**
    312         * Returns the overall range for the dial.
    313         *
    314         * @return The overall range (never <code>null</code>).
    315         *
    316         * @see #setRange(Range)
    317         */
    318        public Range getRange() {
    319            return this.range;
    320        }
    321
    322        /**
    323         * Sets the range for the dial and sends a {@link PlotChangeEvent} to all
    324         * registered listeners.
    325         *
    326         * @param range  the range (<code>null</code> not permitted and zero-length
    327         *               ranges not permitted).
    328         *
    329         * @see #getRange()
    330         */
    331        public void setRange(Range range) {
    332            if (range == null) {
    333                throw new IllegalArgumentException("Null 'range' argument.");
    334            }
    335            if (!(range.getLength() > 0.0)) {
    336                throw new IllegalArgumentException(
    337                        "Range length must be positive.");
    338            }
    339            this.range = range;
    340            notifyListeners(new PlotChangeEvent(this));
    341        }
    342
    343        /**
    344         * Returns the tick size (the interval between ticks on the dial).
    345         *
    346         * @return The tick size.
    347         *
    348         * @see #setTickSize(double)
    349         */
    350        public double getTickSize() {
    351            return this.tickSize;
    352        }
    353
    354        /**
    355         * Sets the tick size and sends a {@link PlotChangeEvent} to all
    356         * registered listeners.
    357         *
    358         * @param size  the tick size (must be > 0).
    359         *
    360         * @see #getTickSize()
    361         */
    362        public void setTickSize(double size) {
    363            if (size <= 0) {
    364                throw new IllegalArgumentException("Requires 'size' > 0.");
    365            }
    366            this.tickSize = size;
    367            notifyListeners(new PlotChangeEvent(this));
    368        }
    369
    370        /**
    371         * Returns the paint used to draw the ticks around the dial.
    372         *
    373         * @return The paint used to draw the ticks around the dial (never
    374         *         <code>null</code>).
    375         *
    376         * @see #setTickPaint(Paint)
    377         */
    378        public Paint getTickPaint() {
    379            return this.tickPaint;
    380        }
    381
    382        /**
    383         * Sets the paint used to draw the tick labels around the dial and sends
    384         * a {@link PlotChangeEvent} to all registered listeners.
    385         *
    386         * @param paint  the paint (<code>null</code> not permitted).
    387         *
    388         * @see #getTickPaint()
    389         */
    390        public void setTickPaint(Paint paint) {
    391            if (paint == null) {
    392                throw new IllegalArgumentException("Null 'paint' argument.");
    393            }
    394            this.tickPaint = paint;
    395            notifyListeners(new PlotChangeEvent(this));
    396        }
    397
    398        /**
    399         * Returns a string describing the units for the dial.
    400         *
    401         * @return The units (possibly <code>null</code>).
    402         *
    403         * @see #setUnits(String)
    404         */
    405        public String getUnits() {
    406            return this.units;
    407        }
    408
    409        /**
    410         * Sets the units for the dial and sends a {@link PlotChangeEvent} to all
    411         * registered listeners.
    412         *
    413         * @param units  the units (<code>null</code> permitted).
    414         *
    415         * @see #getUnits()
    416         */
    417        public void setUnits(String units) {
    418            this.units = units;
    419            notifyListeners(new PlotChangeEvent(this));
    420        }
    421
    422        /**
    423         * Returns the paint for the needle.
    424         *
    425         * @return The paint (never <code>null</code>).
    426         *
    427         * @see #setNeedlePaint(Paint)
    428         */
    429        public Paint getNeedlePaint() {
    430            return this.needlePaint;
    431        }
    432
    433        /**
    434         * Sets the paint used to display the needle and sends a
    435         * {@link PlotChangeEvent} to all registered listeners.
    436         *
    437         * @param paint  the paint (<code>null</code> not permitted).
    438         *
    439         * @see #getNeedlePaint()
    440         */
    441        public void setNeedlePaint(Paint paint) {
    442            if (paint == null) {
    443                throw new IllegalArgumentException("Null 'paint' argument.");
    444            }
    445            this.needlePaint = paint;
    446            notifyListeners(new PlotChangeEvent(this));
    447        }
    448
    449        /**
    450         * Returns the flag that determines whether or not tick labels are visible.
    451         *
    452         * @return The flag.
    453         *
    454         * @see #setTickLabelsVisible(boolean)
    455         */
    456        public boolean getTickLabelsVisible() {
    457            return this.tickLabelsVisible;
    458        }
    459
    460        /**
    461         * Sets the flag that controls whether or not the tick labels are visible
    462         * and sends a {@link PlotChangeEvent} to all registered listeners.
    463         *
    464         * @param visible  the flag.
    465         *
    466         * @see #getTickLabelsVisible()
    467         */
    468        public void setTickLabelsVisible(boolean visible) {
    469            if (this.tickLabelsVisible != visible) {
    470                this.tickLabelsVisible = visible;
    471                notifyListeners(new PlotChangeEvent(this));
    472            }
    473        }
    474
    475        /**
    476         * Returns the tick label font.
    477         *
    478         * @return The font (never <code>null</code>).
    479         *
    480         * @see #setTickLabelFont(Font)
    481         */
    482        public Font getTickLabelFont() {
    483            return this.tickLabelFont;
    484        }
    485
    486        /**
    487         * Sets the tick label font and sends a {@link PlotChangeEvent} to all
    488         * registered listeners.
    489         *
    490         * @param font  the font (<code>null</code> not permitted).
    491         *
    492         * @see #getTickLabelFont()
    493         */
    494        public void setTickLabelFont(Font font) {
    495            if (font == null) {
    496                throw new IllegalArgumentException("Null 'font' argument.");
    497            }
    498            if (!this.tickLabelFont.equals(font)) {
    499                this.tickLabelFont = font;
    500                notifyListeners(new PlotChangeEvent(this));
    501            }
    502        }
    503
    504        /**
    505         * Returns the tick label paint.
    506         *
    507         * @return The paint (never <code>null</code>).
    508         *
    509         * @see #setTickLabelPaint(Paint)
    510         */
    511        public Paint getTickLabelPaint() {
    512            return this.tickLabelPaint;
    513        }
    514
    515        /**
    516         * Sets the tick label paint and sends a {@link PlotChangeEvent} to all
    517         * registered listeners.
    518         *
    519         * @param paint  the paint (<code>null</code> not permitted).
    520         *
    521         * @see #getTickLabelPaint()
    522         */
    523        public void setTickLabelPaint(Paint paint) {
    524            if (paint == null) {
    525                throw new IllegalArgumentException("Null 'paint' argument.");
    526            }
    527            if (!this.tickLabelPaint.equals(paint)) {
    528                this.tickLabelPaint = paint;
    529                notifyListeners(new PlotChangeEvent(this));
    530            }
    531        }
    532
    533        /**
    534         * Returns the tick label format.
    535         *
    536         * @return The tick label format (never <code>null</code>).
    537         *
    538         * @see #setTickLabelFormat(NumberFormat)
    539         */
    540        public NumberFormat getTickLabelFormat() {
    541            return this.tickLabelFormat;
    542        }
    543
    544        /**
    545         * Sets the format for the tick labels and sends a {@link PlotChangeEvent}
    546         * to all registered listeners.
    547         *
    548         * @param format  the format (<code>null</code> not permitted).
    549         *
    550         * @see #getTickLabelFormat()
    551         */
    552        public void setTickLabelFormat(NumberFormat format) {
    553            if (format == null) {
    554                throw new IllegalArgumentException("Null 'format' argument.");
    555            }
    556            this.tickLabelFormat = format;
    557            notifyListeners(new PlotChangeEvent(this));
    558        }
    559
    560        /**
    561         * Returns the font for the value label.
    562         *
    563         * @return The font (never <code>null</code>).
    564         *
    565         * @see #setValueFont(Font)
    566         */
    567        public Font getValueFont() {
    568            return this.valueFont;
    569        }
    570
    571        /**
    572         * Sets the font used to display the value label and sends a
    573         * {@link PlotChangeEvent} to all registered listeners.
    574         *
    575         * @param font  the font (<code>null</code> not permitted).
    576         *
    577         * @see #getValueFont()
    578         */
    579        public void setValueFont(Font font) {
    580            if (font == null) {
    581                throw new IllegalArgumentException("Null 'font' argument.");
    582            }
    583            this.valueFont = font;
    584            notifyListeners(new PlotChangeEvent(this));
    585        }
    586
    587        /**
    588         * Returns the paint for the value label.
    589         *
    590         * @return The paint (never <code>null</code>).
    591         *
    592         * @see #setValuePaint(Paint)
    593         */
    594        public Paint getValuePaint() {
    595            return this.valuePaint;
    596        }
    597
    598        /**
    599         * Sets the paint used to display the value label and sends a
    600         * {@link PlotChangeEvent} to all registered listeners.
    601         *
    602         * @param paint  the paint (<code>null</code> not permitted).
    603         *
    604         * @see #getValuePaint()
    605         */
    606        public void setValuePaint(Paint paint) {
    607            if (paint == null) {
    608                throw new IllegalArgumentException("Null 'paint' argument.");
    609            }
    610            this.valuePaint = paint;
    611            notifyListeners(new PlotChangeEvent(this));
    612        }
    613
    614        /**
    615         * Returns the paint for the dial background.
    616         *
    617         * @return The paint (possibly <code>null</code>).
    618         *
    619         * @see #setDialBackgroundPaint(Paint)
    620         */
    621        public Paint getDialBackgroundPaint() {
    622            return this.dialBackgroundPaint;
    623        }
    624
    625        /**
    626         * Sets the paint used to fill the dial background.  Set this to
    627         * <code>null</code> for no background.
    628         *
    629         * @param paint  the paint (<code>null</code> permitted).
    630         *
    631         * @see #getDialBackgroundPaint()
    632         */
    633        public void setDialBackgroundPaint(Paint paint) {
    634            this.dialBackgroundPaint = paint;
    635            notifyListeners(new PlotChangeEvent(this));
    636        }
    637
    638        /**
    639         * Returns a flag that controls whether or not a rectangular border is
    640         * drawn around the plot area.
    641         *
    642         * @return A flag.
    643         *
    644         * @see #setDrawBorder(boolean)
    645         */
    646        public boolean getDrawBorder() {
    647            return this.drawBorder;
    648        }
    649
    650        /**
    651         * Sets the flag that controls whether or not a rectangular border is drawn
    652         * around the plot area and sends a {@link PlotChangeEvent} to all
    653         * registered listeners.
    654         *
    655         * @param draw  the flag.
    656         *
    657         * @see #getDrawBorder()
    658         */
    659        public void setDrawBorder(boolean draw) {
    660            // TODO: fix output when this flag is set to true
    661            this.drawBorder = draw;
    662            notifyListeners(new PlotChangeEvent(this));
    663        }
    664
    665        /**
    666         * Returns the dial outline paint.
    667         *
    668         * @return The paint.
    669         *
    670         * @see #setDialOutlinePaint(Paint)
    671         */
    672        public Paint getDialOutlinePaint() {
    673            return this.dialOutlinePaint;
    674        }
    675
    676        /**
    677         * Sets the dial outline paint and sends a {@link PlotChangeEvent} to all
    678         * registered listeners.
    679         *
    680         * @param paint  the paint.
    681         *
    682         * @see #getDialOutlinePaint()
    683         */
    684        public void setDialOutlinePaint(Paint paint) {
    685            this.dialOutlinePaint = paint;
    686            notifyListeners(new PlotChangeEvent(this));
    687        }
    688
    689        /**
    690         * Returns the dataset for the plot.
    691         *
    692         * @return The dataset (possibly <code>null</code>).
    693         *
    694         * @see #setDataset(ValueDataset)
    695         */
    696        public ValueDataset getDataset() {
    697            return this.dataset;
    698        }
    699
    700        /**
    701         * Sets the dataset for the plot, replacing the existing dataset if there
    702         * is one, and triggers a {@link PlotChangeEvent}.
    703         *
    704         * @param dataset  the dataset (<code>null</code> permitted).
    705         *
    706         * @see #getDataset()
    707         */
    708        public void setDataset(ValueDataset dataset) {
    709
    710            // if there is an existing dataset, remove the plot from the list of
    711            // change listeners...
    712            ValueDataset existing = this.dataset;
    713            if (existing != null) {
    714                existing.removeChangeListener(this);
    715            }
    716
    717            // set the new dataset, and register the chart as a change listener...
    718            this.dataset = dataset;
    719            if (dataset != null) {
    720                setDatasetGroup(dataset.getGroup());
    721                dataset.addChangeListener(this);
    722            }
    723
    724            // send a dataset change event to self...
    725            DatasetChangeEvent event = new DatasetChangeEvent(this, dataset);
    726            datasetChanged(event);
    727
    728        }
    729
    730        /**
    731         * Returns an unmodifiable list of the intervals for the plot.
    732         *
    733         * @return A list.
    734         *
    735         * @see #addInterval(MeterInterval)
    736         */
    737        public List getIntervals() {
    738            return Collections.unmodifiableList(this.intervals);
    739        }
    740
    741        /**
    742         * Adds an interval and sends a {@link PlotChangeEvent} to all registered
    743         * listeners.
    744         *
    745         * @param interval  the interval (<code>null</code> not permitted).
    746         *
    747         * @see #getIntervals()
    748         * @see #clearIntervals()
    749         */
    750        public void addInterval(MeterInterval interval) {
    751            if (interval == null) {
    752                throw new IllegalArgumentException("Null 'interval' argument.");
    753            }
    754            this.intervals.add(interval);
    755            notifyListeners(new PlotChangeEvent(this));
    756        }
    757
    758        /**
    759         * Clears the intervals for the plot and sends a {@link PlotChangeEvent} to
    760         * all registered listeners.
    761         *
    762         * @see #addInterval(MeterInterval)
    763         */
    764        public void clearIntervals() {
    765            this.intervals.clear();
    766            notifyListeners(new PlotChangeEvent(this));
    767        }
    768
    769        /**
    770         * Returns an item for each interval.
    771         *
    772         * @return A collection of legend items.
    773         */
    774        public LegendItemCollection getLegendItems() {
    775            LegendItemCollection result = new LegendItemCollection();
    776            Iterator iterator = this.intervals.iterator();
    777            while (iterator.hasNext()) {
    778                MeterInterval mi = (MeterInterval) iterator.next();
    779                Paint color = mi.getBackgroundPaint();
    780                if (color == null) {
    781                    color = mi.getOutlinePaint();
    782                }
    783                LegendItem item = new LegendItem(mi.getLabel(), mi.getLabel(),
    784                        null, null, new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0),
    785                        color);
    786                result.add(item);
    787            }
    788            return result;
    789        }
    790
    791        /**
    792         * Draws the plot on a Java 2D graphics device (such as the screen or a
    793         * printer).
    794         *
    795         * @param g2  the graphics device.
    796         * @param area  the area within which the plot should be drawn.
    797         * @param anchor  the anchor point (<code>null</code> permitted).
    798         * @param parentState  the state from the parent plot, if there is one.
    799         * @param info  collects info about the drawing.
    800         */
    801        public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
    802                         PlotState parentState,
    803                         PlotRenderingInfo info) {
    804
    805            if (info != null) {
    806                info.setPlotArea(area);
    807            }
    808
    809            // adjust for insets...
    810            RectangleInsets insets = getInsets();
    811            insets.trim(area);
    812
    813            area.setRect(area.getX() + 4, area.getY() + 4, area.getWidth() - 8,
    814                    area.getHeight() - 8);
    815
    816            // draw the background
    817            if (this.drawBorder) {
    818                drawBackground(g2, area);
    819            }
    820
    821            // adjust the plot area by the interior spacing value
    822            double gapHorizontal = (2 * DEFAULT_BORDER_SIZE);
    823            double gapVertical = (2 * DEFAULT_BORDER_SIZE);
    824            double meterX = area.getX() + gapHorizontal / 2;
    825            double meterY = area.getY() + gapVertical / 2;
    826            double meterW = area.getWidth() - gapHorizontal;
    827            double meterH = area.getHeight() - gapVertical
    828                    + ((this.meterAngle <= 180) && (this.shape != DialShape.CIRCLE)
    829                    ? area.getHeight() / 1.25 : 0);
    830
    831            double min = Math.min(meterW, meterH) / 2;
    832            meterX = (meterX + meterX + meterW) / 2 - min;
    833            meterY = (meterY + meterY + meterH) / 2 - min;
    834            meterW = 2 * min;
    835            meterH = 2 * min;
    836
    837            Rectangle2D meterArea = new Rectangle2D.Double(meterX, meterY, meterW,
    838                    meterH);
    839
    840            Rectangle2D.Double originalArea = new Rectangle2D.Double(
    841                    meterArea.getX() - 4, meterArea.getY() - 4,
    842                    meterArea.getWidth() + 8, meterArea.getHeight() + 8);
    843
    844            double meterMiddleX = meterArea.getCenterX();
    845            double meterMiddleY = meterArea.getCenterY();
    846
    847            // plot the data (unless the dataset is null)...
    848            ValueDataset data = getDataset();
    849            if (data != null) {
    850                double dataMin = this.range.getLowerBound();
    851                double dataMax = this.range.getUpperBound();
    852
    853                Shape savedClip = g2.getClip();
    854                g2.clip(originalArea);
    855                Composite originalComposite = g2.getComposite();
    856                g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
    857                        getForegroundAlpha()));
    858
    859                if (this.dialBackgroundPaint != null) {
    860                    fillArc(g2, originalArea, dataMin, dataMax,
    861                            this.dialBackgroundPaint, true);
    862                }
    863                drawTicks(g2, meterArea, dataMin, dataMax);
    864                drawArcForInterval(g2, meterArea, new MeterInterval("", this.range,
    865                        this.dialOutlinePaint, new BasicStroke(1.0f), null));
    866
    867                Iterator iterator = this.intervals.iterator();
    868                while (iterator.hasNext()) {
    869                    MeterInterval interval = (MeterInterval) iterator.next();
    870                    drawArcForInterval(g2, meterArea, interval);
    871                }
    872
    873                Number n = data.getValue();
    874                if (n != null) {
    875                    double value = n.doubleValue();
    876                    drawValueLabel(g2, meterArea);
    877
    878                    if (this.range.contains(value)) {
    879                        g2.setPaint(this.needlePaint);
    880                        g2.setStroke(new BasicStroke(2.0f));
    881
    882                        double radius = (meterArea.getWidth() / 2)
    883                                        + DEFAULT_BORDER_SIZE + 15;
    884                        double valueAngle = valueToAngle(value);
    885                        double valueP1 = meterMiddleX
    886                                + (radius * Math.cos(Math.PI * (valueAngle / 180)));
    887                        double valueP2 = meterMiddleY
    888                                - (radius * Math.sin(Math.PI * (valueAngle / 180)));
    889
    890                        Polygon arrow = new Polygon();
    891                        if ((valueAngle > 135 && valueAngle < 225)
    892                            || (valueAngle < 45 && valueAngle > -45)) {
    893
    894                            double valueP3 = (meterMiddleY
    895                                    - DEFAULT_CIRCLE_SIZE / 4);
    896                            double valueP4 = (meterMiddleY
    897                                    + DEFAULT_CIRCLE_SIZE / 4);
    898                            arrow.addPoint((int) meterMiddleX, (int) valueP3);
    899                            arrow.addPoint((int) meterMiddleX, (int) valueP4);
    900
    901                        }
    902                        else {
    903                            arrow.addPoint((int) (meterMiddleX
    904                                    - DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY);
    905                            arrow.addPoint((int) (meterMiddleX
    906                                    + DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY);
    907                        }
    908                        arrow.addPoint((int) valueP1, (int) valueP2);
    909                        g2.fill(arrow);
    910
    911                        Ellipse2D circle = new Ellipse2D.Double(meterMiddleX
    912                                - DEFAULT_CIRCLE_SIZE / 2, meterMiddleY
    913                                - DEFAULT_CIRCLE_SIZE / 2, DEFAULT_CIRCLE_SIZE,
    914                                DEFAULT_CIRCLE_SIZE);
    915                        g2.fill(circle);
    916                    }
    917                }
    918
    919                g2.setClip(savedClip);
    920                g2.setComposite(originalComposite);
    921
    922            }
    923            if (this.drawBorder) {
    924                drawOutline(g2, area);
    925            }
    926
    927        }
    928
    929        /**
    930         * Draws the arc to represent an interval.
    931         *
    932         * @param g2  the graphics device.
    933         * @param meterArea  the drawing area.
    934         * @param interval  the interval.
    935         */
    936        protected void drawArcForInterval(Graphics2D g2, Rectangle2D meterArea,
    937                                          MeterInterval interval) {
    938
    939            double minValue = interval.getRange().getLowerBound();
    940            double maxValue = interval.getRange().getUpperBound();
    941            Paint outlinePaint = interval.getOutlinePaint();
    942            Stroke outlineStroke = interval.getOutlineStroke();
    943            Paint backgroundPaint = interval.getBackgroundPaint();
    944
    945            if (backgroundPaint != null) {
    946                fillArc(g2, meterArea, minValue, maxValue, backgroundPaint, false);
    947            }
    948            if (outlinePaint != null) {
    949                if (outlineStroke != null) {
    950                    drawArc(g2, meterArea, minValue, maxValue, outlinePaint,
    951                            outlineStroke);
    952                }
    953                drawTick(g2, meterArea, minValue, true);
    954                drawTick(g2, meterArea, maxValue, true);
    955            }
    956        }
    957
    958        /**
    959         * Draws an arc.
    960         *
    961         * @param g2  the graphics device.
    962         * @param area  the plot area.
    963         * @param minValue  the minimum value.
    964         * @param maxValue  the maximum value.
    965         * @param paint  the paint.
    966         * @param stroke  the stroke.
    967         */
    968        protected void drawArc(Graphics2D g2, Rectangle2D area, double minValue,
    969                               double maxValue, Paint paint, Stroke stroke) {
    970
    971            double startAngle = valueToAngle(maxValue);
    972            double endAngle = valueToAngle(minValue);
    973            double extent = endAngle - startAngle;
    974
    975            double x = area.getX();
    976            double y = area.getY();
    977            double w = area.getWidth();
    978            double h = area.getHeight();
    979            g2.setPaint(paint);
    980            g2.setStroke(stroke);
    981
    982            if (paint != null && stroke != null) {
    983                Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle,
    984                        extent, Arc2D.OPEN);
    985                g2.setPaint(paint);
    986                g2.setStroke(stroke);
    987                g2.draw(arc);
    988            }
    989
    990        }
    991
    992        /**
    993         * Fills an arc on the dial between the given values.
    994         *
    995         * @param g2  the graphics device.
    996         * @param area  the plot area.
    997         * @param minValue  the minimum data value.
    998         * @param maxValue  the maximum data value.
    999         * @param paint  the background paint (<code>null</code> not permitted).
    1000         * @param dial  a flag that indicates whether the arc represents the whole
    1001         *              dial.
    1002         */
    1003        protected void fillArc(Graphics2D g2, Rectangle2D area,
    1004                               double minValue, double maxValue, Paint paint,
    1005                               boolean dial) {
    1006            if (paint == null) {
    1007                throw new IllegalArgumentException("Null 'paint' argument");
    1008            }
    1009            double startAngle = valueToAngle(maxValue);
    1010            double endAngle = valueToAngle(minValue);
    1011            double extent = endAngle - startAngle;
    1012
    1013            double x = area.getX();
    1014            double y = area.getY();
    1015            double w = area.getWidth();
    1016            double h = area.getHeight();
    1017            int joinType = Arc2D.OPEN;
    1018            if (this.shape == DialShape.PIE) {
    1019                joinType = Arc2D.PIE;
    1020            }
    1021            else if (this.shape == DialShape.CHORD) {
    1022                if (dial && this.meterAngle > 180) {
    1023                    joinType = Arc2D.CHORD;
    1024                }
    1025                else {
    1026                    joinType = Arc2D.PIE;
    1027                }
    1028            }
    1029            else if (this.shape == DialShape.CIRCLE) {
    1030                joinType = Arc2D.PIE;
    1031                if (dial) {
    1032                    extent = 360;
    1033                }
    1034            }
    1035            else {
    1036                throw new IllegalStateException("DialShape not recognised.");
    1037            }
    1038
    1039            g2.setPaint(paint);
    1040            Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle, extent,
    1041                    joinType);
    1042            g2.fill(arc);
    1043        }
    1044
    1045        /**
    1046         * Translates a data value to an angle on the dial.
    1047         *
    1048         * @param value  the value.
    1049         *
    1050         * @return The angle on the dial.
    1051         */
    1052        public double valueToAngle(double value) {
    1053            value = value - this.range.getLowerBound();
    1054            double baseAngle = 180 + ((this.meterAngle - 180) / 2);
    1055            return baseAngle - ((value / this.range.getLength()) * this.meterAngle);
    1056        }
    1057
    1058        /**
    1059         * Draws the ticks that subdivide the overall range.
    1060         *
    1061         * @param g2  the graphics device.
    1062         * @param meterArea  the meter area.
    1063         * @param minValue  the minimum value.
    1064         * @param maxValue  the maximum value.
    1065         */
    1066        protected void drawTicks(Graphics2D g2, Rectangle2D meterArea,
    1067                                 double minValue, double maxValue) {
    1068            for (double v = minValue; v <= maxValue; v += this.tickSize) {
    1069                drawTick(g2, meterArea, v);
    1070            }
    1071        }
    1072
    1073        /**
    1074         * Draws a tick.
    1075         *
    1076         * @param g2  the graphics device.
    1077         * @param meterArea  the meter area.
    1078         * @param value  the value.
    1079         */
    1080        protected void drawTick(Graphics2D g2, Rectangle2D meterArea,
    1081                double value) {
    1082            drawTick(g2, meterArea, value, false);
    1083        }
    1084
    1085        /**
    1086         * Draws a tick on the dial.
    1087         *
    1088         * @param g2  the graphics device.
    1089         * @param meterArea  the meter area.
    1090         * @param value  the tick value.
    1091         * @param label  a flag that controls whether or not a value label is drawn.
    1092         */
    1093        protected void drawTick(Graphics2D g2, Rectangle2D meterArea,
    1094                                double value, boolean label) {
    1095
    1096            double valueAngle = valueToAngle(value);
    1097
    1098            double meterMiddleX = meterArea.getCenterX();
    1099            double meterMiddleY = meterArea.getCenterY();
    1100
    1101            g2.setPaint(this.tickPaint);
    1102            g2.setStroke(new BasicStroke(2.0f));
    1103
    1104            double valueP2X = 0;
    1105            double valueP2Y = 0;
    1106
    1107            double radius = (meterArea.getWidth() / 2) + DEFAULT_BORDER_SIZE;
    1108            double radius1 = radius - 15;
    1109
    1110            double valueP1X = meterMiddleX
    1111                    + (radius * Math.cos(Math.PI * (valueAngle / 180)));
    1112            double valueP1Y = meterMiddleY
    1113                    - (radius * Math.sin(Math.PI * (valueAngle / 180)));
    1114
    1115            valueP2X = meterMiddleX
    1116                    + (radius1 * Math.cos(Math.PI * (valueAngle / 180)));
    1117            valueP2Y = meterMiddleY
    1118                    - (radius1 * Math.sin(Math.PI * (valueAngle / 180)));
    1119
    1120            Line2D.Double line = new Line2D.Double(valueP1X, valueP1Y, valueP2X,
    1121                    valueP2Y);
    1122            g2.draw(line);
    1123
    1124            if (this.tickLabelsVisible && label) {
    1125
    1126                String tickLabel =  this.tickLabelFormat.format(value);
    1127                g2.setFont(this.tickLabelFont);
    1128                g2.setPaint(this.tickLabelPaint);
    1129
    1130                FontMetrics fm = g2.getFontMetrics();
    1131                Rectangle2D tickLabelBounds
    1132                    = TextUtilities.getTextBounds(tickLabel, g2, fm);
    1133
    1134                double x = valueP2X;
    1135                double y = valueP2Y;
    1136                if (valueAngle == 90 || valueAngle == 270) {
    1137                    x = x - tickLabelBounds.getWidth() / 2;
    1138                }
    1139                else if (valueAngle < 90 || valueAngle > 270) {
    1140                    x = x - tickLabelBounds.getWidth();
    1141                }
    1142                if ((valueAngle > 135 && valueAngle < 225)
    1143                        || valueAngle > 315 || valueAngle < 45) {
    1144                    y = y - tickLabelBounds.getHeight() / 2;
    1145                }
    1146                else {
    1147                    y = y + tickLabelBounds.getHeight() / 2;
    1148                }
    1149                g2.drawString(tickLabel, (float) x, (float) y);
    1150            }
    1151        }
    1152
    1153        /**
    1154         * Draws the value label just below the center of the dial.
    1155         *
    1156         * @param g2  the graphics device.
    1157         * @param area  the plot area.
    1158         */
    1159        protected void drawValueLabel(Graphics2D g2, Rectangle2D area) {
    1160            g2.setFont(this.valueFont);
    1161            g2.setPaint(this.valuePaint);
    1162            String valueStr = "No value";
    1163            if (this.dataset != null) {
    1164                Number n = this.dataset.getValue();
    1165                if (n != null) {
    1166                    valueStr = this.tickLabelFormat.format(n.doubleValue()) + " "
    1167                             + this.units;
    1168                }
    1169            }
    1170            float x = (float) area.getCenterX();
    1171            float y = (float) area.getCenterY() + DEFAULT_CIRCLE_SIZE;
    1172            TextUtilities.drawAlignedString(valueStr, g2, x, y,
    1173                    TextAnchor.TOP_CENTER);
    1174        }
    1175
    1176        /**
    1177         * Returns a short string describing the type of plot.
    1178         *
    1179         * @return A string describing the type of plot.
    1180         */
    1181        public String getPlotType() {
    1182            return localizationResources.getString("Meter_Plot");
    1183        }
    1184
    1185        /**
    1186         * A zoom method that does nothing.  Plots are required to support the
    1187         * zoom operation.  In the case of a meter plot, it doesn't make sense to
    1188         * zoom in or out, so the method is empty.
    1189         *
    1190         * @param percent   The zoom percentage.
    1191         */
    1192        public void zoom(double percent) {
    1193            // intentionally blank
    1194        }
    1195
    1196        /**
    1197         * Tests the plot for equality with an arbitrary object.  Note that the
    1198         * dataset is ignored for the purposes of testing equality.
    1199         *
    1200         * @param obj  the object (<code>null</code> permitted).
    1201         *
    1202         * @return A boolean.
    1203         */
    1204        public boolean equals(Object obj) {
    1205            if (obj == this) {
    1206                return true;
    1207            }
    1208            if (!(obj instanceof MeterPlot)) {
    1209                return false;
    1210            }
    1211            if (!super.equals(obj)) {
    1212                return false;
    1213            }
    1214            MeterPlot that = (MeterPlot) obj;
    1215            if (!ObjectUtilities.equal(this.units, that.units)) {
    1216                return false;
    1217            }
    1218            if (!ObjectUtilities.equal(this.range, that.range)) {
    1219                return false;
    1220            }
    1221            if (!ObjectUtilities.equal(this.intervals, that.intervals)) {
    1222                return false;
    1223            }
    1224            if (!PaintUtilities.equal(this.dialOutlinePaint,
    1225                    that.dialOutlinePaint)) {
    1226                return false;
    1227            }
    1228            if (this.shape != that.shape) {
    1229                return false;
    1230            }
    1231            if (!PaintUtilities.equal(this.dialBackgroundPaint,
    1232                    that.dialBackgroundPaint)) {
    1233                return false;
    1234            }
    1235            if (!PaintUtilities.equal(this.needlePaint, that.needlePaint)) {
    1236                return false;
    1237            }
    1238            if (!ObjectUtilities.equal(this.valueFont, that.valueFont)) {
    1239                return false;
    1240            }
    1241            if (!PaintUtilities.equal(this.valuePaint, that.valuePaint)) {
    1242                return false;
    1243            }
    1244            if (!PaintUtilities.equal(this.tickPaint, that.tickPaint)) {
    1245                return false;
    1246            }
    1247            if (this.tickSize != that.tickSize) {
    1248                return false;
    1249            }
    1250            if (this.tickLabelsVisible != that.tickLabelsVisible) {
    1251                return false;
    1252            }
    1253            if (!ObjectUtilities.equal(this.tickLabelFont, that.tickLabelFont)) {
    1254                return false;
    1255            }
    1256            if (!PaintUtilities.equal(this.tickLabelPaint, that.tickLabelPaint)) {
    1257                return false;
    1258            }
    1259            if (!ObjectUtilities.equal(this.tickLabelFormat,
    1260                    that.tickLabelFormat)) {
    1261                return false;
    1262            }
    1263            if (this.drawBorder != that.drawBorder) {
    1264                return false;
    1265            }
    1266            if (this.meterAngle != that.meterAngle) {
    1267                return false;
    1268            }
    1269            return true;
    1270        }
    1271
    1272        /**
    1273         * Provides serialization support.
    1274         *
    1275         * @param stream  the output stream.
    1276         *
    1277         * @throws IOException  if there is an I/O error.
    1278         */
    1279        private void writeObject(ObjectOutputStream stream) throws IOException {
    1280            stream.defaultWriteObject();
    1281            SerialUtilities.writePaint(this.dialBackgroundPaint, stream);
    1282            SerialUtilities.writePaint(this.needlePaint, stream);
    1283            SerialUtilities.writePaint(this.valuePaint, stream);
    1284            SerialUtilities.writePaint(this.tickPaint, stream);
    1285            SerialUtilities.writePaint(this.tickLabelPaint, stream);
    1286        }
    1287
    1288        /**
    1289         * Provides serialization support.
    1290         *
    1291         * @param stream  the input stream.
    1292         *
    1293         * @throws IOException  if there is an I/O error.
    1294         * @throws ClassNotFoundException  if there is a classpath problem.
    1295         */
    1296        private void readObject(ObjectInputStream stream)
    1297            throws IOException, ClassNotFoundException {
    1298            stream.defaultReadObject();
    1299            this.dialBackgroundPaint = SerialUtilities.readPaint(stream);
    1300            this.needlePaint = SerialUtilities.readPaint(stream);
    1301            this.valuePaint = SerialUtilities.readPaint(stream);
    1302            this.tickPaint = SerialUtilities.readPaint(stream);
    1303            this.tickLabelPaint = SerialUtilities.readPaint(stream);
    1304            if (this.dataset != null) {
    1305                this.dataset.addChangeListener(this);
    1306            }
    1307        }
    1308
    1309        /**
    1310         * Returns an independent copy (clone) of the plot.  The dataset is NOT
    1311         * cloned - both the original and the clone will have a reference to the
    1312         * same dataset.
    1313         *
    1314         * @return A clone.
    1315         *
    1316         * @throws CloneNotSupportedException if some component of the plot cannot
    1317         *         be cloned.
    1318         */
    1319        public Object clone() throws CloneNotSupportedException {
    1320            MeterPlot clone = (MeterPlot) super.clone();
    1321            clone.tickLabelFormat = (NumberFormat) this.tickLabelFormat.clone();
    1322            // the following relies on the fact that the intervals are immutable
    1323            clone.intervals = new java.util.ArrayList(this.intervals);
    1324            if (clone.dataset != null) {
    1325                clone.dataset.addChangeListener(clone);
    1326            }
    1327            return clone;
    1328        }
    1329
    1330    }