/* ===========================================================
 * JFreeChart : a free chart library for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
 *
 * Project Info:  http://www.jfree.org/jfreechart/index.html
 *
 * This library is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU Lesser General Public License as published by 
 * the Free Software Foundation; either version 2.1 of the License, or 
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
 * USA.  
 *
 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
 * in the United States and other countries.]
 *
 * --------------
 * MeterPlot.java
 * --------------
 * (C) Copyright 2000-2007, by Hari and Contributors.
 *
 * Original Author:  Hari (ourhari@hotmail.com);
 * Contributor(s):   David Gilbert (for Object Refinery Limited);
 *                   Bob Orchard;
 *                   Arnaud Lelievre;
 *                   Nicolas Brodu;
 *                   David Bastend;
 *
 * $Id: MeterPlot.java,v 1.13.2.9 2007/03/21 10:25:00 mungady Exp $
 *
 * Changes
 * -------
 * 01-Apr-2002 : Version 1, contributed by Hari (DG);
 * 23-Apr-2002 : Moved dataset from JFreeChart to Plot (DG);
 * 22-Aug-2002 : Added changes suggest by Bob Orchard, changed Color to Paint 
 *               for consistency, plus added Javadoc comments (DG);
 * 01-Oct-2002 : Fixed errors reported by Checkstyle (DG);
 * 23-Jan-2003 : Removed one constructor (DG);
 * 26-Mar-2003 : Implemented Serializable (DG);
 * 20-Aug-2003 : Changed dataset from MeterDataset --> ValueDataset, added 
 *               equals() method,
 * 08-Sep-2003 : Added internationalization via use of properties 
 *               resourceBundle (RFE 690236) (AL); 
 *               implemented Cloneable, and various other changes (DG);
 * 08-Sep-2003 : Added serialization methods (NB);
 * 11-Sep-2003 : Added cloning support (NB);
 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
 * 25-Sep-2003 : Fix useless cloning. Correct dataset listener registration in 
 *               constructor. (NB)
 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
 * 17-Jan-2004 : Changed to allow dialBackgroundPaint to be set to null - see 
 *               bug 823628 (DG);
 * 07-Apr-2004 : Changed string bounds calculation (DG);
 * 12-May-2004 : Added tickLabelFormat attribute - see RFE 949566.  Also 
 *               updated the equals() method (DG);
 * 02-Nov-2004 : Added sanity checks for range, and only draw the needle if the 
 *               value is contained within the overall range - see bug report 
 *               1056047 (DG);
 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 
 *               release (DG);
 * 02-Feb-2005 : Added optional background paint for each region (DG);
 * 22-Mar-2005 : Removed 'normal', 'warning' and 'critical' regions and put in
 *               facility to define an arbitrary number of MeterIntervals,
 *               based on a contribution by David Bastend (DG);
 * 20-Apr-2005 : Small update for change to LegendItem constructors (DG);
 * 05-May-2005 : Updated draw() method parameters (DG);
 * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG);
 * 10-Nov-2005 : Added tickPaint, tickSize and valuePaint attributes, and
 *               put value label drawing code into a separate method (DG);
 * ------------- JFREECHART 1.0.x ---------------------------------------------
 * 05-Mar-2007 : Restore clip region correctly (see bug 1667750) (DG);
 * 
 */

package org.jfree.chart.plot;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.text.NumberFormat;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;

import org.jfree.chart.LegendItem;
import org.jfree.chart.LegendItemCollection;
import org.jfree.chart.event.PlotChangeEvent;
import org.jfree.data.Range;
import org.jfree.data.general.DatasetChangeEvent;
import org.jfree.data.general.ValueDataset;
import org.jfree.io.SerialUtilities;
import org.jfree.text.TextUtilities;
import org.jfree.ui.RectangleInsets;
import org.jfree.ui.TextAnchor;
import org.jfree.util.ObjectUtilities;
import org.jfree.util.PaintUtilities;

/**
 * A plot that displays a single value in the form of a needle on a dial.  
 * Defined ranges (for example, 'normal', 'warning' and 'critical') can be
 * highlighted on the dial.
 */
public class MeterPlot extends Plot implements Serializable, Cloneable {

    /** For serialization. */
    private static final long serialVersionUID = 2987472457734470962L;
    
    /** The default background paint. */
    static final Paint DEFAULT_DIAL_BACKGROUND_PAINT = Color.black;

    /** The default needle paint. */
    static final Paint DEFAULT_NEEDLE_PAINT = Color.green;

    /** The default value font. */
    static final Font DEFAULT_VALUE_FONT = new Font("SansSerif", Font.BOLD, 12);

    /** The default value paint. */
    static final Paint DEFAULT_VALUE_PAINT = Color.yellow;

    /** The default meter angle. */
    public static final int DEFAULT_METER_ANGLE = 270;

    /** The default border size. */
    public static final float DEFAULT_BORDER_SIZE = 3f;

