package prefuse.action.assignment;

import java.util.logging.Logger;

import prefuse.Constants;
import prefuse.data.tuple.TupleSet;
import prefuse.util.DataLib;
import prefuse.util.MathLib;
import prefuse.util.PrefuseLib;
import prefuse.visual.VisualItem;

/**
 * <p>
 * Assignment Action that assigns size values for a group of items based upon
 * a data field. This action can be used to automatically vary item's on screen
 * sizes proportionally to an underlying data value. Sizes can be assigned along
 * a continuous scale, or can be binned into discrete size groups. Both 1D 
 * (length) and 2D (area) encodings are supported by this function.
 * 2D is assumed by default; use the setIs2DArea method to change this.</p>
 * 
 * <p>
 * The size assignments for numerical data are continuous by default, but can
 * be binned into a few discrete steps (see {@link #setBinCount(int)}).
 * Quantitative data can also be sized on different numerical scales. The
 * default scale is a linear scale (specified by
 * {@link Constants#LINEAR_SCALE}), but logarithmic and square root scales can
 * be used (specified by {@link Constants#LOG_SCALE} and
 * {@link Constants#SQRT_SCALE} respectively. Finally, the scale can be broken
 * into quantiles, reflecting the statistical distribution of the values rather
 * than just the total data value range, using the
 * {@link Constants#QUANTILE_SCALE} value. For the quantile scale to work, you
 * also need to specify the number of bins to use (see
 * {@link #setBinCount(int)}). This value will determine the number of
 * quantiles that the data should be divided into. 
 * </p>
 * 
 * <p>
 * By default, the maximum size value is determined automatically from the
 * data, faithfully representing the scale differences between data values.
 * However, this can sometimes result in very large differences. For
 * example, if the minimum data value is 1.0 and the largest is 200.0, the
 * largest items will be 200 times larger than the smallest. While
 * accurate, this may not result in the most readable display. To correct
 * these cases, use the {@link #setMaximumSize(double)} method to manually
 * set the range of allowed sizes.  By default, the minimum size value is
 * 1.0. This too can be changed using the {@link #setMinimumSize(double)}
 * method.
 * </p>
 * 
 * @author <a href="http://jheer.org">jeffrey heer</a>
 */
public class DataSizeAction extends SizeAction {
    
    protected static final double NO_SIZE = Double.NaN;
    
    protected String m_dataField;
    
    protected double m_minSize = 1;
    protected double m_sizeRange;
    
    protected int m_scale = Constants.LINEAR_SCALE;
    protected int m_bins = Constants.CONTINUOUS;
    
    protected boolean m_inferBounds = true;
    protected boolean m_inferRange = true;
    protected boolean m_is2DArea = true;
    protected double[] m_dist;
    
    protected int m_tempScale;
    
    /**
     * Create a new DataSizeAction.
     * @param group the data group to process
     * @param field the data field to base size assignments on
     */
    public DataSizeAction(String group, String field) {
        super(group, NO_SIZE);
        m_dataField = field;
    }

    /**
     * Create a new DataSizeAction.
     * @param group the data group to process
     * @param field the data field to base size assignments on
     * @param bins the number of discrete size values to use
     */
    public DataSizeAction(String group, String field, int bins) {
        this(group, field, bins, Constants.LINEAR_SCALE);
    }

    /**
     * Create a new DataSizeAction.
     * @param group the data group to process
     * @param field the data field to base size assignments on
     * @param bins the number of discrete size values to use
     * @param scale the scale type to use. One of
     * {@link prefuse.Constants#LINEAR_SCALE},
     * {@link prefuse.Constants#LOG_SCALE},
     * {@link prefuse.Constants#SQRT_SCALE}, or
     * {@link prefuse.Constants#QUANTILE_SCALE}. If a quantile scale is
     * used, the number of bins must be greater than zero. 
     */
    public DataSizeAction(String group, String field, int bins, int scale) {
        super(group, NO_SIZE);
        m_dataField = field;
        setScale(scale);
        setBinCount(bins);
    }
    
    // ------------------------------------------------------------------------
    
    /**
     * Returns the data field used to encode size values.
     * @return the data field that is mapped to size values
     */
    public String getDataField() {
        return m_dataField;
    }
    
