<commit_before>/**!
    Copyright (C) 2012 Michael Wolthers Nielsen, http://moredots.dk

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.
*/

/**!
    @author - Michael Wolthers Nielsen @MichaelWolthers http://moredots.dk
    @contributors - Kristoffer Kjelde @stofferk munkey.dk
    @description - SpriteClip jQuery plugin that provides an object with an interface similar to the ActionScript 3.0 MovieClip
    @version - 1.0
    @TODO:
    - Fix backgroundposition in firefox and in ie7 (firefox doesnt recognize background-position-x, ie7 doesn't recognize background-position)
    - Fix ENTER_FRAME
*/
(function ($) {

    "use strict";

    var SpriteClipEvent = {
        /**
            @property {String} ENTER_FRAME - Is dispatched whenevery a new frame is shown
        */
        ENTER_FRAME: "enterFrame"
    };

    function Timeout (frameRate) {
        this._frameRate = frameRate;
        this.clips = [];
    }

    Timeout.prototype = {

        clips: undefined,
        _frameRate: undefined,
        _timeout: undefined,

        /**
            @public
            @description    Register a clip for rendering at the instances framerate - start the timeout if clip was the first to be added
        */
        register: function (clip) {

            this.clips.push(clip);

            if(this.clips.length === 1) {
                this._start();
            }

        },

        /**
            @public
            @description    Unregister a clip for rendering - will stop the timeout if the clip was the last remaining
        */
        unregister: function (clip) {

            var clips = this.clips,
                i = clips.length - 1;

            //Looks for the clip and remove it
            for ( ; i >= 0 ; i-- ) {
                if (clips[i] === clip) {
                    clips.splice(i, 1);
                    break;
                }
            }

            //If removed clip was the last, kill the timeout
            if(clips.length === 0) {
                this._stop();
            }
        },

        /**
            @private
            @description    Start a timeout at the instance's framerate
        */
        _start: function () {

            this._timeout = setTimeout($.proxy(this._update, this), 1000 / this._frameRate);

        },

        /**
            @private
            @description    Start a timeout at the instance's framerate - will only start if no timeout is currently running
        */
        _stop: function () {

            //Stop and delete the timeout
            clearTimeout(this._timeout);
            delete this._timeout;
        },

        /**
            @private
            @description
        */
        _update: function () {

            var clips = this.clips,
                clip,
                i = clips.length - 1;

            //Render all the clips
            for ( ; i >= 0 ; i-- ) {

                clip = clips[i];

                //Determine if we play or rewind - it's ok to access private properties on the clip because the manager is not exposed
                if (clip._direction === 1) {
                    clip.nextFrame();
                }
                else {
                    clip.prevFrame();
                }

                //Check if we should stop at current frame - it's ok to access private properties on the clip because the manager is not exposed
                if (clip.currentFrame === clip._stopAt || clip._frameHasStop(clip.currentFrame)) {
                    clip.stop();
                }
            }

            //Repeat
            this._timeout = setTimeout($.proxy(this._update, this), 1000 / this._frameRate);
        }

    };

    /**
        @description
    */
    var TimeoutManager = (function () {

        var _timeouts = {};

        /**
            @public
            @description
        */
        function register (clip) {

            var frameRate = clip.frameRate;

            //If no timeout is running for given framerate, add it
            if (!(_timeouts[frameRate] instanceof Timeout)) {
                _timeouts[frameRate] = new Timeout(frameRate);
            }

            //Register the clip on the instance
            _timeouts[frameRate].register(clip);
        }

        /**
            @public
            @description
            @param {SpriteClip} - The clip to unregister
        */
        function unregister (clip) {

            var frameRate = clip.frameRate,
                timeout = _timeouts[frameRate];

            //If there is a timeout for the given framerate, unregister the clip
            if (timeout instanceof Timeout) {

                timeout.unregister(clip);

                //If clip was last for given frameRate, delete the reference
                if (timeout.clips.length < 1) {
                    delete _timeouts[frameRate];
                }
            }

        }

        //Expose
        return {
            register: register,
            unregister: unregister
        };

    }());

    /**
        @param {HTMLElement} elem - The containing DOM node
        @param {Object} [options] - An object literal with properties that will override any default settings of same name
    */
    function SpriteClip (element, options) {

        //Make sure we get a proper instance
        if(!(this instanceof SpriteClip)) {
            return new SpriteClip(element, options);
        }

        this.elem = element;
        this.$elem = $(element);
        this.$dispatcher = $("<div />");

        //Merge passed options into default settings (deep merge so references are wiped)
        this._settings = $.extend(true, {}, this._settings, options);

        //Expose totalFrames and frameRate
        this.totalFrames = this._settings.totalFrames;
        this.frameRate = this._settings.frameRate;

        //Use frameWidth and-height options if passed or default to the elements width/height
        this.frameWidth = this._settings.frameWidth || this.$elem.width() + parseInt(this.$elem.css("padding-left"), 10) + parseInt(this.$elem.css("padding-right"), 10);
        this.frameHeight = this._settings.frameHeight || this.$elem.height() + parseInt(this.$elem.css("padding-top"), 10) + parseInt(this.$elem.css("padding-bottom"), 10);

        //Validate input to make sure we can work with what we've got
        this._validateInitialInput();

    }

    SpriteClip.prototype = {

        //Public - init stuff we are going to need as undefined for performance
        elem: undefined,
        $elem: undefined,
        $dispatcher: undefined,
        totalFrames: undefined,
        frameRate: undefined,
        currentFrame: 1,
        isPlaying: false,

        //Private
        _timeout: undefined,
        _direction: undefined,
        _stopAt: undefined,

        _settings: {
            /**
                @property {Integer} totalFrames - Required - The number of frames that sprite and thereby the animation contains. All frames should be equally spaced in the sprite.
            */
            totalFrames: undefined,

            /**
                @property {Integer} [frameRate=30] - The initial FPS that the animation runs at - can be set after instantiation by modifying this.frameRate
            */
            frameRate: 30,

            /**
                @property {Number[]} - An array of frames to stop at
            */
            stops: [],

            /**
                @property {String} [layout="horizontal"] - Must be set to either horizontal or vertical depending how the sprite sheet is laid out - Default is horizontal
            */
            layout: "horizontal",

            /**
                @property {Number} [frameWidth={Elements width}] - Optional - If frameWidth isn't passed, the animation will default to the width of the container
            */
            frameWidth: undefined,

            /**
                @property {Number} [frameHeight={Elements height}] - Optional - If frameWidth isn't passed, the animation will default to the width of the container
            */
            frameHeight: undefined

        },

        /**
            @public
            @description                    Setter for the stops array - overrides this._settings.stops if passed
            @param {Array} [stops]          Optional - An array of frames to stop at
            @returns                        Returns a read only clone of _settings.stops
        */
        stops: function (stops) {

            if (typeof stops !== "undefined") {

                //Make sure that argument stops is an array if passed
                if (stops instanceof Array) {
                    this._settings.stops = stops;
                }
                else {
                    throw new Error("TypeError: If provided, argument \"stops\" must be an array.");
                }
            }
            return $.extend(true, [], this._stops);
        },

        /**
            @public
            @description                    Jumps to a given frame and stops
            @param {Number} frame           The frame to jump to
        */
        gotoAndStop: function (frame) {
            this._validateFrameInput(frame);
            this.currentFrame = frame;
            this._showFrame(frame);
            this.stop();
        },

        /**
            @public
            @description                    Jumps to a given frame and plays from there
            @param {Number} frame           The frame to jump to
        */
        gotoAndPlay: function (frame) {
            this._validateFrameInput(frame);
            this.stop();
            this.currentFrame = frame;
            this._showFrame(frame);
            this.play();
        },

        /**
            @public
            @description                    Jumps to a given frame and rewinds from there
            @param {Number} frame           The frame to jump to
        */
        gotoAndRewind: function (frame) {
            this._validateFrameInput(frame);
            this.stop();
            this.currentFrame = frame;
            this._showFrame(frame);
            this.rewind();
        },

        /**
            @public
            @description                    Plays to a given frame and stops
            @param {Number} frame           The frame to stop at
        */
        playToAndStop: function (frame) {
            this._validateFrameInput(frame);
            this.play(frame, 1);
        },

        /**
            @public
            @description                    Rewinds to a given frame and stops
            @param {Number} frame           The frame to stop at
        */
        rewindToAndStop: function (frame) {
            this._validateFrameInput(frame);
            this.play(frame, -1);
        },

        /**
            @public
            @description                    Shows next frame
        */
        nextFrame: function () {

            if (this.currentFrame < this.totalFrames) {
                this.currentFrame += 1;
            } else {
                this.currentFrame = 1;
            }
            this._showFrame(this.currentFrame);
        },

        /**
            @public
            @description                    Shows previous frame
        */
        prevFrame: function () {

            if (this.currentFrame > 1) {
                this.currentFrame -= 1;
            } else {
                this.currentFrame = this.totalFrames;
            }
            this._showFrame(this.currentFrame);
        },

        /**
            @public
            @description
            @param {Number} [stopAt]        Optional - The frame the animation should stop at
            @param {Number} [direction=1]   Optional - The direction the animation should play. Defaults to 1 (forward) if anything but -1 is passed
        */
        play: function (stopAt, direction) {

            //Cancel any running timeouts
            this.stop();

            this._stopAt = stopAt;
            this._direction = direction === -1 ? -1 : 1;
            TimeoutManager.register(this);
            this.isPlaying = true;
        },

        /**
            @public
            @description                    Plays the animation backwards
        */
        rewind: function () {

            this.play(undefined, -1);
        },

        /**
            @public
            @description                    Stops the animation by clearing the timeout
        */
        stop: function () {

            TimeoutManager.unregister(this);
            this.isPlaying = false;
        },

        /**
            @private
            @description                    Handles the background-position shift centrally
        */
        _showFrame: function (frame) {

            //Calculate how far we need to move
            var distanceToMove,
                currentPositions = (this.$elem.css("background-position") || this.$elem.css("backgroundPositionX") + " " + this.$elem.css("backgroundPositionY")).split(" "),
                x,
                y;

            if (this._settings.layout === "horizontal") {
                distanceToMove = (frame - 1) * this.frameWidth;
                x = - distanceToMove;
                y = parseInt(currentPositions[1], 10);
            }
            else {
                distanceToMove = (frame - 1) * this.frameHeight;
                x = parseInt(currentPositions[0], 10);
                y = -distanceToMove;
            }

            this.$elem.css("background-position", x + "px" + " " + y + "px");

            //Trigger all eventhandlers bound to the ENTER_FRAME event
            this.$dispatcher.triggerHandler(SpriteClipEvent.ENTER_FRAME);

        },

        /**
            @private
            @helper
            @description                    Check if there is a stop at the current frame
            @param  {Number} frame          Required - the frame to check
            @returns                        Returns true if there is a stop
                                            Returns false if there is not a stop
        */
        _frameHasStop: function (frame) {

            var i = this._settings.stops.length - 1;
            for ( ; i >= 0 ; --i) {

                if (this._settings.stops[i] === frame) {
                    return true;
                }
            }
            return false;
        },

        /**
            @private
            @helper
            @description                    Validates that the passed frame is in bounds
        */
        _validateFrameInput: function (frame) {

            if (typeof frame !== "number") {
                throw new Error("Argument Error: argument \"frame\" must be a number.");
            }
            if( frame < 1 || frame > this.totalFrames ) {
                throw new Error("Argument Error: argument \"frame\" is out of bounds.");
            }
        },

        /**
            @private
            @helper
            @description                    Makes sure we can work with what we have
        */
        _validateInitialInput: function () {

            if (this._settings.layout !== "vertical" && this._settings.layout !== "horizontal") {
                throw new Error("options property \"layout\" must be either \"horizontal\" or \"vertical\".");
            }
            if (isNaN(parseInt(this.frameRate, 10))) {
                throw new Error("options property \"frameRate\" must be parsable as an integer.");
            }
            if (typeof this.totalFrames !== "number") {
                throw new Error("options proberty \"totalFrames\" must be a number");
            }
            if (this._settings.layout === "horizontal" && isNaN(this.frameWidth)) {
                throw new Error("this.frameWidth is not a number. Make sure this.$elem.width() is a Number when we instantiate or pass an explicit value in options.");
            }
            else if (this._settings.layout === "vertical" && isNaN(this.frameHeight)) {
                throw new Error("this.frameHeight is not a number. Make sure this.$elem.height() is a Number when we instantiate or pass an explicit value in options.");
            }
        }

    };

    //Expose on window in case we want to instantiate "normally" instead of via the plugin
    window.SpriteClip = SpriteClip;
    window.SpriteClipEvent = SpriteClipEvent;

    //Register as jQuery plugin
    $.fn["spriteClip"] = function (options) {

        return this.each(function() {
            if ( ! $.data(this, "spriteClip") ) {
                $(this).data("spriteClip", new SpriteClip(this, options));
            }
        });

    };

} (jQuery));<commit_msg>Updated comments<commit_after>/**!
    Copyright (C) 2012 Michael Wolthers Nielsen, http://moredots.dk

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.
*/

