package flare.analytics.optimization
{
    import flare.animate.Transitioner;
    import flare.scale.Scale;
    import flare.util.Arrays;
    import flare.util.Property;
    import flare.vis.Visualization;
    import flare.vis.axis.CartesianAxes;
    import flare.vis.data.DataSprite;
    import flare.vis.operator.Operator;
    
    /**
     * Computes an optimized aspect ratio for drawing a line chart.
     * This operator will update the visualization's bounds to reflect the
     * optimized aspect ratio. Place this operator in an
     * <code>OperatorList</code> <b>before</b> the <code>AxisLayout</code>
     * operator, and set the <code>dataField</code> property to be the
     * same as the axis data field that should be banked. For example, in
     * a time series chart with time on the x-axis, the data field for this
     * operator should be the same as the data field used for the y-axis.
     * By default this class assumes that the data field is being laid out
     * on the y-axis. If this is not the case (e.g., you have a vertically
     * oriented line chart), be sure to set the <code>bankYAxis</code>
     * property to <code>false</code>.
     */
    public class AspectRatioBanker extends Operator
    {
        private var _z:Property = null;
        
        /** The maximum width for the visualization bounds. */
        public var maxWidth:Number = 500;
        /** The maximum height for the visualization bounds. */
        public var maxHeight:Number = 500;
        /** Indicates if the data field is on the y-axis (default true). */
        public var bankYAxis:Boolean = true;
        /** The banking function to use. This is a function that takes an
         *  array of Numbers as input and returns an aspect ratio. It is
         *  expected that this function will be one of the static functions of
         *  this class. The default is <code>averageAbsoluteAngle</code>. */
        public var banker:Function = averageAbsoluteAngle;
        
        /** The data field of the values to bank. */
        public function get dataField():String { return _z.name; }
        public function set dataField(f:String):void {
            _z = Property.$(f); setup();
        }
        
        /**
         * Creates a new AspectRatioBanker. 
         * @param dataField the data field from which pull numeric values from
         *  NodeSprites. These values are then used to determine the optimal
         *  aspect ratio.
         */
        public function AspectRatioBanker(dataField:String=null,
            bankYAxis:Boolean=true, maxWidth:Number=500, maxHeight:Number=500)
        {
            if (dataField) _z = Property.$(dataField);
            this.bankYAxis = bankYAxis;
            this.maxWidth  = maxWidth;
            this.maxHeight = maxHeight;
        }

        // --------------------------------------------------------------------
        
        /** @inheritDoc */
        public override function operate(t:Transitioner=null):void
        {
            if (_z == null) return; // nothing to do
            
            // extract data
            var v:Array = [];
            visualization.data.nodes.visit(function(d:DataSprite):void {
                v.push(_z.getValue(d));
            });
            
            // compute the aspect ratio (= width/height)
            var ar:Number = banker(v);
            if (!bankYAxis) ar = 1/ar;
            ar = adjustToAxes(visualization, ar);
            
            // set visualization bounds and update axes
            visualization.setAspectRatio(ar, maxWidth, maxHeight);
            visualization.axes.update(t);
        }
        
        /**
         * Adjusts an aspect ratio for the "data rectangle" bounding the data
         * points to an new ratio that factors in the axis scale settings.
         * @param ar the desired aspect ratio of the data rectangle
         * @return the adjusted aspect ratio
         */
        private static function adjustToAxes(vis:Visualization, ar:Number):Number
        {
            // get axis scales for each data field
            var axes:CartesianAxes = vis.xyAxes;
            var xsc:Scale = axes.xAxis.axisScale;
            var ysc:Scale = axes.yAxis.axisScale;
            
            // compute adjusted aspect ratio: this is the inverse aspect ratio
            // of the interpolated data rectangle in data space multipled by
            // the desired aspect ratio for the data rectangle in screen space
            var dy:Number, dx:Number;            
            dy = ysc.interpolate(ysc.max) - ysc.interpolate(ysc.min);
            dx = xsc.interpolate(xsc.max) - xsc.interpolate(xsc.min);
            return ar * dy / dx;
        }

        // --------------------------------------------------------------------
        
        /**
         * Bank the average absolute orientation to 45 degrees.
         * "Slopeless" lines are culled before the banking is computed.
         * Solved using Newton-Raphson iteration.
         * <pre>
         * a     = aspect ratio (as height / width)
         * ci    = normalized slope = N * abs(y_i+1 - y_i) / range(y)
         * x     = a * ci
         * f(a)  = sum(atan(x)) / N - pi/4
         * f'(a) = sum(ci/(1 + x^2)) / N
         * </pre>
         * @param a an array of data values to be banked. It is assumed that
         *  values on the opposite axis are evenly spaced.
         * @return the optimized aspect ratio
         */
        public static function averageAbsoluteAngle(a:Array):Number
        {
            var alpha:Number=0, alpha_p:Number, f:Number, fprime:Number;
            var x:Number, Ry:Number = Arrays.max(a) - Arrays.min(a);
            var N:int = a.length-1, iter:int = 0, i:int, j:int;
    
            // compute constants, perform culling
            var c:Array = [];
            for (i=0, j=0; i<N; ++i) {
                var slope:Number = Math.abs(a[i+1] - a[i]) / Ry;
                if (slope > 1e-5) c.push(N * slope);
            }
            N = c.length;
            
            // Newton-Raphson iteration
            do {
                iter++;
                alpha_p = alpha;
                
                // compute function and function derivative
                f = fprime = 0;
                for (i=0; i<N; ++i) {
                    x = c[i] * alpha;
                    f += Math.atan(x);
                    fprime += c[i] / (1 + x*x);
                }
                f /= N;
                fprime /= N;
                f -= Math.PI/4;
                
                // apply the Newton-Raphson increment
                alpha = alpha_p - f/fprime;
                
             // finish iteration when update difference drops beneath tolerance
             } while (Math.abs(alpha - alpha_p) > 1e-5);
             
             return 1/alpha;
        }
        
        /**
         * Bank the median absolute slope to 45 degrees.
         * "Slopeless" lines are culled before the banking is computed.
         * @param a an array of data values to be banked. It is assumed that
         *  values on the opposite axis are evenly spaced.
         * @return the optimized aspect ratio
         */
        public static function medianAbsoluteSlope(a:Array):Number
        {
            var slopes:Array = [], i:int;
            var yRange:Number = Arrays.max(a) - Arrays.min(a);
            
            for (i=1; i<a.length; ++i) {
                var slope:Number = Math.abs(a[i] - a[i-1]);
                if (slope/yRange > 1e-5) {
                    slopes.push(slope);
                }
            }
            slopes.sort(Array.NUMERIC);
            var median:Number = slopes[slopes.length>>1];
            return (median*(a.length-1)) / yRange;
        }

    } // end of class AspectRatioBanker
}