    /**
     * Set the data field used to encode size values.
     * @param field the data field to map to size values
     */
    public void setDataField(String field) {
        m_dataField = field;
    }
    
    /**
     * Returns the scale type used for encoding size values from the data.
     * @return the scale type. One of
     * {@link prefuse.Constants#LINEAR_SCALE},
     * {@link prefuse.Constants#LOG_SCALE}, 
     * {@link prefuse.Constants#SQRT_SCALE},
     * {@link prefuse.Constants#QUANTILE_SCALE}.
     */
    public int getScale() {
        return m_scale;
    }
    
    /**
     * Set the scale (linear, square root, or log) to use for encoding size
     * values from the data.
     * @param scale the scale type to use. This value should be one of
     * {@link prefuse.Constants#LINEAR_SCALE}, 
     * {@link prefuse.Constants#SQRT_SCALE},
     * {@link prefuse.Constants#LOG_SCALE},
     * {@link prefuse.Constants#QUANTILE_SCALE}.
     * If {@link prefuse.Constants#QUANTILE_SCALE} is used, the number of
     * bins to use must also be specified to a value greater than zero using
     * the {@link #setBinCount(int)} method.
     */
    public void setScale(int scale) {
        if ( scale < 0 || scale >= Constants.SCALE_COUNT )
            throw new IllegalArgumentException(
                    "Unrecognized scale value: "+scale);
        m_scale = scale;
    }
    
    /**
     * Returns the number of "bins" or distinct categories of sizes
     * @return the number of bins.
     */
    public int getBinCount() {
        return m_bins;
    }

    /**
     * Sets the number of "bins" or distinct categories of sizes
     * @param count the number of bins to set. The value 
     * {@link Constants#CONTINUOUS} indicates not to use any binning. If the
     * scale type set using the {@link #setScale(int)} method is
     * {@link Constants#QUANTILE_SCALE}, the bin count <strong>must</strong>
     * be greater than zero.
     */
    public void setBinCount(int count) {
        if ( m_scale == Constants.QUANTILE_SCALE && count <= 0 ) {
            throw new IllegalArgumentException(
                "The quantile scale can not be used without binning. " +
                "Use a bin value greater than zero.");
        }
        m_bins = count;
    }
    
    /**
     * Indicates if the size values set by this function represent 2D areas.
     * That is, if the size is a 2D area or a 1D length. The size value will
     * be scaled appropriately to facilitate better perception of size
     * differences.
     * @return true if this instance is configured for area sizes, false for
     *  length sizes.
     * @see prefuse.util.PrefuseLib#getSize2D(double)
     */
    public boolean is2DArea() {
        return m_is2DArea;
    }
    
    /**
     * Sets if the size values set by this function represent 2D areas.
     * That is, if the size is a 2D area or a 1D length. The size value will
     * be scaled appropriately to facilitate better perception of size
     * differences.
     * @param isArea true to configure this instance for area sizes, false for
     *  length sizes
     * @see prefuse.util.PrefuseLib#getSize2D(double)
     */
    public void setIs2DArea(boolean isArea) {
        m_is2DArea = isArea;
    }

    /**
     * Gets the size assigned to the lowest-valued data items, typically 1.0.
     * @return the size for the lowest-valued data items
     */
    public double getMinimumSize() {
        return m_minSize;
    }

    /**
     * Sets the size assigned to the lowest-valued data items. By default,
     * this value is 1.0.
     * @param size the new size for the lowest-valued data items
     */
    public void setMinimumSize(double size) {
        if ( Double.isInfinite(size) || 
             Double.isNaN(size)      ||
             size <= 0 )
        {
           throw new IllegalArgumentException("Minimum size value must be a "
                   + "finite number greater than zero.");
        }
        
        if ( m_inferRange ) {
            m_sizeRange += m_minSize - size;
        }
        m_minSize = size;
    }    
    
    /**
     * Gets the maximum size value that will be assigned by this action. By
     * default, the maximum size value is determined automatically from the
     * data, faithfully representing the scale differences between data values.
     * However, this can sometimes result in very large differences. For
     * example, if the minimum data value is 1.0 and the largest is 200.0, the
     * largest items will be 200 times larger than the smallest. While
     * accurate, this may not result in the most readable display. To correct
     * these cases, use the {@link #setMaximumSize(double)} method to manually
     * set the range of allowed sizes.  
     * @return the current maximum size. For the returned value to accurately
     * reflect the size range used by this action, either the action must
     * have already been run (allowing the value to be automatically computed)
     * or the maximum size must have been explicitly set.
     */
    public double getMaximumSize() {
        return m_minSize + m_sizeRange;
    }
    
