/* ===========================================================
 * JFreeChart : a free chart library for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2000-present, by David Gilbert 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.]
 *
 * ------------
 * PiePlot.java
 * ------------
 * (C) Copyright 2000-present, by Andrzej Porebski and Contributors.
 *
 * Original Author:  Andrzej Porebski;
 * Contributor(s):   David Gilbert;
 *                   Martin Cordova (percentages in labels);
 *                   Richard Atkinson (URL support for image maps);
 *                   Christian W. Zuckschwerdt;
 *                   Arnaud Lelievre;
 *                   Martin Hilpert (patch 1891849);
 *                   Andreas Schroeder (very minor);
 *                   Christoph Beck (bug 2121818);
 *                   Tracy Hiltbrand (Added generics for bug fix);
 * 
 */

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.RadialGradientPaint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.Arc2D;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.QuadCurve2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.TreeMap;
import org.jfree.chart.JFreeChart;

import org.jfree.chart.LegendItem;
import org.jfree.chart.LegendItemCollection;
import org.jfree.chart.PaintMap;
import org.jfree.chart.StrokeMap;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.entity.PieSectionEntity;
import org.jfree.chart.event.PlotChangeEvent;
import org.jfree.chart.labels.PieSectionLabelGenerator;
import org.jfree.chart.labels.PieToolTipGenerator;
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
import org.jfree.chart.text.G2TextMeasurer;
import org.jfree.chart.text.TextBlock;
import org.jfree.chart.text.TextBox;
import org.jfree.chart.text.TextUtils;
import org.jfree.chart.ui.RectangleAnchor;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.chart.ui.TextAnchor;
import org.jfree.chart.urls.PieURLGenerator;
import org.jfree.chart.util.ObjectUtils;
import org.jfree.chart.util.PaintUtils;
import org.jfree.chart.util.Args;
import org.jfree.chart.util.PublicCloneable;
import org.jfree.chart.util.ResourceBundleWrapper;
import org.jfree.chart.util.Rotation;
import org.jfree.chart.util.SerialUtils;
import org.jfree.chart.util.ShadowGenerator;
import org.jfree.chart.util.ShapeUtils;
import org.jfree.chart.util.UnitType;
import org.jfree.data.DefaultKeyedValues;
import org.jfree.data.KeyedValues;
import org.jfree.data.general.DatasetChangeEvent;
import org.jfree.data.general.DatasetUtils;
import org.jfree.data.general.PieDataset;

/**
 * A plot that displays data in the form of a pie chart, using data from any
 * class that implements the {@link PieDataset} interface.
 * The example shown here is generated by the {@code PieChartDemo2.java}
 * program included in the JFreeChart Demo Collection:
 * <br><br>
 * <img src="doc-files/PieChartDemo2.svg" alt="PieChartDemo2.svg">
 * <P>
 * Special notes:
 * <ol>
 * <li>the default starting point is 12 o'clock and the pie sections proceed
 * in a clockwise direction, but these settings can be changed;</li>
 * <li>negative values in the dataset are ignored;</li>
 * <li>there are utility methods for creating a {@link PieDataset} from a
 * {@link org.jfree.data.category.CategoryDataset};</li>
 * </ol>
 *
 * @param <K> Key type for PieDataset
 * 
 * @see Plot
 * @see PieDataset
 */
public class PiePlot<K extends Comparable<K>> extends Plot implements Cloneable, Serializable {

    /** For serialization. */
    private static final long serialVersionUID = -795612466005590431L;

    /** The default interior gap. */
    public static final double DEFAULT_INTERIOR_GAP = 0.08;

    /** The maximum interior gap (currently 40%). */
    public static final double MAX_INTERIOR_GAP = 0.40;

    /** The default starting angle for the pie chart. */
    public static final double DEFAULT_START_ANGLE = 90.0;

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

    /** The default section label paint. */
    public static final Paint DEFAULT_LABEL_PAINT = Color.BLACK;

    /** The default section label background paint. */
    public static final Paint DEFAULT_LABEL_BACKGROUND_PAINT = new Color(255,
            255, 192);

    /** The default section label outline paint. */
    public static final Paint DEFAULT_LABEL_OUTLINE_PAINT = Color.BLACK;

    /** The default section label outline stroke. */
    public static final Stroke DEFAULT_LABEL_OUTLINE_STROKE = new BasicStroke(
            0.5f);

    /** The default section label shadow paint. */
    public static final Paint DEFAULT_LABEL_SHADOW_PAINT = new Color(151, 151,
            151, 128);

    /** The default minimum arc angle to draw. */
    public static final double DEFAULT_MINIMUM_ARC_ANGLE_TO_DRAW = 0.00001;

    /** The dataset for the pie chart. */
    private PieDataset<K> dataset;

    /** The pie index (used by the {@link MultiplePiePlot} class). */
    private int pieIndex;

    /**
     * The amount of space left around the outside of the pie plot, expressed
     * as a percentage of the plot area width and height.
     */
    private double interiorGap;

    /** Flag determining whether to draw an ellipse or a perfect circle. */
    private boolean circular;

    /** The starting angle. */
    private double startAngle;

    /** The direction for the pie segments. */
    private Rotation direction;

    /** The section paint map. */
    private PaintMap sectionPaintMap;

    /** The default section paint (fallback). */
    private transient Paint defaultSectionPaint;

    /**
     * A flag that controls whether or not the section paint is auto-populated
     * from the drawing supplier.
     */
    private boolean autoPopulateSectionPaint;

    /**
     * A flag that controls whether or not an outline is drawn for each
     * section in the plot.
     */
    private boolean sectionOutlinesVisible;

    /** The section outline paint map. */
    private PaintMap sectionOutlinePaintMap;

    /** The default section outline paint (fallback). */
    private transient Paint defaultSectionOutlinePaint;

    /**
     * A flag that controls whether or not the section outline paint is
     * auto-populated from the drawing supplier.
     */
    private boolean autoPopulateSectionOutlinePaint;

    /** The section outline stroke map. */
    private StrokeMap sectionOutlineStrokeMap;

    /** The default section outline stroke (fallback). */
    private transient Stroke defaultSectionOutlineStroke;

    /**
     * A flag that controls whether or not the section outline stroke is
     * auto-populated from the drawing supplier.
     */
    private boolean autoPopulateSectionOutlineStroke;

    /** The shadow paint. */
    private transient Paint shadowPaint = Color.GRAY;

    /** The x-offset for the shadow effect. */
    private double shadowXOffset = 4.0f;

    /** The y-offset for the shadow effect. */
    private double shadowYOffset = 4.0f;

    /** The percentage amount to explode each pie section. */
    private Map<K, Double> explodePercentages;

    /** The section label generator. */
    private PieSectionLabelGenerator labelGenerator;

    /** The font used to display the section labels. */
    private Font labelFont;

    /** The color used to draw the section labels. */
    private transient Paint labelPaint;

    /**
     * The color used to draw the background of the section labels.  If this
     * is {@code null}, the background is not filled.
     */
    private transient Paint labelBackgroundPaint;

    /**
     * The paint used to draw the outline of the section labels
     * ({@code null} permitted).
     */
    private transient Paint labelOutlinePaint;

    /**
     * The stroke used to draw the outline of the section labels
     * ({@code null} permitted).
     */
    private transient Stroke labelOutlineStroke;

    /**
     * The paint used to draw the shadow for the section labels
     * ({@code null} permitted).
     */
    private transient Paint labelShadowPaint;

    /**
     * A flag that controls whether simple or extended labels are used.
     */
    private boolean simpleLabels = true;

    /**
     * The padding between the labels and the label outlines.  This is not
     * allowed to be {@code null}.
     */
    private RectangleInsets labelPadding;

    /**
     * The simple label offset.
     */
    private RectangleInsets simpleLabelOffset;

    /** The maximum label width as a percentage of the plot width. */
    private double maximumLabelWidth = 0.14;

    /**
     * The gap between the labels and the link corner, as a percentage of the
     * plot width.
     */
    private double labelGap = 0.025;

    /** A flag that controls whether or not the label links are drawn. */
    private boolean labelLinksVisible;

    /**
     * The label link style.
     */
    private PieLabelLinkStyle labelLinkStyle = PieLabelLinkStyle.STANDARD;

    /** The link margin. */
    private double labelLinkMargin = 0.025;

    /** The paint used for the label linking lines. */
    private transient Paint labelLinkPaint = Color.BLACK;

    /** The stroke used for the label linking lines. */
    private transient Stroke labelLinkStroke = new BasicStroke(0.5f);

    /**
     * The pie section label distributor.
     */
    private AbstractPieLabelDistributor labelDistributor;

    /** The tooltip generator. */
    private PieToolTipGenerator toolTipGenerator;

    /** The URL generator. */
    private PieURLGenerator urlGenerator;

    /** The legend label generator. */
    private PieSectionLabelGenerator legendLabelGenerator;

    /** A tool tip generator for the legend. */
    private PieSectionLabelGenerator legendLabelToolTipGenerator;

    /**
     * A URL generator for the legend items (optional).
     */
    private PieURLGenerator legendLabelURLGenerator;

    /**
     * A flag that controls whether {@code null} values are ignored.
     */
    private boolean ignoreNullValues;

    /**
     * A flag that controls whether zero values are ignored.
     */
    private boolean ignoreZeroValues;

    /** The legend item shape. */
    private transient Shape legendItemShape;

    /**
     * The smallest arc angle that will get drawn (this is to avoid a bug in
     * various Java implementations that causes the JVM to crash).  See this
     * link for details:
     *
     * http://www.jfree.org/phpBB2/viewtopic.php?t=2707
     *
     * ...and this bug report in the Java Bug Parade:
     *
     * http://developer.java.sun.com/developer/bugParade/bugs/4836495.html
     */
    private double minimumArcAngleToDraw;

    /**
     * The shadow generator for the plot ({@code null} permitted).
     */
    private ShadowGenerator shadowGenerator;

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

    /**
     * This debug flag controls whether or not an outline is drawn showing the
     * interior of the plot region.  This is drawn as a lightGray rectangle
     * showing the padding provided by the 'interiorGap' setting.
     */
    static final boolean DEBUG_DRAW_INTERIOR = false;

    /**
     * This debug flag controls whether or not an outline is drawn showing the
     * link area (in blue) and link ellipse (in yellow).  This controls where
     * the label links have 'elbow' points.
     */
    static final boolean DEBUG_DRAW_LINK_AREA = false;

    /**
     * This debug flag controls whether or not an outline is drawn showing
     * the pie area (in green).
     */
    static final boolean DEBUG_DRAW_PIE_AREA = false;

    /**
     * Creates a new plot.  The dataset is initially set to {@code null}.
     */
    public PiePlot() {
        this(null);
    }

    /**
     * Creates a plot that will draw a pie chart for the specified dataset.
     *
     * @param dataset  the dataset ({@code null} permitted).
     */
    public PiePlot(PieDataset<K> dataset) {
        super();
        this.dataset = dataset;
        if (dataset != null) {
            dataset.addChangeListener(this);
        }
        this.pieIndex = 0;

        this.interiorGap = DEFAULT_INTERIOR_GAP;
        this.circular = true;
        this.startAngle = DEFAULT_START_ANGLE;
        this.direction = Rotation.CLOCKWISE;
        this.minimumArcAngleToDraw = DEFAULT_MINIMUM_ARC_ANGLE_TO_DRAW;

        this.sectionPaintMap = new PaintMap();
        this.defaultSectionPaint = Color.GRAY;
        this.autoPopulateSectionPaint = true;

        this.sectionOutlinesVisible = true;
        this.sectionOutlinePaintMap = new PaintMap();
        this.defaultSectionOutlinePaint = DEFAULT_OUTLINE_PAINT;
        this.autoPopulateSectionOutlinePaint = false;

        this.sectionOutlineStrokeMap = new StrokeMap();
        this.defaultSectionOutlineStroke = DEFAULT_OUTLINE_STROKE;
        this.autoPopulateSectionOutlineStroke = false;

        this.explodePercentages = new TreeMap<>();

        this.labelGenerator = new StandardPieSectionLabelGenerator();
        this.labelFont = DEFAULT_LABEL_FONT;
        this.labelPaint = DEFAULT_LABEL_PAINT;
        this.labelBackgroundPaint = DEFAULT_LABEL_BACKGROUND_PAINT;
        this.labelOutlinePaint = DEFAULT_LABEL_OUTLINE_PAINT;
        this.labelOutlineStroke = DEFAULT_LABEL_OUTLINE_STROKE;
        this.labelShadowPaint = DEFAULT_LABEL_SHADOW_PAINT;
        this.labelLinksVisible = true;
        this.labelDistributor = new PieLabelDistributor(0);

        this.simpleLabels = false;
        this.simpleLabelOffset = new RectangleInsets(UnitType.RELATIVE, 0.18,
                0.18, 0.18, 0.18);
        this.labelPadding = new RectangleInsets(2, 2, 2, 2);

        this.toolTipGenerator = null;
        this.urlGenerator = null;
        this.legendLabelGenerator = new StandardPieSectionLabelGenerator();
        this.legendLabelToolTipGenerator = null;
        this.legendLabelURLGenerator = null;
        this.legendItemShape = Plot.DEFAULT_LEGEND_ITEM_CIRCLE;

        this.ignoreNullValues = false;
        this.ignoreZeroValues = false;

        this.shadowGenerator = null;
    }

