content: /* ===========================================================
   * JFreeChart : a free chart library for the Java(tm) platform
   * ===========================================================
   *
   * (C) Copyright 2000-2014, 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.
   *
   * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
   * Other names may be trademarks of their respective owners.]
   *
   * --------------
   * MeterPlot.java
   * --------------
   * (C) Copyright 2000-2014, 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;
   *
   * 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);
   * 18-May-2007 : Set dataset for LegendItem (DG);
   * 29-Nov-2007 : Fixed serialization bug with dialOutlinePaint (DG);
   * 18-Dec-2008 : Use ResourceBundleWrapper - see patch 1607918 by
   *               Jess Thrysoee (DG);
   * 16-Jun-2012 : Removed JCommon dependencies (DG);
   * 10-Mar-2014 : Removed LegendItemCollection (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.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.ResourceBundle;
  
  import org.jfree.chart.LegendItem;
  import org.jfree.chart.ui.RectangleInsets;
  import org.jfree.chart.ui.TextAnchor;
  import org.jfree.chart.util.ObjectUtilities;
  import org.jfree.chart.util.PaintUtilities;
  import org.jfree.chart.event.PlotChangeEvent;
  import org.jfree.chart.text.TextUtilities;
  import org.jfree.chart.util.ResourceBundleWrapper;
  import org.jfree.chart.util.SerialUtilities;
  import org.jfree.data.Range;
  import org.jfree.data.general.DatasetChangeEvent;
  import org.jfree.data.general.ValueDataset;
  
  /**
   * 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
              = ResourceBundleWrapper.getBundle(
                      "org.jfree.chart.plot.LocalizationBundle");
  
      /**
       * A (possibly empty) list of the {@link MeterInterval}s to be highlighted
       * on the dial.
       */
      private List<MeterInterval> 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<MeterInterval>();
          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;
          fireChangeEvent();
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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;
              fireChangeEvent();
          }
      }
  
      /**
       * 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;
              fireChangeEvent();
          }
      }
  
      /**
       * 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;
              fireChangeEvent();
          }
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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;
          fireChangeEvent();
      }
  
      /**
       * 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<MeterInterval> 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);
          fireChangeEvent();
      }
  
      /**
       * Clears the intervals for the plot and sends a {@link PlotChangeEvent} to
       * all registered listeners.
       *
       * @see #addInterval(MeterInterval)
       */
      public void clearIntervals() {
          this.intervals.clear();
          fireChangeEvent();
      }
  
      /**
       * Returns an item for each interval.
       *
       * @return A collection of legend items.
       */
      @Override
      public List<LegendItem> getLegendItems() {
          List<LegendItem> result = new ArrayList<LegendItem>();
          for (MeterInterval mi : this.intervals) {
              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);
              item.setDataset(getDataset());
              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.
       */
      @Override
      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));
  
              for (MeterInterval interval : this.intervals) {
                  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;
          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;
          double valueP2Y;
  
          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.
       */
      @Override
      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.
       */
      @Override
      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.
       */
      @Override
      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.dialOutlinePaint, 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.dialOutlinePaint = 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.
       */
      @Override
      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<MeterInterval>(this.intervals);
          if (clone.dataset != null) {
              clone.dataset.addChangeListener(clone);
          }
          return clone;
      }
  
  }
  