    /** The default circle size. */
    public static final float DEFAULT_CIRCLE_SIZE = 10f;

    /** The default label font. */
    public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif", 
            Font.BOLD, 10);

    /** The dataset (contains a single value). */
    private ValueDataset dataset;

    /** The dial shape (background shape). */
    private DialShape shape;

    /** The dial extent (measured in degrees). */
    private int meterAngle;
    
    /** The overall range of data values on the dial. */
    private Range range;
    
    /** The tick size. */
    private double tickSize;
    
    /** The paint used to draw the ticks. */
    private transient Paint tickPaint;
    
    /** The units displayed on the dial. */    
    private String units;
    
    /** The font for the value displayed in the center of the dial. */
    private Font valueFont;

    /** The paint for the value displayed in the center of the dial. */
    private transient Paint valuePaint;

    /** A flag that controls whether or not the border is drawn. */
    private boolean drawBorder;

    /** The outline paint. */
    private transient Paint dialOutlinePaint;

    /** The paint for the dial background. */
    private transient Paint dialBackgroundPaint;

    /** The paint for the needle. */
    private transient Paint needlePaint;

    /** A flag that controls whether or not the tick labels are visible. */
    private boolean tickLabelsVisible;

    /** The tick label font. */
    private Font tickLabelFont;

    /** The tick label paint. */
    private transient Paint tickLabelPaint;
    
    /** The tick label format. */
    private NumberFormat tickLabelFormat;

    /** The resourceBundle for the localization. */
    protected static ResourceBundle localizationResources = 
        ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");

    /** 
     * A (possibly empty) list of the {@link MeterInterval}s to be highlighted 
     * on the dial. 
     */
    private List intervals;

    /**
     * Creates a new plot with a default range of <code>0</code> to 
     * <code>100</code> and no value to display.
     */
    public MeterPlot() {
        this(null);   
    }
    
    /**
     * Creates a new plot that displays the value from the supplied dataset.
     *
     * @param dataset  the dataset (<code>null</code> permitted).
     */
    public MeterPlot(ValueDataset dataset) {
        super();
        this.shape = DialShape.CIRCLE;
        this.meterAngle = DEFAULT_METER_ANGLE;
        this.range = new Range(0.0, 100.0);
        this.tickSize = 10.0;
        this.tickPaint = Color.white;
        this.units = "Units";
        this.needlePaint = MeterPlot.DEFAULT_NEEDLE_PAINT;
        this.tickLabelsVisible = true;
        this.tickLabelFont = MeterPlot.DEFAULT_LABEL_FONT;
        this.tickLabelPaint = Color.black;
        this.tickLabelFormat = NumberFormat.getInstance();
        this.valueFont = MeterPlot.DEFAULT_VALUE_FONT;
        this.valuePaint = MeterPlot.DEFAULT_VALUE_PAINT;
        this.dialBackgroundPaint = MeterPlot.DEFAULT_DIAL_BACKGROUND_PAINT;
        this.intervals = new java.util.ArrayList();
        setDataset(dataset);
    }

    /**
     * Returns the dial shape.  The default is {@link DialShape#CIRCLE}).
     * 
     * @return The dial shape (never <code>null</code>).
     * 
     * @see #setDialShape(DialShape)
     */
    public DialShape getDialShape() {
        return this.shape;
    }
    
    /**
     * Sets the dial shape and sends a {@link PlotChangeEvent} to all 
     * registered listeners.
     * 
     * @param shape  the shape (<code>null</code> not permitted).
     * 
     * @see #getDialShape()
     */
    public void setDialShape(DialShape shape) {
        if (shape == null) {
            throw new IllegalArgumentException("Null 'shape' argument.");
        }
        this.shape = shape;
        notifyListeners(new PlotChangeEvent(this));
    }
    
    /**
     * Returns the meter angle in degrees.  This defines, in part, the shape
     * of the dial.  The default is 270 degrees.
     *
     * @return The meter angle (in degrees).
     * 
     * @see #setMeterAngle(int)
     */
    public int getMeterAngle() {
        return this.meterAngle;
    }

    /**
     * Sets the angle (in degrees) for the whole range of the dial and sends 
     * a {@link PlotChangeEvent} to all registered listeners.
     * 
     * @param angle  the angle (in degrees, in the range 1-360).
     * 
     * @see #getMeterAngle()
     */
    public void setMeterAngle(int angle) {
        if (angle < 1 || angle > 360) {
            throw new IllegalArgumentException("Invalid 'angle' (" + angle 
                    + ")");
        }
        this.meterAngle = angle;
        notifyListeners(new PlotChangeEvent(this));
    }

    /**
     * Returns the overall range for the dial.
     * 
     * @return The overall range (never <code>null</code>).
     * 
     * @see #setRange(Range)
     */
    public Range getRange() {
        return this.range;    
    }
    
    /**
     * Sets the range for the dial and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     * 
     * @param range  the range (<code>null</code> not permitted and zero-length
     *               ranges not permitted).
     *             
     * @see #getRange()
     */
    public void setRange(Range range) {
        if (range == null) {
            throw new IllegalArgumentException("Null 'range' argument.");
        }
        if (!(range.getLength() > 0.0)) {
            throw new IllegalArgumentException(
                    "Range length must be positive.");
        }
        this.range = range;
        notifyListeners(new PlotChangeEvent(this));
    }
    
    /**
     * Returns the tick size (the interval between ticks on the dial).
     * 
     * @return The tick size.
     * 
     * @see #setTickSize(double)
     */
    public double getTickSize() {
        return this.tickSize;
    }
    
    /**
     * Sets the tick size and sends a {@link PlotChangeEvent} to all 
     * registered listeners.
     * 
     * @param size  the tick size (must be > 0).
     * 
     * @see #getTickSize()
     */
    public void setTickSize(double size) {
        if (size <= 0) {
            throw new IllegalArgumentException("Requires 'size' > 0.");
        }
        this.tickSize = size;
        notifyListeners(new PlotChangeEvent(this));
    }
    
    /**
     * Returns the paint used to draw the ticks around the dial. 
     * 
     * @return The paint used to draw the ticks around the dial (never 
     *         <code>null</code>).
     *         
     * @see #setTickPaint(Paint)
     */
    public Paint getTickPaint() {
        return this.tickPaint;
    }
    
    /**
     * Sets the paint used to draw the tick labels around the dial and sends
     * a {@link PlotChangeEvent} to all registered listeners.
     * 
     * @param paint  the paint (<code>null</code> not permitted).
     * 
     * @see #getTickPaint()
     */
    public void setTickPaint(Paint paint) {
        if (paint == null) {
            throw new IllegalArgumentException("Null 'paint' argument.");
        }
        this.tickPaint = paint;
        notifyListeners(new PlotChangeEvent(this));
    }

    /**
     * Returns a string describing the units for the dial.
     * 
     * @return The units (possibly <code>null</code>).
     * 
     * @see #setUnits(String)
     */
    public String getUnits() {
        return this.units;
    }
    
    /**
     * Sets the units for the dial and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     * 
     * @param units  the units (<code>null</code> permitted).
     * 
     * @see #getUnits()
     */
    public void setUnits(String units) {
        this.units = units;    
        notifyListeners(new PlotChangeEvent(this));
    }
        
    /**
     * Returns the paint for the needle.
     *
     * @return The paint (never <code>null</code>).
     * 
     * @see #setNeedlePaint(Paint)
     */
    public Paint getNeedlePaint() {
        return this.needlePaint;
    }

    /**
     * Sets the paint used to display the needle and sends a 
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param paint  the paint (<code>null</code> not permitted).
     * 
     * @see #getNeedlePaint()
     */
    public void setNeedlePaint(Paint paint) {
        if (paint == null) {
            throw new IllegalArgumentException("Null 'paint' argument.");
        }
        this.needlePaint = paint;
        notifyListeners(new PlotChangeEvent(this));
    }

    /**
     * Returns the flag that determines whether or not tick labels are visible.
     *
     * @return The flag.
     * 
     * @see #setTickLabelsVisible(boolean)
     */
    public boolean getTickLabelsVisible() {
        return this.tickLabelsVisible;
    }

    /**
     * Sets the flag that controls whether or not the tick labels are visible
     * and sends a {@link PlotChangeEvent} to all registered listeners.
     *
     * @param visible  the flag.
     * 
     * @see #getTickLabelsVisible()
     */
    public void setTickLabelsVisible(boolean visible) {
        if (this.tickLabelsVisible != visible) {
            this.tickLabelsVisible = visible;
            notifyListeners(new PlotChangeEvent(this));
        }
    }

    /**
     * Returns the tick label font.
     *
     * @return The font (never <code>null</code>).
     * 
     * @see #setTickLabelFont(Font)
     */
    public Font getTickLabelFont() {
        return this.tickLabelFont;
    }

    /**
     * Sets the tick label font and sends a {@link PlotChangeEvent} to all 
     * registered listeners.
     *
     * @param font  the font (<code>null</code> not permitted).
     * 
     * @see #getTickLabelFont()
     */
    public void setTickLabelFont(Font font) {
        if (font == null) {
            throw new IllegalArgumentException("Null 'font' argument.");
        }
        if (!this.tickLabelFont.equals(font)) {
            this.tickLabelFont = font;
            notifyListeners(new PlotChangeEvent(this));
        }
    }

    /**
     * Returns the tick label paint.
     *
     * @return The paint (never <code>null</code>).
     * 
     * @see #setTickLabelPaint(Paint)
     */
    public Paint getTickLabelPaint() {
        return this.tickLabelPaint;
    }

    /**
     * Sets the tick label paint and sends a {@link PlotChangeEvent} to all 
     * registered listeners.
     *
     * @param paint  the paint (<code>null</code> not permitted).
     * 
     * @see #getTickLabelPaint()
     */
    public void setTickLabelPaint(Paint paint) {
        if (paint == null) {
            throw new IllegalArgumentException("Null 'paint' argument.");
        }
        if (!this.tickLabelPaint.equals(paint)) {
            this.tickLabelPaint = paint;
            notifyListeners(new PlotChangeEvent(this));
        }
    }

    /**
     * Returns the tick label format.
     * 
     * @return The tick label format (never <code>null</code>).
     * 
     * @see #setTickLabelFormat(NumberFormat)
     */
    public NumberFormat getTickLabelFormat() {
        return this.tickLabelFormat;    
    }
    
    /**
     * Sets the format for the tick labels and sends a {@link PlotChangeEvent} 
     * to all registered listeners.
     * 
     * @param format  the format (<code>null</code> not permitted).
     * 
     * @see #getTickLabelFormat()
     */
    public void setTickLabelFormat(NumberFormat format) {
        if (format == null) {
            throw new IllegalArgumentException("Null 'format' argument.");   
        }
        this.tickLabelFormat = format;
        notifyListeners(new PlotChangeEvent(this));
    }
    
    /**
     * Returns the font for the value label.
     *
     * @return The font (never <code>null</code>).
     * 
     * @see #setValueFont(Font)
     */
    public Font getValueFont() {
        return this.valueFont;
    }

    /**
     * Sets the font used to display the value label and sends a 
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param font  the font (<code>null</code> not permitted).
     * 
     * @see #getValueFont()
     */
    public void setValueFont(Font font) {
        if (font == null) {
            throw new IllegalArgumentException("Null 'font' argument.");
        }
        this.valueFont = font;
        notifyListeners(new PlotChangeEvent(this));
    }

    /**
     * Returns the paint for the value label.
     *
     * @return The paint (never <code>null</code>).
     * 
     * @see #setValuePaint(Paint)
     */
    public Paint getValuePaint() {
        return this.valuePaint;
    }

    /**
     * Sets the paint used to display the value label and sends a 
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param paint  the paint (<code>null</code> not permitted).
     * 
     * @see #getValuePaint()
     */
    public void setValuePaint(Paint paint) {
        if (paint == null) {
            throw new IllegalArgumentException("Null 'paint' argument.");
        }
        this.valuePaint = paint;
        notifyListeners(new PlotChangeEvent(this));
    }

    /**
     * Returns the paint for the dial background.
     *
     * @return The paint (possibly <code>null</code>).
     * 
     * @see #setDialBackgroundPaint(Paint)
     */
    public Paint getDialBackgroundPaint() {
        return this.dialBackgroundPaint;
    }

    /**
     * Sets the paint used to fill the dial background.  Set this to 
     * <code>null</code> for no background.
     *
     * @param paint  the paint (<code>null</code> permitted).
     * 
     * @see #getDialBackgroundPaint()
     */
    public void setDialBackgroundPaint(Paint paint) {
        this.dialBackgroundPaint = paint;
        notifyListeners(new PlotChangeEvent(this));
    }

    /**
     * Returns a flag that controls whether or not a rectangular border is 
     * drawn around the plot area.
     *
     * @return A flag.
     * 
     * @see #setDrawBorder(boolean)
     */
    public boolean getDrawBorder() {
        return this.drawBorder;
    }

    /**
     * Sets the flag that controls whether or not a rectangular border is drawn
     * around the plot area and sends a {@link PlotChangeEvent} to all 
     * registered listeners.
     *
     * @param draw  the flag.
     * 
     * @see #getDrawBorder()
     */
    public void setDrawBorder(boolean draw) {
        // TODO: fix output when this flag is set to true
        this.drawBorder = draw;
        notifyListeners(new PlotChangeEvent(this));
    }

    /**
     * Returns the dial outline paint.
     *
     * @return The paint.
     * 
     * @see #setDialOutlinePaint(Paint)
     */
    public Paint getDialOutlinePaint() {
        return this.dialOutlinePaint;
    }

    /**
     * Sets the dial outline paint and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param paint  the paint.
     * 
     * @see #getDialOutlinePaint()
     */
    public void setDialOutlinePaint(Paint paint) {
        this.dialOutlinePaint = paint;
        notifyListeners(new PlotChangeEvent(this));        
    }

    /**
     * Returns the dataset for the plot.
     * 
     * @return The dataset (possibly <code>null</code>).
     * 
     * @see #setDataset(ValueDataset)
     */
    public ValueDataset getDataset() {
        return this.dataset;
    }
    
    /**
     * Sets the dataset for the plot, replacing the existing dataset if there 
     * is one, and triggers a {@link PlotChangeEvent}.
     * 
     * @param dataset  the dataset (<code>null</code> permitted).
     * 
     * @see #getDataset()
     */
    public void setDataset(ValueDataset dataset) {
        
        // if there is an existing dataset, remove the plot from the list of 
        // change listeners...
        ValueDataset existing = this.dataset;
        if (existing != null) {
            existing.removeChangeListener(this);
        }

        // set the new dataset, and register the chart as a change listener...
        this.dataset = dataset;
        if (dataset != null) {
            setDatasetGroup(dataset.getGroup());
            dataset.addChangeListener(this);
        }

        // send a dataset change event to self...
        DatasetChangeEvent event = new DatasetChangeEvent(this, dataset);
        datasetChanged(event);
        
    }

    /**
     * Returns an unmodifiable list of the intervals for the plot.
     * 
     * @return A list.
     * 
     * @see #addInterval(MeterInterval)
     */
    public List getIntervals() {
        return Collections.unmodifiableList(this.intervals);
    }
    
    /**
     * Adds an interval and sends a {@link PlotChangeEvent} to all registered
     * listeners.
     * 
     * @param interval  the interval (<code>null</code> not permitted).
     * 
     * @see #getIntervals()
     * @see #clearIntervals()
     */
    public void addInterval(MeterInterval interval) {
        if (interval == null) {
            throw new IllegalArgumentException("Null 'interval' argument.");
        }
        this.intervals.add(interval);
        notifyListeners(new PlotChangeEvent(this));
    }
    
    /**
     * Clears the intervals for the plot and sends a {@link PlotChangeEvent} to
     * all registered listeners.
     * 
     * @see #addInterval(MeterInterval)
     */
    public void clearIntervals() {
        this.intervals.clear();
        notifyListeners(new PlotChangeEvent(this));
    }
    
    /**
     * Returns an item for each interval.
     *
     * @return A collection of legend items.
     */
    public LegendItemCollection getLegendItems() {
        LegendItemCollection result = new LegendItemCollection();
        Iterator iterator = this.intervals.iterator();
        while (iterator.hasNext()) {
            MeterInterval mi = (MeterInterval) iterator.next();
            Paint color = mi.getBackgroundPaint();
            if (color == null) {
                color = mi.getOutlinePaint();
            }
            LegendItem item = new LegendItem(mi.getLabel(), mi.getLabel(),
                    null, null, new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0), 
                    color);
            result.add(item);
        }
        return result;
    }

    /**
     * Draws the plot on a Java 2D graphics device (such as the screen or a 
     * printer).
     *
     * @param g2  the graphics device.
     * @param area  the area within which the plot should be drawn.
     * @param anchor  the anchor point (<code>null</code> permitted).
     * @param parentState  the state from the parent plot, if there is one.
     * @param info  collects info about the drawing.
     */
    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
                     PlotState parentState,
                     PlotRenderingInfo info) {

        if (info != null) {
            info.setPlotArea(area);
        }

        // adjust for insets...
        RectangleInsets insets = getInsets();
        insets.trim(area);

        area.setRect(area.getX() + 4, area.getY() + 4, area.getWidth() - 8, 
                area.getHeight() - 8);

        // draw the background
        if (this.drawBorder) {
            drawBackground(g2, area);
        }

        // adjust the plot area by the interior spacing value
        double gapHorizontal = (2 * DEFAULT_BORDER_SIZE);
        double gapVertical = (2 * DEFAULT_BORDER_SIZE);
        double meterX = area.getX() + gapHorizontal / 2;
        double meterY = area.getY() + gapVertical / 2;
        double meterW = area.getWidth() - gapHorizontal;
        double meterH = area.getHeight() - gapVertical
                + ((this.meterAngle <= 180) && (this.shape != DialShape.CIRCLE)
                ? area.getHeight() / 1.25 : 0);

        double min = Math.min(meterW, meterH) / 2;
        meterX = (meterX + meterX + meterW) / 2 - min;
        meterY = (meterY + meterY + meterH) / 2 - min;
        meterW = 2 * min;
        meterH = 2 * min;

        Rectangle2D meterArea = new Rectangle2D.Double(meterX, meterY, meterW, 
                meterH);

        Rectangle2D.Double originalArea = new Rectangle2D.Double(
                meterArea.getX() - 4, meterArea.getY() - 4, 
                meterArea.getWidth() + 8, meterArea.getHeight() + 8);

        double meterMiddleX = meterArea.getCenterX();
        double meterMiddleY = meterArea.getCenterY();

        // plot the data (unless the dataset is null)...
        ValueDataset data = getDataset();
        if (data != null) {
            double dataMin = this.range.getLowerBound();
            double dataMax = this.range.getUpperBound();

            Shape savedClip = g2.getClip();
            g2.clip(originalArea);
            Composite originalComposite = g2.getComposite();
            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                    getForegroundAlpha()));

            if (this.dialBackgroundPaint != null) {
                fillArc(g2, originalArea, dataMin, dataMax, 
                        this.dialBackgroundPaint, true);
            }
            drawTicks(g2, meterArea, dataMin, dataMax);
            drawArcForInterval(g2, meterArea, new MeterInterval("", this.range,
                    this.dialOutlinePaint, new BasicStroke(1.0f), null));
            
            Iterator iterator = this.intervals.iterator();
            while (iterator.hasNext()) {
                MeterInterval interval = (MeterInterval) iterator.next();
                drawArcForInterval(g2, meterArea, interval);
            }

            Number n = data.getValue();
            if (n != null) {
                double value = n.doubleValue();
                drawValueLabel(g2, meterArea);
  
                if (this.range.contains(value)) {
                    g2.setPaint(this.needlePaint);
                    g2.setStroke(new BasicStroke(2.0f));

                    double radius = (meterArea.getWidth() / 2) 
                                    + DEFAULT_BORDER_SIZE + 15;
                    double valueAngle = valueToAngle(value);
                    double valueP1 = meterMiddleX 
                            + (radius * Math.cos(Math.PI * (valueAngle / 180)));
                    double valueP2 = meterMiddleY 
                            - (radius * Math.sin(Math.PI * (valueAngle / 180)));

                    Polygon arrow = new Polygon();
                    if ((valueAngle > 135 && valueAngle < 225)
                        || (valueAngle < 45 && valueAngle > -45)) {

                        double valueP3 = (meterMiddleY 
                                - DEFAULT_CIRCLE_SIZE / 4);
                        double valueP4 = (meterMiddleY 
                                + DEFAULT_CIRCLE_SIZE / 4);
                        arrow.addPoint((int) meterMiddleX, (int) valueP3);
                        arrow.addPoint((int) meterMiddleX, (int) valueP4);
 
                    }
                    else {
                        arrow.addPoint((int) (meterMiddleX 
                                - DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY);
                        arrow.addPoint((int) (meterMiddleX 
                                + DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY);
                    }
                    arrow.addPoint((int) valueP1, (int) valueP2);
                    g2.fill(arrow);

                    Ellipse2D circle = new Ellipse2D.Double(meterMiddleX 
                            - DEFAULT_CIRCLE_SIZE / 2, meterMiddleY 
                            - DEFAULT_CIRCLE_SIZE / 2, DEFAULT_CIRCLE_SIZE, 
                            DEFAULT_CIRCLE_SIZE);
                    g2.fill(circle);
                }
            }
                
            g2.setClip(savedClip);
            g2.setComposite(originalComposite);

        }
        if (this.drawBorder) {
            drawOutline(g2, area);
        }

    }

    /**
     * Draws the arc to represent an interval.
     *
     * @param g2  the graphics device.
     * @param meterArea  the drawing area.
     * @param interval  the interval.
     */
    protected void drawArcForInterval(Graphics2D g2, Rectangle2D meterArea, 
                                      MeterInterval interval) {

        double minValue = interval.getRange().getLowerBound();
        double maxValue = interval.getRange().getUpperBound();
        Paint outlinePaint = interval.getOutlinePaint();
        Stroke outlineStroke = interval.getOutlineStroke();
        Paint backgroundPaint = interval.getBackgroundPaint();
 
        if (backgroundPaint != null) {
            fillArc(g2, meterArea, minValue, maxValue, backgroundPaint, false);
        }
        if (outlinePaint != null) {
            if (outlineStroke != null) {
                drawArc(g2, meterArea, minValue, maxValue, outlinePaint, 
                        outlineStroke);
            }
            drawTick(g2, meterArea, minValue, true);
            drawTick(g2, meterArea, maxValue, true);
        }
    }

    /**
     * Draws an arc.
     *
     * @param g2  the graphics device.
     * @param area  the plot area.
     * @param minValue  the minimum value.
     * @param maxValue  the maximum value.
     * @param paint  the paint.
     * @param stroke  the stroke.
     */
    protected void drawArc(Graphics2D g2, Rectangle2D area, double minValue, 
                           double maxValue, Paint paint, Stroke stroke) {

        double startAngle = valueToAngle(maxValue);
        double endAngle = valueToAngle(minValue);
        double extent = endAngle - startAngle;

        double x = area.getX();
        double y = area.getY();
        double w = area.getWidth();
        double h = area.getHeight();
        g2.setPaint(paint);
        g2.setStroke(stroke);

        if (paint != null && stroke != null) {
            Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle, 
                    extent, Arc2D.OPEN);
            g2.setPaint(paint); 
            g2.setStroke(stroke);
            g2.draw(arc);
        }

    }

    /**
     * Fills an arc on the dial between the given values.
     *
     * @param g2  the graphics device.
     * @param area  the plot area.
     * @param minValue  the minimum data value.
     * @param maxValue  the maximum data value.
     * @param paint  the background paint (<code>null</code> not permitted).
     * @param dial  a flag that indicates whether the arc represents the whole 
     *              dial.
     */
    protected void fillArc(Graphics2D g2, Rectangle2D area, 
                           double minValue, double maxValue, Paint paint,
                           boolean dial) {
        if (paint == null) {
            throw new IllegalArgumentException("Null 'paint' argument");
        }
        double startAngle = valueToAngle(maxValue);
        double endAngle = valueToAngle(minValue);
        double extent = endAngle - startAngle;

        double x = area.getX();
        double y = area.getY();
        double w = area.getWidth();
        double h = area.getHeight();
        int joinType = Arc2D.OPEN;
        if (this.shape == DialShape.PIE) {
            joinType = Arc2D.PIE;
        }
        else if (this.shape == DialShape.CHORD) {
            if (dial && this.meterAngle > 180) {
                joinType = Arc2D.CHORD;
            }
            else {
                joinType = Arc2D.PIE;
            }
        }
        else if (this.shape == DialShape.CIRCLE) {
            joinType = Arc2D.PIE;
            if (dial) {
                extent = 360;
            }
        }
        else {
            throw new IllegalStateException("DialShape not recognised.");
        }

        g2.setPaint(paint);
        Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle, extent, 
                joinType);
        g2.fill(arc);
    }
    
    /**
     * Translates a data value to an angle on the dial.
     *
     * @param value  the value.
     *
     * @return The angle on the dial.
     */
    public double valueToAngle(double value) {
        value = value - this.range.getLowerBound();
        double baseAngle = 180 + ((this.meterAngle - 180) / 2);
        return baseAngle - ((value / this.range.getLength()) * this.meterAngle);
    }

    /**
     * Draws the ticks that subdivide the overall range.
     *
     * @param g2  the graphics device.
     * @param meterArea  the meter area.
     * @param minValue  the minimum value.
     * @param maxValue  the maximum value.
     */
    protected void drawTicks(Graphics2D g2, Rectangle2D meterArea, 
                             double minValue, double maxValue) {
        for (double v = minValue; v <= maxValue; v += this.tickSize) {
            drawTick(g2, meterArea, v);
        }
    }

    /**
     * Draws a tick.
     *
     * @param g2  the graphics device.
     * @param meterArea  the meter area.
     * @param value  the value.
     */
    protected void drawTick(Graphics2D g2, Rectangle2D meterArea, 
            double value) {
        drawTick(g2, meterArea, value, false);
    }

    /**
     * Draws a tick on the dial.
     *
     * @param g2  the graphics device.
     * @param meterArea  the meter area.
     * @param value  the tick value.
     * @param label  a flag that controls whether or not a value label is drawn.
     */
    protected void drawTick(Graphics2D g2, Rectangle2D meterArea,
                            double value, boolean label) {

        double valueAngle = valueToAngle(value);

        double meterMiddleX = meterArea.getCenterX();
        double meterMiddleY = meterArea.getCenterY();

        g2.setPaint(this.tickPaint);
        g2.setStroke(new BasicStroke(2.0f));

        double valueP2X = 0;
        double valueP2Y = 0;

        double radius = (meterArea.getWidth() / 2) + DEFAULT_BORDER_SIZE;
        double radius1 = radius - 15;

        double valueP1X = meterMiddleX 
                + (radius * Math.cos(Math.PI * (valueAngle / 180)));
        double valueP1Y = meterMiddleY 
                - (radius * Math.sin(Math.PI * (valueAngle / 180)));

        valueP2X = meterMiddleX 
                + (radius1 * Math.cos(Math.PI * (valueAngle / 180)));
        valueP2Y = meterMiddleY 
                - (radius1 * Math.sin(Math.PI * (valueAngle / 180)));

        Line2D.Double line = new Line2D.Double(valueP1X, valueP1Y, valueP2X, 
                valueP2Y);
        g2.draw(line);

        if (this.tickLabelsVisible && label) {

            String tickLabel =  this.tickLabelFormat.format(value);
            g2.setFont(this.tickLabelFont);
            g2.setPaint(this.tickLabelPaint);

            FontMetrics fm = g2.getFontMetrics();
            Rectangle2D tickLabelBounds 
                = TextUtilities.getTextBounds(tickLabel, g2, fm);

            double x = valueP2X;
            double y = valueP2Y;
            if (valueAngle == 90 || valueAngle == 270) {
                x = x - tickLabelBounds.getWidth() / 2;
            }
            else if (valueAngle < 90 || valueAngle > 270) {
                x = x - tickLabelBounds.getWidth();
            }
            if ((valueAngle > 135 && valueAngle < 225) 
                    || valueAngle > 315 || valueAngle < 45) {
                y = y - tickLabelBounds.getHeight() / 2;
            }
            else {
                y = y + tickLabelBounds.getHeight() / 2;
            }
            g2.drawString(tickLabel, (float) x, (float) y);
        }
    }
    
    /**
     * Draws the value label just below the center of the dial.
     * 
     * @param g2  the graphics device.
     * @param area  the plot area.
     */
    protected void drawValueLabel(Graphics2D g2, Rectangle2D area) {
        g2.setFont(this.valueFont);
        g2.setPaint(this.valuePaint);
        String valueStr = "No value";
        if (this.dataset != null) {
            Number n = this.dataset.getValue();
            if (n != null) {
                valueStr = this.tickLabelFormat.format(n.doubleValue()) + " " 
                         + this.units;
            }
        }
        float x = (float) area.getCenterX();
        float y = (float) area.getCenterY() + DEFAULT_CIRCLE_SIZE;
        TextUtilities.drawAlignedString(valueStr, g2, x, y, 
                TextAnchor.TOP_CENTER);
    }

    /**
     * Returns a short string describing the type of plot.
     *
     * @return A string describing the type of plot.
     */
    public String getPlotType() {
        return localizationResources.getString("Meter_Plot");
    }

    /**
     * A zoom method that does nothing.  Plots are required to support the 
     * zoom operation.  In the case of a meter plot, it doesn't make sense to 
     * zoom in or out, so the method is empty.
     *
     * @param percent   The zoom percentage.
     */
    public void zoom(double percent) {
        // intentionally blank
    }
    
    /**
     * Tests the plot for equality with an arbitrary object.  Note that the 
     * dataset is ignored for the purposes of testing equality.
     * 
     * @param obj  the object (<code>null</code> permitted).
     * 
     * @return A boolean.
     */
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }   
        if (!(obj instanceof MeterPlot)) {
            return false;   
        }
        if (!super.equals(obj)) {
            return false;
        }
        MeterPlot that = (MeterPlot) obj;
        if (!ObjectUtilities.equal(this.units, that.units)) {
            return false;   
        }
        if (!ObjectUtilities.equal(this.range, that.range)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.intervals, that.intervals)) {
            return false;   
        }
        if (!PaintUtilities.equal(this.dialOutlinePaint, 
                that.dialOutlinePaint)) {
            return false;   
        }
        if (this.shape != that.shape) {
            return false;   
        }
        if (!PaintUtilities.equal(this.dialBackgroundPaint, 
                that.dialBackgroundPaint)) {
            return false;   
        }
        if (!PaintUtilities.equal(this.needlePaint, that.needlePaint)) {
            return false;   
        }
        if (!ObjectUtilities.equal(this.valueFont, that.valueFont)) {
            return false;   
        }
        if (!PaintUtilities.equal(this.valuePaint, that.valuePaint)) {
            return false;   
        }
        if (!PaintUtilities.equal(this.tickPaint, that.tickPaint)) {
            return false;
        }
        if (this.tickSize != that.tickSize) {
            return false;
        }
        if (this.tickLabelsVisible != that.tickLabelsVisible) {
            return false;   
        }
        if (!ObjectUtilities.equal(this.tickLabelFont, that.tickLabelFont)) {
            return false;   
        }
        if (!PaintUtilities.equal(this.tickLabelPaint, that.tickLabelPaint)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.tickLabelFormat, 
                that.tickLabelFormat)) {
            return false;   
        }
        if (this.drawBorder != that.drawBorder) {
            return false;   
        }
        if (this.meterAngle != that.meterAngle) {
            return false;   
        }
        return true;      
    }
    
    /**
     * Provides serialization support.
     *
     * @param stream  the output stream.
     *
     * @throws IOException  if there is an I/O error.
     */
    private void writeObject(ObjectOutputStream stream) throws IOException {
        stream.defaultWriteObject();
        SerialUtilities.writePaint(this.dialBackgroundPaint, stream);
        SerialUtilities.writePaint(this.needlePaint, stream);
        SerialUtilities.writePaint(this.valuePaint, stream);
        SerialUtilities.writePaint(this.tickPaint, stream);
        SerialUtilities.writePaint(this.tickLabelPaint, stream);
    }
    
    /**
     * Provides serialization support.
     *
     * @param stream  the input stream.
     *
     * @throws IOException  if there is an I/O error.
     * @throws ClassNotFoundException  if there is a classpath problem.
     */
    private void readObject(ObjectInputStream stream) 
        throws IOException, ClassNotFoundException {
        stream.defaultReadObject();
        this.dialBackgroundPaint = SerialUtilities.readPaint(stream);
        this.needlePaint = SerialUtilities.readPaint(stream);
        this.valuePaint = SerialUtilities.readPaint(stream);
        this.tickPaint = SerialUtilities.readPaint(stream);
        this.tickLabelPaint = SerialUtilities.readPaint(stream);
        if (this.dataset != null) {
            this.dataset.addChangeListener(this);
        }
    }

    /** 
     * Returns an independent copy (clone) of the plot.  The dataset is NOT 
     * cloned - both the original and the clone will have a reference to the
     * same dataset.
     * 
     * @return A clone.
     * 
     * @throws CloneNotSupportedException if some component of the plot cannot
     *         be cloned.
     */
    public Object clone() throws CloneNotSupportedException {
        MeterPlot clone = (MeterPlot) super.clone();
        clone.tickLabelFormat = (NumberFormat) this.tickLabelFormat.clone();
        // the following relies on the fact that the intervals are immutable
        clone.intervals = new java.util.ArrayList(this.intervals);
        if (clone.dataset != null) {
            clone.dataset.addChangeListener(clone); 
        }
        return clone;
    }

}