/**!
    @author - Michael Wolthers Nielsen @MichaelWolthers http://moredots.dk
    @contributors - Kristoffer Kjelde @stofferk munkey.dk
    @description - SpriteClip jQuery plugin that provides an object with an interface similar to the ActionScript 3.0 MovieClip
    @version - 1.0
    @TODO:
    - Fix backgroundposition in firefox and in ie7 (firefox doesnt recognize background-position-x, ie7 doesn't recognize background-position)
    - Fix ENTER_FRAME
*/
(function ($) {

    "use strict";

    var SpriteClipEvent = {
        /**
            @property {String} ENTER_FRAME - Is dispatched whenevery a new frame is shown
        */
        ENTER_FRAME: "enterFrame"
    };

    /**
        @constructor
    */
    function Timeout (frameRate) {
        this._frameRate = frameRate;
        this.clips = [];
    }

    Timeout.prototype = {

        clips: undefined,
        _frameRate: undefined,
        _timeout: undefined,

        /**
            @public
            @description    Register a clip for rendering at the instances framerate - start the timeout if clip was the first to be added
        */
        register: function (clip) {

            this.clips.push(clip);

            //Start the timeout if the clip is the first to be registered
            if(this.clips.length === 1) {
                this._start();
            }

        },

        /**
            @public
            @description    Unregister a clip for rendering - will stop the timeout if the clip was the last remaining
        */
        unregister: function (clip) {

            var clips = this.clips,
                i = clips.length - 1;

            //Look for the clip and remove it if found
            for ( ; i >= 0 ; i-- ) {
                if (clips[i] === clip) {
                    clips.splice(i, 1);
                    break;
                }
            }

            //If removed clip was the last, kill the timeout
            if(clips.length === 0) {
                this._stop();
            }
        },

        /**
            @private
            @description    Start a timeout at the instance's framerate
        */
        _start: function () {

            this._timeout = setTimeout($.proxy(this._update, this), 1000 / this._frameRate);

        },

        /**
            @private
            @description    Start a timeout at the instance's framerate - will only start if no timeout is currently running
        */
        _stop: function () {

            //Stop and delete the timeout
            clearTimeout(this._timeout);
            delete this._timeout;
        },

        /**
            @private
            @description    The loop that is responsible for updating all registered clips
        */
        _update: function () {

            var clips = this.clips,
                clip,
                i = clips.length - 1;

            //Render all the clips
            for ( ; i >= 0 ; i-- ) {

                clip = clips[i];

                //Determine if we play or rewind - it's ok to access private properties on the clip because the manager is not exposed
                if (clip._direction === 1) {
                    clip.nextFrame();
                }
                else {
                    clip.prevFrame();
                }

                //Check if we should stop at current frame - it's ok to access private properties on the clip because the manager is not exposed
                if (clip.currentFrame === clip._stopAt || clip._frameHasStop(clip.currentFrame)) {
                    clip.stop();
                }
            }

            //Repeat
            this._timeout = setTimeout($.proxy(this._update, this), 1000 / this._frameRate);
        }

    };

    /**
        @static
        @description    Because we only want 1 timeout for any number of clips that are running at the same framerate,
                        we handle the updating of playing clips in this manager so for any number of clips that are running the same
                        framerate, only 1 timeout will be running.
    */
    var TimeoutManager = (function () {

        var _timeouts = {};

        /**
            @public
            @description    Registers a clip for updating
        */
        function register (clip) {

            var frameRate = clip.frameRate;

            //If no timeout is running for given framerate, add it
            if (!(_timeouts[frameRate] instanceof Timeout)) {
                _timeouts[frameRate] = new Timeout(frameRate);
            }

            //Register the clip on the instance
            _timeouts[frameRate].register(clip);
        }

        /**
            @public
            @description    Unregister a clip from updating
            @param {SpriteClip} - The clip to unregister
        */
        function unregister (clip) {

            var frameRate = clip.frameRate,
                timeout = _timeouts[frameRate];

            //If there is a timeout for the given framerate, unregister the clip
            if (timeout instanceof Timeout) {

                timeout.unregister(clip);

                //If clip was last for given frameRate, delete the reference
                if (timeout.clips.length < 1) {
                    delete _timeouts[frameRate];
                }
            }

        }

        //Expose
        return {
            register: register,
            unregister: unregister
        };

    }());

    /**
        @constructor
        @param {HTMLElement} elem - The containing DOM node
        @param {Object} [options] - An object literal with properties that will override any default settings of same name
    */
    function SpriteClip (element, options) {

        //Make sure we get a proper instance
        if(!(this instanceof SpriteClip)) {
            return new SpriteClip(element, options);
        }

        //Save a reference to the dom element and jquery-wrapped dom element
        this.elem = element;
        this.$elem = $(element);

        //The element HAS an eventdispatcher instead of BEING an eventdispatcher. We use a dummy jQuery elemt for this because
        //jQuery elements already have a great event system (.bind, .on, .off, .triggerHandler, .trigger etc.)
        this.$dispatcher = $("<div />");

        //Merge passed options into default settings (deep merge so references are wiped)
        this._settings = $.extend(true, {}, this._settings, options);

        //Expose totalFrames and frameRate - note that these will be READY_ONLY and changing them
        //will not affect the framerate at which the instance plays
        this.totalFrames = this._settings.totalFrames;
        this.frameRate = this._settings.frameRate;

        //Use frameWidth and-height options if passed or default to the elements width/height
        this.frameWidth = this._settings.frameWidth || this.$elem.width() + parseInt(this.$elem.css("padding-left"), 10) + parseInt(this.$elem.css("padding-right"), 10);
        this.frameHeight = this._settings.frameHeight || this.$elem.height() + parseInt(this.$elem.css("padding-top"), 10) + parseInt(this.$elem.css("padding-bottom"), 10);

        //Validate input to make sure we can work with what we've got
        this._validateInitialInput();

    }

    SpriteClip.prototype = {

        //Public - define stuff we are going to need as undefined for performance
        elem: undefined,
        $elem: undefined,
        $dispatcher: undefined,

        totalFrames: undefined,
        frameRate: undefined,
        currentFrame: 1,
        isPlaying: false,

        //Private
        _timeout: undefined,
        _direction: undefined,
        _stopAt: undefined,

        _settings: {
            /**
                @property {Integer} totalFrames - Required - The number of frames that the sprite and thereby the animation contains. All frames must be equally spaced in the sprite.
            */
            totalFrames: undefined,

            /**
                @property {Integer} [frameRate=30] - The initial FPS that the animation runs at - can be set after instantiation by modifying this.frameRate
            */
            frameRate: 30,

            /**
                @property {Number[]} - An array of frames to stop at
            */
            stops: [],

            /**
                @property {String} [layout="horizontal"] - Must be set to either horizontal or vertical depending how the sprite sheet is laid out - Default is horizontal
            */
            layout: "horizontal",

            /**
                @property {Number} [frameWidth={Elements width}] - Optional - If frameWidth isn't passed, the animation will default to the width of the container
            */
            frameWidth: undefined,

            /**
                @property {Number} [frameHeight={Elements height}] - Optional - If frameWidth isn't passed, the animation will default to the width of the container
            */
            frameHeight: undefined

        },

        /**
            @public
            @description                    Setter for the stops array - overrides this._settings.stops if passed
            @param {Array} [stops]          Optional - An array of frames to stop at
            @returns                        Returns a read only clone of _settings.stops
        */
        stops: function (stops) {

            if (typeof stops !== "undefined") {

                //Make sure that argument stops is an array if passed
                if (stops instanceof Array) {
                    this._settings.stops = stops;
                }
                else {
                    throw new Error("TypeError: If provided, argument \"stops\" must be an array.");
                }
            }
            return $.extend(true, [], this._stops);
        },

        /**
            @public
            @description                    Jumps to a given frame and stops
            @param {Number} frame           The frame to jump to
        */
        gotoAndStop: function (frame) {
            this._validateFrameInput(frame);
            this.currentFrame = frame;
            this._showFrame(frame);
            this.stop();
        },

        /**
            @public
            @description                    Jumps to a given frame and plays from there
            @param {Number} frame           The frame to jump to
        */
        gotoAndPlay: function (frame) {
            this._validateFrameInput(frame);
            this.stop();
            this.currentFrame = frame;
            this._showFrame(frame);
            this.play();
        },

        /**
            @public
            @description                    Jumps to a given frame and rewinds from there
            @param {Number} frame           The frame to jump to
        */
        gotoAndRewind: function (frame) {
            this._validateFrameInput(frame);
            this.stop();
            this.currentFrame = frame;
            this._showFrame(frame);
            this.rewind();
        },

        /**
            @public
            @description                    Plays to a given frame and stops
            @param {Number} frame           The frame to stop at
        */
        playToAndStop: function (frame) {
            this._validateFrameInput(frame);
            this.play(frame, 1);
        },

        /**
            @public
            @description                    Rewinds to a given frame and stops
            @param {Number} frame           The frame to stop at
        */
        rewindToAndStop: function (frame) {
            this._validateFrameInput(frame);
            this.play(frame, -1);
        },

        /**
            @public
            @description                    Shows next frame
        */
        nextFrame: function () {

            if (this.currentFrame < this.totalFrames) {
                this.currentFrame += 1;
            } else {
                this.currentFrame = 1;
            }
            this._showFrame(this.currentFrame);
        },

        /**
            @public
            @description                    Shows previous frame
        */
        prevFrame: function () {

            if (this.currentFrame > 1) {
                this.currentFrame -= 1;
            } else {
                this.currentFrame = this.totalFrames;
            }
            this._showFrame(this.currentFrame);
        },

        /**
            @public
            @description
            @param {Number} [stopAt]        Optional - The frame the animation should stop at
            @param {Number} [direction=1]   Optional - The direction the animation should play. Defaults to 1 (forward) if anything but -1 is passed
        */
        play: function (stopAt, direction) {

            //Cancel any running timeouts
            this.stop();

            this._stopAt = stopAt;
            this._direction = direction === -1 ? -1 : 1;
            TimeoutManager.register(this);
            this.isPlaying = true;
        },

        /**
            @public
            @description                    Plays the animation backwards
        */
        rewind: function () {

            this.play(undefined, -1);
        },

        /**
            @public
            @description                    Stops the animation by clearing the timeout
        */
        stop: function () {

            TimeoutManager.unregister(this);
            this.isPlaying = false;
        },

        /**
            @private
            @description                    Handles the background-position shift centrally
        */
        _showFrame: function (frame) {

            //Calculate how far we need to move
            var distanceToMove,
                currentPositions = (this.$elem.css("background-position") || this.$elem.css("backgroundPositionX") + " " + this.$elem.css("backgroundPositionY")).split(" "),
                x,
                y;

            if (this._settings.layout === "horizontal") {
                distanceToMove = (frame - 1) * this.frameWidth;
                x = - distanceToMove;
                y = parseInt(currentPositions[1], 10);
            }
            else {
                distanceToMove = (frame - 1) * this.frameHeight;
                x = parseInt(currentPositions[0], 10);
                y = -distanceToMove;
            }

            this.$elem.css("background-position", x + "px" + " " + y + "px");

            //Trigger all eventhandlers bound to the ENTER_FRAME event
            this.$dispatcher.triggerHandler(SpriteClipEvent.ENTER_FRAME);

        },

        /**
            @private
            @helper
            @description                    Check if there is a stop at the current frame
            @param  {Number} frame          Required - the frame to check
            @returns                        Returns true if there is a stop
                                            Returns false if there is not a stop
        */
        _frameHasStop: function (frame) {

            var i = this._settings.stops.length - 1;
            for ( ; i >= 0 ; --i) {

                if (this._settings.stops[i] === frame) {
                    return true;
                }
            }
            return false;
        },

        /**
            @private
            @helper
            @description                    Validates that the passed frame is in bounds
        */
        _validateFrameInput: function (frame) {

            if (typeof frame !== "number") {
                throw new Error("Argument Error: argument \"frame\" must be a number.");
            }
            if( frame < 1 || frame > this.totalFrames ) {
                throw new Error("Argument Error: argument \"frame\" is out of bounds.");
            }
        },

        /**
            @private
            @helper
            @description                    Makes sure we can work with what we have
        */
        _validateInitialInput: function () {

            if (this._settings.layout !== "vertical" && this._settings.layout !== "horizontal") {
                throw new Error("options property \"layout\" must be either \"horizontal\" or \"vertical\".");
            }
            if (isNaN(parseInt(this.frameRate, 10))) {
                throw new Error("options property \"frameRate\" must be parsable as an integer.");
            }
            if (typeof this.totalFrames !== "number") {
                throw new Error("options proberty \"totalFrames\" must be a number");
            }
            if (this._settings.layout === "horizontal" && isNaN(this.frameWidth)) {
                throw new Error("this.frameWidth is not a number. Make sure this.$elem.width() is a Number when we instantiate or pass an explicit value in options.");
            }
            else if (this._settings.layout === "vertical" && isNaN(this.frameHeight)) {
                throw new Error("this.frameHeight is not a number. Make sure this.$elem.height() is a Number when we instantiate or pass an explicit value in options.");
            }
        }

    };

    //Expose on window in case we want to instantiate "normally" instead of via the plugin
    window.SpriteClip = SpriteClip;
    window.SpriteClipEvent = SpriteClipEvent;

    //Register as jQuery plugin
    $.fn["spriteClip"] = function (options) {

        return this.each(function() {
            if ( ! $.data(this, "spriteClip") ) {
                $(this).data("spriteClip", new SpriteClip(this, options));
            }
        });

    };

} (jQuery));<|endoftext|>