    /**
     * Returns the dataset.
     *
     * @return The dataset (possibly {@code null}).
     *
     * @see #setDataset(PieDataset)
     */
    public PieDataset<K> getDataset() {
        return this.dataset;
    }

    /**
     * Sets the dataset and sends a {@link DatasetChangeEvent} to 'this'.
     *
     * @param dataset  the dataset ({@code null} permitted).
     *
     * @see #getDataset()
     */
    public void setDataset(PieDataset<K> dataset) {
        // if there is an existing dataset, remove the plot from the list of
        // change listeners...
        PieDataset<K> 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 the pie index (this is used by the {@link MultiplePiePlot} class
     * to track subplots).
     *
     * @return The pie index.
     *
     * @see #setPieIndex(int)
     */
    public int getPieIndex() {
        return this.pieIndex;
    }

    /**
     * Sets the pie index (this is used by the {@link MultiplePiePlot} class to
     * track subplots).
     *
     * @param index  the index.
     *
     * @see #getPieIndex()
     */
    public void setPieIndex(int index) {
        this.pieIndex = index;
    }

    /**
     * Returns the start angle for the first pie section.  This is measured in
     * degrees starting from 3 o'clock and measuring anti-clockwise.
     *
     * @return The start angle.
     *
     * @see #setStartAngle(double)
     */
    public double getStartAngle() {
        return this.startAngle;
    }

    /**
     * Sets the starting angle and sends a {@link PlotChangeEvent} to all
     * registered listeners.  The initial default value is 90 degrees, which
     * corresponds to 12 o'clock.  A value of zero corresponds to 3 o'clock...
     * this is the encoding used by Java's Arc2D class.
     *
     * @param angle  the angle (in degrees).
     *
     * @see #getStartAngle()
     */
    public void setStartAngle(double angle) {
        this.startAngle = angle;
        fireChangeEvent();
    }

    /**
     * Returns the direction in which the pie sections are drawn (clockwise or
     * anti-clockwise).
     *
     * @return The direction (never {@code null}).
     *
     * @see #setDirection(Rotation)
     */
    public Rotation getDirection() {
        return this.direction;
    }

    /**
     * Sets the direction in which the pie sections are drawn and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param direction  the direction ({@code null} not permitted).
     *
     * @see #getDirection()
     */
    public void setDirection(Rotation direction) {
        Args.nullNotPermitted(direction, "direction");
        this.direction = direction;
        fireChangeEvent();

    }

    /**
     * Returns the interior gap, measured as a percentage of the available
     * drawing space.
     *
     * @return The gap (as a percentage of the available drawing space).
     *
     * @see #setInteriorGap(double)
     */
    public double getInteriorGap() {
        return this.interiorGap;
    }

    /**
     * Sets the interior gap and sends a {@link PlotChangeEvent} to all
     * registered listeners.  This controls the space between the edges of the
     * pie plot and the plot area itself (the region where the section labels
     * appear).
     *
     * @param percent  the gap (as a percentage of the available drawing space).
     *
     * @see #getInteriorGap()
     */
    public void setInteriorGap(double percent) {

        if ((percent < 0.0) || (percent > MAX_INTERIOR_GAP)) {
            throw new IllegalArgumentException(
                "Invalid 'percent' (" + percent + ") argument.");
        }

        if (this.interiorGap != percent) {
            this.interiorGap = percent;
            fireChangeEvent();
        }

    }

    /**
     * Returns a flag indicating whether the pie chart is circular, or
     * stretched into an elliptical shape.
     *
     * @return A flag indicating whether the pie chart is circular.
     *
     * @see #setCircular(boolean)
     */
    public boolean isCircular() {
        return this.circular;
    }

    /**
     * A flag indicating whether the pie chart is circular, or stretched into
     * an elliptical shape.
     *
     * @param flag  the new value.
     *
     * @see #isCircular()
     */
    public void setCircular(boolean flag) {
        setCircular(flag, true);
    }

    /**
     * Sets the circular attribute and, if requested, sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param circular  the new value of the flag.
     * @param notify  notify listeners?
     *
     * @see #isCircular()
     */
    public void setCircular(boolean circular, boolean notify) {
        this.circular = circular;
        if (notify) {
            fireChangeEvent();
        }
    }

    /**
     * Returns the flag that controls whether {@code null} values in the
     * dataset are ignored.
     *
     * @return A boolean.
     *
     * @see #setIgnoreNullValues(boolean)
     */
    public boolean getIgnoreNullValues() {
        return this.ignoreNullValues;
    }

    /**
     * Sets a flag that controls whether {@code null} values are ignored,
     * and sends a {@link PlotChangeEvent} to all registered listeners.  At
     * present, this only affects whether or not the key is presented in the
     * legend.
     *
     * @param flag  the flag.
     *
     * @see #getIgnoreNullValues()
     * @see #setIgnoreZeroValues(boolean)
     */
    public void setIgnoreNullValues(boolean flag) {
        this.ignoreNullValues = flag;
        fireChangeEvent();
    }

    /**
     * Returns the flag that controls whether zero values in the
     * dataset are ignored.
     *
     * @return A boolean.
     *
     * @see #setIgnoreZeroValues(boolean)
     */
    public boolean getIgnoreZeroValues() {
        return this.ignoreZeroValues;
    }

    /**
     * Sets a flag that controls whether zero values are ignored,
     * and sends a {@link PlotChangeEvent} to all registered listeners.  This
     * only affects whether or not a label appears for the non-visible
     * pie section.
     *
     * @param flag  the flag.
     *
     * @see #getIgnoreZeroValues()
     * @see #setIgnoreNullValues(boolean)
     */
    public void setIgnoreZeroValues(boolean flag) {
        this.ignoreZeroValues = flag;
        fireChangeEvent();
    }

    //// SECTION PAINT ////////////////////////////////////////////////////////

    /**
     * Returns the paint for the specified section.  This is equivalent to
     * {@code lookupSectionPaint(section, getAutoPopulateSectionPaint())}.
     *
     * @param key  the section key.
     *
     * @return The paint for the specified section.
     *
     * @see #lookupSectionPaint(Comparable, boolean)
     */
    protected Paint lookupSectionPaint(Comparable key) {
        return lookupSectionPaint(key, getAutoPopulateSectionPaint());
    }

    /**
     * Returns the paint for the specified section.  The lookup involves these
     * steps:
     * <ul>
     * <li>if {@link #getSectionPaint(Comparable)} is non-{@code null} return
     *         it;</li>
     * <li>if {@link #getSectionPaint(Comparable)} is {@code null} but
     *         {@code autoPopulate} is {@code true}, attempt to fetch
     *         a new paint from the drawing supplier
     *         ({@link #getDrawingSupplier()});
     * <li>if all else fails, return {@link #getDefaultSectionPaint()}.
     * </ul>
     *
     * @param key  the section key.
     * @param autoPopulate  a flag that controls whether the drawing supplier
     *     is used to auto-populate the section paint settings.
     *
     * @return The paint.
     */
    protected Paint lookupSectionPaint(Comparable key, boolean autoPopulate) { 

        // if not, check if there is a paint defined for the specified key
        Paint result = this.sectionPaintMap.getPaint(key);
        if (result != null) {
            return result;
        }

        // nothing defined - do we autoPopulate?
        if (autoPopulate) {
            DrawingSupplier ds = getDrawingSupplier();
            if (ds != null) {
                result = ds.getNextPaint();
                this.sectionPaintMap.put(key, result);
            }
            else {
                result = this.defaultSectionPaint;
            }
        }
        else {
            result = this.defaultSectionPaint;
        }
        return result;
    }

    /**
     * Returns a key for the specified section. The preferred way of doing this
     * now is to link the attributes directly to the section key (there are new
     * methods for this, starting from version 1.0.3).
     *
     * @param section  the section index.
     *
     * @return The key.
     */
    protected K getSectionKey(int section) {
        K key = null;
        if (this.dataset != null) {
            if (section >= 0 && section < this.dataset.getItemCount()) {
                key = this.dataset.getKey(section);
            }
        }
        return key;
    }

    /**
     * Returns the paint associated with the specified key, or
     * {@code null} if there is no paint associated with the key.
     *
     * @param key  the key ({@code null} not permitted).
     *
     * @return The paint associated with the specified key, or
     *     {@code null}.
     *
     * @throws IllegalArgumentException if {@code key} is
     *     {@code null}.
     *
     * @see #setSectionPaint(Comparable, Paint)
     */
    public Paint getSectionPaint(Comparable key) {
        // null argument check delegated...
        return this.sectionPaintMap.getPaint(key);
    }

    /**
     * Sets the paint associated with the specified key, and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param key  the key ({@code null} not permitted).
     * @param paint  the paint.
     *
     * @throws IllegalArgumentException if {@code key} is
     *     {@code null}.
     *
     * @see #getSectionPaint(Comparable)
     */
    public void setSectionPaint(Comparable key, Paint paint) {
        // null argument check delegated...
        this.sectionPaintMap.put(key, paint);
        fireChangeEvent();
    }

    /**
     * Clears the section paint settings for this plot and, if requested, sends
     * a {@link PlotChangeEvent} to all registered listeners.  Be aware that
     * if the {@code autoPopulateSectionPaint} flag is set, the section
     * paints may be repopulated using the same colours as before.
     *
     * @param notify  notify listeners?
     *
     * @see #autoPopulateSectionPaint
     */
    public void clearSectionPaints(boolean notify) {
        this.sectionPaintMap.clear();
        if (notify) {
            fireChangeEvent();
        }
    }

    /**
     * Returns the default section paint.  This is used when no other paint is
     * defined, which is rare.  The default value is {@code Color.GRAY}.
     *
     * @return The paint (never {@code null}).
     *
     * @see #setDefaultSectionPaint(Paint)
     */
    public Paint getDefaultSectionPaint() {
        return this.defaultSectionPaint;
    }

    /**
     * Sets the default section paint and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param paint  the paint ({@code null} not permitted).
     *
     * @see #getDefaultSectionPaint()
     */
    public void setDefaultSectionPaint(Paint paint) {
        Args.nullNotPermitted(paint, "paint");
        this.defaultSectionPaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the flag that controls whether or not the section paint is
     * auto-populated by the {@link #lookupSectionPaint(Comparable)} method.
     *
     * @return A boolean.
     */
    public boolean getAutoPopulateSectionPaint() {
        return this.autoPopulateSectionPaint;
    }

    /**
     * Sets the flag that controls whether or not the section paint is
     * auto-populated by the {@link #lookupSectionPaint(Comparable)} method,
     * and sends a {@link PlotChangeEvent} to all registered listeners.
     *
     * @param auto  auto-populate?
     */
    public void setAutoPopulateSectionPaint(boolean auto) {
        this.autoPopulateSectionPaint = auto;
        fireChangeEvent();
    }

    //// SECTION OUTLINE PAINT ////////////////////////////////////////////////

    /**
     * Returns the flag that controls whether or not the outline is drawn for
     * each pie section.
     *
     * @return The flag that controls whether or not the outline is drawn for
     *         each pie section.
     *
     * @see #setSectionOutlinesVisible(boolean)
     */
    public boolean getSectionOutlinesVisible() {
        return this.sectionOutlinesVisible;
    }

    /**
     * Sets the flag that controls whether or not the outline is drawn for
     * each pie section, and sends a {@link PlotChangeEvent} to all registered
     * listeners.
     *
     * @param visible  the flag.
     *
     * @see #getSectionOutlinesVisible()
     */
    public void setSectionOutlinesVisible(boolean visible) {
        this.sectionOutlinesVisible = visible;
        fireChangeEvent();
    }

    /**
     * Returns the outline paint for the specified section.  This is equivalent
     * to {@code lookupSectionPaint(section, 
     * getAutoPopulateSectionOutlinePaint())}.
     *
     * @param key  the section key.
     *
     * @return The paint for the specified section.
     *
     * @see #lookupSectionOutlinePaint(Comparable, boolean)
     */
    protected Paint lookupSectionOutlinePaint(Comparable key) {
        return lookupSectionOutlinePaint(key,
                getAutoPopulateSectionOutlinePaint());
    }

    /**
     * Returns the outline paint for the specified section.  The lookup
     * involves these steps:
     * <ul>
     * <li>if {@link #getSectionOutlinePaint(Comparable)} is
     *         non-{@code null} return it;</li>
     * <li>if {@link #getSectionOutlinePaint(Comparable)} is {@code null} but
     *         {@code autoPopulate} is {@code true}, attempt to fetch
     *         a new outline paint from the drawing supplier
     *         ({@link #getDrawingSupplier()});
     * <li>if all else fails, return {@link #getDefaultSectionOutlinePaint()}.
     * </ul>
     *
     * @param key  the section key.
     * @param autoPopulate  a flag that controls whether the drawing supplier
     *     is used to auto-populate the section outline paint settings.
     *
     * @return The paint.
     */
    protected Paint lookupSectionOutlinePaint(Comparable key,
            boolean autoPopulate) {

        // if not, check if there is a paint defined for the specified key
        Paint result = this.sectionOutlinePaintMap.getPaint(key);
        if (result != null) {
            return result;
        }

        // nothing defined - do we autoPopulate?
        if (autoPopulate) {
            DrawingSupplier ds = getDrawingSupplier();
            if (ds != null) {
                result = ds.getNextOutlinePaint();
                this.sectionOutlinePaintMap.put(key, result);
            }
            else {
                result = this.defaultSectionOutlinePaint;
            }
        }
        else {
            result = this.defaultSectionOutlinePaint;
        }
        return result;
    }

    /**
     * Returns the outline paint associated with the specified key, or
     * {@code null} if there is no paint associated with the key.
     *
     * @param key  the key ({@code null} not permitted).
     *
     * @return The paint associated with the specified key, or
     *     {@code null}.
     *
     * @throws IllegalArgumentException if {@code key} is
     *     {@code null}.
     *
     * @see #setSectionOutlinePaint(Comparable, Paint)
     */
    public Paint getSectionOutlinePaint(Comparable key) {
        // null argument check delegated...
        return this.sectionOutlinePaintMap.getPaint(key);
    }

    /**
     * Sets the outline paint associated with the specified key, and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param key  the key ({@code null} not permitted).
     * @param paint  the paint.
     *
     * @throws IllegalArgumentException if {@code key} is
     *     {@code null}.
     *
     * @see #getSectionOutlinePaint(Comparable)
     */
    public void setSectionOutlinePaint(Comparable key, Paint paint) {
        // null argument check delegated...
        this.sectionOutlinePaintMap.put(key, paint);
        fireChangeEvent();
    }

    /**
     * Clears the section outline paint settings for this plot and, if
     * requested, sends a {@link PlotChangeEvent} to all registered listeners.
     * Be aware that if the {@code autoPopulateSectionPaint} flag is set,
     * the section paints may be repopulated using the same colours as before.
     *
     * @param notify  notify listeners?
     *
     * @see #autoPopulateSectionOutlinePaint
     */
    public void clearSectionOutlinePaints(boolean notify) {
        this.sectionOutlinePaintMap.clear();
        if (notify) {
            fireChangeEvent();
        }
    }

    /**
     * Returns the default section paint.  This is used when no other paint is
     * available.
     *
     * @return The paint (never {@code null}).
     *
     * @see #setDefaultSectionOutlinePaint(Paint)
     */
    public Paint getDefaultSectionOutlinePaint() {
        return this.defaultSectionOutlinePaint;
    }

    /**
     * Sets the default section paint.
     *
     * @param paint  the paint ({@code null} not permitted).
     *
     * @see #getDefaultSectionOutlinePaint()
     */
    public void setDefaultSectionOutlinePaint(Paint paint) {
        Args.nullNotPermitted(paint, "paint");
        this.defaultSectionOutlinePaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the flag that controls whether or not the section outline paint
     * is auto-populated by the {@link #lookupSectionOutlinePaint(Comparable)}
     * method.
     *
     * @return A boolean.
     */
    public boolean getAutoPopulateSectionOutlinePaint() {
        return this.autoPopulateSectionOutlinePaint;
    }

    /**
     * Sets the flag that controls whether or not the section outline paint is
     * auto-populated by the {@link #lookupSectionOutlinePaint(Comparable)}
     * method, and sends a {@link PlotChangeEvent} to all registered listeners.
     *
     * @param auto  auto-populate?
     */
    public void setAutoPopulateSectionOutlinePaint(boolean auto) {
        this.autoPopulateSectionOutlinePaint = auto;
        fireChangeEvent();
    }

    //// SECTION OUTLINE STROKE ///////////////////////////////////////////////

    /**
     * Returns the outline stroke for the specified section.  This is
     * equivalent to {@code lookupSectionOutlineStroke(section,
     * getAutoPopulateSectionOutlineStroke())}.
     *
     * @param key  the section key.
     *
     * @return The stroke for the specified section.
     *
     * @see #lookupSectionOutlineStroke(Comparable, boolean)
     */
    protected Stroke lookupSectionOutlineStroke(Comparable key) {
        return lookupSectionOutlineStroke(key,
                getAutoPopulateSectionOutlineStroke());
    }

    /**
     * Returns the outline stroke for the specified section.  The lookup
     * involves these steps:
     * <ul>
     * <li>if {@link #getSectionOutlineStroke(Comparable)} is
     *         non-{@code null} return it;</li>
     * <li>if {@link #getSectionOutlineStroke(Comparable)} is {@code null} but
     *         {@code autoPopulate} is {@code true}, attempt to fetch
     *         a new outline stroke from the drawing supplier
     *         ({@link #getDrawingSupplier()});
     * <li>if all else fails, return {@link #getDefaultSectionOutlineStroke()}.
     * </ul>
     *
     * @param key  the section key.
     * @param autoPopulate  a flag that controls whether the drawing supplier
     *     is used to auto-populate the section outline stroke settings.
     *
     * @return The stroke.
     */
    protected Stroke lookupSectionOutlineStroke(Comparable key,
            boolean autoPopulate) {

        // if not, check if there is a stroke defined for the specified key
        Stroke result = this.sectionOutlineStrokeMap.getStroke(key);
        if (result != null) {
            return result;
        }

        // nothing defined - do we autoPopulate?
        if (autoPopulate) {
            DrawingSupplier ds = getDrawingSupplier();
            if (ds != null) {
                result = ds.getNextOutlineStroke();
                this.sectionOutlineStrokeMap.put(key, result);
            }
            else {
                result = this.defaultSectionOutlineStroke;
            }
        }
        else {
            result = this.defaultSectionOutlineStroke;
        }
        return result;
    }

    /**
     * Returns the outline stroke associated with the specified key, or
     * {@code null} if there is no stroke associated with the key.
     *
     * @param key  the key ({@code null} not permitted).
     *
     * @return The stroke associated with the specified key, or
     *     {@code null}.
     *
     * @throws IllegalArgumentException if {@code key} is
     *     {@code null}.
     *
     * @see #setSectionOutlineStroke(Comparable, Stroke)
     */
    public Stroke getSectionOutlineStroke(Comparable key) {
        // null argument check delegated...
        return this.sectionOutlineStrokeMap.getStroke(key);
    }

    /**
     * Sets the outline stroke associated with the specified key, and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param key  the key ({@code null} not permitted).
     * @param stroke  the stroke.
     *
     * @throws IllegalArgumentException if {@code key} is
     *     {@code null}.
     *
     * @see #getSectionOutlineStroke(Comparable)
     */
    public void setSectionOutlineStroke(Comparable key, Stroke stroke) {
        // null argument check delegated...
        this.sectionOutlineStrokeMap.put(key, stroke);
        fireChangeEvent();
    }

    /**
     * Clears the section outline stroke settings for this plot and, if
     * requested, sends a {@link PlotChangeEvent} to all registered listeners.
     * Be aware that if the {@code autoPopulateSectionPaint} flag is set,
     * the section paints may be repopulated using the same colours as before.
     *
     * @param notify  notify listeners?
     *
     * @see #autoPopulateSectionOutlineStroke
     */
    public void clearSectionOutlineStrokes(boolean notify) {
        this.sectionOutlineStrokeMap.clear();
        if (notify) {
            fireChangeEvent();
        }
    }

    /**
     * Returns the default section stroke.  This is used when no other stroke is
     * available.
     *
     * @return The stroke (never {@code null}).
     *
     * @see #setDefaultSectionOutlineStroke(Stroke)
     */
    public Stroke getDefaultSectionOutlineStroke() {
        return this.defaultSectionOutlineStroke;
    }

    /**
     * Sets the default section stroke.
     *
     * @param stroke  the stroke ({@code null} not permitted).
     *
     * @see #getDefaultSectionOutlineStroke()
     */
    public void setDefaultSectionOutlineStroke(Stroke stroke) {
        Args.nullNotPermitted(stroke, "stroke");
        this.defaultSectionOutlineStroke = stroke;
        fireChangeEvent();
    }

    /**
     * Returns the flag that controls whether or not the section outline stroke
     * is auto-populated by the {@link #lookupSectionOutlinePaint(Comparable)}
     * method.
     *
     * @return A boolean.
     */
    public boolean getAutoPopulateSectionOutlineStroke() {
        return this.autoPopulateSectionOutlineStroke;
    }

    /**
     * Sets the flag that controls whether or not the section outline stroke is
     * auto-populated by the {@link #lookupSectionOutlineStroke(Comparable)}
     * method, and sends a {@link PlotChangeEvent} to all registered listeners.
     *
     * @param auto  auto-populate?
     */
    public void setAutoPopulateSectionOutlineStroke(boolean auto) {
        this.autoPopulateSectionOutlineStroke = auto;
        fireChangeEvent();
    }

    /**
     * Returns the shadow paint.
     *
     * @return The paint (possibly {@code null}).
     *
     * @see #setShadowPaint(Paint)
     */
    public Paint getShadowPaint() {
        return this.shadowPaint;
    }

    /**
     * Sets the shadow paint and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param paint  the paint ({@code null} permitted).
     *
     * @see #getShadowPaint()
     */
    public void setShadowPaint(Paint paint) {
        this.shadowPaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the x-offset for the shadow effect.
     *
     * @return The offset (in Java2D units).
     *
     * @see #setShadowXOffset(double)
     */
    public double getShadowXOffset() {
        return this.shadowXOffset;
    }

    /**
     * Sets the x-offset for the shadow effect and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param offset  the offset (in Java2D units).
     *
     * @see #getShadowXOffset()
     */
    public void setShadowXOffset(double offset) {
        this.shadowXOffset = offset;
        fireChangeEvent();
    }

    /**
     * Returns the y-offset for the shadow effect.
     *
     * @return The offset (in Java2D units).
     *
     * @see #setShadowYOffset(double)
     */
    public double getShadowYOffset() {
        return this.shadowYOffset;
    }

    /**
     * Sets the y-offset for the shadow effect and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param offset  the offset (in Java2D units).
     *
     * @see #getShadowYOffset()
     */
    public void setShadowYOffset(double offset) {
        this.shadowYOffset = offset;
        fireChangeEvent();
    }

    /**
     * Returns the amount that the section with the specified key should be
     * exploded.
     *
     * @param key  the key ({@code null} not permitted).
     *
     * @return The amount that the section with the specified key should be
     *     exploded.
     *
     * @throws IllegalArgumentException if {@code key} is
     *     {@code null}.
     *
     * @see #setExplodePercent(Comparable, double)
     */
    public double getExplodePercent(K key) {
        double result = 0.0;
        if (this.explodePercentages != null) {
            Number percent = (Number) this.explodePercentages.get(key);
            if (percent != null) {
                result = percent.doubleValue();
            }
        }
        return result;
    }

    /**
     * Sets the amount that a pie section should be exploded and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param key  the section key ({@code null} not permitted).
     * @param percent  the explode percentage (0.30 = 30 percent).
     *
     * @see #getExplodePercent(Comparable)
     */
    public void setExplodePercent(K key, double percent) {
        Args.nullNotPermitted(key, "key");
        if (this.explodePercentages == null) {
            this.explodePercentages = new TreeMap<>();
        }
        this.explodePercentages.put(key, percent);
        fireChangeEvent();
    }

    /**
     * Returns the maximum explode percent.
     *
     * @return The percent.
     */
    public double getMaximumExplodePercent() {
        if (this.dataset == null) {
            return 0.0;
        }
        double result = 0.0;
        for (K key : this.dataset.getKeys()) {
            Double explode = this.explodePercentages.get(key);
            if (explode != null) {
                result = Math.max(result, explode);
            }
        }
        return result;
    }

    /**
     * Returns the section label generator.
     *
     * @return The generator (possibly {@code null}).
     *
     * @see #setLabelGenerator(PieSectionLabelGenerator)
     */
    public PieSectionLabelGenerator getLabelGenerator() {
        return this.labelGenerator;
    }

    /**
     * Sets the section label generator and sends a {@link PlotChangeEvent} to
     * all registered listeners.
     *
     * @param generator  the generator ({@code null} permitted).
     *
     * @see #getLabelGenerator()
     */
    public void setLabelGenerator(PieSectionLabelGenerator generator) {
        this.labelGenerator = generator;
        fireChangeEvent();
    }

    /**
     * Returns the gap between the edge of the pie and the labels, expressed as
     * a percentage of the plot width.
     *
     * @return The gap (a percentage, where 0.05 = five percent).
     *
     * @see #setLabelGap(double)
     */
    public double getLabelGap() {
        return this.labelGap;
    }

    /**
     * Sets the gap between the edge of the pie and the labels (expressed as a
     * percentage of the plot width) and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param gap  the gap (a percentage, where 0.05 = five percent).
     *
     * @see #getLabelGap()
     */
    public void setLabelGap(double gap) {
        this.labelGap = gap;
        fireChangeEvent();
    }

    /**
     * Returns the maximum label width as a percentage of the plot width.
     *
     * @return The width (a percentage, where 0.20 = 20 percent).
     *
     * @see #setMaximumLabelWidth(double)
     */
    public double getMaximumLabelWidth() {
        return this.maximumLabelWidth;
    }

    /**
     * Sets the maximum label width as a percentage of the plot width and sends
     * a {@link PlotChangeEvent} to all registered listeners.
     *
     * @param width  the width (a percentage, where 0.20 = 20 percent).
     *
     * @see #getMaximumLabelWidth()
     */
    public void setMaximumLabelWidth(double width) {
        this.maximumLabelWidth = width;
        fireChangeEvent();
    }

    /**
     * Returns the flag that controls whether or not label linking lines are
     * visible.
     *
     * @return A boolean.
     *
     * @see #setLabelLinksVisible(boolean)
     */
    public boolean getLabelLinksVisible() {
        return this.labelLinksVisible;
    }

    /**
     * Sets the flag that controls whether or not label linking lines are
     * visible and sends a {@link PlotChangeEvent} to all registered listeners.
     * Please take care when hiding the linking lines - depending on the data
     * values, the labels can be displayed some distance away from the
     * corresponding pie section.
     *
     * @param visible  the flag.
     *
     * @see #getLabelLinksVisible()
     */
    public void setLabelLinksVisible(boolean visible) {
        this.labelLinksVisible = visible;
        fireChangeEvent();
    }

    /**
     * Returns the label link style.
     *
     * @return The label link style (never {@code null}).
     *
     * @see #setLabelLinkStyle(PieLabelLinkStyle)
     */
    public PieLabelLinkStyle getLabelLinkStyle() {
        return this.labelLinkStyle;
    }

    /**
     * Sets the label link style and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param style  the new style ({@code null} not permitted).
     *
     * @see #getLabelLinkStyle()
     */
    public void setLabelLinkStyle(PieLabelLinkStyle style) {
        Args.nullNotPermitted(style, "style");
        this.labelLinkStyle = style;
        fireChangeEvent();
    }

    /**
     * Returns the margin (expressed as a percentage of the width or height)
     * between the edge of the pie and the link point.
     *
     * @return The link margin (as a percentage, where 0.05 is five percent).
     *
     * @see #setLabelLinkMargin(double)
     */
    public double getLabelLinkMargin() {
        return this.labelLinkMargin;
    }

    /**
     * Sets the link margin and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param margin  the margin.
     *
     * @see #getLabelLinkMargin()
     */
    public void setLabelLinkMargin(double margin) {
        this.labelLinkMargin = margin;
        fireChangeEvent();
    }

    /**
     * Returns the paint used for the lines that connect pie sections to their
     * corresponding labels.
     *
     * @return The paint (never {@code null}).
     *
     * @see #setLabelLinkPaint(Paint)
     */
    public Paint getLabelLinkPaint() {
        return this.labelLinkPaint;
    }

    /**
     * Sets the paint used for the lines that connect pie sections to their
     * corresponding labels, and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param paint  the paint ({@code null} not permitted).
     *
     * @see #getLabelLinkPaint()
     */
    public void setLabelLinkPaint(Paint paint) {
        Args.nullNotPermitted(paint, "paint");
        this.labelLinkPaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the stroke used for the label linking lines.
     *
     * @return The stroke.
     *
     * @see #setLabelLinkStroke(Stroke)
     */
    public Stroke getLabelLinkStroke() {
        return this.labelLinkStroke;
    }

    /**
     * Sets the link stroke and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param stroke  the stroke.
     *
     * @see #getLabelLinkStroke()
     */
    public void setLabelLinkStroke(Stroke stroke) {
        Args.nullNotPermitted(stroke, "stroke");
        this.labelLinkStroke = stroke;
        fireChangeEvent();
    }

    /**
     * Returns the distance that the end of the label link is embedded into
     * the plot, expressed as a percentage of the plot's radius.
     * <br><br>
     * This method is overridden in the {@link RingPlot} class to resolve
     * bug 2121818.
     *
     * @return {@code 0.10}.
     */
    protected double getLabelLinkDepth() {
        return 0.1;
    }

    /**
     * Returns the section label font.
     *
     * @return The font (never {@code null}).
     *
     * @see #setLabelFont(Font)
     */
    public Font getLabelFont() {
        return this.labelFont;
    }

    /**
     * Sets the section label font and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param font  the font ({@code null} not permitted).
     *
     * @see #getLabelFont()
     */
    public void setLabelFont(Font font) {
        Args.nullNotPermitted(font, "font");
        this.labelFont = font;
        fireChangeEvent();
    }

    /**
     * Returns the section label paint.
     *
     * @return The paint (never {@code null}).
     *
     * @see #setLabelPaint(Paint)
     */
    public Paint getLabelPaint() {
        return this.labelPaint;
    }

    /**
     * Sets the section label paint and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param paint  the paint ({@code null} not permitted).
     *
     * @see #getLabelPaint()
     */
    public void setLabelPaint(Paint paint) {
        Args.nullNotPermitted(paint, "paint");
        this.labelPaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the section label background paint.
     *
     * @return The paint (possibly {@code null}).
     *
     * @see #setLabelBackgroundPaint(Paint)
     */
    public Paint getLabelBackgroundPaint() {
        return this.labelBackgroundPaint;
    }

    /**
     * Sets the section label background paint and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param paint  the paint ({@code null} permitted).
     *
     * @see #getLabelBackgroundPaint()
     */
    public void setLabelBackgroundPaint(Paint paint) {
        this.labelBackgroundPaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the section label outline paint.
     *
     * @return The paint (possibly {@code null}).
     *
     * @see #setLabelOutlinePaint(Paint)
     */
    public Paint getLabelOutlinePaint() {
        return this.labelOutlinePaint;
    }

    /**
     * Sets the section label outline paint and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param paint  the paint ({@code null} permitted).
     *
     * @see #getLabelOutlinePaint()
     */
    public void setLabelOutlinePaint(Paint paint) {
        this.labelOutlinePaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the section label outline stroke.
     *
     * @return The stroke (possibly {@code null}).
     *
     * @see #setLabelOutlineStroke(Stroke)
     */
    public Stroke getLabelOutlineStroke() {
        return this.labelOutlineStroke;
    }

    /**
     * Sets the section label outline stroke and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param stroke  the stroke ({@code null} permitted).
     *
     * @see #getLabelOutlineStroke()
     */
    public void setLabelOutlineStroke(Stroke stroke) {
        this.labelOutlineStroke = stroke;
        fireChangeEvent();
    }

    /**
     * Returns the section label shadow paint.
     *
     * @return The paint (possibly {@code null}).
     *
     * @see #setLabelShadowPaint(Paint)
     */
    public Paint getLabelShadowPaint() {
        return this.labelShadowPaint;
    }

    /**
     * Sets the section label shadow paint and sends a {@link PlotChangeEvent}
     * to all registered listeners.
     *
     * @param paint  the paint ({@code null} permitted).
     *
     * @see #getLabelShadowPaint()
     */
    public void setLabelShadowPaint(Paint paint) {
        this.labelShadowPaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the label padding.
     *
     * @return The label padding (never {@code null}).
     *
     * @see #setLabelPadding(RectangleInsets)
     */
    public RectangleInsets getLabelPadding() {
        return this.labelPadding;
    }

    /**
     * Sets the padding between each label and its outline and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param padding  the padding ({@code null} not permitted).
     *
     * @see #getLabelPadding()
     */
    public void setLabelPadding(RectangleInsets padding) {
        Args.nullNotPermitted(padding, "padding");
        this.labelPadding = padding;
        fireChangeEvent();
    }

    /**
     * Returns the flag that controls whether simple or extended labels are
     * displayed on the plot.
     *
     * @return A boolean.
     */
    public boolean getSimpleLabels() {
        return this.simpleLabels;
    }

    /**
     * Sets the flag that controls whether simple or extended labels are
     * displayed on the plot, and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param simple  the new flag value.
     */
    public void setSimpleLabels(boolean simple) {
        this.simpleLabels = simple;
        fireChangeEvent();
    }

    /**
     * Returns the offset used for the simple labels, if they are displayed.
     *
     * @return The offset (never {@code null}).
     *
     * @see #setSimpleLabelOffset(RectangleInsets)
     */
    public RectangleInsets getSimpleLabelOffset() {
        return this.simpleLabelOffset;
    }

    /**
     * Sets the offset for the simple labels and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param offset  the offset ({@code null} not permitted).
     *
     * @see #getSimpleLabelOffset()
     */
    public void setSimpleLabelOffset(RectangleInsets offset) {
        Args.nullNotPermitted(offset, "offset");
        this.simpleLabelOffset = offset;
        fireChangeEvent();
    }

    /**
     * Returns the object responsible for the vertical layout of the pie
     * section labels.
     *
     * @return The label distributor (never {@code null}).
     */
    public AbstractPieLabelDistributor getLabelDistributor() {
        return this.labelDistributor;
    }

    /**
     * Sets the label distributor and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param distributor  the distributor ({@code null} not permitted).
     */
    public void setLabelDistributor(AbstractPieLabelDistributor distributor) {
        Args.nullNotPermitted(distributor, "distributor");
        this.labelDistributor = distributor;
        fireChangeEvent();
    }

    /**
     * Returns the tool tip generator, an object that is responsible for
     * generating the text items used for tool tips by the plot.  If the
     * generator is {@code null}, no tool tips will be created.
     *
     * @return The generator (possibly {@code null}).
     *
     * @see #setToolTipGenerator(PieToolTipGenerator)
     */
    public PieToolTipGenerator getToolTipGenerator() {
        return this.toolTipGenerator;
    }

    /**
     * Sets the tool tip generator and sends a {@link PlotChangeEvent} to all
     * registered listeners.  Set the generator to {@code null} if you
     * don't want any tool tips.
     *
     * @param generator  the generator ({@code null} permitted).
     *
     * @see #getToolTipGenerator()
     */
    public void setToolTipGenerator(PieToolTipGenerator generator) {
        this.toolTipGenerator = generator;
        fireChangeEvent();
    }

    /**
     * Returns the URL generator.
     *
     * @return The generator (possibly {@code null}).
     *
     * @see #setURLGenerator(PieURLGenerator)
     */
    public PieURLGenerator getURLGenerator() {
        return this.urlGenerator;
    }

    /**
     * Sets the URL generator and sends a {@link PlotChangeEvent} to all
     * registered listeners.
     *
     * @param generator  the generator ({@code null} permitted).
     *
     * @see #getURLGenerator()
     */
    public void setURLGenerator(PieURLGenerator generator) {
        this.urlGenerator = generator;
        fireChangeEvent();
    }

    /**
     * Returns the minimum arc angle that will be drawn.  Pie sections for an
     * angle smaller than this are not drawn, to avoid a JDK bug.
     *
     * @return The minimum angle.
     *
     * @see #setMinimumArcAngleToDraw(double)
     */
    public double getMinimumArcAngleToDraw() {
        return this.minimumArcAngleToDraw;
    }

    /**
     * Sets the minimum arc angle that will be drawn.  Pie sections for an
     * angle smaller than this are not drawn, to avoid a JDK bug.  See this
     * link for details:
     * <br><br>
     * <a href="http://www.jfree.org/phpBB2/viewtopic.php?t=2707">
     * http://www.jfree.org/phpBB2/viewtopic.php?t=2707</a>
     * <br><br>
     * ...and this bug report in the Java Bug Parade:
     * <br><br>
     * <a href=
     * "http://developer.java.sun.com/developer/bugParade/bugs/4836495.html">
     * http://developer.java.sun.com/developer/bugParade/bugs/4836495.html</a>
     *
     * @param angle  the minimum angle.
     *
     * @see #getMinimumArcAngleToDraw()
     */
    public void setMinimumArcAngleToDraw(double angle) {
        this.minimumArcAngleToDraw = angle;
    }

    /**
     * Returns the shape used for legend items.
     *
     * @return The shape (never {@code null}).
     *
     * @see #setLegendItemShape(Shape)
     */
    public Shape getLegendItemShape() {
        return this.legendItemShape;
    }

    /**
     * Sets the shape used for legend items and sends a {@link PlotChangeEvent}
     * to all registered listeners.
     *
     * @param shape  the shape ({@code null} not permitted).
     *
     * @see #getLegendItemShape()
     */
    public void setLegendItemShape(Shape shape) {
        Args.nullNotPermitted(shape, "shape");
        this.legendItemShape = shape;
        fireChangeEvent();
    }

    /**
     * Returns the legend label generator.
     *
     * @return The legend label generator (never {@code null}).
     *
     * @see #setLegendLabelGenerator(PieSectionLabelGenerator)
     */
    public PieSectionLabelGenerator getLegendLabelGenerator() {
        return this.legendLabelGenerator;
    }

    /**
     * Sets the legend label generator and sends a {@link PlotChangeEvent} to
     * all registered listeners.
     *
     * @param generator  the generator ({@code null} not permitted).
     *
     * @see #getLegendLabelGenerator()
     */
    public void setLegendLabelGenerator(PieSectionLabelGenerator generator) {
        Args.nullNotPermitted(generator, "generator");
        this.legendLabelGenerator = generator;
        fireChangeEvent();
    }

    /**
     * Returns the legend label tool tip generator.
     *
     * @return The legend label tool tip generator (possibly {@code null}).
     *
     * @see #setLegendLabelToolTipGenerator(PieSectionLabelGenerator)
     */
    public PieSectionLabelGenerator getLegendLabelToolTipGenerator() {
        return this.legendLabelToolTipGenerator;
    }

    /**
     * Sets the legend label tool tip generator and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param generator  the generator ({@code null} permitted).
     *
     * @see #getLegendLabelToolTipGenerator()
     */
    public void setLegendLabelToolTipGenerator(
            PieSectionLabelGenerator generator) {
        this.legendLabelToolTipGenerator = generator;
        fireChangeEvent();
    }

    /**
     * Returns the legend label URL generator.
     *
     * @return The legend label URL generator (possibly {@code null}).
     *
     * @see #setLegendLabelURLGenerator(PieURLGenerator)
     */
    public PieURLGenerator getLegendLabelURLGenerator() {
        return this.legendLabelURLGenerator;
    }

    /**
     * Sets the legend label URL generator and sends a
     * {@link PlotChangeEvent} to all registered listeners.
     *
     * @param generator  the generator ({@code null} permitted).
     *
     * @see #getLegendLabelURLGenerator()
     */
    public void setLegendLabelURLGenerator(PieURLGenerator generator) {
        this.legendLabelURLGenerator = generator;
        fireChangeEvent();
    }

    /**
     * Returns the shadow generator for the plot, if any.
     * 
     * @return The shadow generator (possibly {@code null}).
     */
    public ShadowGenerator getShadowGenerator() {
        return this.shadowGenerator;
    }

    /**
     * Sets the shadow generator for the plot and sends a
     * {@link PlotChangeEvent} to all registered listeners.  Note that this is
     * a bitmap drop-shadow generation facility and is separate from the
     * vector based show option that is controlled via the
     * {@link #setShadowPaint(java.awt.Paint)} method.
     *
     * @param generator  the generator ({@code null} permitted).
     */
    public void setShadowGenerator(ShadowGenerator generator) {
        this.shadowGenerator = generator;
        fireChangeEvent();
    }

    /**
     * Handles a mouse wheel rotation (this method is intended for use by the
     * {@code MouseWheelHandler} class).
     *
     * @param rotateClicks  the number of rotate clicks on the the mouse wheel.
     */
    public void handleMouseWheelRotation(int rotateClicks) {
        setStartAngle(this.startAngle + rotateClicks * 4.0);
    }

    /**
     * Initialises the drawing procedure.  This method will be called before
     * the first item is rendered, giving the plot an opportunity to initialise
     * any state information it wants to maintain.
     *
     * @param g2  the graphics device.
     * @param plotArea  the plot area ({@code null} not permitted).
     * @param plot  the plot.
     * @param index  the secondary index ({@code null} for primary
     *               renderer).
     * @param info  collects chart rendering information for return to caller.
     *
     * @return A state object (maintains state information relevant to one
     *         chart drawing).
     */
    public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea,
            PiePlot<?> plot, Integer index, PlotRenderingInfo info) {

        PiePlotState state = new PiePlotState(info);
        state.setPassesRequired(2);
        if (this.dataset != null) {
            state.setTotal(DatasetUtils.calculatePieDatasetTotal(
                    plot.getDataset()));
        }
        state.setLatestAngle(plot.getStartAngle());
        return state;

    }

    /**
     * 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} permitted).
     * @param parentState  the state from the parent plot, if there is one.
     * @param info  collects info about the drawing
     *              ({@code null} permitted).
     */
    @Override
    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
                     PlotState parentState, PlotRenderingInfo info) {

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

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

        drawBackground(g2, area);
        drawOutline(g2, area);

        Shape savedClip = g2.getClip();
        g2.clip(area);

        Composite originalComposite = g2.getComposite();
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                getForegroundAlpha()));

        if (!DatasetUtils.isEmptyOrNull(this.dataset)) {
            Graphics2D savedG2 = g2;
            boolean suppressShadow = Boolean.TRUE.equals(g2.getRenderingHint(
                    JFreeChart.KEY_SUPPRESS_SHADOW_GENERATION));
            BufferedImage dataImage = null;
            if (this.shadowGenerator != null && !suppressShadow) {
                dataImage = new BufferedImage((int) area.getWidth(),
                    (int) area.getHeight(), BufferedImage.TYPE_INT_ARGB);
                g2 = dataImage.createGraphics();
                g2.translate(-area.getX(), -area.getY());
                g2.setRenderingHints(savedG2.getRenderingHints());
            }
            drawPie(g2, area, info);
            if (this.shadowGenerator != null && !suppressShadow) {
                BufferedImage shadowImage 
                        = this.shadowGenerator.createDropShadow(dataImage);
                g2 = savedG2;
                g2.drawImage(shadowImage, (int) area.getX() 
                        + this.shadowGenerator.calculateOffsetX(), 
                        (int) area.getY()
                        + this.shadowGenerator.calculateOffsetY(), null);
                g2.drawImage(dataImage, (int) area.getX(), (int) area.getY(), 
                        null);
            }
        }
        else {
            drawNoDataMessage(g2, area);
        }

        g2.setClip(savedClip);
        g2.setComposite(originalComposite);

        drawOutline(g2, area);

    }

    /**
     * Draws the pie.
     *
     * @param g2  the graphics device.
     * @param plotArea  the plot area.
     * @param info  chart rendering info.
     */
    protected void drawPie(Graphics2D g2, Rectangle2D plotArea,
                           PlotRenderingInfo info) {

        PiePlotState state = initialise(g2, plotArea, this, null, info);

        // adjust the plot area for interior spacing and labels...
        double labelReserve = 0.0;
        if (this.labelGenerator != null && !this.simpleLabels) {
            labelReserve = this.labelGap + this.maximumLabelWidth;
        }
        double gapHorizontal = plotArea.getWidth() * labelReserve * 2.0;
        double gapVertical = plotArea.getHeight() * this.interiorGap * 2.0;


        if (DEBUG_DRAW_INTERIOR) {
            double hGap = plotArea.getWidth() * this.interiorGap;
            double vGap = plotArea.getHeight() * this.interiorGap;

            double igx1 = plotArea.getX() + hGap;
            double igx2 = plotArea.getMaxX() - hGap;
            double igy1 = plotArea.getY() + vGap;
            double igy2 = plotArea.getMaxY() - vGap;
            g2.setPaint(Color.GRAY);
            g2.draw(new Rectangle2D.Double(igx1, igy1, igx2 - igx1,
                    igy2 - igy1));
        }

        double linkX = plotArea.getX() + gapHorizontal / 2;
        double linkY = plotArea.getY() + gapVertical / 2;
        double linkW = plotArea.getWidth() - gapHorizontal;
        double linkH = plotArea.getHeight() - gapVertical;

        // make the link area a square if the pie chart is to be circular...
        if (this.circular) {
            double min = Math.min(linkW, linkH) / 2;
            linkX = (linkX + linkX + linkW) / 2 - min;
            linkY = (linkY + linkY + linkH) / 2 - min;
            linkW = 2 * min;
            linkH = 2 * min;
        }

        // the link area defines the dog leg points for the linking lines to
        // the labels
        Rectangle2D linkArea = new Rectangle2D.Double(linkX, linkY, linkW,
                linkH);
        state.setLinkArea(linkArea);

        if (DEBUG_DRAW_LINK_AREA) {
            g2.setPaint(Color.BLUE);
            g2.draw(linkArea);
            g2.setPaint(Color.YELLOW);
            g2.draw(new Ellipse2D.Double(linkArea.getX(), linkArea.getY(),
                    linkArea.getWidth(), linkArea.getHeight()));
        }

        // the explode area defines the max circle/ellipse for the exploded
        // pie sections.  it is defined by shrinking the linkArea by the
        // linkMargin factor.
        double lm = 0.0;
        if (!this.simpleLabels) {
            lm = this.labelLinkMargin;
        }
        double hh = linkArea.getWidth() * lm * 2.0;
        double vv = linkArea.getHeight() * lm * 2.0;
        Rectangle2D explodeArea = new Rectangle2D.Double(linkX + hh / 2.0,
                linkY + vv / 2.0, linkW - hh, linkH - vv);

        state.setExplodedPieArea(explodeArea);

        // the pie area defines the circle/ellipse for regular pie sections.
        // it is defined by shrinking the explodeArea by the explodeMargin
        // factor.
        double maximumExplodePercent = getMaximumExplodePercent();
        double percent = maximumExplodePercent / (1.0 + maximumExplodePercent);

        double h1 = explodeArea.getWidth() * percent;
        double v1 = explodeArea.getHeight() * percent;
        Rectangle2D pieArea = new Rectangle2D.Double(explodeArea.getX()
                + h1 / 2.0, explodeArea.getY() + v1 / 2.0,
                explodeArea.getWidth() - h1, explodeArea.getHeight() - v1);

        if (DEBUG_DRAW_PIE_AREA) {
            g2.setPaint(Color.GREEN);
            g2.draw(pieArea);
        }
        state.setPieArea(pieArea);
        state.setPieCenterX(pieArea.getCenterX());
        state.setPieCenterY(pieArea.getCenterY());
        state.setPieWRadius(pieArea.getWidth() / 2.0);
        state.setPieHRadius(pieArea.getHeight() / 2.0);

        // plot the data (unless the dataset is null)...
        if ((this.dataset != null) && (this.dataset.getKeys().size() > 0)) {

            List<K> keys = this.dataset.getKeys();
            double totalValue = DatasetUtils.calculatePieDatasetTotal(
                    this.dataset);

            int passesRequired = state.getPassesRequired();
            for (int pass = 0; pass < passesRequired; pass++) {
                double runningTotal = 0.0;
                for (int section = 0; section < keys.size(); section++) {
                    Number n = this.dataset.getValue(section);
                    if (n != null) {
                        double value = n.doubleValue();
                        if (value > 0.0) {
                            runningTotal += value;
                            drawItem(g2, section, explodeArea, state, pass);
                        }
                    }
                }
            }
            if (this.simpleLabels) {
                drawSimpleLabels(g2, keys, totalValue, plotArea, linkArea,
                        state);
            }
            else {
                drawLabels(g2, keys, totalValue, plotArea, linkArea, state);
            }

        }
        else {
            drawNoDataMessage(g2, plotArea);
        }
    }

    /**
     * Draws a single data item.
     *
     * @param g2  the graphics device ({@code null} not permitted).
     * @param section  the section index.
     * @param dataArea  the data plot area.
     * @param state  state information for one chart.
     * @param currentPass  the current pass index.
     */
    protected void drawItem(Graphics2D g2, int section, Rectangle2D dataArea,
                            PiePlotState state, int currentPass) {

        Number n = this.dataset.getValue(section);
        if (n == null) {
            return;
        }
        double value = n.doubleValue();
        double angle1 = 0.0;
        double angle2 = 0.0;

        if (this.direction == Rotation.CLOCKWISE) {
            angle1 = state.getLatestAngle();
            angle2 = angle1 - value / state.getTotal() * 360.0;
        }
        else if (this.direction == Rotation.ANTICLOCKWISE) {
            angle1 = state.getLatestAngle();
            angle2 = angle1 + value / state.getTotal() * 360.0;
        }
        else {
            throw new IllegalStateException("Rotation type not recognised.");
        }

        double angle = (angle2 - angle1);
        if (Math.abs(angle) > getMinimumArcAngleToDraw()) {
            double ep = 0.0;
            double mep = getMaximumExplodePercent();
            if (mep > 0.0) {
                ep = getExplodePercent(dataset.getKey(section)) / mep;
            }
            Rectangle2D arcBounds = getArcBounds(state.getPieArea(),
                    state.getExplodedPieArea(), angle1, angle, ep);
            Arc2D.Double arc = new Arc2D.Double(arcBounds, angle1, angle,
                    Arc2D.PIE);

            if (currentPass == 0) {
                if (this.shadowPaint != null && this.shadowGenerator == null) {
                    Shape shadowArc = ShapeUtils.createTranslatedShape(
                            arc, (float) this.shadowXOffset,
                            (float) this.shadowYOffset);
                    g2.setPaint(this.shadowPaint);
                    g2.fill(shadowArc);
                }
            }
            else if (currentPass == 1) {
                K key = getSectionKey(section);
                Paint paint = lookupSectionPaint(key, state);
                g2.setPaint(paint);
                g2.fill(arc);

                Paint outlinePaint = lookupSectionOutlinePaint(key);
                Stroke outlineStroke = lookupSectionOutlineStroke(key);
                if (this.sectionOutlinesVisible) {
                    g2.setPaint(outlinePaint);
                    g2.setStroke(outlineStroke);
                    g2.draw(arc);
                }

                // update the linking line target for later
                // add an entity for the pie section
                if (state.getInfo() != null) {
                    EntityCollection entities = state.getEntityCollection();
                    if (entities != null) {
                        String tip = null;
                        if (this.toolTipGenerator != null) {
                            tip = this.toolTipGenerator.generateToolTip(
                                    this.dataset, key);
                        }
                        String url = null;
                        if (this.urlGenerator != null) {
                            url = this.urlGenerator.generateURL(this.dataset,
                                    key, this.pieIndex);
                        }
                        PieSectionEntity entity = new PieSectionEntity(
                                arc, this.dataset, this.pieIndex, section, key,
                                tip, url);
                        entities.add(entity);
                    }
                }
            }
        }
        state.setLatestAngle(angle2);
    }

    /**
     * Draws the pie section labels in the simple form.
     *
     * @param g2  the graphics device.
     * @param keys  the section keys.
     * @param totalValue  the total value for all sections in the pie.
     * @param plotArea  the plot area.
     * @param pieArea  the area containing the pie.
     * @param state  the plot state.
     */
    protected void drawSimpleLabels(Graphics2D g2, List<K> keys,
            double totalValue, Rectangle2D plotArea, Rectangle2D pieArea,
            PiePlotState state) {

        Composite originalComposite = g2.getComposite();
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                1.0f));

        Rectangle2D labelsArea = this.simpleLabelOffset.createInsetRectangle(
                pieArea);
        double runningTotal = 0.0;
        for (K key : keys) {
            boolean include;
            double v = 0.0;
            Number n = getDataset().getValue(key);
            if (n == null) {
                include = !getIgnoreNullValues();
            }
            else {
                v = n.doubleValue();
                include = getIgnoreZeroValues() ? v > 0.0 : v >= 0.0;
            }

            if (include) {
                runningTotal = runningTotal + v;
                // work out the mid angle (0 - 90 and 270 - 360) = right,
                // otherwise left
                double mid = getStartAngle() + (getDirection().getFactor()
                        * ((runningTotal - v / 2.0) * 360) / totalValue);

                Arc2D arc = new Arc2D.Double(labelsArea, getStartAngle(),
                        mid - getStartAngle(), Arc2D.OPEN);
                int x = (int) arc.getEndPoint().getX();
                int y = (int) arc.getEndPoint().getY();

                PieSectionLabelGenerator myLabelGenerator = getLabelGenerator();
                if (myLabelGenerator == null) {
                    continue;
                }
                String label = myLabelGenerator.generateSectionLabel(
                        this.dataset, key);
                if (label == null) {
                    continue;
                }
                g2.setFont(this.labelFont);
                FontMetrics fm = g2.getFontMetrics();
                Rectangle2D bounds = TextUtils.getTextBounds(label, g2, fm);
                Rectangle2D out = this.labelPadding.createOutsetRectangle(
                        bounds);
                Shape bg = ShapeUtils.createTranslatedShape(out,
                        x - bounds.getCenterX(), y - bounds.getCenterY());
                if (this.labelShadowPaint != null
                        && this.shadowGenerator == null) {
                    Shape shadow = ShapeUtils.createTranslatedShape(bg,
                            this.shadowXOffset, this.shadowYOffset);
                    g2.setPaint(this.labelShadowPaint);
                    g2.fill(shadow);
                }
                if (this.labelBackgroundPaint != null) {
                    g2.setPaint(this.labelBackgroundPaint);
                    g2.fill(bg);
                }
                if (this.labelOutlinePaint != null
                        && this.labelOutlineStroke != null) {
                    g2.setPaint(this.labelOutlinePaint);
                    g2.setStroke(this.labelOutlineStroke);
                    g2.draw(bg);
                }

                g2.setPaint(this.labelPaint);
                g2.setFont(this.labelFont);
                TextUtils.drawAlignedString(label, g2, x, y,
                        TextAnchor.CENTER);

            }
        }

        g2.setComposite(originalComposite);

    }

    /**
     * Draws the labels for the pie sections.
     *
     * @param g2  the graphics device.
     * @param keys  the keys.
     * @param totalValue  the total value.
     * @param plotArea  the plot area.
     * @param linkArea  the link area.
     * @param state  the state.
     */
    protected void drawLabels(Graphics2D g2, List<K> keys, double totalValue,
                              Rectangle2D plotArea, Rectangle2D linkArea,
                              PiePlotState state) {

        Composite originalComposite = g2.getComposite();
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                1.0f));

        // classify the keys according to which side the label will appear...
        DefaultKeyedValues leftKeys = new DefaultKeyedValues();
        DefaultKeyedValues rightKeys = new DefaultKeyedValues();

        double runningTotal = 0.0;
        for (K key : keys) {
            boolean include;
            double v = 0.0;
            Number n = this.dataset.getValue(key);
            if (n == null) {
                include = !this.ignoreNullValues;
            }
            else {
                v = n.doubleValue();
                include = this.ignoreZeroValues ? v > 0.0 : v >= 0.0;
            }

            if (include) {
                runningTotal = runningTotal + v;
                // work out the mid angle (0 - 90 and 270 - 360) = right,
                // otherwise left
                double mid = this.startAngle + (this.direction.getFactor()
                        * ((runningTotal - v / 2.0) * 360) / totalValue);
                if (Math.cos(Math.toRadians(mid)) < 0.0) {
                    leftKeys.addValue(key, mid);
                }
                else {
                    rightKeys.addValue(key, mid);
                }
            }
        }

        g2.setFont(getLabelFont());

        // calculate the max label width from the plot dimensions, because
        // a circular pie can leave a lot more room for labels...
        double marginX = plotArea.getX();
        double gap = plotArea.getWidth() * this.labelGap;
        double ww = linkArea.getX() - gap - marginX;
        float labelWidth = (float) this.labelPadding.trimWidth(ww);

        // draw the labels...
        if (this.labelGenerator != null) {
            drawLeftLabels(leftKeys, g2, plotArea, linkArea, labelWidth,
                    state);
            drawRightLabels(rightKeys, g2, plotArea, linkArea, labelWidth,
                    state);
        }
        g2.setComposite(originalComposite);

    }

    /**
     * Draws the left labels.
     *
     * @param leftKeys  a collection of keys and angles (to the middle of the
     *         section, in degrees) for the sections on the left side of the
     *         plot.
     * @param g2  the graphics device.
     * @param plotArea  the plot area.
     * @param linkArea  the link area.
     * @param maxLabelWidth  the maximum label width.
     * @param state  the state.
     */
    protected void drawLeftLabels(KeyedValues<K> leftKeys, Graphics2D g2,
                                  Rectangle2D plotArea, Rectangle2D linkArea,
                                  float maxLabelWidth, PiePlotState state) {

        this.labelDistributor.clear();
        double lGap = plotArea.getWidth() * this.labelGap;
        double verticalLinkRadius = state.getLinkArea().getHeight() / 2.0;
        for (int i = 0; i < leftKeys.getItemCount(); i++) {
            String label = this.labelGenerator.generateSectionLabel(
                    this.dataset, leftKeys.getKey(i));
            if (label != null) {
                TextBlock block = TextUtils.createTextBlock(label,
                        this.labelFont, this.labelPaint, maxLabelWidth,
                        new G2TextMeasurer(g2));
                TextBox labelBox = new TextBox(block);
                labelBox.setBackgroundPaint(this.labelBackgroundPaint);
                labelBox.setOutlinePaint(this.labelOutlinePaint);
                labelBox.setOutlineStroke(this.labelOutlineStroke);
                if (this.shadowGenerator == null) {
                    labelBox.setShadowPaint(this.labelShadowPaint);
                }
                else {
                    labelBox.setShadowPaint(null);
                }
                labelBox.setInteriorGap(this.labelPadding);
                double theta = Math.toRadians(
                        leftKeys.getValue(i).doubleValue());
                double baseY = state.getPieCenterY() - Math.sin(theta)
                               * verticalLinkRadius;
                double hh = labelBox.getHeight(g2);

                this.labelDistributor.addPieLabelRecord(new PieLabelRecord(
                        leftKeys.getKey(i), theta, baseY, labelBox, hh,
                        lGap / 2.0 + lGap / 2.0 * -Math.cos(theta), 1.0
                        - getLabelLinkDepth()
                        + getExplodePercent(leftKeys.getKey(i))));
            }
        }
        double hh = plotArea.getHeight();
        double gap = hh * getInteriorGap();
        this.labelDistributor.distributeLabels(plotArea.getMinY() + gap,
                hh - 2 * gap);
        for (int i = 0; i < this.labelDistributor.getItemCount(); i++) {
            drawLeftLabel(g2, state,
                    this.labelDistributor.getPieLabelRecord(i));
        }
    }

    /**
     * Draws the right labels.
     *
     * @param keys  the keys.
     * @param g2  the graphics device.
     * @param plotArea  the plot area.
     * @param linkArea  the link area.
     * @param maxLabelWidth  the maximum label width.
     * @param state  the state.
     */
    protected void drawRightLabels(KeyedValues<K> keys, Graphics2D g2,
                                   Rectangle2D plotArea, Rectangle2D linkArea,
                                   float maxLabelWidth, PiePlotState state) {

        // draw the right labels...
        this.labelDistributor.clear();
        double lGap = plotArea.getWidth() * this.labelGap;
        double verticalLinkRadius = state.getLinkArea().getHeight() / 2.0;

        for (int i = 0; i < keys.getItemCount(); i++) {
            String label = this.labelGenerator.generateSectionLabel(
                    this.dataset, keys.getKey(i));

            if (label != null) {
                TextBlock block = TextUtils.createTextBlock(label,
                        this.labelFont, this.labelPaint, maxLabelWidth,
                        new G2TextMeasurer(g2));
                TextBox labelBox = new TextBox(block);
                labelBox.setBackgroundPaint(this.labelBackgroundPaint);
                labelBox.setOutlinePaint(this.labelOutlinePaint);
                labelBox.setOutlineStroke(this.labelOutlineStroke);
                if (this.shadowGenerator == null) {
                    labelBox.setShadowPaint(this.labelShadowPaint);
                }
                else {
                    labelBox.setShadowPaint(null);
                }
                labelBox.setInteriorGap(this.labelPadding);
                double theta = Math.toRadians(keys.getValue(i).doubleValue());
                double baseY = state.getPieCenterY()
                              - Math.sin(theta) * verticalLinkRadius;
                double hh = labelBox.getHeight(g2);
                this.labelDistributor.addPieLabelRecord(new PieLabelRecord(
                        keys.getKey(i), theta, baseY, labelBox, hh,
                        lGap / 2.0 + lGap / 2.0 * Math.cos(theta),
                        1.0 - getLabelLinkDepth()
                        + getExplodePercent(keys.getKey(i))));
            }
        }
        double hh = plotArea.getHeight();
        double gap = 0.00; //hh * getInteriorGap();
        this.labelDistributor.distributeLabels(plotArea.getMinY() + gap,
                hh - 2 * gap);
        for (int i = 0; i < this.labelDistributor.getItemCount(); i++) {
            drawRightLabel(g2, state,
                    this.labelDistributor.getPieLabelRecord(i));
        }

    }

    /**
     * Returns a collection of legend items for the pie chart.
     *
     * @return The legend items (never {@code null}).
     */
    @Override
    public LegendItemCollection getLegendItems() {

        LegendItemCollection result = new LegendItemCollection();
        if (this.dataset == null) {
            return result;
        }
        List<K> keys = this.dataset.getKeys();
        int section = 0;
        Shape shape = getLegendItemShape();
        for (K key : keys) {
            Number n = this.dataset.getValue(key);
            boolean include;
            if (n == null) {
                include = !this.ignoreNullValues;
            }
            else {
                double v = n.doubleValue();
                if (v == 0.0) {
                    include = !this.ignoreZeroValues;
                }
                else {
                    include = v > 0.0;
                }
            }
            if (include) {
                String label = this.legendLabelGenerator.generateSectionLabel(
                        this.dataset, key);
                if (label != null) {
                    String description = label;
                    String toolTipText = null;
                    if (this.legendLabelToolTipGenerator != null) {
                        toolTipText = this.legendLabelToolTipGenerator
                                .generateSectionLabel(this.dataset, key);
                    }
                    String urlText = null;
                    if (this.legendLabelURLGenerator != null) {
                        urlText = this.legendLabelURLGenerator.generateURL(
                                this.dataset, key, this.pieIndex);
                    }
                    Paint paint = lookupSectionPaint(key);
                    Paint outlinePaint = lookupSectionOutlinePaint(key);
                    Stroke outlineStroke = lookupSectionOutlineStroke(key);
                    LegendItem item = new LegendItem(label, description,
                            toolTipText, urlText, true, shape, true, paint,
                            true, outlinePaint, outlineStroke,
                            false,          // line not visible
                            new Line2D.Float(), new BasicStroke(), Color.BLACK);
                    item.setDataset(getDataset());
                    item.setSeriesIndex(this.dataset.getIndex(key));
                    item.setSeriesKey(key);
                    result.add(item);
                }
                section++;
            }
            else {
                section++;
            }
        }
        return result;
    }

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

    /**
     * Returns a rectangle that can be used to create a pie section (taking
     * into account the amount by which the pie section is 'exploded').
     *
     * @param unexploded  the area inside which the unexploded pie sections are
     *                    drawn.
     * @param exploded  the area inside which the exploded pie sections are
     *                  drawn.
     * @param angle  the start angle.
     * @param extent  the extent of the arc.
     * @param explodePercent  the amount by which the pie section is exploded.
     *
     * @return A rectangle that can be used to create a pie section.
     */
    protected Rectangle2D getArcBounds(Rectangle2D unexploded,
                                       Rectangle2D exploded,
                                       double angle, double extent,
                                       double explodePercent) {

        if (explodePercent == 0.0) {
            return unexploded;
        }
        Arc2D arc1 = new Arc2D.Double(unexploded, angle, extent / 2,
                Arc2D.OPEN);
        Point2D point1 = arc1.getEndPoint();
        Arc2D.Double arc2 = new Arc2D.Double(exploded, angle, extent / 2,
                Arc2D.OPEN);
        Point2D point2 = arc2.getEndPoint();
        double deltaX = (point1.getX() - point2.getX()) * explodePercent;
        double deltaY = (point1.getY() - point2.getY()) * explodePercent;
        return new Rectangle2D.Double(unexploded.getX() - deltaX,
                unexploded.getY() - deltaY, unexploded.getWidth(),
                unexploded.getHeight());
    }

    /**
     * Draws a section label on the left side of the pie chart.
     *
     * @param g2  the graphics device.
     * @param state  the state.
     * @param record  the label record.
     */
    protected void drawLeftLabel(Graphics2D g2, PiePlotState state,
                                 PieLabelRecord record) {

        double anchorX = state.getLinkArea().getMinX();
        double targetX = anchorX - record.getGap();
        double targetY = record.getAllocatedY();

        if (this.labelLinksVisible) {
            double theta = record.getAngle();
            double linkX = state.getPieCenterX() + Math.cos(theta)
                    * state.getPieWRadius() * record.getLinkPercent();
            double linkY = state.getPieCenterY() - Math.sin(theta)
                    * state.getPieHRadius() * record.getLinkPercent();
            double elbowX = state.getPieCenterX() + Math.cos(theta)
                    * state.getLinkArea().getWidth() / 2.0;
            double elbowY = state.getPieCenterY() - Math.sin(theta)
                    * state.getLinkArea().getHeight() / 2.0;
            double anchorY = elbowY;
            g2.setPaint(this.labelLinkPaint);
            g2.setStroke(this.labelLinkStroke);
            PieLabelLinkStyle style = getLabelLinkStyle();
            if (style.equals(PieLabelLinkStyle.STANDARD)) {
                g2.draw(new Line2D.Double(linkX, linkY, elbowX, elbowY));
                g2.draw(new Line2D.Double(anchorX, anchorY, elbowX, elbowY));
                g2.draw(new Line2D.Double(anchorX, anchorY, targetX, targetY));
            }
            else if (style.equals(PieLabelLinkStyle.QUAD_CURVE)) {
                QuadCurve2D q = new QuadCurve2D.Float();
                q.setCurve(targetX, targetY, anchorX, anchorY, elbowX, elbowY);
                g2.draw(q);
                g2.draw(new Line2D.Double(elbowX, elbowY, linkX, linkY));
            }
            else if (style.equals(PieLabelLinkStyle.CUBIC_CURVE)) {
                CubicCurve2D c = new CubicCurve2D .Float();
                c.setCurve(targetX, targetY, anchorX, anchorY, elbowX, elbowY,
                        linkX, linkY);
                g2.draw(c);
            }
        }
        TextBox tb = record.getLabel();
        tb.draw(g2, (float) targetX, (float) targetY, RectangleAnchor.RIGHT);

    }

    /**
     * Draws a section label on the right side of the pie chart.
     *
     * @param g2  the graphics device.
     * @param state  the state.
     * @param record  the label record.
     */
    protected void drawRightLabel(Graphics2D g2, PiePlotState state,
                                  PieLabelRecord record) {

        double anchorX = state.getLinkArea().getMaxX();
        double targetX = anchorX + record.getGap();
        double targetY = record.getAllocatedY();

        if (this.labelLinksVisible) {
            double theta = record.getAngle();
            double linkX = state.getPieCenterX() + Math.cos(theta)
                    * state.getPieWRadius() * record.getLinkPercent();
            double linkY = state.getPieCenterY() - Math.sin(theta)
                    * state.getPieHRadius() * record.getLinkPercent();
            double elbowX = state.getPieCenterX() + Math.cos(theta)
                    * state.getLinkArea().getWidth() / 2.0;
            double elbowY = state.getPieCenterY() - Math.sin(theta)
                    * state.getLinkArea().getHeight() / 2.0;
            double anchorY = elbowY;
            g2.setPaint(this.labelLinkPaint);
            g2.setStroke(this.labelLinkStroke);
            PieLabelLinkStyle style = getLabelLinkStyle();
            if (style.equals(PieLabelLinkStyle.STANDARD)) {
                g2.draw(new Line2D.Double(linkX, linkY, elbowX, elbowY));
                g2.draw(new Line2D.Double(anchorX, anchorY, elbowX, elbowY));
                g2.draw(new Line2D.Double(anchorX, anchorY, targetX, targetY));
            }
            else if (style.equals(PieLabelLinkStyle.QUAD_CURVE)) {
                QuadCurve2D q = new QuadCurve2D.Float();
                q.setCurve(targetX, targetY, anchorX, anchorY, elbowX, elbowY);
                g2.draw(q);
                g2.draw(new Line2D.Double(elbowX, elbowY, linkX, linkY));
            }
            else if (style.equals(PieLabelLinkStyle.CUBIC_CURVE)) {
                CubicCurve2D c = new CubicCurve2D .Float();
                c.setCurve(targetX, targetY, anchorX, anchorY, elbowX, elbowY,
                        linkX, linkY);
                g2.draw(c);
            }
        }

        TextBox tb = record.getLabel();
        tb.draw(g2, (float) targetX, (float) targetY, RectangleAnchor.LEFT);

    }

    /**
     * Returns the center for the specified section.
     * Checks to see if the section is exploded and recalculates the
     * new center if so.
     *
     * @param state  PiePlotState
     * @param key  section key.
     *
     * @return The center for the specified section.
     */
    protected Point2D getArcCenter(PiePlotState state, K key) {
        Point2D center = new Point2D.Double(state.getPieCenterX(), state
            .getPieCenterY());

        double ep = getExplodePercent(key);
        double mep = getMaximumExplodePercent();
        if (mep > 0.0) {
            ep = ep / mep;
        }
        if (ep != 0) {
            Rectangle2D pieArea = state.getPieArea();
            Rectangle2D expPieArea = state.getExplodedPieArea();
            double angle1, angle2;
            Number n = this.dataset.getValue(key);
            double value = n.doubleValue();

            if (this.direction == Rotation.CLOCKWISE) {
                angle1 = state.getLatestAngle();
                angle2 = angle1 - value / state.getTotal() * 360.0;
            } else if (this.direction == Rotation.ANTICLOCKWISE) {
                angle1 = state.getLatestAngle();
                angle2 = angle1 + value / state.getTotal() * 360.0;
            } else {
                throw new IllegalStateException("Rotation type not recognised.");
            }
            double angle = (angle2 - angle1);

            Arc2D arc1 = new Arc2D.Double(pieArea, angle1, angle / 2,
                    Arc2D.OPEN);
            Point2D point1 = arc1.getEndPoint();
            Arc2D.Double arc2 = new Arc2D.Double(expPieArea, angle1, angle / 2,
                    Arc2D.OPEN);
            Point2D point2 = arc2.getEndPoint();
            double deltaX = (point1.getX() - point2.getX()) * ep;
            double deltaY = (point1.getY() - point2.getY()) * ep;

            center = new Point2D.Double(state.getPieCenterX() - deltaX,
                     state.getPieCenterY() - deltaY);

        }
        return center;
    }

    /**
     * Returns the paint for the specified section. This is equivalent to
     * {@code lookupSectionPaint(section)}.  Checks to see if the user set the 
     * {@code Paint} to be of type {@code RadialGradientPaint} and if so it 
     * adjusts the center and radius to match the Pie.
     *
     * @param key  the section key.
     * @param state  PiePlotState.
     *
     * @return The paint for the specified section.
     */
    protected Paint lookupSectionPaint(K key, PiePlotState state) {
        Paint paint = lookupSectionPaint(key, getAutoPopulateSectionPaint());
        // for a RadialGradientPaint we adjust the center and radius to match
        // the current pie segment...
        if (paint instanceof RadialGradientPaint) {
            RadialGradientPaint rgp = (RadialGradientPaint) paint;
            Point2D center = getArcCenter(state, key);
            float radius = (float) Math.max(state.getPieHRadius(), 
                    state.getPieWRadius());
            float[] fractions = rgp.getFractions();
            Color[] colors = rgp.getColors();
            paint = new RadialGradientPaint(center, radius, fractions, colors);
        }
        return paint;
    }

    /**
     * Tests this plot for equality with an arbitrary object.  Note that the
     * plot's dataset is NOT included in the test for equality.
     *
     * @param obj the object to test against ({@code null} permitted).
     *
     * @return {@code true} or {@code false}.
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof PiePlot)) {
            return false;
        }
        if (!super.equals(obj)) {
            return false;
        }
        PiePlot that = (PiePlot) obj;
        if (this.pieIndex != that.pieIndex) {
            return false;
        }
        if (this.interiorGap != that.interiorGap) {
            return false;
        }
        if (this.circular != that.circular) {
            return false;
        }
        if (this.startAngle != that.startAngle) {
            return false;
        }
        if (this.direction != that.direction) {
            return false;
        }
        if (this.ignoreZeroValues != that.ignoreZeroValues) {
            return false;
        }
        if (this.ignoreNullValues != that.ignoreNullValues) {
            return false;
        }
        if (!Objects.equals(this.sectionPaintMap,
                that.sectionPaintMap)) {
            return false;
        }
        if (!PaintUtils.equal(this.defaultSectionPaint,
                that.defaultSectionPaint)) {
            return false;
        }
        if (this.sectionOutlinesVisible != that.sectionOutlinesVisible) {
            return false;
        }
        if (!Objects.equals(this.sectionOutlinePaintMap,
                that.sectionOutlinePaintMap)) {
            return false;
        }
        if (!PaintUtils.equal(this.defaultSectionOutlinePaint,
                that.defaultSectionOutlinePaint)) {
            return false;
        }
        if (!Objects.equals(this.sectionOutlineStrokeMap,
                that.sectionOutlineStrokeMap)) {
            return false;
        }
        if (!Objects.equals(this.defaultSectionOutlineStroke,
                that.defaultSectionOutlineStroke)) {
            return false;
        }
        if (!PaintUtils.equal(this.shadowPaint, that.shadowPaint)) {
            return false;
        }
        if (!(this.shadowXOffset == that.shadowXOffset)) {
            return false;
        }
        if (!(this.shadowYOffset == that.shadowYOffset)) {
            return false;
        }
        if (!Objects.equals(this.explodePercentages,
                that.explodePercentages)) {
            return false;
        }
        if (!Objects.equals(this.labelGenerator,
                that.labelGenerator)) {
            return false;
        }
        if (!Objects.equals(this.labelFont, that.labelFont)) {
            return false;
        }
        if (!PaintUtils.equal(this.labelPaint, that.labelPaint)) {
            return false;
        }
        if (!PaintUtils.equal(this.labelBackgroundPaint,
                that.labelBackgroundPaint)) {
            return false;
        }
        if (!PaintUtils.equal(this.labelOutlinePaint,
                that.labelOutlinePaint)) {
            return false;
        }
        if (!Objects.equals(this.labelOutlineStroke,
                that.labelOutlineStroke)) {
            return false;
        }
        if (!PaintUtils.equal(this.labelShadowPaint,
                that.labelShadowPaint)) {
            return false;
        }
        if (this.simpleLabels != that.simpleLabels) {
            return false;
        }
        if (!this.simpleLabelOffset.equals(that.simpleLabelOffset)) {
            return false;
        }
        if (!this.labelPadding.equals(that.labelPadding)) {
            return false;
        }
        if (!(this.maximumLabelWidth == that.maximumLabelWidth)) {
            return false;
        }
        if (!(this.labelGap == that.labelGap)) {
            return false;
        }
        if (!(this.labelLinkMargin == that.labelLinkMargin)) {
            return false;
        }
        if (this.labelLinksVisible != that.labelLinksVisible) {
            return false;
        }
        if (!this.labelLinkStyle.equals(that.labelLinkStyle)) {
            return false;
        }
        if (!PaintUtils.equal(this.labelLinkPaint, that.labelLinkPaint)) {
            return false;
        }
        if (!Objects.equals(this.labelLinkStroke,
                that.labelLinkStroke)) {
            return false;
        }
        if (!Objects.equals(this.toolTipGenerator,
                that.toolTipGenerator)) {
            return false;
        }
        if (!Objects.equals(this.urlGenerator, that.urlGenerator)) {
            return false;
        }
        if (!(this.minimumArcAngleToDraw == that.minimumArcAngleToDraw)) {
            return false;
        }
        if (!ShapeUtils.equal(this.legendItemShape, that.legendItemShape)) {
            return false;
        }
        if (!Objects.equals(this.legendLabelGenerator,
                that.legendLabelGenerator)) {
            return false;
        }
        if (!Objects.equals(this.legendLabelToolTipGenerator,
                that.legendLabelToolTipGenerator)) {
            return false;
        }
        if (!Objects.equals(this.legendLabelURLGenerator,
                that.legendLabelURLGenerator)) {
            return false;
        }
        if (this.autoPopulateSectionPaint != that.autoPopulateSectionPaint) {
            return false;
        }
        if (this.autoPopulateSectionOutlinePaint
                != that.autoPopulateSectionOutlinePaint) {
            return false;
        }
        if (this.autoPopulateSectionOutlineStroke
                != that.autoPopulateSectionOutlineStroke) {
            return false;
        }
        if (!Objects.equals(this.shadowGenerator,
                that.shadowGenerator)) {
            return false;
        }
        // can't find any difference...
        return true;
    }

    /**
     * Generates a hashcode.  Note that, as with the equals method, the dataset
     * is NOT included in the hashcode.
     * 
     * @return the hashcode
     */
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 73 * hash + this.pieIndex;
        hash = 73 * hash + (int) (Double.doubleToLongBits(this.interiorGap) ^ (Double.doubleToLongBits(this.interiorGap) >>> 32));
        hash = 73 * hash + (this.circular ? 1 : 0);
        hash = 73 * hash + (int) (Double.doubleToLongBits(this.startAngle) ^ (Double.doubleToLongBits(this.startAngle) >>> 32));
        hash = 73 * hash + Objects.hashCode(this.direction);
        hash = 73 * hash + Objects.hashCode(this.sectionPaintMap);
        hash = 73 * hash + Objects.hashCode(this.defaultSectionPaint);
        hash = 73 * hash + (this.autoPopulateSectionPaint ? 1 : 0);
        hash = 73 * hash + (this.sectionOutlinesVisible ? 1 : 0);
        hash = 73 * hash + Objects.hashCode(this.sectionOutlinePaintMap);
        hash = 73 * hash + Objects.hashCode(this.defaultSectionOutlinePaint);
        hash = 73 * hash + (this.autoPopulateSectionOutlinePaint ? 1 : 0);
        hash = 73 * hash + Objects.hashCode(this.sectionOutlineStrokeMap);
        hash = 73 * hash + Objects.hashCode(this.defaultSectionOutlineStroke);
        hash = 73 * hash + (this.autoPopulateSectionOutlineStroke ? 1 : 0);
        hash = 73 * hash + Objects.hashCode(this.shadowPaint);
        hash = 73 * hash + (int) (Double.doubleToLongBits(this.shadowXOffset) ^ (Double.doubleToLongBits(this.shadowXOffset) >>> 32));
        hash = 73 * hash + (int) (Double.doubleToLongBits(this.shadowYOffset) ^ (Double.doubleToLongBits(this.shadowYOffset) >>> 32));
        hash = 73 * hash + Objects.hashCode(this.explodePercentages);
        hash = 73 * hash + Objects.hashCode(this.labelGenerator);
        hash = 73 * hash + Objects.hashCode(this.labelFont);
        hash = 73 * hash + Objects.hashCode(this.labelPaint);
        hash = 73 * hash + Objects.hashCode(this.labelBackgroundPaint);
        hash = 73 * hash + Objects.hashCode(this.labelOutlinePaint);
        hash = 73 * hash + Objects.hashCode(this.labelOutlineStroke);
        hash = 73 * hash + Objects.hashCode(this.labelShadowPaint);
        hash = 73 * hash + (this.simpleLabels ? 1 : 0);
        hash = 73 * hash + Objects.hashCode(this.labelPadding);
        hash = 73 * hash + Objects.hashCode(this.simpleLabelOffset);
        hash = 73 * hash + (int) (Double.doubleToLongBits(this.maximumLabelWidth) ^ (Double.doubleToLongBits(this.maximumLabelWidth) >>> 32));
        hash = 73 * hash + (int) (Double.doubleToLongBits(this.labelGap) ^ (Double.doubleToLongBits(this.labelGap) >>> 32));
        hash = 73 * hash + (this.labelLinksVisible ? 1 : 0);
        hash = 73 * hash + Objects.hashCode(this.labelLinkStyle);
        hash = 73 * hash + (int) (Double.doubleToLongBits(this.labelLinkMargin) ^ (Double.doubleToLongBits(this.labelLinkMargin) >>> 32));
        hash = 73 * hash + Objects.hashCode(this.labelLinkPaint);
        hash = 73 * hash + Objects.hashCode(this.labelLinkStroke);
        hash = 73 * hash + Objects.hashCode(this.toolTipGenerator);
        hash = 73 * hash + Objects.hashCode(this.urlGenerator);
        hash = 73 * hash + Objects.hashCode(this.legendLabelGenerator);
        hash = 73 * hash + Objects.hashCode(this.legendLabelToolTipGenerator);
        hash = 73 * hash + Objects.hashCode(this.legendLabelURLGenerator);
        hash = 73 * hash + (this.ignoreNullValues ? 1 : 0);
        hash = 73 * hash + (this.ignoreZeroValues ? 1 : 0);
        hash = 73 * hash + Objects.hashCode(this.legendItemShape);
        hash = 73 * hash + (int) (Double.doubleToLongBits(this.minimumArcAngleToDraw) ^ (Double.doubleToLongBits(this.minimumArcAngleToDraw) >>> 32));
        hash = 73 * hash + Objects.hashCode(this.shadowGenerator);
        return hash;
    }

    /**
     * Returns a clone of the plot.
     *
     * @return A clone.
     *
     * @throws CloneNotSupportedException if some component of the plot does
     *         not support cloning.
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        PiePlot clone = (PiePlot) super.clone();
        clone.sectionPaintMap = (PaintMap) this.sectionPaintMap.clone();
        clone.sectionOutlinePaintMap 
                = (PaintMap) this.sectionOutlinePaintMap.clone();
        clone.sectionOutlineStrokeMap 
                = (StrokeMap) this.sectionOutlineStrokeMap.clone();
        clone.explodePercentages = new TreeMap<>(this.explodePercentages);
        if (this.labelGenerator != null) {
            clone.labelGenerator = (PieSectionLabelGenerator) 
                    ObjectUtils.clone(this.labelGenerator);
        }
        if (clone.dataset != null) {
            clone.dataset.addChangeListener(clone);
        }
        if (this.urlGenerator instanceof PublicCloneable) {
            clone.urlGenerator = (PieURLGenerator) ObjectUtils.clone(
                    this.urlGenerator);
        }
        clone.legendItemShape = ShapeUtils.clone(this.legendItemShape);
        if (this.legendLabelGenerator != null) {
            clone.legendLabelGenerator = (PieSectionLabelGenerator)
                    ObjectUtils.clone(this.legendLabelGenerator);
        }
        if (this.legendLabelToolTipGenerator != null) {
            clone.legendLabelToolTipGenerator = (PieSectionLabelGenerator)
                    ObjectUtils.clone(this.legendLabelToolTipGenerator);
        }
        if (this.legendLabelURLGenerator instanceof PublicCloneable) {
            clone.legendLabelURLGenerator = (PieURLGenerator)
                    ObjectUtils.clone(this.legendLabelURLGenerator);
        }
        return clone;
    }

    /**
     * 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();
        SerialUtils.writePaint(this.defaultSectionPaint, stream);
        SerialUtils.writePaint(this.defaultSectionOutlinePaint, stream);
        SerialUtils.writeStroke(this.defaultSectionOutlineStroke, stream);
        SerialUtils.writePaint(this.shadowPaint, stream);
        SerialUtils.writePaint(this.labelPaint, stream);
        SerialUtils.writePaint(this.labelBackgroundPaint, stream);
        SerialUtils.writePaint(this.labelOutlinePaint, stream);
        SerialUtils.writeStroke(this.labelOutlineStroke, stream);
        SerialUtils.writePaint(this.labelShadowPaint, stream);
        SerialUtils.writePaint(this.labelLinkPaint, stream);
        SerialUtils.writeStroke(this.labelLinkStroke, stream);
        SerialUtils.writeShape(this.legendItemShape, 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.defaultSectionPaint = SerialUtils.readPaint(stream);
        this.defaultSectionOutlinePaint = SerialUtils.readPaint(stream);
        this.defaultSectionOutlineStroke = SerialUtils.readStroke(stream);
        this.shadowPaint = SerialUtils.readPaint(stream);
        this.labelPaint = SerialUtils.readPaint(stream);
        this.labelBackgroundPaint = SerialUtils.readPaint(stream);
        this.labelOutlinePaint = SerialUtils.readPaint(stream);
        this.labelOutlineStroke = SerialUtils.readStroke(stream);
        this.labelShadowPaint = SerialUtils.readPaint(stream);
        this.labelLinkPaint = SerialUtils.readPaint(stream);
        this.labelLinkStroke = SerialUtils.readStroke(stream);
        this.legendItemShape = SerialUtils.readShape(stream);
    }

}
