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