<commit_before>const request = require('../requests');
const async = require('async');

/**
 * The service which provides build information by using the VSTS REST API
 *  v2.0. Although the naming of the variables suggest usage with VSTS, the
 *  code is compatible with both VSTS and TFS. The naming is simply the
 *  artifact of making the code generalised for both after initial usage
 *  with VSTS.
 * @public
 * @constructor
 * @see https://www.visualstudio.com/en-us/docs/integrate/api/build/overview
 */
function VSTSRestBuilds() {
  let basicAuth = null;
  let url = null;
  let project = null;
  let collection = null;
  let params = null;
  let includeQueued = null;
  let previousBuildsToGet = [];
  let apiVersion = null;
  let showBuildStep = null;

  /**
   * This object is the representation of resultFilter mentioned in the docs
   * @private
   * @see https://www.visualstudio.com/en-us/docs/integrate/api/build/builds
   */
  const resultFilter = Object.freeze({
    succeeded:          'succeeded',
    partiallySucceeded: 'partiallySucceeded',
    failed:             'failed',
    canceled:           'canceled'
  });

  /** This object is the representation of statusFilter mentioned in the docs
   * @private
   * @see https://www.visualstudio.com/en-us/docs/integrate/api/build/builds
   */
  const statusFilter = Object.freeze({
    inProgress: 'inProgress',
    completed:  'completed',
    cancelling: 'cancelling',
    postponed:  'postponed',
    notStarted: 'notStarted',
    all:        'all',
  });

  const timelineRecordState = Object.freeze({
    completed:  'completed',
    inProgress: 'inProgress',
    pending:    'pending'
  });

  /**
   * This object defines the color scheme used.
   * @private
   */
  const colorScheme = Object.freeze({
    succeeded:          'Green',
    partiallySucceeded: '#F8A800',
    failed:             'Red',
    canceled:           'Gray',
    inProgress:         '#0078D7',
    completed:          'Green',
    cancelling:         '#0078D7',
    postponed:          'Gray',
    notStarted:         'Gray',
    all:                'Gray'
  });

  /**
   * This object defines the compatable api versions that we are allowed to use
   * @private
   */
  const allowedAPIVersions = Object.freeze({
    '2.0':      '2.0',
    '4.1':      '4.1',
    undefined:  '2.0'
  });

  /**
   * @typedef {Object} Build
   * @property {string} definition Build definition id
   * @property {Date} startedAt Build start time
   * @property {Date} finishedAt Build finish time
   * @property {boolean} hasErrors Does the resulting build have errors?
   * @property {boolean} hasWarnings Did the build give some warnings?
   * @property {boolean} isRunning Is the build currently running?
   * @property {boolean} isQueued Is the build currently waiting in the queue?
   * @property {string} id Unique ID of the build
   * @property {string} number Build number
   * @property {string} project Name of the project
   * @property {string} reason Reason for building the project
   * @property {string} requestedFor Name of the Requester
   * @property {string} status The color to be used for displaying
   * @property {string} statusText The status of the build
   * @property {string} url URL of the project
   */

  /**
   * It is a node-style callback.
   * @callback buildsInfoRequestCallback
   * @param {Error|null} err It is an instance of Error
   * @param {Array<Build>} listOfBuilds It is an array of {@link Build}
   */

  /**
   * It exposes the API needed by the application to check the status of builds.
   * @name check
   * @function
   * @public
   * @instance
   * @memberOf VSTSRestBuilds
   * @param {buildsInfoRequestCallback} cb Callback which handles the
   *  requested build information
   */
  this.check = (callback) => {
    if (basicAuth && url && project) {
      getListOfBuilds(callback);
      return;
    }
    callback('incomplete configuration');
    return;
  };

  /**
   * @typedef {Object} VSTSRestBuildsConfiguration
   * @property {string} url VS Team Services account
   *  ({https://{account.visualstudio.com) or TFS server ({http://server:port}).
   * @property {string} project Team project ID or name
   * @property {string} queryparams Additional queryparams to filter the data
   *  and provide additional options
   * @property {string} username Username
   * @property {string} pat Personal Access Token with access to Builds
   *  information
   * @property {boolean} includeQueued Show queued builds
   * @property {string} apiVersion The api version to use
   * @property {boolean} showBuildStep Adds the current build step to the statusString of the build variable
   */

  /**
   * It exposes the API needed by the application to provide
   *  configuration parameters.
   * @name check
   * @function
   * @public
   * @instance
   * @memberOf VSTSRestBuilds
   * @param {VSTSRestBuildsConfiguration} config Configuration parameters
   */
  this.configure = (config) => {
    /**
     * It exposes the configuration passed to the
     * configure instance method.
     * @public
     * @instance
     */
    this.configuration = config;
    basicAuth = new Buffer(`${config.username}:${config.pat}`)
      .toString('base64');
    url = config.url;
    params = config.queryparams;
    project = config.project;
    collection = config.collection || 'DefaultCollection';
    includeQueued = config.includeQueued || false;
    apiVersion = allowedAPIVersions[config.apiVersion] || '2.0';
    showBuildStep = config.showBuildStep || false;

    console.log(config,apiVersion);
  };

  /**
   * @private
   * @param {buildsInfoRequestCallback} cb Callback which handles the
   *  requested build information
   */
  const getListOfBuilds = (callback) => {
    const apiUrl = `${url}/${collection}/${project}/_apis/build/builds?api-version=${apiVersion}${params}`;
    let options = {
      url : apiUrl,
      headers: {
        Authorization: `Basic ${basicAuth}`,
      },
    };

    // Set up our dependency tree, similar to angular
    // http://caolan.github.io/async/v2/docs.html#autoInject
    async.autoInject({
      // ### 1. Get the list of builds ###
      get_builds: (callback) => {
        request.makeRequest(options, (err, body) => {
          transformData(err, body, callback);
        });
      },
      // ### 2. Get any previous builds ###
      get_previous_builds: (get_builds, callback) => {
        // No builds to get ? then nothing to add
        if (previousBuildsToGet.length === 0) { callback(null); return; }
        getPreviousBuilds(get_builds, callback);
      },
      // ### 3. Merge our 2 arrays ###
      merge_builds: (get_builds, get_previous_builds, callback) => {
        let builds = [];
        if (get_previous_builds && get_previous_builds.shift()) {
          builds = [...new Set([...get_builds.shift(),...get_previous_builds.shift()])]; // Merge arrays, removing duplicates
        } else {
          builds = get_builds;
        }
        callback(null, builds);
      },
      // ### 4. Get the current build steps/stage for each build ###
      get_build_steps: (merge_builds, callback) => {
        // Only get the build step if we are allowed to
        if (!showBuildStep) { callback(null, merge_builds); return; }

        // http://caolan.github.io/async/v2/docs.html#map
        // async.map( collection, async function(collection), callback)
        async.map(merge_builds, getLatestBuildStep, (err, results) => {
          callback(null, results);
        });
      }
    }, (err, results) => {
      // Pass back to the monitor app
      callback(err, results.get_build_steps);
    });

    /**
     * Transforms the data received from the request to VSTS REST API
     * @private
     * @param {any} err If the value is truthy, it indicates an error has
     *  occurred.
     * @param {object} body It contains the response body from VSTS REST API
     * @param {buildsInfoRequestCallback} cb Callback which handles the
     *  requested build information
     * @return {null}
     */
    const transformData = (err, body, callback) => {
      if (err) {
        callback(err);
        return;
      }
      if (!(body && body.value)) {
        callback('No values found');
        return;
      }
      // Filter out any dummy empty objects
      const transformedData = body.value.map(transformer).filter((val) => { return Object.keys(val).length; });

      callback(null, transformedData);
    };

    /**
     * The function transforms the data from VSTS API to
     *  the accepted by callback
     * @name transformer
     * @private
     * @param {object} build individual build information object returned
     *  from API
     * @returns {Build} the object is in the format accepted by the application
     */
    const transformer = (build) => {
      let color = colorScheme[
        build.result ?
          resultFilter[build.result] :
          (build.status === statusFilter.notStarted ?
            statusFilter[statusFilter.notStarted] :
            statusFilter[statusFilter.inProgress]
          )
      ];

      let text = build.result ?
      build.result :
        (build.status === statusFilter.notStarted ?
          statusFilter.notStarted :
          statusFilter.inProgress
        );

      let webUrl = build._links ?
        (build._links.web ? build._links.web.href : build.url) :
        build.url;

      let getProjectName = build => {
        if (build.definition.project) return build.definition.project.name;
        if (build.project) return build.project.name;

        return null;
      };

      let result = {
        definition: build.definition.name,
        finishedAt: build.finishTime ? new Date(build.finishTime) : '',
        hasErrors: build.result === resultFilter.failed,
        hasWarnings: build.result === resultFilter.partiallySucceeded,
        id: build.id,
        isRunning: build.status === statusFilter.inProgress,
        isQueued: build.status === statusFilter.notStarted,
        number: build.buildNumber,
        project: getProjectName(build),
        queuedAt: build.queueTime ? new Date(build.queueTime) : new Date(),
        reason: build.reason,
        requestedFor: build.requestedFor ? build.requestedFor.displayName : '',
        startedAt: new Date(build.startTime),
        status: color,
        statusText: text,
        timeline: build._links.timeline ? build._links.timeline.href : '',
        url: webUrl
      };

      // Only show queued builds if we're told to
      if (result.isQueued && !includeQueued) {
        previousBuildsToGet.push(result);
        return {};  // Return a dummy empty object (that we will remove later)
      }

      return result;
    };
  };

  /**
   * This function makes an individual API call for each build we need
   *  to get the previous version for.
   * @private
   * @param {buildsInfoRequsetCallback} callback Callback which handles the
   *  requested build information. Each build *must* be returned in the callback,
   *  whether its been changed or not.
   */
  const getPreviousBuilds = (currentBuilds, callback) => {
    async.map(previousBuildsToGet, (build, callback) => {
      let def = build.definition;

      // If we already have a previous build, then we don't need to get another one
      const hasPreviousBuild = (val) => { return (val.definition === build.definition) && (val.project === build.project); };
      if (currentBuilds.some(hasPreviousBuild)) {
        callback(null);
        return;
      }

      // Get the second to last build instead
      const options = {
        url: `${url}/${collection}/${project}/_apis/build/builds?api-version=${apiVersion}&definitions=${def}&$top=2`
      };

      request.makeRequest(options, (err, body) => {
        if (err) { callback(err); return; }
        if (!(body && body.value)) {
          console.log('No previous builds found'); // Don't break the rest of the builds if we can't get a previous one
          callback(null);
          return;
         }

        let prevBuild = [body.value[1]];

        if (prevBuild) {
          const transformedData = prevBuild.map(transformer);
          callback(null, transformedData[0]);
          return;
        }

        console.log('Unable to fetch previous build');  // Don't break the rest of the builds if we can't get a previous one
        callback(null);

      });

    }, (err, results) => {
      callback(null, results);
      previousBuildsToGet = [];
    });
  };

  /**
   * This function gets the most recent timeline record (aka step) for a build
   * @private
   * @param {string} timelineURL the url to the timeline VSTS API
   */
  const getLatestBuildStep = (build, callback) => {
    const timelineURL = build.timeline;
    if (!timelineURL || timelineURL === '') {
      console.log('no timeline url');
      callback(null, build);
      return;
    }

    const apiUrl = timelineURL;
    const options = {
      url : apiUrl,
      headers: {
        Authorization: `Basic ${basicAuth}`,
      },
    };

    request.makeRequest(options, (err, body) => {
      if (err) {
        console.log('getLatestBuildStep:', err);
        callback(null, build);
        return;
      }

      if (!(body && body.records)) {
        console.log('getLatestBuildStep invalid Body', body);
        callback(null, build);
        return;
      }

      // As of API version 2.0 there is no better way of doing this, we *have* to retrieve everything
      let records = body.records.sort( (a, b) => {
        return a.order - b.order;
      });

      for (let key in records) {
        let record = records[key];
        if (record.state === timelineRecordState.inProgress && record.type === 'Task') {
          build.statusText += ' - ' + record.name;
        }
      }

      // Return our modified build
      callback(null, build);
    });
  };
}

