content: /* ===========================================================
   * JFreeChart : a free chart library for the Java(tm) platform
   * ===========================================================
   *
   * (C) Copyright 2000-2011, 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.]
   *
   * -----------------------------------
   * DefaultIntervalCategoryDataset.java
   * -----------------------------------
   * (C) Copyright 2002-2008, by Jeremy Bowman and Contributors.
   *
   * Original Author:  Jeremy Bowman;
   * Contributor(s):   David Gilbert (for Object Refinery Limited);
   *
   * Changes
   * -------
   * 29-Apr-2002 : Version 1, contributed by Jeremy Bowman (DG);
   * 24-Oct-2002 : Amendments for changes made to the dataset interface (DG);
   * ------------- JFREECHART 1.0.x ---------------------------------------------
   * 08-Mar-2007 : Added equals() and clone() overrides (DG);
   * 25-Feb-2008 : Fix for the special case where the dataset is empty, see bug
   *               1897580 (DG)
   * 18-Dec-2008 : Use ResourceBundleWrapper - see patch 1607918 by
   *               Jess Thrysoee (DG);
   *
   */
  
  package org.jfree.data.category;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.List;
  import java.util.ResourceBundle;
  
  import org.jfree.chart.util.ResourceBundleWrapper;
  import org.jfree.data.DataUtilities;
  import org.jfree.data.UnknownKeyException;
  import org.jfree.data.general.AbstractSeriesDataset;
  
  /**
   * A convenience class that provides a default implementation of the
   * {@link IntervalCategoryDataset} interface.
   * <p>
   * The standard constructor accepts data in a two dimensional array where the
   * first dimension is the series, and the second dimension is the category.
   */
  public class DefaultIntervalCategoryDataset extends AbstractSeriesDataset
          implements IntervalCategoryDataset {
  
      /** The series keys. */
      private Comparable[] seriesKeys;
  
      /** The category keys. */
      private Comparable[] categoryKeys;
  
      /** Storage for the start value data. */
      private Number[][] startData;
  
      /** Storage for the end value data. */
      private Number[][] endData;
  
      /**
       * Creates a new dataset using the specified data values and automatically
       * generated series and category keys.
       *
       * @param starts  the starting values for the intervals (<code>null</code>
       *                not permitted).
       * @param ends  the ending values for the intervals (<code>null</code> not
       *                permitted).
       */
      public DefaultIntervalCategoryDataset(double[][] starts, double[][] ends) {
          this(DataUtilities.createNumberArray2D(starts),
                  DataUtilities.createNumberArray2D(ends));
      }
  
      /**
       * Constructs a dataset and populates it with data from the array.
       * <p>
       * The arrays are indexed as data[series][category].  Series and category
       * names are automatically generated - you can change them using the
       * {@link #setSeriesKeys(Comparable[])} and
       * {@link #setCategoryKeys(Comparable[])} methods.
       *
       * @param starts  the start values data.
       * @param ends  the end values data.
       */
      public DefaultIntervalCategoryDataset(Number[][] starts, Number[][] ends) {
          this(null, null, starts, ends);
      }
  
      /**
       * Constructs a DefaultIntervalCategoryDataset, populates it with data
       * from the arrays, and uses the supplied names for the series.
       * <p>
       * Category names are generated automatically ("Category 1", "Category 2",
       * etc).
       *
       * @param seriesNames  the series names (if <code>null</code>, series names
       *         will be generated automatically).
       * @param starts  the start values data, indexed as data[series][category].
       * @param ends  the end values data, indexed as data[series][category].
       */
      public DefaultIntervalCategoryDataset(String[] seriesNames,
                                            Number[][] starts,
                                            Number[][] ends) {
  
          this(seriesNames, null, starts, ends);
  
      }
  
      /**
       * Constructs a DefaultIntervalCategoryDataset, populates it with data
       * from the arrays, and uses the supplied names for the series and the
       * supplied objects for the categories.
       *
       * @param seriesKeys  the series keys (if <code>null</code>, series keys
       *         will be generated automatically).
       * @param categoryKeys  the category keys (if <code>null</code>, category
       *         keys will be generated automatically).
       * @param starts  the start values data, indexed as data[series][category].
       * @param ends  the end values data, indexed as data[series][category].
       */
      public DefaultIntervalCategoryDataset(Comparable[] seriesKeys,
                                            Comparable[] categoryKeys,
                                            Number[][] starts,
                                            Number[][] ends) {
  
          this.startData = starts;
          this.endData = ends;
  
          if (starts != null && ends != null) {
  
              String baseName = "org.jfree.data.resources.DataPackageResources";
              ResourceBundle resources = ResourceBundleWrapper.getBundle(
                      baseName);
  
              int seriesCount = starts.length;
              if (seriesCount != ends.length) {
                  String errMsg = "DefaultIntervalCategoryDataset: the number "
                      + "of series in the start value dataset does "
                      + "not match the number of series in the end "
                      + "value dataset.";
                  throw new IllegalArgumentException(errMsg);
              }
              if (seriesCount > 0) {
  
                  // set up the series names...
                  if (seriesKeys != null) {
  
                      if (seriesKeys.length != seriesCount) {
                          throw new IllegalArgumentException(
                                  "The number of series keys does not "
                                  + "match the number of series in the data.");
                      }
  
                      this.seriesKeys = seriesKeys;
                  }
                  else {
                      String prefix = resources.getString(
                              "series.default-prefix") + " ";
                      this.seriesKeys = generateKeys(seriesCount, prefix);
                  }
  
                  // set up the category names...
                  int categoryCount = starts[0].length;
                  if (categoryCount != ends[0].length) {
                      String errMsg = "DefaultIntervalCategoryDataset: the "
                                  + "number of categories in the start value "
                                  + "dataset does not match the number of "
                                  + "categories in the end value dataset.";
                      throw new IllegalArgumentException(errMsg);
                  }
                  if (categoryKeys != null) {
                      if (categoryKeys.length != categoryCount) {
                          throw new IllegalArgumentException(
                                  "The number of category keys does not match "
                                  + "the number of categories in the data.");
                      }
                      this.categoryKeys = categoryKeys;
                  }
                  else {
                      String prefix = resources.getString(
                              "categories.default-prefix") + " ";
                      this.categoryKeys = generateKeys(categoryCount, prefix);
                  }
  
              }
              else {
                  this.seriesKeys = new Comparable[0];
                  this.categoryKeys = new Comparable[0];
              }
          }
  
      }
  
      /**
       * Returns the number of series in the dataset (possibly zero).
       *
       * @return The number of series in the dataset.
       *
       * @see #getRowCount()
       * @see #getCategoryCount()
       */
      @Override
      public int getSeriesCount() {
          int result = 0;
          if (this.startData != null) {
              result = this.startData.length;
          }
          return result;
      }
  
      /**
       * Returns a series index.
       *
       * @param seriesKey  the series key.
       *
       * @return The series index.
       *
       * @see #getRowIndex(Comparable)
       * @see #getSeriesKey(int)
       */
      public int getSeriesIndex(Comparable seriesKey) {
          int result = -1;
          for (int i = 0; i < this.seriesKeys.length; i++) {
              if (seriesKey.equals(this.seriesKeys[i])) {
                  result = i;
                  break;
              }
          }
          return result;
      }
  
      /**
       * Returns the name of the specified series.
       *
       * @param series  the index of the required series (zero-based).
       *
       * @return The name of the specified series.
       *
       * @see #getSeriesIndex(Comparable)
       */
      @Override
      public Comparable getSeriesKey(int series) {
          if ((series >= getSeriesCount()) || (series < 0)) {
              throw new IllegalArgumentException("No such series : " + series);
          }
          return this.seriesKeys[series];
      }
  
      /**
       * Sets the names of the series in the dataset.
       *
       * @param seriesKeys  the new keys (<code>null</code> not permitted, the
       *         length of the array must match the number of series in the
       *         dataset).
       *
       * @see #setCategoryKeys(Comparable[])
       */
      public void setSeriesKeys(Comparable[] seriesKeys) {
          if (seriesKeys == null) {
              throw new IllegalArgumentException("Null 'seriesKeys' argument.");
          }
          if (seriesKeys.length != getSeriesCount()) {
              throw new IllegalArgumentException(
                      "The number of series keys does not match the data.");
          }
          this.seriesKeys = seriesKeys;
          fireDatasetChanged();
      }
  
      /**
       * Returns the number of categories in the dataset.
       *
       * @return The number of categories in the dataset.
       *
       * @see #getColumnCount()
       */
      public int getCategoryCount() {
          int result = 0;
          if (this.startData != null) {
              if (getSeriesCount() > 0) {
                  result = this.startData[0].length;
              }
          }
          return result;
      }
  
      /**
       * Returns a list of the categories in the dataset.  This method supports
       * the {@link CategoryDataset} interface.
       *
       * @return A list of the categories in the dataset.
       *
       * @see #getRowKeys()
       */
      @Override
      public List<Comparable> getColumnKeys() {
          // the CategoryDataset interface expects a list of categories, but
          // we've stored them in an array...
          if (this.categoryKeys == null) {
              return new ArrayList<Comparable>();
          }
          else {
              return Collections.unmodifiableList(Arrays.asList(
                      this.categoryKeys));
          }
      }
  
      /**
       * Sets the categories for the dataset.
       *
       * @param categoryKeys  an array of objects representing the categories in
       *                      the dataset.
       *
       * @see #getRowKeys()
       * @see #setSeriesKeys(Comparable[])
       */
      public void setCategoryKeys(Comparable[] categoryKeys) {
          if (categoryKeys == null) {
              throw new IllegalArgumentException("Null 'categoryKeys' argument.");
          }
          if (categoryKeys.length != getCategoryCount()) {
              throw new IllegalArgumentException(
                      "The number of categories does not match the data.");
          }
          for (Comparable categoryKey : categoryKeys) {
              if (categoryKey == null) {
                  throw new IllegalArgumentException(
                          "DefaultIntervalCategoryDataset.setCategoryKeys(): "
                                  + "null category not permitted.");
              }
          }
          this.categoryKeys = categoryKeys;
          fireDatasetChanged();
      }
  
      /**
       * Returns the data value for one category in a series.
       * <P>
       * This method is part of the CategoryDataset interface.  Not particularly
       * meaningful for this class...returns the end value.
       *
       * @param series    The required series (zero based index).
       * @param category  The required category.
       *
       * @return The data value for one category in a series (null possible).
       *
       * @see #getEndValue(Comparable, Comparable)
       */
      @Override
      public Number getValue(Comparable series, Comparable category) {
          int seriesIndex = getSeriesIndex(series);
          if (seriesIndex < 0) {
              throw new UnknownKeyException("Unknown 'series' key.");
          }
          int itemIndex = getColumnIndex(category);
          if (itemIndex < 0) {
              throw new UnknownKeyException("Unknown 'category' key.");
          }
          return getValue(seriesIndex, itemIndex);
      }
  
      /**
       * Returns the data value for one category in a series.
       * <P>
       * This method is part of the CategoryDataset interface.  Not particularly
       * meaningful for this class...returns the end value.
       *
       * @param series  the required series (zero based index).
       * @param category  the required category.
       *
       * @return The data value for one category in a series (null possible).
       *
       * @see #getEndValue(int, int)
       */
      @Override
      public Number getValue(int series, int category) {
          return getEndValue(series, category);
      }
  
      /**
       * Returns the start data value for one category in a series.
       *
       * @param series  the required series.
       * @param category  the required category.
       *
       * @return The start data value for one category in a series
       *         (possibly <code>null</code>).
       *
       * @see #getStartValue(int, int)
       */
      @Override
      public Number getStartValue(Comparable series, Comparable category) {
          int seriesIndex = getSeriesIndex(series);
          if (seriesIndex < 0) {
              throw new UnknownKeyException("Unknown 'series' key.");
          }
          int itemIndex = getColumnIndex(category);
          if (itemIndex < 0) {
              throw new UnknownKeyException("Unknown 'category' key.");
          }
          return getStartValue(seriesIndex, itemIndex);
      }
  
      /**
       * Returns the start data value for one category in a series.
       *
       * @param series  the required series (zero based index).
       * @param category  the required category.
       *
       * @return The start data value for one category in a series
       *         (possibly <code>null</code>).
       *
       * @see #getStartValue(Comparable, Comparable)
       */
      @Override
      public Number getStartValue(int series, int category) {
  
          // check arguments...
          if ((series < 0) || (series >= getSeriesCount())) {
              throw new IllegalArgumentException(
                  "DefaultIntervalCategoryDataset.getValue(): "
                  + "series index out of range.");
          }
  
          if ((category < 0) || (category >= getCategoryCount())) {
              throw new IllegalArgumentException(
                  "DefaultIntervalCategoryDataset.getValue(): "
                  + "category index out of range.");
          }
  
          // fetch the value...
          return this.startData[series][category];
  
      }
  
      /**
       * Returns the end data value for one category in a series.
       *
       * @param series  the required series.
       * @param category  the required category.
       *
       * @return The end data value for one category in a series (null possible).
       *
       * @see #getEndValue(int, int)
       */
      @Override
      public Number getEndValue(Comparable series, Comparable category) {
          int seriesIndex = getSeriesIndex(series);
          if (seriesIndex < 0) {
              throw new UnknownKeyException("Unknown 'series' key.");
          }
          int itemIndex = getColumnIndex(category);
          if (itemIndex < 0) {
              throw new UnknownKeyException("Unknown 'category' key.");
          }
          return getEndValue(seriesIndex, itemIndex);
      }
  
      /**
       * Returns the end data value for one category in a series.
       *
       * @param series  the required series (zero based index).
       * @param category  the required category.
       *
       * @return The end data value for one category in a series (null possible).
       *
       * @see #getEndValue(Comparable, Comparable)
       */
      @Override
      public Number getEndValue(int series, int category) {
          if ((series < 0) || (series >= getSeriesCount())) {
              throw new IllegalArgumentException(
                  "DefaultIntervalCategoryDataset.getValue(): "
                  + "series index out of range.");
          }
  
          if ((category < 0) || (category >= getCategoryCount())) {
              throw new IllegalArgumentException(
                  "DefaultIntervalCategoryDataset.getValue(): "
                  + "category index out of range.");
          }
  
          return this.endData[series][category];
      }
  
      /**
       * Sets the start data value for one category in a series.
       *
       * @param series  the series (zero-based index).
       * @param category  the category.
       *
       * @param value The value.
       *
       * @see #setEndValue(int, Comparable, Number)
       */
      public void setStartValue(int series, Comparable category, Number value) {
  
          // does the series exist?
          if ((series < 0) || (series > getSeriesCount() - 1)) {
              throw new IllegalArgumentException(
                  "DefaultIntervalCategoryDataset.setValue: "
                  + "series outside valid range.");
          }
  
          // is the category valid?
          int categoryIndex = getCategoryIndex(category);
          if (categoryIndex < 0) {
              throw new IllegalArgumentException(
                  "DefaultIntervalCategoryDataset.setValue: "
                  + "unrecognised category.");
          }
  
          // update the data...
          this.startData[series][categoryIndex] = value;
          fireDatasetChanged();
  
      }
  
      /**
       * Sets the end data value for one category in a series.
       *
       * @param series  the series (zero-based index).
       * @param category  the category.
       *
       * @param value the value.
       *
       * @see #setStartValue(int, Comparable, Number)
       */
      public void setEndValue(int series, Comparable category, Number value) {
  
          // does the series exist?
          if ((series < 0) || (series > getSeriesCount() - 1)) {
              throw new IllegalArgumentException(
                  "DefaultIntervalCategoryDataset.setValue: "
                  + "series outside valid range.");
          }
  
          // is the category valid?
          int categoryIndex = getCategoryIndex(category);
          if (categoryIndex < 0) {
              throw new IllegalArgumentException(
                  "DefaultIntervalCategoryDataset.setValue: "
                  + "unrecognised category.");
          }
  
          // update the data...
          this.endData[series][categoryIndex] = value;
          fireDatasetChanged();
  
      }
  
      /**
       * Returns the index for the given category.
       *
       * @param category  the category (<code>null</code> not permitted).
       *
       * @return The index.
       *
       * @see #getColumnIndex(Comparable)
       */
      public int getCategoryIndex(Comparable category) {
          int result = -1;
          for (int i = 0; i < this.categoryKeys.length; i++) {
              if (category.equals(this.categoryKeys[i])) {
                  result = i;
                  break;
              }
          }
          return result;
      }
  
      /**
       * Generates an array of keys, by appending a space plus an integer
       * (starting with 1) to the supplied prefix string.
       *
       * @param count  the number of keys required.
       * @param prefix  the name prefix.
       *
       * @return An array of <i>prefixN</i> with N = { 1 .. count}.
       */
      private Comparable[] generateKeys(int count, String prefix) {
          Comparable[] result = new Comparable[count];
          String name;
          for (int i = 0; i < count; i++) {
              name = prefix + (i + 1);
              result[i] = name;
          }
          return result;
      }
  
      /**
       * Returns a column key.
       *
       * @param column  the column index.
       *
       * @return The column key.
       *
       * @see #getRowKey(int)
       */
      @Override
      public Comparable getColumnKey(int column) {
          return this.categoryKeys[column];
      }
  
      /**
       * Returns a column index.
       *
       * @param columnKey  the column key (<code>null</code> not permitted).
       *
       * @return The column index.
       *
       * @see #getCategoryIndex(Comparable)
       */
      @Override
      public int getColumnIndex(Comparable columnKey) {
          if (columnKey == null) {
              throw new IllegalArgumentException("Null 'columnKey' argument.");
          }
          return getCategoryIndex(columnKey);
      }
  
      /**
       * Returns a row index.
       *
       * @param rowKey  the row key.
       *
       * @return The row index.
       *
       * @see #getSeriesIndex(Comparable)
       */
      @Override
      public int getRowIndex(Comparable rowKey) {
          return getSeriesIndex(rowKey);
      }
  
      /**
       * Returns a list of the series in the dataset.  This method supports the
       * {@link CategoryDataset} interface.
       *
       * @return A list of the series in the dataset.
       *
       * @see #getColumnKeys()
       */
      @Override
      public List<Comparable> getRowKeys() {
          // the CategoryDataset interface expects a list of series, but
          // we've stored them in an array...
          if (this.seriesKeys == null) {
              return new java.util.ArrayList<Comparable>();
          }
          else {
              return Collections.unmodifiableList(Arrays.asList(this.seriesKeys));
          }
      }
  
      /**
       * Returns the name of the specified series.
       *
       * @param row  the index of the required row/series (zero-based).
       *
       * @return The name of the specified series.
       *
       * @see #getColumnKey(int)
       */
      @Override
      public Comparable getRowKey(int row) {
          if ((row >= getRowCount()) || (row < 0)) {
              throw new IllegalArgumentException(
                      "The 'row' argument is out of bounds.");
          }
          return this.seriesKeys[row];
      }
  
      /**
       * Returns the number of categories in the dataset.  This method is part of
       * the {@link CategoryDataset} interface.
       *
       * @return The number of categories in the dataset.
       *
       * @see #getCategoryCount()
       * @see #getRowCount()
       */
      @Override
      public int getColumnCount() {
          return this.categoryKeys.length;
      }
  
      /**
       * Returns the number of series in the dataset (possibly zero).
       *
       * @return The number of series in the dataset.
       *
       * @see #getSeriesCount()
       * @see #getColumnCount()
       */
      @Override
      public int getRowCount() {
          return this.seriesKeys.length;
      }
  
      /**
       * Tests this dataset for equality with an arbitrary object.
       *
       * @param obj  the object (<code>null</code> permitted).
       *
       * @return A boolean.
       */
      @Override
      public boolean equals(Object obj) {
          if (obj == this) {
              return true;
          }
          if (!(obj instanceof DefaultIntervalCategoryDataset)) {
              return false;
          }
          DefaultIntervalCategoryDataset that
                  = (DefaultIntervalCategoryDataset) obj;
          if (!Arrays.equals(this.seriesKeys, that.seriesKeys)) {
              return false;
          }
          if (!Arrays.equals(this.categoryKeys, that.categoryKeys)) {
              return false;
          }
          if (!equal(this.startData, that.startData)) {
              return false;
          }
          if (!equal(this.endData, that.endData)) {
              return false;
          }
          // seem to be the same...
          return true;
      }
  
      /**
       * Returns a clone of this dataset.
       *
       * @return A clone.
       *
       * @throws CloneNotSupportedException if there is a problem cloning the
       *         dataset.
       */
      @Override
      public Object clone() throws CloneNotSupportedException {
          DefaultIntervalCategoryDataset clone
                  = (DefaultIntervalCategoryDataset) super.clone();
          clone.categoryKeys = this.categoryKeys.clone();
          clone.seriesKeys = this.seriesKeys.clone();
          clone.startData = clone(this.startData);
          clone.endData = clone(this.endData);
          return clone;
      }
  
      /**
       * Tests two double[][] arrays for equality.
       *
       * @param array1  the first array (<code>null</code> permitted).
       * @param array2  the second arrray (<code>null</code> permitted).
       *
       * @return A boolean.
       */
      private static boolean equal(Number[][] array1, Number[][] array2) {
          if (array1 == null) {
              return (array2 == null);
          }
          if (array2 == null) {
              return false;
          }
          if (array1.length != array2.length) {
              return false;
          }
          for (int i = 0; i < array1.length; i++) {
              if (!Arrays.equals(array1[i], array2[i])) {
                  return false;
              }
          }
          return true;
      }
  
      /**
       * Clones a two dimensional array of <code>Number</code> objects.
       *
       * @param array  the array (<code>null</code> not permitted).
       *
       * @return A clone of the array.
       */
      private static Number[][] clone(Number[][] array) {
          if (array == null) {
              throw new IllegalArgumentException("Null 'array' argument.");
          }
          Number[][] result = new Number[array.length][];
          for (int i = 0; i < array.length; i++) {
              Number[] child = array[i];
              Number[] copychild = new Number[child.length];
              System.arraycopy(child, 0, copychild, 0, child.length);
              result[i] = copychild;
          }
          return result;
      }
  
  }
  