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