module.exports = VSTSRestBuilds;
<commit_msg>Try and reduce the API calls made to TFS to avoid tripping the CircuitBreaker as much<commit_after>const request = require('../requests');
const async = require('async');

/**
 * The service which provides build information by using the VSTS REST API
 *  v2.0. Although the naming of the variables suggest usage with VSTS, the
 *  code is compatible with both VSTS and TFS. The naming is simply the
 *  artifact of making the code generalised for both after initial usage
 *  with VSTS.
 * @public
 * @constructor
 * @see https://www.visualstudio.com/en-us/docs/integrate/api/build/overview
 */
function VSTSRestBuilds() {
  let basicAuth = null;
  let url = null;
  let project = null;
  let collection = null;
  let params = null;
  let includeQueued = null;
  let previousBuildsToGet = [];
  let apiVersion = null;
  let showBuildStep = null;

  /**
   * This object is the representation of resultFilter mentioned in the docs
   * @private
   * @see https://www.visualstudio.com/en-us/docs/integrate/api/build/builds
   */
  const resultFilter = Object.freeze({
    succeeded:          'succeeded',
    partiallySucceeded: 'partiallySucceeded',
    failed:             'failed',
    canceled:           'canceled'
  });

  /** This object is the representation of statusFilter mentioned in the docs
   * @private
   * @see https://www.visualstudio.com/en-us/docs/integrate/api/build/builds
   */
  const statusFilter = Object.freeze({
    inProgress: 'inProgress',
    completed:  'completed',
    cancelling: 'cancelling',
    postponed:  'postponed',
    notStarted: 'notStarted',
    all:        'all',
  });

  const timelineRecordState = Object.freeze({
    completed:  'completed',
    inProgress: 'inProgress',
    pending:    'pending'
  });

  /**
   * This object defines the color scheme used.
   * @private
   */
  const colorScheme = Object.freeze({
    succeeded:          'Green',
    partiallySucceeded: '#F8A800',
    failed:             'Red',
    canceled:           'Gray',
    inProgress:         '#0078D7',
    completed:          'Green',
    cancelling:         '#0078D7',
    postponed:          'Gray',
    notStarted:         'Gray',
    all:                'Gray'
  });

  /**
   * This object defines the compatable api versions that we are allowed to use
   * @private
   */
  const allowedAPIVersions = Object.freeze({
    '2.0':      '2.0',
    '4.1':      '4.1',
    undefined:  '2.0'
  });

  /**
   * @typedef {Object} Build
   * @property {string} definition Build definition id
   * @property {Date} startedAt Build start time
   * @property {Date} finishedAt Build finish time
   * @property {boolean} hasErrors Does the resulting build have errors?
   * @property {boolean} hasWarnings Did the build give some warnings?
   * @property {boolean} isRunning Is the build currently running?
   * @property {boolean} isQueued Is the build currently waiting in the queue?
   * @property {string} id Unique ID of the build
   * @property {string} number Build number
   * @property {string} project Name of the project
   * @property {string} reason Reason for building the project
   * @property {string} requestedFor Name of the Requester
   * @property {string} status The color to be used for displaying
   * @property {string} statusText The status of the build
   * @property {string} url URL of the project
   */

  /**
   * It is a node-style callback.
   * @callback buildsInfoRequestCallback
   * @param {Error|null} err It is an instance of Error
   * @param {Array<Build>} listOfBuilds It is an array of {@link Build}
   */

  /**
   * It exposes the API needed by the application to check the status of builds.
   * @name check
   * @function
   * @public
   * @instance
   * @memberOf VSTSRestBuilds
   * @param {buildsInfoRequestCallback} cb Callback which handles the
   *  requested build information
   */
  this.check = (callback) => {
    if (basicAuth && url && project) {
      getListOfBuilds(callback);
      return;
    }
    callback('incomplete configuration');
    return;
  };

  /**
   * @typedef {Object} VSTSRestBuildsConfiguration
   * @property {string} url VS Team Services account
   *  ({https://{account.visualstudio.com) or TFS server ({http://server:port}).
   * @property {string} project Team project ID or name
   * @property {string} queryparams Additional queryparams to filter the data
   *  and provide additional options
   * @property {string} username Username
   * @property {string} pat Personal Access Token with access to Builds
   *  information
   * @property {boolean} includeQueued Show queued builds
   * @property {string} apiVersion The api version to use
   * @property {boolean} showBuildStep Adds the current build step to the statusString of the build variable
   */

  /**
   * It exposes the API needed by the application to provide
   *  configuration parameters.
   * @name check
   * @function
   * @public
   * @instance
   * @memberOf VSTSRestBuilds
   * @param {VSTSRestBuildsConfiguration} config Configuration parameters
   */
  this.configure = (config) => {
    /**
     * It exposes the configuration passed to the
     * configure instance method.
     * @public
     * @instance
     */
    this.configuration = config;
    basicAuth = new Buffer(`${config.username}:${config.pat}`)
      .toString('base64');
    url = config.url;
    params = config.queryparams;
    project = config.project;
    collection = config.collection || 'DefaultCollection';
    includeQueued = config.includeQueued || false;
    apiVersion = allowedAPIVersions[config.apiVersion] || '2.0';
    showBuildStep = config.showBuildStep || false;

    console.log(config,apiVersion);
  };

  /**
   * @private
   * @param {buildsInfoRequestCallback} cb Callback which handles the
   *  requested build information
   */
  const getListOfBuilds = (callback) => {
    const apiUrl = `${url}/${collection}/${project}/_apis/build/builds?api-version=${apiVersion}${params}`;
    let options = {
      url : apiUrl,
      headers: {
        Authorization: `Basic ${basicAuth}`,
      },
    };

    // Set up our dependency tree, similar to angular
    // http://caolan.github.io/async/v2/docs.html#autoInject
    async.autoInject({
      // ### 1. Get the list of builds ###
      get_builds: (callback) => {
        request.makeRequest(options, (err, body) => {
          transformData(err, body, callback);
        });
      },
      // ### 2. Get any previous builds ###
      get_previous_builds: (get_builds, callback) => {
        // No builds to get ? then nothing to add
        if (previousBuildsToGet.length === 0) { callback(null); return; }
        getPreviousBuilds(get_builds, callback);
      },
      // ### 3. Merge our 2 arrays ###
      merge_builds: (get_builds, get_previous_builds, callback) => {
        let builds = [];
        if (get_previous_builds && get_previous_builds.shift()) {
          builds = [...new Set([...get_builds.shift(),...get_previous_builds.shift()])]; // Merge arrays, removing duplicates
        } else {
          builds = get_builds;
        }
        callback(null, builds);
      },
      // ### 4. Get the current build steps/stage for each build ###
      get_build_steps: (merge_builds, callback) => {
        // Only get the build step if we are allowed to
        if (!showBuildStep) { callback(null, merge_builds); return; }

        // http://caolan.github.io/async/v2/docs.html#map
        // async.map( collection, async function(collection), callback)
        async.map(merge_builds, getLatestBuildStep, (err, results) => {
          callback(null, results);
        });
      }
    }, (err, results) => {
      // Pass back to the monitor app
      callback(err, results.get_build_steps);
    });

    /**
     * Transforms the data received from the request to VSTS REST API
     * @private
     * @param {any} err If the value is truthy, it indicates an error has
     *  occurred.
     * @param {object} body It contains the response body from VSTS REST API
     * @param {buildsInfoRequestCallback} cb Callback which handles the
     *  requested build information
     * @return {null}
     */
    const transformData = (err, body, callback) => {
      if (err) {
        callback(err);
        return;
      }
      if (!(body && body.value)) {
        callback('No values found');
        return;
      }
      // Filter out any dummy empty objects
      const transformedData = body.value.map(transformer).filter((val) => { return Object.keys(val).length; });

      callback(null, transformedData);
    };

    /**
     * The function transforms the data from VSTS API to
     *  the accepted by callback
     * @name transformer
     * @private
     * @param {object} build individual build information object returned
     *  from API
     * @returns {Build} the object is in the format accepted by the application
     */
    const transformer = (build) => {
      let color = colorScheme[
        build.result ?
          resultFilter[build.result] :
          (build.status === statusFilter.notStarted ?
            statusFilter[statusFilter.notStarted] :
            statusFilter[statusFilter.inProgress]
          )
      ];

      let text = build.result ?
      build.result :
        (build.status === statusFilter.notStarted ?
          statusFilter.notStarted :
          statusFilter.inProgress
        );

      let webUrl = build._links ?
        (build._links.web ? build._links.web.href : build.url) :
        build.url;

      let getProjectName = build => {
        if (build.definition.project) return build.definition.project.name;
        if (build.project) return build.project.name;

        return null;
      };

      let result = {
        definition: build.definition.name,
        finishedAt: build.finishTime ? new Date(build.finishTime) : '',
        hasErrors: build.result === resultFilter.failed,
        hasWarnings: build.result === resultFilter.partiallySucceeded,
        id: build.id,
        isRunning: build.status === statusFilter.inProgress,
        isQueued: build.status === statusFilter.notStarted,
        number: build.buildNumber,
        project: getProjectName(build),
        queuedAt: build.queueTime ? new Date(build.queueTime) : new Date(),
        reason: build.reason,
        requestedFor: build.requestedFor ? build.requestedFor.displayName : '',
        startedAt: new Date(build.startTime),
        status: color,
        statusText: text,
        timeline: build._links.timeline ? build._links.timeline.href : '',
        url: webUrl
      };

      // Only show queued builds if we're told to
      if (result.isQueued && !includeQueued) {
        previousBuildsToGet.push(result);
        return {};  // Return a dummy empty object (that we will remove later)
      }

      return result;
    };
  };

  /**
   * This function makes an individual API call for each build we need
   *  to get the previous version for.
   * @private
   * @param {buildsInfoRequsetCallback} callback Callback which handles the
   *  requested build information. Each build *must* be returned in the callback,
   *  whether its been changed or not.
   */
  const getPreviousBuilds = (currentBuilds, callback) => {
    async.map(previousBuildsToGet, (build, callback) => {
      let def = build.definition;

      // If we already have a previous build, then we don't need to get another one
      const hasPreviousBuild = (val) => { return (val.definition === build.definition) && (val.project === build.project); };
      if (currentBuilds.some(hasPreviousBuild)) {
        callback(null);
        return;
      }

      // Get the second to last build instead
      const options = {
        url: `${url}/${collection}/${project}/_apis/build/builds?api-version=${apiVersion}&definitions=${def}&$top=2`
      };

      request.makeRequest(options, (err, body) => {
        if (err) { callback(err); return; }
        if (!(body && body.value)) {
          console.log('No previous builds found'); // Don't break the rest of the builds if we can't get a previous one
          callback(null);
          return;
         }

        let prevBuild = [body.value[1]];

        if (prevBuild) {
          const transformedData = prevBuild.map(transformer);
          callback(null, transformedData[0]);
          return;
        }

        console.log('Unable to fetch previous build');  // Don't break the rest of the builds if we can't get a previous one
        callback(null);

      });

    }, (err, results) => {
      callback(null, results);
      previousBuildsToGet = [];
    });
  };

  /**
   * This function gets the most recent timeline record (aka step) for a build
   * @private
   * @param {string} timelineURL the url to the timeline VSTS API
   */
  const getLatestBuildStep = (build, callback) => {
    const timelineURL = build.timeline;
    if (!timelineURL || timelineURL === '') {
      console.log('no timeline url');
      callback(null, build);
      return;
    }

    if (!build.isRunning) {
        // The build is already finished, no need to get the latest step
        callback(null, build);
        return;
    }

    const apiUrl = timelineURL;
    const options = {
      url : apiUrl,
      headers: {
        Authorization: `Basic ${basicAuth}`,
      },
    };

    request.makeRequest(options, (err, body) => {
      if (err) {
        console.log('getLatestBuildStep:', err);
        callback(null, build);
        return;
      }

      if (!(body && body.records)) {
        console.log('getLatestBuildStep invalid Body', body);
        callback(null, build);
        return;
      }

      // As of API version 2.0 there is no better way of doing this, we *have* to retrieve everything
      let records = body.records.sort( (a, b) => {
        return a.order - b.order;
      });

      for (let key in records) {
        let record = records[key];
        if (record.state === timelineRecordState.inProgress && record.type === 'Task') {
          build.statusText += ' - ' + record.name;
        }
      }

      // Return our modified build
      callback(null, build);
    });
  };
}

