content: /* ===========================================================
   * JFreeChart : a free chart library for the Java(tm) platform
   * ===========================================================
   *
   * (C) Copyright 2000-2012, 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.]
   *
   * -----------------------
   * IntervalXYDelegate.java
   * -----------------------
   * (C) Copyright 2004-2012, by Andreas Schroeder and Contributors.
   *
   * Original Author:  Andreas Schroeder;
   * Contributor(s):   David Gilbert (for Object Refinery Limited);
   *
   * Changes
   * -------
   * 31-Mar-2004 : Version 1 (AS);
   * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
   *               getYValue() (DG);
   * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
   * 04-Nov-2004 : Added argument check for setIntervalWidth() method (DG);
   * 17-Nov-2004 : New methods to reflect changes in DomainInfo (DG);
   * 11-Jan-2005 : Removed deprecated methods in preparation for the 1.0.0
   *               release (DG);
   * 21-Feb-2005 : Made public and added equals() method (DG);
   * 06-Oct-2005 : Implemented DatasetChangeListener to recalculate
   *               autoIntervalWidth (DG);
   * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
   * 06-Mar-2009 : Implemented hashCode() (DG);
   * 17-Jun-2012 : Removed JCommon dependencies (DG);
   *
   */
  
  package org.jfree.data.xy;
  
  import java.io.Serializable;
  
  import org.jfree.chart.HashUtilities;
  import org.jfree.chart.util.PublicCloneable;
  import org.jfree.data.DomainInfo;
  import org.jfree.data.Range;
  import org.jfree.data.RangeInfo;
  import org.jfree.data.general.DatasetChangeEvent;
  import org.jfree.data.general.DatasetChangeListener;
  import org.jfree.data.general.DatasetUtilities;
  
  /**
   * A delegate that handles the specification or automatic calculation of the
   * interval surrounding the x-values in a dataset.  This is used to extend
   * a regular {@link XYDataset} to support the {@link IntervalXYDataset}
   * interface.
   * <p>
   * The decorator pattern was not used because of the several possibly
   * implemented interfaces of the decorated instance (e.g.
   * {@link TableXYDataset}, {@link RangeInfo}, {@link DomainInfo} etc.).
   * <p>
   * The width can be set manually or calculated automatically. The switch
   * autoWidth allows to determine which behavior is used. The auto width
   * calculation tries to find the smallest gap between two x-values in the
   * dataset.  If there is only one item in the series, the auto width
   * calculation fails and falls back on the manually set interval width (which
   * is itself defaulted to 1.0).
   */
  public class IntervalXYDelegate implements DatasetChangeListener,
          DomainInfo, Serializable, Cloneable, PublicCloneable {
  
      /** For serialization. */
      private static final long serialVersionUID = -685166711639592857L;
  
      /**
       * The dataset to enhance.
       */
      private XYDataset dataset;
  
      /**
       * A flag to indicate whether the width should be calculated automatically.
       */
      private boolean autoWidth;
  
      /**
       * A value between 0.0 and 1.0 that indicates the position of the x-value
       * within the interval.
       */
      private double intervalPositionFactor;
  
      /**
       * The fixed interval width (defaults to 1.0).
       */
      private double fixedIntervalWidth;
  
      /**
       * The automatically calculated interval width.
       */
      private double autoIntervalWidth;
  
      /**
       * Creates a new delegate that.
       *
       * @param dataset  the underlying dataset (<code>null</code> not permitted).
       */
      public IntervalXYDelegate(XYDataset dataset) {
          this(dataset, true);
      }
  
      /**
       * Creates a new delegate for the specified dataset.
       *
       * @param dataset  the underlying dataset (<code>null</code> not permitted).
       * @param autoWidth  a flag that controls whether the interval width is
       *                   calculated automatically.
       */
      public IntervalXYDelegate(XYDataset dataset, boolean autoWidth) {
          if (dataset == null) {
              throw new IllegalArgumentException("Null 'dataset' argument.");
          }
          this.dataset = dataset;
          this.autoWidth = autoWidth;
          this.intervalPositionFactor = 0.5;
          this.autoIntervalWidth = Double.POSITIVE_INFINITY;
          this.fixedIntervalWidth = 1.0;
      }
  
      /**
       * Returns <code>true</code> if the interval width is automatically
       * calculated, and <code>false</code> otherwise.
       *
       * @return A boolean.
       */
      public boolean isAutoWidth() {
          return this.autoWidth;
      }
  
      /**
       * Sets the flag that indicates whether the interval width is automatically
       * calculated.  If the flag is set to <code>true</code>, the interval is
       * recalculated.
       * <p>
       * Note: recalculating the interval amounts to changing the data values
       * represented by the dataset.  The calling dataset must fire an
       * appropriate {@link DatasetChangeEvent}.
       *
       * @param b  a boolean.
       */
      public void setAutoWidth(boolean b) {
          this.autoWidth = b;
          if (b) {
              this.autoIntervalWidth = recalculateInterval();
          }
      }
  
      /**
       * Returns the interval position factor.
       *
       * @return The interval position factor.
       */
      public double getIntervalPositionFactor() {
          return this.intervalPositionFactor;
      }
  
      /**
       * Sets the interval position factor.  This controls how the interval is
       * aligned to the x-value.  For a value of 0.5, the interval is aligned
       * with the x-value in the center.  For a value of 0.0, the interval is
       * aligned with the x-value at the lower end of the interval, and for a
       * value of 1.0, the interval is aligned with the x-value at the upper
       * end of the interval.
       * <br><br>
       * Note that changing the interval position factor amounts to changing the
       * data values represented by the dataset.  Therefore, the dataset that is
       * using this delegate is responsible for generating the
       * appropriate {@link DatasetChangeEvent}.
       *
       * @param d  the new interval position factor (in the range
       *           <code>0.0</code> to <code>1.0</code> inclusive).
       */
      public void setIntervalPositionFactor(double d) {
          if (d < 0.0 || 1.0 < d) {
              throw new IllegalArgumentException(
                      "Argument 'd' outside valid range.");
          }
          this.intervalPositionFactor = d;
      }
  
      /**
       * Returns the fixed interval width.
       *
       * @return The fixed interval width.
       */
      public double getFixedIntervalWidth() {
          return this.fixedIntervalWidth;
      }
  
      /**
       * Sets the fixed interval width and, as a side effect, sets the
       * <code>autoWidth</code> flag to <code>false</code>.
       * <br><br>
       * Note that changing the interval width amounts to changing the data
       * values represented by the dataset.  Therefore, the dataset
       * that is using this delegate is responsible for generating the
       * appropriate {@link DatasetChangeEvent}.
       *
       * @param w  the width (negative values not permitted).
       */
      public void setFixedIntervalWidth(double w) {
          if (w < 0.0) {
              throw new IllegalArgumentException("Negative 'w' argument.");
          }
          this.fixedIntervalWidth = w;
          this.autoWidth = false;
      }
  
      /**
       * Returns the interval width.  This method will return either the
       * auto calculated interval width or the manually specified interval
       * width, depending on the {@link #isAutoWidth()} result.
       *
       * @return The interval width to use.
       */
      public double getIntervalWidth() {
          if (isAutoWidth() && !Double.isInfinite(this.autoIntervalWidth)) {
              // everything is fine: autoWidth is on, and an autoIntervalWidth
              // was set.
              return this.autoIntervalWidth;
          }
          else {
              // either autoWidth is off or autoIntervalWidth was not set.
              return this.fixedIntervalWidth;
          }
      }
  
      /**
       * Returns the start value of the x-interval for an item within a series.
       *
       * @param series  the series index.
       * @param item  the item index.
       *
       * @return The start value of the x-interval (possibly <code>null</code>).
       *
       * @see #getStartXValue(int, int)
       */
      public Number getStartX(int series, int item) {
          Number startX = null;
          Number x = this.dataset.getX(series, item);
          if (x != null) {
              startX = x.doubleValue()
                      - (getIntervalPositionFactor() * getIntervalWidth());
          }
          return startX;
      }
  
      /**
       * Returns the start value of the x-interval for an item within a series.
       *
       * @param series  the series index.
       * @param item  the item index.
       *
       * @return The start value of the x-interval.
       *
       * @see #getStartX(int, int)
       */
      public double getStartXValue(int series, int item) {
          return this.dataset.getXValue(series, item)
                  - getIntervalPositionFactor() * getIntervalWidth();
      }
  
      /**
       * Returns the end value of the x-interval for an item within a series.
       *
       * @param series  the series index.
       * @param item  the item index.
       *
       * @return The end value of the x-interval (possibly <code>null</code>).
       *
       * @see #getEndXValue(int, int)
       */
      public Number getEndX(int series, int item) {
          Number endX = null;
          Number x = this.dataset.getX(series, item);
          if (x != null) {
              endX = x.doubleValue()
                      + ((1.0 - getIntervalPositionFactor()) * getIntervalWidth());
          }
          return endX;
      }
  
      /**
       * Returns the end value of the x-interval for an item within a series.
       *
       * @param series  the series index.
       * @param item  the item index.
       *
       * @return The end value of the x-interval.
       *
       * @see #getEndX(int, int)
       */
      public double getEndXValue(int series, int item) {
          return this.dataset.getXValue(series, item)
                  + (1.0 - getIntervalPositionFactor()) * getIntervalWidth();
      }
  
      /**
       * Returns the minimum x-value in the dataset.
       *
       * @param includeInterval  a flag that determines whether or not the
       *                         x-interval is taken into account.
       *
       * @return The minimum value.
       */
      @Override
      public double getDomainLowerBound(boolean includeInterval) {
          double result = Double.NaN;
          Range r = getDomainBounds(includeInterval);
          if (r != null) {
              result = r.getLowerBound();
          }
          return result;
      }
  
      /**
       * Returns the maximum x-value in the dataset.
       *
       * @param includeInterval  a flag that determines whether or not the
       *                         x-interval is taken into account.
       *
       * @return The maximum value.
       */
      @Override
      public double getDomainUpperBound(boolean includeInterval) {
          double result = Double.NaN;
          Range r = getDomainBounds(includeInterval);
          if (r != null) {
              result = r.getUpperBound();
          }
          return result;
      }
  
      /**
       * Returns the range of the values in the dataset's domain, including
       * or excluding the interval around each x-value as specified.
       *
       * @param includeInterval  a flag that determines whether or not the
       *                         x-interval should be taken into account.
       *
       * @return The range.
       */
      @Override
      public Range getDomainBounds(boolean includeInterval) {
          // first get the range without the interval, then expand it for the
          // interval width
          Range range = DatasetUtilities.findDomainBounds(this.dataset, false);
          if (includeInterval && range != null) {
              double lowerAdj = getIntervalWidth() * getIntervalPositionFactor();
              double upperAdj = getIntervalWidth() - lowerAdj;
              range = new Range(range.getLowerBound() - lowerAdj,
                  range.getUpperBound() + upperAdj);
          }
          return range;
      }
  
      /**
       * Handles events from the dataset by recalculating the interval if
       * necessary.
       *
       * @param e  the event.
       */
      @Override
      public void datasetChanged(DatasetChangeEvent e) {
          // TODO: by coding the event with some information about what changed
          // in the dataset, we could make the recalculation of the interval
          // more efficient in some cases (for instance, if the change is
          // just an update to a y-value, then the x-interval doesn't need
          // updating)...
          if (this.autoWidth) {
              this.autoIntervalWidth = recalculateInterval();
          }
      }
  
      /**
       * Recalculate the minimum width "from scratch".
       *
       * @return The minimum width.
       */
      private double recalculateInterval() {
          double result = Double.POSITIVE_INFINITY;
          int seriesCount = this.dataset.getSeriesCount();
          for (int series = 0; series < seriesCount; series++) {
              result = Math.min(result, calculateIntervalForSeries(series));
          }
          return result;
      }
  
      /**
       * Calculates the interval width for a given series.
       *
       * @param series  the series index.
       *
       * @return The interval width.
       */
      private double calculateIntervalForSeries(int series) {
          double result = Double.POSITIVE_INFINITY;
          int itemCount = this.dataset.getItemCount(series);
          if (itemCount > 1) {
              double prev = this.dataset.getXValue(series, 0);
              for (int item = 1; item < itemCount; item++) {
                  double x = this.dataset.getXValue(series, item);
                  result = Math.min(result, x - prev);
                  prev = x;
              }
          }
          return result;
      }
  
      /**
       * Tests the delegate for equality with an arbitrary object.  The
       * equality test considers two delegates to be equal if they would
       * calculate the same intervals for any given dataset (for this reason, the
       * dataset itself is NOT included in the equality test, because it is just
       * a reference back to the current 'owner' of the delegate).
       *
       * @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 IntervalXYDelegate)) {
              return false;
          }
          IntervalXYDelegate that = (IntervalXYDelegate) obj;
          if (this.autoWidth != that.autoWidth) {
              return false;
          }
          if (this.intervalPositionFactor != that.intervalPositionFactor) {
              return false;
          }
          if (this.fixedIntervalWidth != that.fixedIntervalWidth) {
              return false;
          }
          return true;
      }
  
      /**
       * @return A clone of this delegate.
       *
       * @throws CloneNotSupportedException if the object cannot be cloned.
       */
      @Override
      public Object clone() throws CloneNotSupportedException {
          return super.clone();
      }
  
      /**
       * Returns a hash code for this instance.
       *
       * @return A hash code.
       */
      @Override
      public int hashCode() {
          int hash = 5;
          hash = HashUtilities.hashCode(hash, this.autoWidth);
          hash = HashUtilities.hashCode(hash, this.intervalPositionFactor);
          hash = HashUtilities.hashCode(hash, this.fixedIntervalWidth);
          return hash;
      }
  
  }
  