    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
    087package org.jfree.chart.plot;
    088
    089import java.awt.AlphaComposite;
    090import java.awt.BasicStroke;
    091import java.awt.Color;
    092import java.awt.Composite;
    093import java.awt.Font;
    094import java.awt.FontMetrics;
    095import java.awt.Graphics2D;
    096import java.awt.Paint;
    097import java.awt.Polygon;
    098import java.awt.Shape;
    099import java.awt.Stroke;
    100import java.awt.geom.Arc2D;
    101import java.awt.geom.Ellipse2D;
    102import java.awt.geom.Line2D;
    103import java.awt.geom.Point2D;
    104import java.awt.geom.Rectangle2D;
    105import java.io.IOException;
    106import java.io.ObjectInputStream;
    107import java.io.ObjectOutputStream;
    108import java.io.Serializable;
    109import java.text.NumberFormat;
    110import java.util.Collections;
    111import java.util.Iterator;
    112import java.util.List;
    113import java.util.ResourceBundle;
    114
    115import org.jfree.chart.LegendItem;
    116import org.jfree.chart.LegendItemCollection;
    117import org.jfree.chart.event.PlotChangeEvent;
    118import org.jfree.chart.util.ResourceBundleWrapper;
    119import org.jfree.data.Range;
    120import org.jfree.data.general.DatasetChangeEvent;
    121import org.jfree.data.general.ValueDataset;
    122import org.jfree.io.SerialUtilities;
    123import org.jfree.text.TextUtilities;
    124import org.jfree.ui.RectangleInsets;
    125import org.jfree.ui.TextAnchor;
    126import org.jfree.util.ObjectUtilities;
    127import 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 */
    134public 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}