module.exports = VSTSRestBuilds;
<|endoftext|>


<commit_before>import React, {Component} from 'react';
import { Button, Panel, FormControls } from 'react-bootstrap';
import TitleBar from '../components/TitleBar';
import ProvisionInformation from '../components/ProvisionInformation';
import BatchModel from '../models/BatchModel';

export default class BatchView extends Component {
  constructor(props) {
    super(props);
    this.state = {
      batch: {},
      showInformation: false
    };
  }

  componentDidMount() {
    let self = this;
    BatchModel.findById(this.props.params.id, (err, batch) => {
      console.log(batch);
      self.setState({batch: batch});
    });
  }

  toggle() {
    this.setState({showInformation: !this.state.showInformation});
  }

  render() {
    let address = {};
    let contact = {};
    let stationUrl;
    let provisions;
    let donor = {};
    if (this.state.batch.station) {
      contact = this.state.batch.station._contacts[0];
      address = contact._address;
      stationUrl = `#/stations/${this.state.batch.station.id}`;
      provisions = this.state.batch.provisionActivities.map(activity => {
        return {
          name: activity.provisionRequirement.name,
          volume: activity.promised
        };
      });
      donor = this.state.batch._contact;
    }
    return (

        <TitleBar />

您欲捐贈的物資已經登記，請記下物資寄送的資訊，並且將物資寄送於此：

          <Panel header="寄送資訊" bsStyle="primary">
            <form className="form-horizontal">
              <FormControls.Static label="地址"
                labelClassName="col-xs-2" wrapperClassName="col-xs-10"
                value={`${address.zipCode} ${address.city} ${address.district} ${address.detail}`} />
              <FormControls.Static label="收件人"
                labelClassName="col-xs-2" wrapperClassName="col-xs-10"
                value={`${contact.name} (編號：${this.state.batch.trackingNumber})`} />
              <FormControls.Static label="聯絡電話"
                labelClassName="col-xs-2" wrapperClassName="col-xs-10"
                value={`${contact.phone}`} />
            </form>
          </Panel>

            您也可以 <Button onClick={this.toggle.bind(this)}>按此</Button> 查看您送出的物資資訊，或 <Button href={stationUrl}>回到物資站頁面</Button>。

          <Panel collapsible expanded={this.state.showInformation}>
            <ProvisionInformation
              contact={donor}
              provisions={provisions}
            />
          </Panel>

    );
  }
}
<commit_msg>remove ProvisionInformation<commit_after>import React, {Component} from 'react';
import { Button, Panel, FormControls } from 'react-bootstrap';
import TitleBar from '../components/TitleBar';
import ProvisionInformation from '../components/ProvisionInformation';
import BatchModel from '../models/BatchModel';

