Source: webgazer.js

define(['RidgeReg', 'RidgeWeightedReg', 'RidgeThreadedReg', 'ClmGaze', 'TrackingjsGaze', 'Js_objectdetectGaze', 'LinearReg'] , 
function(RidgeReg, RidgeWeightedReg, RidgeThreadedReg, ClmGaze, TrackingjsGaze, Js_objectdetectGaze, LinearReg) {    
    //strict mode for type safety
    "use strict"
    
    //PRIVATE VARIABLES
   
    /**
     * Top level control module
     * @alias module:webgazer
     * @exports webgazer
     */
    var webgazer = {};

    //params Object to be passed into tracker and regression constructors
    //contains various potentially useful knowledge like the video size and data collection rates
    var params = {};
    
    //video elements
    params.videoScale = 1;
    var videoElement = null;
    var videoElementCanvas = null;
    params.videoElementId = 'webgazerVideoFeed'; 
    params.videoElementCanvasId = 'webgazerVideoCanvas';
    params.imgWidth = 1280;
    params.imgHeight = 720;

    //DEBUG variables
    //debug control boolean
    var showGazeDot = false;
    //debug element (starts offscreen)
    var gazeDot = document.createElement('div');
    gazeDot.style.position = 'absolute';
    gazeDot.style.left = '20px'; //'-999em';
    gazeDot.style.width = '10px';
    gazeDot.style.height = '10px';
    gazeDot.style.background = 'red';
    gazeDot.style.display = 'none';

    var debugVideoLoc = '';
        
    // loop parameters
    var clockStart = performance.now();
    params.dataTimestep = 50; 
    var paused = false;
    //registered callback for loop
    var nopCallback = function(data, time) {};
    var callback = nopCallback;

    //Types that regression systems should handle
    //Describes the source of data so that regression systems may ignore or handle differently the various generating events
    var eventTypes = ['click', 'move'];
    

    //movelistener timeout clock parameters
    var moveClock = performance.now();
    params.moveTickSize = 50; //milliseconds

    //lookup tables
    var curTrackerMap = {
        'clmtrackr': function() { return new ClmGaze(params); },
        'trackingjs': function() { return new TrackingjsGaze(params); },
        'js_objectdetect': function() { return new Js_objectdetectGaze(params); }
    };
    var regressionMap = {
        'ridge': function() { return new RidgeReg(params); },
        'weightedRidge': function() { return new RidgeWeightedReg(params); },
        'threadedRidge': function() { return new RidgeRegThreaded(params); },
        'linear': function() { return new LinearReg(params); }
    };
    //currently used tracker and regression models, defaults to clmtrackr and linear regression
    var curTracker = curTrackerMap['clmtrackr']();
    var regs = [regressionMap['ridge']()];


    //localstorage name
    var localstorageLabel = 'webgazerGlobalData';
    //settings Object for future storage of settings
    var settings = {};
    var data = [];
    var defaults = {
        'data': [],
        'settings': {},
    };

    //PRIVATE FUNCTIONS

    /**
     * gets the pupil features by following the pipeline which threads an eyes Object through each call:
     * curTracker gets eye patches -> blink detector -> pupil detection 
     * @param {Canvas} canvas - a canvas which will have the video drawn onto it
     * @param {number} width - the width of canvas
     * @param {number} height - the height of canvas
     */
    function getPupilFeatures(canvas, width, height) {
        if (!canvas) {
            return;
        }
        paintCurrentFrame(canvas, width, height);
        try {
            return blinkDetector.detectBlink(curTracker.getEyePatches(canvas, width, height));
        } catch(err) {
            console.log(err);
            return null;
        }
    }

    /**
     * gets the most current frame of video and paints it to a resized version of the canvas with width and height
     * @param {canvas} canvas - the canvas to paint the video on to
     * @param {integer} width - the new width of the canvas
     * @param {integer} height - the new height of the canvas
     */
    function paintCurrentFrame(canvas, width, height) {
        //imgWidth = videoElement.videoWidth * videoScale;
        //imgHeight = videoElement.videoHeight * videoScale;
        if (canvas.width != width) { 
            canvas.width = width;
        }
        if (canvas.height != height) {
            canvas.height = height;
        }

        var ctx = canvas.getContext('2d');
        ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
    }

    /**
     *  paints the video to a canvas and runs the prediction pipeline to get a prediction
     *  @param {integer} [regModelIndex] - if specified, gives a specific regression model prediction, otherwise gives all predictions
     *  @return {Object} prediction - Object containing the prediction data
     *  @return {integer} prediction.x - the x screen coordinate predicted
     *  @return {integer} prediction.y - the y screen coordinate predicted
     *  @return {Array} prediction.all - if regModelIndex is unset, an array of prediction Objects each with correspodning x and y attributes
     */
    function getPrediction(regModelIndex) {
        var predictions = [];
        var features = getPupilFeatures(videoElementCanvas, params.imgWidth, params.imgHeight);
        if (regs.length == 0) {
            console.log('regression not set, call setRegression()');
            return null;
        }
        for (var reg in regs) {
            predictions.push(regs[reg].predict(features));
        }
        if (regModelIndex !== undefined) {
            return predictions[regModelIndex] == null ? null : {
                'x' : predictions[regModelIndex].x,
                'y' : predictions[regModelIndex].y,
            };
        } else {
            return predictions.length == 0 || predictions[0] == null ? null : {
                'x' : predictions[0].x,
                'y' : predictions[0].y,
                'all' : predictions
            };
        }
    }

    /**
     * runs every available animation frame if webgazer is not paused
     */
    var smoothingVals = new webgazer.util.DataWindow(4);
    function loop() {
        var gazeData = getPrediction();
        var elapsedTime = performance.now() - clockStart;

        callback(gazeData, elapsedTime);

        if (gazeData && showGazeDot) {
            smoothingVals.push(gazeData);
            var x = 0;
            var y = 0;
            var len = smoothingVals.length;
            for (var d in smoothingVals.data) {
                x += smoothingVals.get(d).x;
                y += smoothingVals.get(d).y;
            }
            var pred = webgazer.util.bound({'x':x/len, 'y':y/len});
            gazeDot.style.top = window.scrollY + pred.y + 'px';
            gazeDot.style.left = window.scrollX + pred.x + 'px';
        }

        if (!paused) {
            //setTimeout(loop, params.dataTimestep);
            requestAnimationFrame(loop);
        }
    }

    /**
     * records click data and passes it to the regression model
     */
    var clickListener = function(event) {
        if (paused) {
            return;
        }
        var features = getPupilFeatures(videoElementCanvas, params.imgWidth, params.imgHeight);
        if (regs.length == 0) {
            console.log('regression not set, call setRegression()');
            return null;
        }
        for (var reg in regs) {
            regs[reg].addData(features, [event.clientX, event.clientY], eventTypes[0]); // eventType[0] === 'click'
        }
    }

    /**
     * records mouse movement data and passes it to the regression model
     */
    var moveListener = function(event) {
        if (paused) {
            return;
        }

        var now = performance.now();
        if (now < moveClock + params.moveTickSize) {
            return;
        } else {
            moveClock = now;
        }
        var features = getPupilFeatures(videoElementCanvas, params.imgWidth, params.imgHeight);
        if (regs.length == 0) {
            console.log('regression not set, call setRegression()');
            return null;
        }
        for (var reg in regs) {
            regs[reg].addData(features, [event.clientX, event.clientY], eventTypes[1]); //eventType[1] === 'move'
        }
    }

    /** loads the global data and passes it to the regression model 
     * 
     */
    function loadGlobalData() {
        var storage = JSON.parse(window.localStorage.getItem(localstorageLabel)) || defaults;
        settings = storage.settings;
        data = storage.data;
        for (var reg in regs) {
            regs[reg].setData(storage.data);
        }
    }
   
   /**
    * constructs the global storage Object and adds it to localstorage
    */
    function setGlobalData() {
        var storage = {
            'settings': settings,
            'data': regs[0].getData() || data
        };
        window.localStorage.setItem(localstorageLabel, JSON.stringify(storage));
        //TODO data should probably be stored in webgazer Object instead of each regression model
        //     -> requires duplication of data, but is likely easier on regression model implementors
    }

    /*
     * clears data from model and global storage
     */
    function clearData() {
        window.localStorage.set(localstorageLabel, undefined);
        for (var reg in regs) {
            regs[reg].setData([]);
        }
    }


    /**
     * initializes all needed dom elements and begins the loop
     */
    function init(videoSrc) {
        videoElement = document.createElement('video');
        videoElement.id = params.videoElementId; 
        videoElement.autoplay = true;
        console.log(videoElement);
        videoElement.style.display = 'none';

        //turn the stream into a magic URL 
        videoElement.src = videoSrc;  
        document.body.appendChild(videoElement);

        videoElementCanvas = document.createElement('canvas'); 
        videoElementCanvas.id = params.videoElementCanvasId;
        videoElementCanvas.style.display = 'none';
        document.body.appendChild(videoElementCanvas);


        //third argument set to true so that we get event on 'capture' instead of 'bubbling'
        //this prevents a client using event.stopPropagation() preventing our access to the click
        document.addEventListener('click', clickListener, true);
        document.addEventListener('mousemove', moveListener, true);

        document.body.appendChild(gazeDot);

        //BEGIN CALLBACK LOOP
        paused = false;

        clockStart = performance.now();

        loop();
    }

    //PUBLIC FUNCTIONS - CONTROL

    /**
     * starts all state related to webgazer -> dataLoop, video collection, click listener
     */
    webgazer.begin = function() {
        loadGlobalData();

        if (debugVideoLoc) {
            init(debugVideoLoc);
            return webgazer;
        }

        //SETUP VIDEO ELEMENTS
        navigator.getUserMedia = navigator.getUserMedia ||
            navigator.webkitGetUserMedia ||
            navigator.mediaDevices.getUserMedia;

        if(navigator.getUserMedia != null){ 
            var options = { 
                video:true, 
            }; 	     
            //request webcam access 
            navigator.getUserMedia(options, 
                    function(stream){
                        console.log('video stream created');
                        init(window.URL.createObjectURL(stream));                    
                    }, 
                    function(e){ 
                        console.log("No stream"); 
                        videoElement = null;
                    });
        }

        return webgazer;
    }

    /*
     * checks if webgazer has finished initializing after calling begin()
     * @return {boolean} if webgazer is ready
     */
    webgazer.isReady = function() {
        if (videoElementCanvas == null) {
            return false;
        }
        paintCurrentFrame(videoElementCanvas, params.imgWidth, params.imgHeight);
        return videoElementCanvas.width > 0;
    }

    /*
     * stops collection of data and predictions
     * @return {webgazer} this
     */
    webgazer.pause = function() {
        paused = true;
        return webgazer;
    }

    /*
     * resumes collection of data and predictions if paused
     * @return {webgazer} this
     */
    webgazer.resume = function() {
        if (!paused) {
            return webgazer;
        }
        paused = false;
        loop();
        return webgazer;
    }

    /**
     * stops collection of data and removes dom modifications, must call begin() to reset up
     * @return {webgazer} this
     */
    webgazer.end = function() {
        //loop may run an extra time and fail due to removed elements
        paused = true;
        //remove video element and canvas
        document.body.removeChild(videoElement);
        document.body.removeChild(videoElementCanvas);

        setGlobalData();
        return webgazer;
    }

    //PUBLIC FUNCTIONS - DEBUG

    /**
     * returns if the browser is compatible with webgazer
     * @return {boolean} if browser is compatible
     */
    webgazer.detectCompatibility = function() {
        navigator.getUserMedia = navigator.getUserMedia ||
            navigator.webkitGetUserMedia ||
            navigator.mediaDevices.getUserMedia;

        return navigator.getUserMedia !== undefined;
    }

    /**
     * displays the calibration point for debugging
     * @return {webgazer} this
     */
    webgazer.showPredictionPoints = function(bool) {
        showGazeDot = bool;
        gazeDot.style.left = '-999em';
        gazeDot.style.display = bool ? 'block' : 'none';
        return webgazer;
    }

    /**
     *  set a static video file to be used instead of webcam video
     *  @param {string} videoLoc - video file location
     *  @return {webgazer} this
     */
    webgazer.setStaticVideo = function(videoLoc) {
       debugVideoLoc = videoLoc;
       return webgazer;
    }

    //SETTERS
    /**
     * sets the tracking module
     * @param {string} the name of the tracking module to use
     * @return {webgazer} this
     */
    webgazer.setTracker = function(name) {
        if (curTrackerMap[name] == undefined) {
            console.log('Invalid tracker selection');
            console.log('Options are: ');
            for (var t in curTrackerMap) {
                console.log(t);
            }
            return webgazer;
        }
        curTracker = curTrackerMap[name]();    
        return webgazer;
    }

    /**
     * sets the regression module and clears any other regression modules
     * @param {string} the name of the regression module to use
     * @return {webgazer} this
     */
    webgazer.setRegression = function(name) {
        if (regressionMap[name] == undefined) {
            console.log('Invalid regression selection');
            console.log('Options are: ');
            for (var reg in regressionMap) {
                console.log(reg);
            }
            return webgazer;
        }
        data = regs[0].getData();
        regs = [regressionMap[name]()];
        regs[0].setData(data);
        return webgazer;
    }

    /**
     * adds a new tracker module so that it can be used by setTracker()
     * @param {string} name - the new name of the tracker
     * @param {function} constructor - the constructor of the curTracker Object
     * @return {webgazer} this
     */
    webgazer.addTrackerModule = function(name, constructor) {
        curTrackerMap[name] = function() {
            contructor();
        };
    }

    /**
     * adds a new regression module so that it can be used by setRegression() and addRegression()
     * @param {string} name - the new name of the regression
     * @param {function} constructor - the constructor of the regression Object
     * @param {webgazer} this
     */
    webgazer.addRegressionModule = function(name, constructor) {
        regressionMap[name] = function() {
            contructor();
        };
    }
    /**
     * adds a new regression module to the list of regression modules, seeding its data from the first regression module
     * @param {string} name - the string name of the regression module to add
     * @return {webgazer} this
     */
    webgazer.addRegression = function(name) {
        var newReg = regressionMap[name]();
        data = regs[0].getData();
        newReg.setData(data);
        regs.push(newReg);
        return webgazer;
    }

    /**
     * sets a callback to be executed on every gaze event (currently all time steps)
     * @param {gazeListener} listener - callback to handle a gaze prediction event
     * @return {webgazer} this
     */
    webgazer.setGazeListener = function(listener) {
        callback = listener;
        return webgazer;
    }

    /**
     * Handles gaze events by providing a prediction Object and elapsed time
     * @callback gazeListener
     * @param {Object} prediction - Object containing the prediction data
     *  @param {integer} prediction.x - the x screen coordinate predicted
     *  @param {integer} prediction.y - the y screen coordinate predicted
     *  @param {Array} prediction.all - if regModelIndex is unset, an array of prediction Objects each with correspodning x and y attributes
     *  @param {integer} elapsedTime - amount of time since begin() was called
     */


    /**
     * removes the callback set by setGazeListener
     * @return {webgazer} this
     */
    webgazer.clearGazeListener = function() {
        callback = nopCallback;
        return webgazer;
    }

    //GETTERS
    /**
     * returns the tracker currently in use
     * @return {tracker} an Object following the tracker interface
     */
    webgazer.getTracker = function() {
        return curTracker;
    }
    
    /**
     * returns the regression currently in use
     * @return {array} an array of Objects following the regression interface
     */
    webgazer.getRegression = function() {
        return regs;
    }

    /**
     * requests an immediate prediction
     *  @return {Object} prediction - Object containing the prediction data
     *  @return {integer} prediction.x - the x screen coordinate predicted
     *  @return {integer} prediction.y - the y screen coordinate predicted
     *  @return {Array} prediction.all - if regModelIndex is unset, an array of prediction Objects each with correspodning x and y attributes
     */
    webgazer.getCurrentPrediction = function() {
        return getPrediction(); 
    }

    /**
     * returns the different event types that may be passed to regressions when calling regression.addData()
     * @return {array} array of strings where each string is an event type
     */
    params.getEventTypes = function() { 
        return eventTypes.slice(); 
    }

    return webgazer;
})