content: /* ===========================================================
   * JFreeChart : a free chart library for the Java(tm) platform
   * ===========================================================
   *
   * (C) Copyright 2000-2014, by Object Refinery Limited and Contributors.
   *
   * Project Info:  http://www.jfree.org/jfreechart/index.html
   *
   * This library is free software; you can redistribute it and/or modify it
   * under the terms of the GNU Lesser General Public License as published by
   * the Free Software Foundation; either version 2.1 of the License, or
   * (at your option) any later version.
   *
   * This library is distributed in the hope that it will be useful, but
   * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
   * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
   * License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
   * USA.
   *
   * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
   * Other names may be trademarks of their respective owners.]
   *
   * --------------------
   * MultiplePiePlot.java
   * --------------------
   * (C) Copyright 2004-2014, by Object Refinery Limited.
   *
   * Original Author:  David Gilbert (for Object Refinery Limited);
   * Contributor(s):   Brian Cabana (patch 1943021);
   *
   * Changes
   * -------
   * 29-Jan-2004 : Version 1 (DG);
   * 31-Mar-2004 : Added setPieIndex() call during drawing (DG);
   * 20-Apr-2005 : Small change for update to LegendItem constructors (DG);
   * 05-May-2005 : Updated draw() method parameters (DG);
   * 16-Jun-2005 : Added get/setDataset() and equals() methods (DG);
   * ------------- JFREECHART 1.0.x ---------------------------------------------
   * 06-Apr-2006 : Fixed bug 1190647 - legend and section colors not consistent
   *               when aggregation limit is specified (DG);
   * 27-Sep-2006 : Updated draw() method for deprecated code (DG);
   * 17-Jan-2007 : Updated prefetchSectionPaints() to check settings in
   *               underlying PiePlot (DG);
   * 17-May-2007 : Added argument check to setPieChart() (DG);
   * 18-May-2007 : Set dataset for LegendItem (DG);
   * 18-Apr-2008 : In the constructor, register the plot as a dataset listener -
   *               see patch 1943021 from Brian Cabana (DG);
   * 30-Dec-2008 : Added legendItemShape field, and fixed cloning bug (DG);
   * 09-Jan-2009 : See ignoreNullValues to true for sub-chart (DG);
   * 01-Jun-2009 : Set series key in getLegendItems() (DG);
   * 17-Jun-2012 : Removed JCommon dependencies (DG);
   * 10-Mar-2014 : Removed LegendItemCollection (DG);
   *
   */
  
  package org.jfree.chart.plot;
  
  import java.awt.Color;
  import java.awt.Font;
  import java.awt.Graphics2D;
  import java.awt.Paint;
  import java.awt.Rectangle;
  import java.awt.Shape;
  import java.awt.geom.Ellipse2D;
  import java.awt.geom.Point2D;
  import java.awt.geom.Rectangle2D;
  import java.io.IOException;
  import java.io.ObjectInputStream;
  import java.io.ObjectOutputStream;
  import java.io.Serializable;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  import org.jfree.chart.ChartRenderingInfo;
  import org.jfree.chart.JFreeChart;
  import org.jfree.chart.LegendItem;
  import org.jfree.chart.ui.RectangleEdge;
  import org.jfree.chart.ui.RectangleInsets;
  import org.jfree.chart.util.ObjectUtilities;
  import org.jfree.chart.util.PaintUtilities;
  import org.jfree.chart.util.ShapeUtilities;
  import org.jfree.chart.util.TableOrder;
  import org.jfree.chart.event.PlotChangeEvent;
  import org.jfree.chart.title.TextTitle;
  import org.jfree.chart.util.SerialUtilities;
  import org.jfree.data.category.CategoryDataset;
  import org.jfree.data.category.CategoryToPieDataset;
  import org.jfree.data.general.DatasetChangeEvent;
  import org.jfree.data.general.DatasetUtilities;
  import org.jfree.data.general.PieDataset;
  
  /**
   * A plot that displays multiple pie plots using data from a
   * {@link CategoryDataset}.
   */
  public class MultiplePiePlot extends Plot implements Cloneable, Serializable {
  
      /** For serialization. */
      private static final long serialVersionUID = -355377800470807389L;
  
      /** The chart object that draws the individual pie charts. */
      private JFreeChart pieChart;
  
      /** The dataset. */
      private CategoryDataset dataset;
  
      /** The data extract order (by row or by column). */
      private TableOrder dataExtractOrder;
  
      /** The pie section limit percentage. */
      private double limit = 0.0;
  
      /**
       * The key for the aggregated items.
       *
       * @since 1.0.2
       */
      private Comparable aggregatedItemsKey;
  
      /**
       * The paint for the aggregated items.
       *
       * @since 1.0.2
       */
      private transient Paint aggregatedItemsPaint;
  
      /**
       * The colors to use for each section.
       *
       * @since 1.0.2
       */
      private transient Map<Comparable, Paint> sectionPaints;
  
      /**
       * The legend item shape (never null).
       *
       * @since 1.0.12
       */
      private transient Shape legendItemShape;
  
      /**
       * Creates a new plot with no data.
       */
      public MultiplePiePlot() {
          this(null);
      }
  
      /**
       * Creates a new plot.
       *
       * @param dataset  the dataset (<code>null</code> permitted).
       */
      public MultiplePiePlot(CategoryDataset dataset) {
          super();
          setDataset(dataset);
          PiePlot piePlot = new PiePlot(null);
          piePlot.setIgnoreNullValues(true);
          this.pieChart = new JFreeChart(piePlot);
          this.pieChart.removeLegend();
          this.dataExtractOrder = TableOrder.BY_COLUMN;
          this.pieChart.setBackgroundPaint(null);
          TextTitle seriesTitle = new TextTitle("Series Title",
                  new Font("SansSerif", Font.BOLD, 12));
          seriesTitle.setPosition(RectangleEdge.BOTTOM);
          this.pieChart.setTitle(seriesTitle);
          this.aggregatedItemsKey = "Other";
          this.aggregatedItemsPaint = Color.LIGHT_GRAY;
          this.sectionPaints = new HashMap<Comparable, Paint>();
          this.legendItemShape = new Ellipse2D.Double(-4.0, -4.0, 8.0, 8.0);
      }
  
      /**
       * Returns the dataset used by the plot.
       *
       * @return The dataset (possibly <code>null</code>).
       */
      public CategoryDataset getDataset() {
          return this.dataset;
      }
  
      /**
       * Sets the dataset used by the plot and sends a {@link PlotChangeEvent}
       * to all registered listeners.
       *
       * @param dataset  the dataset (<code>null</code> permitted).
       */
      public void setDataset(CategoryDataset dataset) {
          // if there is an existing dataset, remove the plot from the list of
          // change listeners...
          if (this.dataset != null) {
              this.dataset.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 to trigger plot change event
          datasetChanged(new DatasetChangeEvent(this, dataset));
      }
  
      /**
       * Returns the pie chart that is used to draw the individual pie plots.
       * Note that there are some attributes on this chart instance that will
       * be ignored at rendering time (for example, legend item settings).
       *
       * @return The pie chart (never <code>null</code>).
       *
       * @see #setPieChart(JFreeChart)
       */
      public JFreeChart getPieChart() {
          return this.pieChart;
      }
  
      /**
       * Sets the chart that is used to draw the individual pie plots.  The
       * chart's plot must be an instance of {@link PiePlot}.
       *
       * @param pieChart  the pie chart (<code>null</code> not permitted).
       *
       * @see #getPieChart()
       */
      public void setPieChart(JFreeChart pieChart) {
          if (pieChart == null) {
              throw new IllegalArgumentException("Null 'pieChart' argument.");
          }
          if (!(pieChart.getPlot() instanceof PiePlot)) {
              throw new IllegalArgumentException("The 'pieChart' argument must "
                      + "be a chart based on a PiePlot.");
          }
          this.pieChart = pieChart;
          fireChangeEvent();
      }
  
      /**
       * Returns the data extract order (by row or by column).
       *
       * @return The data extract order (never <code>null</code>).
       */
      public TableOrder getDataExtractOrder() {
          return this.dataExtractOrder;
      }
  
      /**
       * Sets the data extract order (by row or by column) and sends a
       * {@link PlotChangeEvent} to all registered listeners.
       *
       * @param order  the order (<code>null</code> not permitted).
       */
      public void setDataExtractOrder(TableOrder order) {
          if (order == null) {
              throw new IllegalArgumentException("Null 'order' argument");
          }
          this.dataExtractOrder = order;
          fireChangeEvent();
      }
  
      /**
       * Returns the limit (as a percentage) below which small pie sections are
       * aggregated.
       *
       * @return The limit percentage.
       */
      public double getLimit() {
          return this.limit;
      }
  
      /**
       * Sets the limit below which pie sections are aggregated.
       * Set this to 0.0 if you don't want any aggregation to occur.
       *
       * @param limit  the limit percent.
       */
      public void setLimit(double limit) {
          this.limit = limit;
          fireChangeEvent();
      }
  
      /**
       * Returns the key for aggregated items in the pie plots, if there are any.
       * The default value is "Other".
       *
       * @return The aggregated items key.
       *
       * @since 1.0.2
       */
      public Comparable getAggregatedItemsKey() {
          return this.aggregatedItemsKey;
      }
  
      /**
       * Sets the key for aggregated items in the pie plots.  You must ensure
       * that this doesn't clash with any keys in the dataset.
       *
       * @param key  the key (<code>null</code> not permitted).
       *
       * @since 1.0.2
       */
      public void setAggregatedItemsKey(Comparable key) {
          if (key == null) {
              throw new IllegalArgumentException("Null 'key' argument.");
          }
          this.aggregatedItemsKey = key;
          fireChangeEvent();
      }
  
      /**
       * Returns the paint used to draw the pie section representing the
       * aggregated items.  The default value is <code>Color.LIGHT_GRAY</code>.
       *
       * @return The paint.
       *
       * @since 1.0.2
       */
      public Paint getAggregatedItemsPaint() {
          return this.aggregatedItemsPaint;
      }
  
      /**
       * Sets the paint used to draw the pie section representing the aggregated
       * items and sends a {@link PlotChangeEvent} to all registered listeners.
       *
       * @param paint  the paint (<code>null</code> not permitted).
       *
       * @since 1.0.2
       */
      public void setAggregatedItemsPaint(Paint paint) {
          if (paint == null) {
              throw new IllegalArgumentException("Null 'paint' argument.");
          }
          this.aggregatedItemsPaint = paint;
          fireChangeEvent();
      }
  
      /**
       * Returns a short string describing the type of plot.
       *
       * @return The plot type.
       */
      @Override
      public String getPlotType() {
          return "Multiple Pie Plot";
           // TODO: need to fetch this from localised resources
      }
  
      /**
       * Returns the shape used for legend items.
       *
       * @return The shape (never <code>null</code>).
       *
       * @see #setLegendItemShape(Shape)
       *
       * @since 1.0.12
       */
      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</code> not permitted).
       *
       * @see #getLegendItemShape()
       *
       * @since 1.0.12
       */
      public void setLegendItemShape(Shape shape) {
          if (shape == null) {
              throw new IllegalArgumentException("Null 'shape' argument.");
          }
          this.legendItemShape = shape;
          fireChangeEvent();
      }
  
      /**
       * Draws the plot on a Java 2D graphics device (such as the screen or a
       * printer).
       *
       * @param g2  the graphics device.
       * @param area  the area within which the plot should be drawn.
       * @param anchor  the anchor point (<code>null</code> permitted).
       * @param parentState  the state from the parent plot, if there is one.
       * @param info  collects info about the drawing.
       */
      @Override
      public void draw(Graphics2D g2,
                       Rectangle2D area,
                       Point2D anchor,
                       PlotState parentState,
                       PlotRenderingInfo info) {
  
  
          // adjust the drawing area for the plot insets (if any)...
          RectangleInsets insets = getInsets();
          insets.trim(area);
          drawBackground(g2, area);
          drawOutline(g2, area);
  
          // check that there is some data to display...
          if (DatasetUtilities.isEmptyOrNull(this.dataset)) {
              drawNoDataMessage(g2, area);
              return;
          }
  
          int pieCount;
          if (this.dataExtractOrder == TableOrder.BY_ROW) {
              pieCount = this.dataset.getRowCount();
          }
          else {
              pieCount = this.dataset.getColumnCount();
          }
  
          // the columns variable is always >= rows
          int displayCols = (int) Math.ceil(Math.sqrt(pieCount));
          int displayRows
              = (int) Math.ceil((double) pieCount / (double) displayCols);
  
          // swap rows and columns to match plotArea shape
          if (displayCols > displayRows && area.getWidth() < area.getHeight()) {
              int temp = displayCols;
              displayCols = displayRows;
              displayRows = temp;
          }
  
          prefetchSectionPaints();
  
          int x = (int) area.getX();
          int y = (int) area.getY();
          int width = ((int) area.getWidth()) / displayCols;
          int height = ((int) area.getHeight()) / displayRows;
          int row = 0;
          int column = 0;
          int diff = (displayRows * displayCols) - pieCount;
          int xoffset = 0;
          Rectangle rect = new Rectangle();
  
          for (int pieIndex = 0; pieIndex < pieCount; pieIndex++) {
              rect.setBounds(x + xoffset + (width * column), y + (height * row),
                      width, height);
  
              String title;
              if (this.dataExtractOrder == TableOrder.BY_ROW) {
                  title = this.dataset.getRowKey(pieIndex).toString();
              }
              else {
                  title = this.dataset.getColumnKey(pieIndex).toString();
              }
              this.pieChart.setTitle(title);
  
              PieDataset piedataset;
              PieDataset dd = new CategoryToPieDataset(this.dataset,
                      this.dataExtractOrder, pieIndex);
              if (this.limit > 0.0) {
                  piedataset = DatasetUtilities.createConsolidatedPieDataset(
                          dd, this.aggregatedItemsKey, this.limit);
              }
              else {
                  piedataset = dd;
              }
              PiePlot piePlot = (PiePlot) this.pieChart.getPlot();
              piePlot.setDataset(piedataset);
              piePlot.setPieIndex(pieIndex);
  
              // update the section colors to match the global colors...
              for (int i = 0; i < piedataset.getItemCount(); i++) {
                  Comparable key = piedataset.getKey(i);
                  Paint p;
                  if (key.equals(this.aggregatedItemsKey)) {
                      p = this.aggregatedItemsPaint;
                  }
                  else {
                      p = this.sectionPaints.get(key);
                  }
                  piePlot.setSectionPaint(key, p);
              }
  
              ChartRenderingInfo subinfo = null;
              if (info != null) {
                  subinfo = new ChartRenderingInfo();
              }
              this.pieChart.draw(g2, rect, subinfo);
              if (info != null) {
                  info.getOwner().getEntityCollection().addAll(
                          subinfo.getEntityCollection());
                  info.addSubplotInfo(subinfo.getPlotInfo());
              }
  
              ++column;
              if (column == displayCols) {
                  column = 0;
                  ++row;
  
                  if (row == displayRows - 1 && diff != 0) {
                      xoffset = (diff * width) / 2;
                  }
              }
          }
  
      }
  
      /**
       * For each key in the dataset, check the <code>sectionPaints</code>
       * cache to see if a paint is associated with that key and, if not,
       * fetch one from the drawing supplier.  These colors are cached so that
       * the legend and all the subplots use consistent colors.
       */
      private void prefetchSectionPaints() {
  
          // pre-fetch the colors for each key...this is because the subplots
          // may not display every key, but we need the coloring to be
          // consistent...
  
          PiePlot piePlot = (PiePlot) getPieChart().getPlot();
  
          if (this.dataExtractOrder == TableOrder.BY_ROW) {
              // column keys provide potential keys for individual pies
              for (int c = 0; c < this.dataset.getColumnCount(); c++) {
                  Comparable key = this.dataset.getColumnKey(c);
                  Paint p = piePlot.getSectionPaint(key);
                  if (p == null) {
                      p = this.sectionPaints.get(key);
                      if (p == null) {
                          p = getDrawingSupplier().getNextPaint();
                      }
                  }
                  this.sectionPaints.put(key, p);
              }
          }
          else {
              // row keys provide potential keys for individual pies
              for (int r = 0; r < this.dataset.getRowCount(); r++) {
                  Comparable key = this.dataset.getRowKey(r);
                  Paint p = piePlot.getSectionPaint(key);
                  if (p == null) {
                      p = this.sectionPaints.get(key);
                      if (p == null) {
                          p = getDrawingSupplier().getNextPaint();
                      }
                  }
                  this.sectionPaints.put(key, p);
              }
          }
  
      }
  
      /**
       * Returns a collection of legend items for the pie chart.
       *
       * @return The legend items.
       */
      @Override
      public List<LegendItem> getLegendItems() {
          List<LegendItem> result = new ArrayList<LegendItem>();
          if (this.dataset == null) {
              return result;
          }
  
          List<Comparable> keys = null;
          prefetchSectionPaints();
          if (this.dataExtractOrder == TableOrder.BY_ROW) {
              keys = this.dataset.getColumnKeys();
          }
          else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
              keys = this.dataset.getRowKeys();
          }
          if (keys == null) {
              return result;
          }
          int section = 0;
          for (Comparable key : keys) {
              String label = key.toString();  // TODO: use a generator here
              String description = label;
              Paint paint = this.sectionPaints.get(key);
              LegendItem item = new LegendItem(label, description, null,
                      null, getLegendItemShape(), paint,
                      Plot.DEFAULT_OUTLINE_STROKE, paint);
              item.setSeriesKey(key);
              item.setSeriesIndex(section);
              item.setDataset(getDataset());
              result.add(item);
              section++;
          }
          if (this.limit > 0.0) {
              LegendItem a = new LegendItem(this.aggregatedItemsKey.toString(),
                      this.aggregatedItemsKey.toString(), null, null,
                      getLegendItemShape(), this.aggregatedItemsPaint,
                      Plot.DEFAULT_OUTLINE_STROKE, this.aggregatedItemsPaint);
              result.add(a);
          }
          return result;
      }
  
      /**
       * Tests this plot for equality with an arbitrary object.  Note that the
       * plot's dataset is not considered in the equality test.
       *
       * @param obj  the object (<code>null</code> permitted).
       *
       * @return <code>true</code> if this plot is equal to <code>obj</code>, and
       *     <code>false</code> otherwise.
       */
      @Override
      public boolean equals(Object obj) {
          if (obj == this) {
              return true;
          }
          if (!(obj instanceof MultiplePiePlot)) {
              return false;
          }
          MultiplePiePlot that = (MultiplePiePlot) obj;
          if (this.dataExtractOrder != that.dataExtractOrder) {
              return false;
          }
          if (this.limit != that.limit) {
              return false;
          }
          if (!this.aggregatedItemsKey.equals(that.aggregatedItemsKey)) {
              return false;
          }
          if (!PaintUtilities.equal(this.aggregatedItemsPaint,
                  that.aggregatedItemsPaint)) {
              return false;
          }
          if (!ObjectUtilities.equal(this.pieChart, that.pieChart)) {
              return false;
          }
          if (!ShapeUtilities.equal(this.legendItemShape, that.legendItemShape)) {
              return false;
          }
          if (!super.equals(obj)) {
              return false;
          }
          return true;
      }
  
      /**
       * 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 {
          MultiplePiePlot clone = (MultiplePiePlot) super.clone();
          clone.pieChart = (JFreeChart) this.pieChart.clone();
          clone.sectionPaints = new HashMap<Comparable, Paint>(this.sectionPaints);
          clone.legendItemShape = ShapeUtilities.clone(this.legendItemShape);
          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();
          SerialUtilities.writePaint(this.aggregatedItemsPaint, stream);
          SerialUtilities.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.aggregatedItemsPaint = SerialUtilities.readPaint(stream);
          this.legendItemShape = SerialUtilities.readShape(stream);
          this.sectionPaints = new HashMap<Comparable, Paint>();
      }
  
  }
  