export default class BatchView extends Component {
  constructor(props) {
    super(props);
    this.state = {
      batch: {},
      showInformation: false
    };
  }

  componentDidMount() {
    let self = this;
    BatchModel.findById(this.props.params.id, (err, batch) => {
      console.log(batch);
      self.setState({batch: batch});
    });
  }

  toggle() {
    this.setState({showInformation: !this.state.showInformation});
  }

  render() {
    let address = {};
    let contact = {};
    let stationUrl;
    let provisions;
    let donor = {};
    if (this.state.batch.station) {
      contact = this.state.batch.station._contacts[0];
      address = contact._address;
      stationUrl = `#/stations/${this.state.batch.station.id}`;
      provisions = this.state.batch.provisionActivities.map(activity => {
        return {
          name: activity.provisionRequirement.name,
          volume: activity.promised
        };
      });
      donor = this.state.batch._contact;
    }
    return (

        <TitleBar />

您欲捐贈的物資已經登記，請記下物資寄送的資訊，並且將物資寄送於此：

          <Panel header="寄送資訊" bsStyle="primary">
            <form className="form-horizontal">
              <FormControls.Static label="地址"
                labelClassName="col-xs-2" wrapperClassName="col-xs-10"
                value={`${address.zipCode} ${address.city} ${address.district} ${address.detail}`} />
              <FormControls.Static label="收件人"
                labelClassName="col-xs-2" wrapperClassName="col-xs-10"
                value={`${contact.name} (編號：${this.state.batch.trackingNumber})`} />
              <FormControls.Static label="聯絡電話"
                labelClassName="col-xs-2" wrapperClassName="col-xs-10"
                value={`${contact.phone}`} />
            </form>
          </Panel>

            您的捐贈與寄送資訊稍後也會寄送一封電子郵件至您的信箱。

    );
  }
}
<|endoftext|>