    /**
     * Set the maximum size value that will be assigned by this action. By
     * default, the maximum size value is determined automatically from the
     * data, faithfully representing the scale differences between data values.
     * However, this can sometimes result in very large differences. For
     * example, if the minimum data value is 1.0 and the largest is 200.0, the
     * largest items will be 200 times larger than the smallest. While
     * accurate, this may not result in the most readable display. To correct
     * these cases, use the {@link #setMaximumSize(double)} method to manually
     * set the range of allowed sizes. 
     * @param maxSize the maximum size to use. If this value is less than or
     * equal to zero, infinite, or not a number (NaN) then the input value
     * will be ignored and instead automatic inference of the size range
     * will be performed. 
     */
    public void setMaximumSize(double maxSize) {
        if ( Double.isInfinite(maxSize) || 
             Double.isNaN(maxSize)      ||
             maxSize <= 0 )
        {
            m_inferRange = true;
        } else {
            m_inferRange = false;
            m_sizeRange = maxSize-m_minSize;
        }
    }
    
    /**
     * This operation is not supported by the DataSizeAction type.
     * Calling this method will result in a thrown exception.
     * @see prefuse.action.assignment.SizeAction#setDefaultSize(double)
     * @throws UnsupportedOperationException
     */
    public void setDefaultSize(double defaultSize) {
        throw new UnsupportedOperationException();
    }
    
    // ------------------------------------------------------------------------
    
    /**
     * @see prefuse.action.EncoderAction#setup()
     */
    protected void setup() {
        TupleSet ts = m_vis.getGroup(m_group);
        
        // cache the scale value in case it gets changed due to error
        m_tempScale = m_scale;
        
        if ( m_inferBounds ) {
            if ( m_scale == Constants.QUANTILE_SCALE && m_bins > 0 ) {
                double[] values =
                    DataLib.toDoubleArray(ts.tuples(), m_dataField);
                m_dist = MathLib.quantiles(m_bins, values);
            } else {
                // check for non-binned quantile scale error
                if ( m_scale == Constants.QUANTILE_SCALE ) {
                    Logger.getLogger(getClass().getName()).warning(
                            "Can't use quantile scale with no binning. " +
                            "Defaulting to linear scale. Set the bin value " +
                            "greater than zero to use a quantile scale.");
                    m_scale = Constants.LINEAR_SCALE;
                }
                m_dist = new double[2];
                m_dist[0]= DataLib.min(ts, m_dataField).getDouble(m_dataField);
                m_dist[1]= DataLib.max(ts, m_dataField).getDouble(m_dataField);
            }
            if ( m_inferRange ) {
                m_sizeRange = m_dist[m_dist.length-1]/m_dist[0] - m_minSize;
            }
        }
    }
    
    /**
     * @see prefuse.action.EncoderAction#finish()
     */
    protected void finish() {
        // reset scale in case it needed to be changed due to errors
        m_scale = m_tempScale;
    }
    
    /**
     * @see prefuse.action.assignment.SizeAction#getSize(prefuse.visual.VisualItem)
     */
    public double getSize(VisualItem item) {
        // check for any cascaded rules first
        double size = super.getSize(item);
        if ( !Double.isNaN(size) ) {
            return size;
        }
        
        // otherwise perform data-driven assignment
        double v = item.getDouble(m_dataField);
        double f = MathLib.interp(m_scale, v, m_dist);
        if ( m_bins < 1 ) {
            // continuous scale
            v = m_minSize + f * m_sizeRange;
        } else {
            // binned sizes
            int bin = f < 1.0 ? (int)(f*m_bins) : m_bins-1;
            v = m_minSize + bin*(m_sizeRange/(m_bins-1));
        }
        // return the size value. if this action is configured to return
        // 2-dimensional sizes (ie area rather than length) then the
        // size value is appropriately scaled first
        return m_is2DArea ? PrefuseLib.getSize2D(v) : v;
    }
    
} // end of class DataSizeAction
