# Source: ui/flow.reel/flow-translate-composer.js

    var TranslateComposer = require("../../composer/translate-composer").TranslateComposer,
        Point = require("../../core/geometry/point").Point,
        convertPointFromPageToNode = Point.convertPointFromPageToNode;

    /**
     * @class FlowTranslateComposer
     * @extends TranslateComposer
     */
    var FlowTranslateComposer = exports.FlowTranslateComposer = TranslateComposer.specialize( /** @lends FlowTranslateComposer# */ {

        constructor: {
            value: function FlowTranslateComposer() {
                this.handleMousewheel = this.handleWheel;
            }
        },

        stealChildrenPointer: {
            value: true
        },

        _linearScrollingVector: {
            value: [-300, 0]
        },

        /**
         * A constant 2d vector used to transform a drag vector into a scroll vector
         */
        linearScrollingVector: {
            get: function () {
                return this._linearScrollingVector;
            },
            set: function (value) {
                this._linearScrollingVector = value;
            }
        },

        /**
         *  How fast the cursor has to be moving before translating starts. Only
         *  applied when another component has claimed the pointer.
         *  @type {number}
         *  @default 500
         */
        startTranslateSpeed: {
            value: 500
        },

        startTranslateRadius: {
            value: 8
        },

        // TODO doc
        /**
         */
        _startPageX: {
            value: null
        },

        // TODO doc
        /**
         */
        _startPageY: {
            value: null
        },

        // TODO doc
        /**
         */
        _pageX: {
            value: null
        },

        // TODO doc
        /**
         */
        _pageY: {
            value: null
        },

        // TODO doc
        /**
         */
        _pointerStartX: {
            value: null
        },

        // TODO doc
        /**
         */
        _pointerStartY: {
            value: null
        },

        // TODO doc
        /**
         */
        _contentOffsetX: {
            value: null
        },

        // TODO doc
        /**
         */
        _contentOffsetY: {
            value: null
        },

        _superStart: {
            value: TranslateComposer.prototype._start
        },

        // TODO doc
        /**
         */
        _start: {
            value: function (x, y, target, timeStamp) {
                this._superStart(x, y, target, timeStamp);

                // TODO: Review using getComputedStyle outside draw cycle
                var computedStyle = window.getComputedStyle(this._element, null),
                    borderLeft = this.convertCssPixelsPropertyStringToNumber(computedStyle.getPropertyValue("border-left-width")),
                    borderTop = this.convertCssPixelsPropertyStringToNumber(computedStyle.getPropertyValue("border-top-width")),
                    paddingLeft = this.convertCssPixelsPropertyStringToNumber(computedStyle.getPropertyValue("padding-left")),
                    paddingTop = this.convertCssPixelsPropertyStringToNumber(computedStyle.getPropertyValue("padding-top")),
                    point = convertPointFromPageToNode(this._element, new Point().init(x, y));

                this._pointerStartX = this._pointerX = point.x - borderLeft - paddingLeft;
                this._pointerStartY = this._pointerY = point.y - borderTop - paddingTop;
                this._contentOffsetX = this._startPageX - this._pointerStartX;
                this._contentOffsetY = this._startPageY - this._pointerStartY;
                this._computePointedElement();
                this._startPageX = this._pageX = x;
                this._startPageY = this._pageY = y;
                this._startScroll = this._scroll;
                this._previousScrollDelta = 0;
                this._scrollEnd = null;
            }
        },

        // TODO doc
        /**
         */
        _analyzeMovement: {
            value: function (event) {
                var velocity = event.velocity,
                    speed, dX, dY;

                if (!velocity) {
                    return;
                }
                speed = velocity.speed;
                if (speed >= this.startTranslateSpeed) {
                    this._stealPointer();
                } else {
                    dX = this.startPageX - event.pageX;
                    dY = this.startPageY - event.pageY;
                    if (dX * dX + dY * dY > this.startTranslateRadius * this.startTranslateRadius) {
                        this._stealPointer();
                    }
                }
            }
        },

        // TODO doc
        /**
         */
        _dispatchTranslateStart: {
            value: function (x, y) {
                var translateStartEvent = document.createEvent("CustomEvent");

                translateStartEvent.initCustomEvent("translateStart", true, true, null);
                translateStartEvent.scroll = this._scroll;
                translateStartEvent.translateX = 0;
                translateStartEvent.translateY = 0;
                translateStartEvent.pointer = this._observedPointer;
                this.dispatchEvent(translateStartEvent);
            }
        },

        // TODO doc
        /**
         */
        _dispatchTranslateEnd: {
            value: function () {
                var translateEndEvent = document.createEvent("CustomEvent");

                translateEndEvent.initCustomEvent("translateEnd", true, true, null);
                translateEndEvent.scroll = this._scroll;
                translateEndEvent.translateX = 0;
                translateEndEvent.translateY = 0;
                translateEndEvent.pointer = this._observedPointer;
                this.dispatchEvent(translateEndEvent);
            }
        },

        _dispatchTranslateCancel: {
            value: function () {
                var translateCancelEvent = document.createEvent("CustomEvent");

                translateCancelEvent.initCustomEvent("translateCancel", true, true, null);
                translateCancelEvent.scroll = this._scroll;
                translateCancelEvent.translateX = 0;
                translateCancelEvent.translateY = 0;
                translateCancelEvent.pointer = this._observedPointer;
                this.dispatchEvent(translateCancelEvent);
            }
        },

        // TODO doc
        /**
         */
        _dispatchTranslate: {
            value: function () {
                var translateEvent = document.createEvent("CustomEvent");
                translateEvent.initCustomEvent("translate", true, true, null);
                translateEvent.scroll = this._scroll;
                translateEvent.translateX = 0;
                translateEvent.translateY = 0;
                translateEvent.pointer = this._observedPointer;
                this.dispatchEvent(translateEvent);
            }
        },

        // TODO doc
        /**
         */
        _move: {
            value: function (x, y) {
                var pointerDelta;

                if (this._isFirstMove) {
                    this._dispatchTranslateStart();
                    this._isFirstMove = false;
                }
                this._pageX = x;
                this._pageY = y;
                this._updateLinearScroll();
                if (this._shouldDispatchTranslate) {
                    this._dispatchTranslate();
                }
            }
        },

        // TODO doc
        /**
         */
        _end: {
            value: function (event) {
                /*this.startTime = Date.now();
                if (!this._isFirstMove) {
                    this._dispatchTranslateEnd();
                }
                this._releaseInterest();*/
                if (this.eventManager.isPointerClaimedByComponent(this._observedPointer, this)) {
                    this.startTime = Date.now();
                    this.endX = this.startX = this._pageX;
                    this.endY = this.startY = this._pageY;

                    var velocity = event.velocity;

                    if ((this._hasMomentum) && ((velocity.speed>40) || this.translateStrideX)) {
                        if (this._axis !== "vertical") {
                            this.momentumX = velocity.x * this._pointerSpeedMultiplier * (this._invertXAxis ? 1 : -1);
                        } else {
                            this.momentumX = 0;
                        }
                        if (this._axis !== "horizontal") {
                            this.momentumY = velocity.y * this._pointerSpeedMultiplier * (this._invertYAxis ? 1 : -1);
                        } else {
                            this.momentumY=0;
                        }
                        this.endX = this.startX + (this.momentumX * this.__momentumDuration / 2000);
                        this.endY = this.startY + (this.momentumY * this.__momentumDuration / 2000);
                        this.startStrideXTime = null;
                        this.startStrideYTime = null;
                        this.animateMomentum = true;
                    } else {
                        this.animateMomentum = false;
                    }

                    if (this.animateMomentum) {
                        this._animationInterval();
                    } else if (!this._isFirstMove) {
                        // Only dispatch a translateEnd if a translate start has occured
                        this._dispatchTranslateEnd();
                    }
                }
                this._releaseInterest();
            }
        },

        _translateEndTimeout: {
            value: null
        },

        _mousewheelStrideTimeout: {
            value: null
        },

        _previousDelta: {
            value: 0
        },

        _listenToWheelEvent: {
            value: true
        },

        captureWheel: {
            value: function () {
                if (!this.eventManager.componentClaimingPointer(this._WHEEL_POINTER)) {
                    this.eventManager.claimPointer(this._WHEEL_POINTER, this.component);
                }
            }
        },

        // TODO Add wheel event listener for Firefox
        // TODO doc
        /**
         */
        handleWheel: {
            value: function (event) {
                var self = this;

                // If this composers' component is claiming the "wheel" pointer then handle the event
                if (this.eventManager.isPointerClaimedByComponent(this._WHEEL_POINTER, this.component)) {
                    var oldScroll = this._scroll,
                        deltaX = event.wheelDeltaX || -event.deltaX || 0,
                        deltaY = event.wheelDeltaY || -event.deltaY || 0,
                        delta;

                    if (this.translateStrideX) {
                        clearTimeout(this._mousewheelStrideTimeout);
                        if (Math.abs(this._linearScrollingVector[0]) > Math.abs(this._linearScrollingVector[1])) {
                            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                                delta = this._linearScrollingVector[0] * -deltaX / Math.abs(this._linearScrollingVector[0]);
                            } else {
                                delta = 0;
                            }
                        } else {
                            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                                delta = 0;
                            } else {
                                delta = this._linearScrollingVector[1] * -deltaY / Math.abs(this._linearScrollingVector[1]);
                            }
                        }
                        if ((this._mousewheelStrideTimeout === null) || (Math.abs(delta) > Math.abs(this._previousDelta * (this._mousewheelStrideTimeout === null ? 2 : 4)))) {
                            if (delta > 1) {
                                this.callDelegateMethod("previousStride", this);
                            } else {
                                if (delta < -1) {
                                    this.callDelegateMethod("nextStride", this);
                                }
                            }
                        }
                        this._mousewheelStrideTimeout = setTimeout(function () {
                            self._mousewheelStrideTimeout = null;
                            self._previousDelta = 0;
                        }, 70);
                        self._previousDelta = delta;
                        if (delta !== 0 && this._shouldPreventDefault(event)) {
                            event.preventDefault();
                        }
                    } else {
                        if (this._translateEndTimeout === null) {
                            this._dispatchTranslateStart();
                        }
                        this._pageX = this._pageX + ((deltaX * 20) / 100);
                        this._pageY = this._pageY + ((deltaY * 20) / 100);
                        this._updateScroll();
                        this._dispatchTranslate();
                        clearTimeout(this._translateEndTimeout);
                        this._translateEndTimeout = setTimeout(function () {
                            self._dispatchTranslateEnd();
                            self._translateEndTimeout = null;
                        }, 400);

                        // If we're not at one of the extremes (i.e. the scroll actually
                        // changed the translate) then we want to preventDefault to stop
                        // the page scrolling.
                        if (oldScroll !== this._scroll && this._shouldPreventDefault(event)) {
                            event.preventDefault();
                        }
                    }
                    this.eventManager.forfeitPointer(this._WHEEL_POINTER, this.component);
                }
            }
        },

        // TODO doc
        /**
         */
        _scroll: {
            value: 0
        },

        // TODO doc
        /**
         */
        scroll: {
            get: function () {
                return this._scroll;
            },
            set: function (value) {
                if ((this.minScroll !== null) && (value < this.minScroll)) {
                    value = this.minScroll;
                }
                if ((this.maxScroll !== null) && (value > this.maxScroll)) {
                    value = this.maxScroll;
                }
                this._scroll = value;
            }
        },

        // TODO doc
        /**
         */
        minScroll: {
            value: null
        },

        // TODO doc
        /**
         */
        maxScroll: {
            value: null
        },

        // TODO doc
        /**
         */
        _flow: {
            value: null
        },

        // TODO doc
        /**
         */
        flow: {
            get: function () {
                return this._flow;
            },
            set: function (value) {
                this._flow = value;
                this.component = value;
            }
        },

        // TODO doc
        /**
         */
        _updateScroll: {
            value: function () {
                this._updateLinearScroll();
            }
        },

        // TODO doc
        /**
         */
        _updateLinearScroll: {
            value: function () {
                var flow = this._flow,
                    ratio = flow.isCameraEnabled ? 500 / flow._height : 1,
                    x = ((this._pageX - this._startPageX) * this._linearScrollingVector[0] * ratio * flow._sceneScaleX.denominator) / flow._sceneScaleX.numerator,
                    y = ((this._pageY - this._startPageY) * this._linearScrollingVector[1] * ratio * flow._sceneScaleY.denominator) / flow._sceneScaleY.numerator,
                    squaredMagnitude = this._linearScrollingVector[0] * this._linearScrollingVector[0] + this._linearScrollingVector[1] * this._linearScrollingVector[1],
                    scroll = (x + y) / squaredMagnitude;

                this.scroll += scroll - this._previousScrollDelta;
                this._previousScrollDelta = scroll;
            }
        },

        // TODO doc
        /**
         */
        frame: {
            value: function (timestamp) {
                if (this.isAnimating) {
                    this._animationInterval();
                }
            }
        },

        // TODO doc
        /**
         */
        convertCssPixelsPropertyStringToNumber: {
            value: function (property) {
                if (typeof property === "string") {
                    if (property.substr(-2) === "px") {
                        return property.substr(0, property.length - 2) * 1;
                    } else {
                        return 0;
                    }
                } else {
                    return 0;
                }
            }
        },

        _rayPointDistance: {
            value: function (rayVector, point) {
                var dotProduct,
                    magnitude,
                    x, y, z;

                dotProduct = rayVector[0] * point[0] + rayVector[1] * point[1] + rayVector[2] * point[2];
                if (dotProduct >= 0) {
                    magnitude = rayVector[0] * rayVector[0] + rayVector[1] * rayVector[1] + rayVector[2] * rayVector[2];
                    dotProduct /= magnitude;
                    x = rayVector[0] * dotProduct - point[0];
                    y = rayVector[1] * dotProduct - point[1];
                    z = rayVector[2] * dotProduct - point[2];
                    return Math.sqrt(x * x + y * y + z * z);
                } else {
                    // behind ray
                    return false;
                }
            }
        },

        // TODO doc
        /**
         */
        _closerIndex: {
            value: null
        },

        _computePointedElement_spline: {
            value: []
        },

        // TODO doc
        /**
         */
        _computePointedElement: {
            value: function () {
                var splinePaths = this._flow._splinePaths,
                    pathsLength = splinePaths.length;

                if (pathsLength) {
                    var flow = this._flow,
                        vX = flow._viewpointTargetPoint[0] - flow._viewpointPosition[0],
                        vZ = flow._viewpointTargetPoint[2] - flow._viewpointPosition[2],
                        yAngle = Math.atan2(vX, vZ),
                        tmpZ = vZ * Math.cos(-yAngle) - vX * Math.sin(-yAngle),
                        xAngle = Math.atan2(flow._viewpointTargetPoint[1] - flow._viewpointPosition[1], tmpZ),
                        x2 = this._element.clientWidth * 0.5 - this._pointerX,
                        y2 = this._pointerY - this._element.clientHeight * 0.5,
                        perspective = (this._element.offsetHeight * 0.5) / Math.tan((flow._viewpointFov * flow._doublePI) * (1 / 720)),
                        z2, tmp,
                        splines = this._computePointedElement_spline,
                        visibleIndexes = flow._visibleIndexes,
                        length = visibleIndexes.length,
                        pathIndex,
                        slideIndex,
                        slideTime,
                        scale = flow._sceneScale,
                        closerIndex = null,
                        closerTime = null,
                        minDistance = 1e100,
                        distance,
                        indexTime,
                        rotation,
                        corner,
                        edge1,
                        edge2,
                        rayVector,
                        offset,
                        i;

                    tmp = perspective * Math.cos(xAngle) - y2 * Math.sin(xAngle);
                    y2 = perspective * Math.sin(xAngle) + y2 * Math.cos(xAngle);
                    z2 = tmp * Math.cos(yAngle) - x2 * Math.sin(yAngle);
                    x2 = tmp * Math.sin(yAngle) + x2 * Math.cos(yAngle);
                    rayVector = [x2, y2, z2];
                    for (i = 0; i < splinePaths.length; i++) {
                        for (i = 0; i < splinePaths.length; i++) {
                            splines[i] = splinePaths[i].transform([
                                scale.x.numerator / scale.x.denominator, 0, 0, 0,
                                0, scale.y.numerator / scale.y.denominator, 0, 0,
                                0, 0, scale.z.numerator / scale.z.denominator, 0,
                                -flow._viewpointPosition[0] + flow._firstIterationWidth * 0.5 + flow._firstIterationOffsetLeft,
                                -flow._viewpointPosition[1] + flow._firstIterationHeight * 0.5 + flow._firstIterationOffsetTop,
                                -flow._viewpointPosition[2],
                                1
                            ]);
                        }
                    }
                    for (i = 0; i < length; i++) {
                        offset = this._flow.offset(visibleIndexes[i]);
                        pathIndex = offset.pathIndex;
                        slideTime = offset.slideTime;
                        indexTime = splines[pathIndex]._convertSplineTimeToBezierIndexTime(slideTime);
                        if (indexTime !== null) {
                            var pos = splines[pathIndex].getPositionAtIndexTime(indexTime);
                            distance = this._rayPointDistance(rayVector, pos);
                            if (distance !== false) {
                                if (distance < minDistance) {
                                    minDistance = distance;
                                    closerIndex = visibleIndexes[i];
                                }
                            }
                        }
                    }
                    this._closerIndex = closerIndex;
                    splines.length = 0;
                }
            }
        },

        // TODO
        /**
         */
        _previousScrollDelta: {
            value: 0
        },

        // TODO
        /**
         */
        _startScroll: {
            value: 0
        },

        // TODO doc
        /**
         */
        _translateStride: {
            value: null
        },

        // TODO doc
        /**
         */
        translateStride: {
            get: function () {
                return this._translateStride;
            },
            set: function (value) {
                this._translateStride = value;
                this.translateStrideX = value;
            }
        },

        // TODO doc
        /**
         */
        _scrollEnd: {
            value: null
        },

        // TODO doc
        /**
         */
        _scrollStart: {
            value: null
        },

        // TODO doc
        /**
         */
        _hasMomentum: {
            value: true
        },

        isLimitedToSingleStride: {
            value: false
        },

        // TODO doc
        /**
         */
        _animationInterval: {
            value: function () {
                var time = Date.now(), t, t2, tmp, tmpX, tmpY, animateStride = false, scroll, min, max;

                min = this.minScroll;
                max = this.maxScroll;
                this.minScroll = null;
                this.maxScroll = null;
                if (this._scrollEnd === null) {
                    this._scrollStart = this.scroll;
                    this._pageX = this.endX;
                    this._pageY = this.endY;
                    this._updateScroll();
                    this._scrollEnd = this.scroll;
                    if (this.isLimitedToSingleStride && this.translateStrideX) {
                        if (this._scrollEnd > Math.floor(this._scrollStart) + this.translateStrideX) {
                            this._scrollEnd = Math.floor(this._scrollStart) + this.translateStrideX;
                        }
                        if (this._scrollEnd < Math.ceil(this._scrollStart) - this.translateStrideX) {
                            this._scrollEnd = Math.ceil(this._scrollStart) - this.translateStrideX;
                        }
                    }
                    this._pageX = this.startX;
                    this._pageY = this.startY;
                    this._updateScroll();
                }
                if (this.animateMomentum) {
                    t = time - this.startTime;
                    if (t < this.__momentumDuration) {
                        this._pageX = this.startX + ((this.momentumX+this.momentumX*(this.__momentumDuration-t)/this.__momentumDuration)*t/1000)/2;
                        this._pageY = this.startY + ((this.momentumY+this.momentumY*(this.__momentumDuration-t)/this.__momentumDuration)*t/1000)/2;
                        this._updateScroll();
                        if (this.translateStrideX && (this.startStrideXTime === null) && ((this.__momentumDuration - t < this.translateStrideDuration) || (Math.abs(this.scroll - this._scrollEnd) < this.translateStrideX * 0.75))) {
                            this.startStrideXTime = time;
                            this._strideStartScroll = this._scroll;
                        }
                    } else {
                        this.animateMomentum = false;
                    }
                } else {
                    if (this.startStrideXTime === null) {
                        this.startStrideXTime = this.startTime;
                        this._strideStartScroll = this._scrollStart;
                    }
                }
                scroll = this.scroll;
                if (this.startStrideXTime && (time - this.startStrideXTime > 0)) {
                    tmp = Math.round(this._scrollEnd / this.translateStrideX);
                    if (time - this.startStrideXTime < this.translateStrideDuration) {
                        t = this._bezierTValue((time - this.startStrideXTime) / this.translateStrideDuration, 0.275, 0, 0.275, 1);
                        t2 = (time - this.startStrideXTime) / this.translateStrideDuration;
                        scroll = scroll * (1 - t2) + ((tmp *  this.translateStrideX) * t + (this._strideStartScroll) * (1 - t)) * t2;
                        animateStride = true;
                    } else {
                        scroll = tmp * this.translateStrideX;
                        this.animateMomentum = false;
                    }
                }
                this.minScroll = min;
                this.maxScroll = max;
                if (scroll < min) {
                    scroll = min;
                    this.animateMomentum = false;
                    animateStride = false;
                }
                if (scroll > max) {
                    scroll = max;
                    this.animateMomentum = false;
                    animateStride = false;
                }
                this.scroll = scroll;
                this.isAnimating = this.animateMomentum || animateStride;
                if (this.isAnimating) {
                    this.needsFrame=true;
                } else {
                    this._dispatchTranslateEnd();
                    this._scrollEnd = null;
                }
            }
        }

    });