<commit_before>/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2020 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var Formats = require('../Formats');
var LayerData = require('../mapdata/LayerData');
var MapData = require('../mapdata/MapData');
var Tile = require('../Tile');

/**
 * Parses a 2D array of tile indexes into a new MapData object with a single layer.
 *
 * @function Phaser.Tilemaps.Parsers.Parse2DArray
 * @since 3.0.0
 *
 * @param {string} name - The name of the tilemap, used to set the name on the MapData.
 * @param {integer[][]} data - 2D array, CSV string or Tiled JSON object.
 * @param {integer} tileWidth - The width of a tile in pixels.
 * @param {integer} tileHeight - The height of a tile in pixels.
 * @param {boolean} insertNull - Controls how empty tiles, tiles with an index of -1, in the map
 * data are handled. If `true`, empty locations will get a value of `null`. If `false`, empty
 * location will get a Tile object with an index of -1. If you've a large sparsely populated map and
 * the tile data doesn't need to change then setting this value to `true` will help with memory
 * consumption. However if your map is small or you need to update the tiles dynamically, then leave
 * the default value set.
 *
 * @return {Phaser.Tilemaps.MapData} The MapData object.
 */
var Parse2DArray = function (name, data, tileWidth, tileHeight, insertNull)
{
    var layerData = new LayerData({
        tileWidth: tileWidth,
        tileHeight: tileHeight
    });
    var mapData = new MapData({
        name: name,
        tileWidth: tileWidth,
        tileHeight: tileHeight,
        format: Formats.ARRAY_2D,
        layers: [ layerData ]
    });

    var tiles = [];
    var height = data.length;
    var width = 0;

    for (var y = 0; y < data.length; y++)
    {
        tiles[y] = [];
        var row = data[y];

        for (var x = 0; x < row.length; x++)
        {
            var tileIndex = parseInt(row[x], 10);

            if (isNaN(tileIndex) || tileIndex === -1)
            {
                tiles[y][x] = insertNull
                    ? null
                    : new Tile(layerData, -1, x, y, tileWidth, tileHeight);
            }
            else
            {
                tiles[y][x] = new Tile(layerData, tileIndex, x, y, tileWidth, tileHeight);
            }
        }

        if (width === 0)
        {
            width = row.length;
        }
    }

    mapData.width = layerData.width = width;
    mapData.height = layerData.height = height;
    mapData.widthInPixels = layerData.widthInPixels = width * tileWidth;
    mapData.heightInPixels = layerData.heightInPixels = height * tileHeight;
    layerData.data = tiles;

    return mapData;
};

module.exports = Parse2DArray;
<commit_msg>removed useless change<commit_after>/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2020 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var Formats = require('../Formats');
var LayerData = require('../mapdata/LayerData');
var MapData = require('../mapdata/MapData');
var Tile = require('../Tile');

/**
 * Parses a 2D array of tile indexes into a new MapData object with a single layer.
 *
 * @function Phaser.Tilemaps.Parsers.Parse2DArray
 * @since 3.0.0
 *
 * @param {string} name - The name of the tilemap, used to set the name on the MapData.
 * @param {integer[][]} data - 2D array, CSV string or Tiled JSON object.
 * @param {integer} tileWidth - The width of a tile in pixels.
 * @param {integer} tileHeight - The height of a tile in pixels.
 * @param {boolean} insertNull - Controls how empty tiles, tiles with an index of -1, in the map
 * data are handled. If `true`, empty locations will get a value of `null`. If `false`, empty
 * location will get a Tile object with an index of -1. If you've a large sparsely populated map and
 * the tile data doesn't need to change then setting this value to `true` will help with memory
 * consumption. However if your map is small or you need to update the tiles dynamically, then leave
 * the default value set.
 *
 * @return {Phaser.Tilemaps.MapData} The MapData object.
 */
var Parse2DArray = function (name, data, tileWidth, tileHeight, insertNull)
{
    var layerData = new LayerData({
        tileWidth: tileWidth,
        tileHeight: tileHeight
    });

    var mapData = new MapData({
        name: name,
        tileWidth: tileWidth,
        tileHeight: tileHeight,
        format: Formats.ARRAY_2D,
        layers: [ layerData ]
    });

    var tiles = [];
    var height = data.length;
    var width = 0;

    for (var y = 0; y < data.length; y++)
    {
        tiles[y] = [];
        var row = data[y];

        for (var x = 0; x < row.length; x++)
        {
            var tileIndex = parseInt(row[x], 10);

            if (isNaN(tileIndex) || tileIndex === -1)
            {
                tiles[y][x] = insertNull
                    ? null
                    : new Tile(layerData, -1, x, y, tileWidth, tileHeight);
            }
            else
            {
                tiles[y][x] = new Tile(layerData, tileIndex, x, y, tileWidth, tileHeight);
            }
        }

        if (width === 0)
        {
            width = row.length;
        }
    }

    mapData.width = layerData.width = width;
    mapData.height = layerData.height = height;
    mapData.widthInPixels = layerData.widthInPixels = width * tileWidth;
    mapData.heightInPixels = layerData.heightInPixels = height * tileHeight;
    layerData.data = tiles;

    return mapData;
};

module.exports = Parse2DArray;
<|endoftext|>