<commit_before>(function (root) {
    // BigPipe 依赖的各种处理器
    // 可以通过此部分换成 [lazyrender](https://github.com/rgrove/lazyload/)，
    // 以达到更好的 js 并发下载性能。

    var d = document;
    var head = d.getElementsByTagName('head')[0];
    var loadedRes = {};
    var loadingRes = {};

    // get broswer info
    var browser = (function () {
        var ua = navigator.userAgent.toLowerCase();
        var match = /(webkit)[ \/]([\w.]+)/.exec(ua) ||
            /(opera)(?:.*version)?[ \/]([\w.]+)/.exec(ua) ||
            /(msie) ([\w.]+)/.exec(ua) || !/compatible/.test(ua) && /(mozilla)(?:.*? rv:([\w.]+))?/.exec(ua) ||
            [];
        return match[1];
    })();

    // load Js and excute it.
    // 加载 JS 并执行它。
    function loadJs(url, ignoreDuplicate, cb) {
        if (ignoreDuplicate && loadedRes[url]) {
            cb && cb();
            return;
        }
        if (ignoreDuplicate && loadingRes[url]) {
            loadingRes[url].push(cb);
            return;
        }
        loadingRes[url] = loadingRes[url] || [];
        var script = d.createElement('script');
        var loaded = false;
        var wrap = function () {
            if (loaded) {
                return;
            }
            loaded = true;
            loadedRes[url] = true;
            if (ignoreDuplicate) {
                for (var i = 0; i < loadingRes[url].length; i++) {
                    loadingRes[url][i] && loadingRes[url][i]();
                }
                loadingRes[url] = null;
            }
            cb && cb();
        };
        script.setAttribute('src', url);
        script.setAttribute('type', 'text/javascript');
        script.onload = script.onerror = wrap;
        script.onreadystatechange = wrap;
        head.appendChild(script);
    }

    // load css and apply it.
    // 加载 css, 并应用该样式。
    function loadCss(url, ignoreDuplicate, cb) {
        if (ignoreDuplicate && loadedRes[url]) {
            cb && cb();
            return;
        }
        if (ignoreDuplicate && loadingRes[url]) {
            loadingRes[url].push(cb);
            return;
        }
        loadingRes[url] = loadingRes[url] || [];
        var link = d.createElement('link');
        link.type = 'text/css';
        link.rel = 'stylesheet';
        link.href = url;

        if (browser === 'msie') {
            link.onreadystatechange = function () {
                /loaded|complete/.test(link.readyState) && cb();
            }
        }
        else if (browser === 'opera') {
            link.onload = cb;
        }
        else {
            // FF, Safari, Chrome
            (function () {
                try {
                    link.sheet.cssRule;
                }
                catch (e) {
                    setTimeout(arguments.callee, 20);
                    return;
                }
                loadedRes[url] = true;
                if (ignoreDuplicate) {
                    for (var i = 0; i < loadingRes[url].length; i++) {
                        loadingRes[url][i] && loadingRes[url][i]();
                    }
                    loadingRes[url] = null;
                }
                cb();
            })();
        }

        head.appendChild(link);
    }

    // eval js code.
    // 直接执行 js 代码。
    function globalEval(code) {
        var script;

        code = code.replace(/^\s+/, '').replace(/\s+$/, '');

        if (code) {
            if (code.indexOf('use strict') === 1) {
                script = document.createElement('script');
                script.text = code;
                head.appendChild(script).parentNode.removeChild(script);
            }
            else {
                eval(code);
            }
        }
    }

    // append style cod to dom.
    // 直接应用样式代码到页面。
    function appendStyle(code) {
        var dom = document.createElement('style');
        dom.innerHTML = code;
        head.appendChild(dom);
    }

    // ajax 请求，简单实现，可以修改成使用 jQuery.ajax 如果已经依赖了的话。
    function ajax(url, cb, data) {
        var xhr = new(window.XMLHttpRequest || ActiveXObject)('Microsoft.XMLHTTP');

        xhr.onreadystatechange = function () {
            if (this.readyState == 4) {
                cb(this.responseText);
            }
        };
        xhr.open(data ? 'POST' : 'GET', url + '&t=' + (new Date()).getTime(), true);

        if (data) {
            xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        }
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.send(data);
    }

    // 可以换成 jQuery.extend
    function mixin(a, b) {
        if (a && b) {
            for (var k in b) {
                if (b.hasOwnProperty(k)) {
                    a[k] = b[k];
                }
            }
        }
        return a;
    }

    var Util = {
        loadJs: loadJs,
        loadCss: loadCss,
        appendStyle: appendStyle,
        globalEval: globalEval,
        ajax: ajax,
        mixin: mixin
    };

    // expose it.
    root.BigPipeUtil = Util;
})(this);

(function (root) {
    // Event
    var Util = root.BigPipeUtil,
        slice = [].slice,
        protos;

    protos = {

        on: function (name, callback) {
            var me = this,
                set, handler;

            set = this._events || (this._events = []);
            set.push({
                e: name,
                cb: callback,
                id: set.length,
                ctx: this
            });

            return this;
        },

        once: function (name, callback) {
            var me = this,
                once;

            once = function () {
                me.off(name, once);
                return callback.apply(me, arguments);
            };

            once._cb = callback;
            return me.on(name, once);
        },

        off: function (name, cb) {
            var events = this._events;

            if (!name && !cb) {
                this._events = [];
                return this;
            }

            each(findHandlers(events, name, cb), function (item) {
                delete events[item.id];
            });

            return this;
        },

        trigger: function (type) {
            var args, events, allEvents;

            if (!this._events || !type) {
                return this;
            }

            args = slice.call(arguments, 1);
            events = findHandlers(this._events, type);
            return triggerHanders(events, args);
        }
    };

    // 根据条件过滤出事件handlers.
    function findHandlers(arr, name, callback) {
        var ret = [];

        each(arr, function (handler) {
            if (handler &&
                (!name || handler.e === name) &&
                (!callback || handler.cb === callback || handler.cb._cb === callback)) {
                ret.push(handler);
            }
        });

        return ret;
    }

    function each(arr, iterator) {
        for (var i = 0, len = arr.length, item; i < len; i++) {
            item = arr[i];
            iterator.call(item, item, i);
        }
    }

    function triggerHanders(events, args) {
        var i = -1,
            len = events.length,
            handler;

        while (++i < len) {
            handler = events[i];

            if (handler.cb.apply(handler.ctx, args) === false) {
                break;
            }
        }
    }

    root.BigPipeEvent = Util.mixin({
        mixto: function (obj) {
            return Util.mixin(obj, protos);
        }
    }, protos);

})(this);

(function (root) {
    var Util = root.BigPipeUtil;
    var Event = root.BigPipeEvent;
    var BigPipe = function () {

        // The order of render pagelet.
        // - load css
        // - append style
        // - insert html
        // - load js.
        // - exec scripts.
        //
        // data
        //  - container       optional
        //  - id              pagelet Id
        //  - html            pagelet content
        //  - js              []
        //  - css             []
        //  - styles          []
        //  - scripts         []
        //  - done            function
        // onDomInserted called when dom inserted.
        function PageLet(data, onDomInserted) {
            var remaining = 0;

            var loadCss = function () {
                // load css
                if (data.css && data.css.length) {
                    remaining = data.css.length;
                    for (var i = 0, len = remaining; i < len; i++) {
                        Util.loadCss(data.css[i], BigPipe.ignoreDuplicate, function () {
                            --remaining || insertDom();
                        });
                    }
                }
                else {
                    insertDom();
                }
            };

            var insertDom = function () {
                var i, len, dom, node, text, scriptText, temp;

                // insert style code
                if (data.styles && data.styles.length) {
                    for (i = 0, len = data.styles.length; i < len; i++) {
                        Util.appendStyle(data.styles[i]);
                    }
                }

                dom = data.container && typeof data.container === 'string' ?
                    document.getElementById(data.container) :
                    (data.container || document.getElementById(data.id));

                if (data.extend) {
                    temp = document.createElement('div');
                    temp.innerHTML = data.html;
                    while (temp.firstChild) {
                        dom.appendChild(temp.firstChild);
                    }
                }
                else {
                    dom.innerHTML = data.html;
                }
                setTimeout(function () {
                    onDomInserted(data);
                }, 0);
            };

            var loadJs = function (callback) {
                var len = data.js && data.js.length;
                var remaining = len,
                    i;

                // exec data.scripts
                var next = function () {
                    var i, len;

                    // eval scripts.
                    if (data.scripts && data.scripts.length) {
                        for (i = 0, len = data.scripts.length; i < len; i++) {
                            Util.globalEval(data.scripts[i]);
                        }
                    }

                    callback && callback(data);
                };

                if (!len) {
                    next && next();
                    return;
                }

                for (i = 0; i < len; i++) {
                    Util.loadJs(data.js[i], BigPipe.ignoreDuplicate, next && function () {
                        --remaining || next();
                    });
                }
            };

            return {
                loadCss: loadCss,
                loadJs: loadJs
            };
        }

        var count = 0,
            pagelets = [],
            /* registered pagelets */
            currentPagelet = null,
            currReqID = null,
            cache = {},
            globalBigPipeLoadIndex = 0;

        return {

            // This is method will be executed automaticlly.
            //
            // - after chunk output pagelet.
            // - after async load quickling pagelet.
            onPageletArrive: function (obj) {
                currReqID = obj.reqID;
                // console.log('arrive', obj.id);
                this.trigger('pageletarrive', obj);

                var pagelet = PageLet(obj, function () {
                    // console.log('dom ready', obj.id);
                    var item;
                    count--;
                    // enforce js executed after dom inserted.
                    if (count === 0) {
                        while ((item = pagelets.shift())) {
                            BigPipe.trigger('pageletinsert', pagelet, item.pageletData);
                            // console.log('pagelet exec js', item.pageletData.id);
                            item.loadJs(function () {
                                // console.log('pagelet exec done', item.pageletData.id);
                                BigPipe.trigger('pageletdone', pagelet, item.pageletData);
                            });
                        }
                    }
                });
                pagelet.pageletData = obj;
                pagelets.push(pagelet);
                count++;
                pagelet.loadCss();
            },

            // Async load quicking pagelet.
            // An quickling pagelet rendered only after someone called the
            // BigPipe.load('pageletId');
            //
            // BigPipe.load(pageleteIds);
            // BigPipe.load({
            //   pagelets: ['pageletA']
            //   param: 'key=val&kay=vel'
            //   container: dom or {
            //      pageletA: dom,
            //      pageletB: another dom
            //   },
            //   cb: function() {
            //
            //   }
            // });
            //
            // params:
            //   - pagelets       pagelet id or array of pagelet id.
            //   - param          extra params for the ajax call.
            //   - container      by default, the pagelet will be rendered in
            //                    some document node with the same id. With this
            //                    option the pagelet can be renndered in
            //                    specified document node.
            //   - extend
            load: function (pagelets) {
                var args = [];
                var currentPageUrl = location.href;
                var containers = {};
                var pageletRequestID = globalBigPipeLoadIndex++;
                var obj, i, id, cb, remaining = 0,
                    search, url, param, container;

                // convert arguments.
                // so we can accept
                //
                // - string with pagelet ID
                // - string with pagelet IDs split by `,`.
                // - array of string with pagelet ID
                // - object with pagelets key.
                if (pagelets instanceof Array) {
                    obj = {
                        pagelets: pagelets
                    };
                }
                else {
                    obj = typeof pagelets === 'string' ? {
                        pagelets: pagelets
                    } : pagelets;
                    pagelets = obj.pagelet || obj.pagelets;
                    typeof pagelets === 'string' &&
                        (pagelets = pagelets.split(/\s*,\s*/));
                }

                for (i = pagelets.length - 1; i >= 0; i--) {
                    id = pagelets[i];
                    args.push('pagelets[]=' + id);
                    container = obj.container && obj.container[id] || obj.container;
                    containers[id] = container;
                }
                args.push('reqID=' + pageletRequestID);

                function onPageArrive(data) {
                    // !data.reqID 用于兼容老版本未返回reqID的情况
                    if (data.reqID === undefined || data.reqID === pageletRequestID) {
                        var id = data.id;
                        // console.log('req', data.reqID, 'pagelet', data.id, 'arrive');
                        remaining++;
                        containers[id] && (data.container = containers[id]);
                        data.extend = obj.extend;
                    }
                }

                BigPipe.on('pageletarrive', onPageArrive);
                obj.search && args.push(obj.search);
                if (obj.url) {
                    url = obj.url + (obj.url.indexOf('?') === -1 ? '?' : '&') + args.join('&');
                }
                else {
                    url = (location.search ? location.search + '&' : '?') + args.join('&');
                }
                BigPipe.on('pageletdone', function (pagelet, res) {
                    // !res.reqID 用于兼容老版本未返回reqID的情况
                    if (res.reqID === undefined || res.reqID === pageletRequestID) {
                        remaining--;
                        // console.log('req', res.reqID, 'pagelet', res.id, 'done');
                        if (remaining === 0) {
                            // console.log('req', res.reqID, 'done');
                            BigPipe.off('pageletdone', arguments.callee);
                            BigPipe.off('pageletarrive', onPageArrive);
                            obj.cb && obj.cb();
                        }
                    }
                });

                // console.log('req', pageletRequestID, url, 'start');

                if (cache[obj.cacheID]) {
                    var requestCache = cache[obj.cacheID];
                    // 同步requestID
                    pageletRequestID = requestCache.reqID;
                    Util.globalEval(requestCache.content);
                }
                else {
                    Util.ajax(url, function (res) {
                        // if the page url has been moved.
                        if (currentPageUrl !== location.href) {
                            return;
                        }
                        Util.globalEval(res);
                        if (obj.cacheID) {
                            cache[obj.cacheID] = {
                                content: res,
                                reqID: currReqID
                            };
                        }
                    });
                }
            }
        };
    }();

    Event.mixto(BigPipe);
    BigPipe.ignoreDuplicate = true;
    window.BigPipe = BigPipe;
})(this);
<commit_msg>client: fix problem with nested pagelet<commit_after>(function (root) {
    // BigPipe 依赖的各种处理器
    // 可以通过此部分换成 [lazyrender](https://github.com/rgrove/lazyload/)，
    // 以达到更好的 js 并发下载性能。

    var d = document;
    var head = d.getElementsByTagName('head')[0];
    var loadedRes = {};
    var loadingRes = {};

    // get broswer info
    var browser = (function () {
        var ua = navigator.userAgent.toLowerCase();
        var match = /(webkit)[ \/]([\w.]+)/.exec(ua) ||
            /(opera)(?:.*version)?[ \/]([\w.]+)/.exec(ua) ||
            /(msie) ([\w.]+)/.exec(ua) || !/compatible/.test(ua) && /(mozilla)(?:.*? rv:([\w.]+))?/.exec(ua) ||
            [];
        return match[1];
    })();

    // load Js and excute it.
    // 加载 JS 并执行它。
    function loadJs(url, ignoreDuplicate, cb) {
        if (ignoreDuplicate && loadedRes[url]) {
            cb && cb();
            return;
        }
        if (ignoreDuplicate && loadingRes[url]) {
            loadingRes[url].push(cb);
            return;
        }
        loadingRes[url] = loadingRes[url] || [];
        var script = d.createElement('script');
        var loaded = false;
        var wrap = function () {
            if (loaded) {
                return;
            }
            loaded = true;
            loadedRes[url] = true;
            if (ignoreDuplicate) {
                for (var i = 0; i < loadingRes[url].length; i++) {
                    loadingRes[url][i] && loadingRes[url][i]();
                }
                loadingRes[url] = null;
            }
            cb && cb();
        };
        script.setAttribute('src', url);
        script.setAttribute('type', 'text/javascript');
        script.onload = script.onerror = wrap;
        script.onreadystatechange = wrap;
        head.appendChild(script);
    }

    // load css and apply it.
    // 加载 css, 并应用该样式。
    function loadCss(url, ignoreDuplicate, cb) {
        if (ignoreDuplicate && loadedRes[url]) {
            cb && cb();
            return;
        }
        if (ignoreDuplicate && loadingRes[url]) {
            loadingRes[url].push(cb);
            return;
        }
        loadingRes[url] = loadingRes[url] || [];
        var link = d.createElement('link');
        link.type = 'text/css';
        link.rel = 'stylesheet';
        link.href = url;

        if (browser === 'msie') {
            link.onreadystatechange = function () {
                /loaded|complete/.test(link.readyState) && cb();
            }
        }
        else if (browser === 'opera') {
            link.onload = cb;
        }
        else {
            // FF, Safari, Chrome
            (function () {
                try {
                    link.sheet.cssRule;
                }
                catch (e) {
                    setTimeout(arguments.callee, 20);
                    return;
                }
                loadedRes[url] = true;
                if (ignoreDuplicate) {
                    for (var i = 0; i < loadingRes[url].length; i++) {
                        loadingRes[url][i] && loadingRes[url][i]();
                    }
                    loadingRes[url] = null;
                }
                cb();
            })();
        }

        head.appendChild(link);
    }

    // eval js code.
    // 直接执行 js 代码。
    function globalEval(code) {
        var script;

        code = code.replace(/^\s+/, '').replace(/\s+$/, '');

        if (code) {
            if (code.indexOf('use strict') === 1) {
                script = document.createElement('script');
                script.text = code;
                head.appendChild(script).parentNode.removeChild(script);
            }
            else {
                eval(code);
            }
        }
    }

    // append style cod to dom.
    // 直接应用样式代码到页面。
    function appendStyle(code) {
        var dom = document.createElement('style');
        dom.innerHTML = code;
        head.appendChild(dom);
    }

    // ajax 请求，简单实现，可以修改成使用 jQuery.ajax 如果已经依赖了的话。
    function ajax(url, cb, data) {
        var xhr = new(window.XMLHttpRequest || ActiveXObject)('Microsoft.XMLHTTP');

        xhr.onreadystatechange = function () {
            if (this.readyState == 4) {
                cb(this.responseText);
            }
        };
        xhr.open(data ? 'POST' : 'GET', url + '&t=' + (new Date()).getTime(), true);

        if (data) {
            xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        }
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.send(data);
    }

    // 可以换成 jQuery.extend
    function mixin(a, b) {
        if (a && b) {
            for (var k in b) {
                if (b.hasOwnProperty(k)) {
                    a[k] = b[k];
                }
            }
        }
        return a;
    }

    var Util = {
        loadJs: loadJs,
        loadCss: loadCss,
        appendStyle: appendStyle,
        globalEval: globalEval,
        ajax: ajax,
        mixin: mixin
    };

    // expose it.
    root.BigPipeUtil = Util;
})(this);

(function (root) {
    // Event
    var Util = root.BigPipeUtil,
        slice = [].slice,
        protos;

    protos = {

        on: function (name, callback) {
            var me = this,
                set, handler;

            set = this._events || (this._events = []);
            set.push({
                e: name,
                cb: callback,
                id: set.length,
                ctx: this
            });

            return this;
        },

        once: function (name, callback) {
            var me = this,
                once;

            once = function () {
                me.off(name, once);
                return callback.apply(me, arguments);
            };

            once._cb = callback;
            return me.on(name, once);
        },

        off: function (name, cb) {
            var events = this._events;

            if (!name && !cb) {
                this._events = [];
                return this;
            }

            each(findHandlers(events, name, cb), function (item) {
                delete events[item.id];
            });

            return this;
        },

        trigger: function (type) {
            var args, events, allEvents;

            if (!this._events || !type) {
                return this;
            }

            args = slice.call(arguments, 1);
            events = findHandlers(this._events, type);
            return triggerHanders(events, args);
        }
    };

    // 根据条件过滤出事件handlers.
    function findHandlers(arr, name, callback) {
        var ret = [];

        each(arr, function (handler) {
            if (handler &&
                (!name || handler.e === name) &&
                (!callback || handler.cb === callback || handler.cb._cb === callback)) {
                ret.push(handler);
            }
        });

        return ret;
    }

    function each(arr, iterator) {
        for (var i = 0, len = arr.length, item; i < len; i++) {
            item = arr[i];
            iterator.call(item, item, i);
        }
    }

    function triggerHanders(events, args) {
        var i = -1,
            len = events.length,
            handler;

        while (++i < len) {
            handler = events[i];

            if (handler.cb.apply(handler.ctx, args) === false) {
                break;
            }
        }
    }

    root.BigPipeEvent = Util.mixin({
        mixto: function (obj) {
            return Util.mixin(obj, protos);
        }
    }, protos);

})(this);

(function (root) {
    var Util = root.BigPipeUtil;
    var Event = root.BigPipeEvent;
    var BigPipe = function () {
        var insertQueue = [];

        // The order of render pagelet.
        // - load css
        // - append style
        // - insert html
        // - load js.
        // - exec scripts.
        //
        // data
        //  - container       optional
        //  - id              pagelet Id
        //  - html            pagelet content
        //  - js              []
        //  - css             []
        //  - styles          []
        //  - scripts         []
        //  - done            function
        // onDomInserted called when dom inserted.
        function PageLet(data, onDomInserted) {
            var remaining = 0;
            var item = {
                cssLoaded: false,
                finish: function () {
                    insertDom();
                }
            };

            var loadCss = function () {
                insertQueue.push(item);
                // load css
                if (data.css && data.css.length) {
                    remaining = data.css.length;
                    for (var i = 0, len = remaining; i < len; i++) {
                        Util.loadCss(data.css[i], BigPipe.ignoreDuplicate, function () {
                            --remaining;
                            if (remaining === 0) {
                                item.cssLoaded = true;
                                finishInsertQueue();
                            }
                        });
                    }
                }
                else {
                    item.cssLoaded = true;
                    finishInsertQueue();
                }
            };

            var finishInsertQueue = function () {
                var item = insertQueue[0];
                while (item && item.cssLoaded) {
                    insertQueue.shift();
                    item.finish();
                    item = insertQueue[0];
                }
            };

            var insertDom = function () {
                var i, len, dom, node, text, scriptText, temp;

                // insert style code
                if (data.styles && data.styles.length) {
                    for (i = 0, len = data.styles.length; i < len; i++) {
                        Util.appendStyle(data.styles[i]);
                    }
                }

                dom = data.container && typeof data.container === 'string' ?
                    document.getElementById(data.container) :
                    (data.container || document.getElementById(data.id));

                if (data.extend) {
                    temp = document.createElement('div');
                    temp.innerHTML = data.html;
                    while (temp.firstChild) {
                        dom.appendChild(temp.firstChild);
                    }
                }
                else {
                    dom.innerHTML = data.html;
                }
                setTimeout(function () {
                    onDomInserted(data);
                }, 0);
            };

            var loadJs = function (callback) {
                var len = data.js && data.js.length;
                var remaining = len;
                var i;

                // exec data.scripts
                var next = function () {
                    var i, len;

                    // eval scripts.
                    if (data.scripts && data.scripts.length) {
                        for (i = 0, len = data.scripts.length; i < len; i++) {
                            Util.globalEval(data.scripts[i]);
                        }
                    }

                    callback && callback(data);
                };

                if (!len) {
                    next && next();
                    return;
                }

                for (i = 0; i < len; i++) {
                    Util.loadJs(data.js[i], BigPipe.ignoreDuplicate, next && function () {
                        --remaining || next();
                    });
                }
            };

            return {
                loadCss: loadCss,
                loadJs: loadJs
            };
        }

        var count = 0,
            pagelets = [],
            /* registered pagelets */
            currReqID = null,
            cache = {},
            globalBigPipeLoadIndex = 0;

        return {

            // This is method will be executed automaticlly.
            //
            // - after chunk output pagelet.
            // - after async load quickling pagelet.
            onPageletArrive: function (obj) {
                currReqID = obj.reqID;
                // console.log('arrive', obj.id);
                this.trigger('pageletarrive', obj);
                var pagelet = new PageLet(obj, function () {
                    // console.log('dom ready', obj.id);
                    var item;
                    count--;
                    // enforce js executed after dom inserted.
                    if (count === 0) {
                        while ((item = pagelets.shift())) {
                            BigPipe.trigger('pageletinsert', pagelet, item.pageletData);
                            // console.log('pagelet exec js', item.pageletData.id);
                            item.loadJs((function (item) {
                                // console.log('pagelet exec done', item.pageletData.id);
                                BigPipe.trigger('pageletdone', pagelet, item.pageletData);
                            })(item));
                        }
                    }
                });
                pagelet.pageletData = obj;
                pagelets.push(pagelet);
                count++;
                pagelet.loadCss();
            },

            // Async load quicking pagelet.
            // An quickling pagelet rendered only after someone called the
            // BigPipe.load('pageletId');
            //
            // BigPipe.load(pageleteIds);
            // BigPipe.load({
            //   pagelets: ['pageletA']
            //   param: 'key=val&kay=vel'
            //   container: dom or {
            //      pageletA: dom,
            //      pageletB: another dom
            //   },
            //   cb: function() {
            //
            //   }
            // });
            //
            // params:
            //   - pagelets       pagelet id or array of pagelet id.
            //   - param          extra params for the ajax call.
            //   - container      by default, the pagelet will be rendered in
            //                    some document node with the same id. With this
            //                    option the pagelet can be renndered in
            //                    specified document node.
            //   - extend
            load: function (pagelets) {
                var args = [];
                var currentPageUrl = location.href;
                var containers = {};
                var pageletRequestID = globalBigPipeLoadIndex++;
                var obj, i, id, cb, remaining = 0,
                    search, url, param, container;

                // convert arguments.
                // so we can accept
                //
                // - string with pagelet ID
                // - string with pagelet IDs split by `,`.
                // - array of string with pagelet ID
                // - object with pagelets key.
                if (pagelets instanceof Array) {
                    obj = {
                        pagelets: pagelets
                    };
                }
                else {
                    obj = typeof pagelets === 'string' ? {
                        pagelets: pagelets
                    } : pagelets;
                    pagelets = obj.pagelet || obj.pagelets;
                    typeof pagelets === 'string' &&
                        (pagelets = pagelets.split(/\s*,\s*/));
                }

                for (i = pagelets.length - 1; i >= 0; i--) {
                    id = pagelets[i];
                    args.push('pagelets[]=' + id);
                    container = obj.container && obj.container[id] || obj.container;
                    containers[id] = container;
                }
                args.push('reqID=' + pageletRequestID);

                function onPageArrive(data) {
                    // !data.reqID 用于兼容老版本未返回reqID的情况
                    if (data.reqID === undefined || data.reqID === pageletRequestID) {
                        var id = data.id;
                        // console.log('req', data.reqID, 'pagelet', data.id, 'arrive');
                        remaining++;
                        containers[id] && (data.container = containers[id]);
                        data.extend = obj.extend;
                    }
                }

                BigPipe.on('pageletarrive', onPageArrive);
                obj.search && args.push(obj.search);
                if (obj.url) {
                    url = obj.url + (obj.url.indexOf('?') === -1 ? '?' : '&') + args.join('&');
                }
                else {
                    url = (location.search ? location.search + '&' : '?') + args.join('&');
                }
                BigPipe.on('pageletdone', function (pagelet, res) {
                    // !res.reqID 用于兼容老版本未返回reqID的情况
                    if (res.reqID === undefined || res.reqID === pageletRequestID) {
                        remaining--;
                        // console.log('req', res.reqID, 'pagelet', res.id, 'done');
                        if (remaining === 0) {
                            // console.log('req', res.reqID, 'done');
                            BigPipe.off('pageletdone', arguments.callee);
                            BigPipe.off('pageletarrive', onPageArrive);
                            obj.cb && obj.cb();
                        }
                    }
                });

                // console.log('req', pageletRequestID, url, 'start');

                if (cache[obj.cacheID]) {
                    var requestCache = cache[obj.cacheID];
                    // 同步requestID
                    pageletRequestID = requestCache.reqID;
                    Util.globalEval(requestCache.content);
                }
                else {
                    Util.ajax(url, function (res) {
                        // if the page url has been moved.
                        if (currentPageUrl !== location.href) {
                            return;
                        }
                        Util.globalEval(res);
                        if (obj.cacheID) {
                            cache[obj.cacheID] = {
                                content: res,
                                reqID: currReqID
                            };
                        }
                    });
                }
            }
        };
    }();

    Event.mixto(BigPipe);
    BigPipe.ignoreDuplicate = true;
    window.BigPipe = BigPipe;
})(this);
<|endoftext|>


<commit_before>/**
 * Created by Minhaj on 6/20/15.
 */

var utils = require('utils');
var tesseract = require('node-tesseract');
var textcleaner = require('textcleaner');
var multer  = require('multer');
var express  = require('express');
var fs = require('fs');
var hocr = require('node-hocr');
var Dropbox = require("dropbox");
var dropboxClient;

module.exports = function(app) {
    app.use(multer(
        {
            dest: './.tmp/',
            inMemory: false
        }
    ));

    app.post("/api/ocr", process);
    app.post("/api/digits", digits);
    app.get("/api/benchmark", benchmark);
    app.get("/server_check", server_check);

};

var getDropboxClient = function() {

    console.log("Getting dropbox client");

    if (!dropboxClient) {
        dropboxClient = new Dropbox.Client({
            key: "mbwfmlzklm1fi1d",
            secret: "giazksdcfzo8u9g"
        });
    }

    dropboxClient.authDriver(new Dropbox.AuthDriver.NodeServer(8191));

    return dropboxClient;
}

var authenticateDropboxClient = function(callback) {

    var dbClient = getDropboxClient();
    console.log("Authenticate dropbox client");

    dbClient.authenticate(function(error, client) {

      console.log('Auth callback');

      console.log(error);
      console.log(client);

      if (error) {
        callback(error, null);
      } else {
        callback(null, client);
      }
    });
}

var uploadFileToDropbox = function(image, filename, callback) {
    authenticateDropboxClient(function(error, client) {

        console.log("Upload file to dropbox");

        console.log(error);
        console.log(client);

        fs.readFile(image, function(frerror, data) {
          // No encoding passed, readFile produces a Buffer instance
          console.log(filename);
          console.log(frerror);

          if (frerror) {
             callback(frerror, null);
             return;
          }

          client.writeFile(filename, data, function(dberror, stat) {
             callback(dberror, stat);
             return;
          });
        });
    });
}

var server_check = function(req,res) {

    tesseract.version(function(err, data) {
        if (!err) {
            res.json(200, data);
        } else {
            res.json(500, err);
        }
    });
}

/**
 * Following steps done under this functions.
 *
 * 1. Uploads image under '.tmp' folder.
 * 2. Grab text from image using 'tesseract-ocr'.
 * 3. Delete image from hardisk.
 * 4. Return text in json format.
 *
 * @param req
 * @param res
 */

 var benchmark = function(req, res) {

    var expect = 9373;

    var image = __dirname + '/../test/test_5.jpg';

    //best for dark 04944 :
    // b_0.6_c_-0.1_p_4  49445774
    // b_0.6_c_0.6_p_4  0494411
    // b_0.6_c_0.5_p_4  04944152
    // b_0.7_c_0.4_p_5_ 0494415

    //best for dark 019676
    // b_0.6_c_0.8_p_5_01967931

    //best for medium 21138 :
    // b_0.4_c_0.8_p_5_1388
    // b_0.4_c_0.8_p_5_1388.jpg
    // b_0.4_c_0.85_p_15_8138

    for (var p=5;p<=5;p+=1) {
        for (var b=-30;b<=90;b+=10) {
            for (var c=0; c<=100;c+=10) {

            preprocessImageWithParams(image, b/100, c/100,p,function(err, isLcd, preprocessedImage, br, co, po) {
                if(!err) {
                        performOCR(preprocessedImage, isLcd, function(err, text) {
                            if (parseInt(text) != expect) {
                                utils.copyFileSync(preprocessedImage, __dirname + '/../uploads/meter_photo_b_'+br+'_c_'+co+'_p_'+po+'_'+text+'.jpg');
                            } else {
                                utils.copyFileSync(preprocessedImage, __dirname + '/../uploads/YES_meter_photo_b_'+br+'_c_'+co+'_p_'+po+'_'+text+'.jpg');
                            }

                            fs.unlink(preprocessedImage, function (err) {
                                if (err){
                                    callback(err,null)
                                }
                            });
                        });
                    }
                });
            }
        }
    }
    res.json(200, 'started');
 }

 var digits = function(req, res) {

    console.log('Start processing: %j', req.files.image.path);

    var crop = req.body.crop;

    if (!crop) {
        console.err("Please put 'crop' parameter into POST request");
        return;
    }

    var cropRect = new utils.Rect(crop);
    var image = req.files.image.path;
    performOcrForImage(image,cropRect,req,res);
};

var process = function(req, res) {
    var image = req.files.file.path;
    performOcrForImage(image, null, req, res);
};

function performOcrForImage(image, cropRect, req,res ) {

    //create copy of original image
    preprocessImage(image,cropRect,function(preprocessedImages) {

        if (preprocessedImages == null) {
            res.json(200, "");
        }

        console.log('\nImage preprocessed: '+preprocessedImages.length+'\n');

        var hocrs = [];
        var results = [];
        var ocrDone = 0;

        for (var index = 0; index < preprocessedImages.length; index++) {

            var stepImage = preprocessedImages[index];

            performOCR(stepImage.path, stepImage.isLcd, function(err, text) {

                ocrDone++;

                if (text != null) {
                    hocrs.push(text);
                }

                if (ocrDone == preprocessedImages.length) {

                    parseNextHocr(hocrs,0, results, function(endResults) {

                        var winner;

                        if (endResults != null) {
                            console.log("\nCandidates:");
                            endResults.forEach(logArrayElements);

                            var maxConfidence = Math.max.apply(Math,endResults.map(function(o){
                                if (o == null) return 0;
                                return o.confidence;
                            }));

                            var winners = endResults.filter(function (o) {

                                if (o == null) {
                                    return false;
                                }

                                return o.confidence == maxConfidence;
                            });

                            console.log("\nWinners:");
                            console.log(winners);

                            winner = winners[0];
                        }

                        if (winner == null) {
                            winner = new utils.MeterRead("",0);
                        }

                        var filename = 'meter_photo_'+utils.datetimestamp()+'-original-'+winner.word+'.jpg';
                        utils.copyFileSync(image, __dirname + '/../uploads/'+filename);

                        uploadFileToDropbox(image,filename,function(error, stats) {

                            console.log(error);
                            console.log(stats);

                            fs.unlink(image, function (err) {
                                if (err){
                                    callback(err,null)
                                }
                            });
                        });

                        res.json(200, meterReadToJson(winner));
                    });
                }
            });
        }
    });
}

function preprocessImage(path, cropRect, callback) {
    textcleaner.process(path, cropRect,function(preprocessedImages) {
        callback(preprocessedImages);
    });
}

function preprocessImageWithParams(path, b, c,p, callback) {
    textcleaner.benchmark(path, b, c, p,function(err, isLcd, preprocessedImage) {
        if(err) {
            console.error(err);
            callback(err,isLcd,null, b, c, p);
        } else {
            callback(null,isLcd, preprocessedImage, b, c,p);
        }
    });
}

function meterReadFromData(data) {

    if (data.length == 0) return null;
    if (data[0].par.length == 0) return null;
    if (data[0].par[0].line.length == 0) return null;
    if (data[0].par[0].line[0].words.length == 0) return null;

    var words = data[0].par[0].line[0].words;
    console.log(words);

    var totalConfidence = 0;
    var totalCounter = 0;
    var fullWord = "";

    for (var index=0; index <words.length; index++) {

            var word = words[index];
            var singleWord = word.data;
            if (singleWord != null) {
                singleWord = singleWord.replace(/\n/g, '');
                singleWord = singleWord.replace(/ /g,'')
                singleWord = singleWord.replace(/\./g,'')
                singleWord = singleWord.replace('"', '');
                singleWord = singleWord.replace(/-/g, '');
            }

            if (singleWord != null) {
                fullWord = fullWord + singleWord;
            }

            if (word != null) {
                var params = word.infos.split("x_wconf");
                var confidence = params[1];
                confidence = confidence.replace(/ /g,'');
                totalConfidence += parseInt(confidence);
                totalCounter++;
            }
    }

    if (totalCounter > 0) {
        totalConfidence = totalConfidence / totalCounter;
    }

    var meterRead = new utils.MeterRead(fullWord,totalConfidence);
    return meterRead;
}

function meterReadToJson(meterRead) {
    return {"meter_read": meterRead.word, "confidence": meterRead.confidence.toFixed(2)};
}

function parseNextHocr(hocrs, index, results, callback) {

        if (index >= hocrs.length) {
            callback(results);
            return;
        }

        console.log("Current step of parsing: "+index);

        parseHocr(hocrs[index],function(err,image) {

            if (err == null) {
                results.push(image);
            }

            parseNextHocr(hocrs, index+1, results,callback);
        });
 }

 function parseHocr(hocr_value, callback) {

   var hocr2 = new hocr.Hocr(hocr_value, function(error, data) {
        if (error) {
          callback(error, null);
        } else {
          var meterRead = meterReadFromData(data);
          callback(null, meterRead);
        }
   });
}

function logArrayElements(element, index, array) {
    if (element == null) return;
    console.log('"'+element.word+'" = ' + element.confidence + '%');
}

function performOCR(path, isLcd, callback) {

    // Recognize text of any language in any format
    tesseract.process(path,isLcd,function(err, text) {
        if(err) {
            console.error(err);
        } else {
            callback(null, text);
        }
    });

}<commit_msg>more logs<commit_after>/**
 * Created by Minhaj on 6/20/15.
 */

var utils = require('utils');
var tesseract = require('node-tesseract');
var textcleaner = require('textcleaner');
var multer  = require('multer');
var express  = require('express');
var fs = require('fs');
var hocr = require('node-hocr');
var Dropbox = require("dropbox");
var dropboxClient;

module.exports = function(app) {
    app.use(multer(
        {
            dest: './.tmp/',
            inMemory: false
        }
    ));

    app.post("/api/ocr", process);
    app.post("/api/digits", digits);
    app.get("/api/benchmark", benchmark);
    app.get("/server_check", server_check);

};

var getDropboxClient = function() {

    console.log("Getting dropbox client");

    if (!dropboxClient) {
        dropboxClient = new Dropbox.Client({
            key: "mbwfmlzklm1fi1d",
            secret: "giazksdcfzo8u9g"
        });
    }

    dropboxClient.authDriver(new Dropbox.AuthDriver.NodeServer(8191));

    return dropboxClient;
}

var authenticateDropboxClient = function(callback) {

    var dbClient = getDropboxClient();
    console.log("Authenticate dropbox client");

    dbClient.authenticate(function(error, client) {

      console.log('Auth callback');

      console.log(error);
      console.log(client);

      if (error) {
        callback(error, null);
      } else {
        callback(null, client);
      }
    });
}

var uploadFileToDropbox = function(image, filename, callback) {
    authenticateDropboxClient(function(error, client) {

        console.log("Upload file to dropbox");

        console.log(error);
        console.log(client);

        fs.readFile(image, function(frerror, data) {
          // No encoding passed, readFile produces a Buffer instance
          console.log(filename);
          console.log(frerror);

          if (frerror) {
             callback(frerror, null);
             return;
          }

          client.writeFile(filename, data, function(dberror, stat) {
             callback(dberror, stat);
             return;
          });
        });
    });
}

var server_check = function(req,res) {

    tesseract.version(function(err, data) {
        if (!err) {
            res.json(200, data);
        } else {
            res.json(500, err);
        }
    });
}

/**
 * Following steps done under this functions.
 *
 * 1. Uploads image under '.tmp' folder.
 * 2. Grab text from image using 'tesseract-ocr'.
 * 3. Delete image from hardisk.
 * 4. Return text in json format.
 *
 * @param req
 * @param res
 */

 var benchmark = function(req, res) {

    var expect = 9373;

    var image = __dirname + '/../test/test_5.jpg';

    //best for dark 04944 :
    // b_0.6_c_-0.1_p_4  49445774
    // b_0.6_c_0.6_p_4  0494411
    // b_0.6_c_0.5_p_4  04944152
    // b_0.7_c_0.4_p_5_ 0494415

    //best for dark 019676
    // b_0.6_c_0.8_p_5_01967931

    //best for medium 21138 :
    // b_0.4_c_0.8_p_5_1388
    // b_0.4_c_0.8_p_5_1388.jpg
    // b_0.4_c_0.85_p_15_8138

    for (var p=5;p<=5;p+=1) {
        for (var b=-30;b<=90;b+=10) {
            for (var c=0; c<=100;c+=10) {

            preprocessImageWithParams(image, b/100, c/100,p,function(err, isLcd, preprocessedImage, br, co, po) {
                if(!err) {
                        performOCR(preprocessedImage, isLcd, function(err, text) {
                            if (parseInt(text) != expect) {
                                utils.copyFileSync(preprocessedImage, __dirname + '/../uploads/meter_photo_b_'+br+'_c_'+co+'_p_'+po+'_'+text+'.jpg');
                            } else {
                                utils.copyFileSync(preprocessedImage, __dirname + '/../uploads/YES_meter_photo_b_'+br+'_c_'+co+'_p_'+po+'_'+text+'.jpg');
                            }

                            fs.unlink(preprocessedImage, function (err) {
                                if (err){
                                    callback(err,null)
                                }
                            });
                        });
                    }
                });
            }
        }
    }
    res.json(200, 'started');
 }

 var digits = function(req, res) {

    console.log('Start processing: %j', req.files.image.path);

    var crop = req.body.crop;

    if (!crop) {
        console.err("Please put 'crop' parameter into POST request");
        return;
    }

    var cropRect = new utils.Rect(crop);
    var image = req.files.image.path;
    performOcrForImage(image,cropRect,req,res);
};

var process = function(req, res) {
    var image = req.files.file.path;
    performOcrForImage(image, null, req, res);
};

function performOcrForImage(image, cropRect, req,res ) {

    //create copy of original image
    preprocessImage(image,cropRect,function(preprocessedImages) {

        if (preprocessedImages == null) {
            res.json(200, "");
        }

        console.log('\nImage preprocessed: '+preprocessedImages.length+'\n');

        var hocrs = [];
        var results = [];
        var ocrDone = 0;

        for (var index = 0; index < preprocessedImages.length; index++) {

            var stepImage = preprocessedImages[index];

            performOCR(stepImage.path, stepImage.isLcd, function(err, text) {

                ocrDone++;

                if (text != null) {
                    hocrs.push(text);
                }

                if (ocrDone == preprocessedImages.length) {

                    parseNextHocr(hocrs,0, results, function(endResults) {

                        var winner;

                        if (endResults != null) {
                            console.log("\nCandidates:");
                            endResults.forEach(logArrayElements);

                            var maxConfidence = Math.max.apply(Math,endResults.map(function(o){
                                if (o == null) return 0;
                                return o.confidence;
                            }));

                            var winners = endResults.filter(function (o) {

                                if (o == null) {
                                    return false;
                                }

                                return o.confidence == maxConfidence;
                            });

                            console.log("\nWinners:");
                            console.log(winners);

                            winner = winners[0];
                        }

                        if (winner == null) {
                            winner = new utils.MeterRead("",0);
                        }

                        var filename = 'meter_photo_'+utils.datetimestamp()+'-original-'+winner.word+'.jpg';
                        utils.copyFileSync(image, __dirname + '/../uploads/'+filename);

                        uploadFileToDropbox(image,filename,function(error, stats) {

                            console.log(error);
                            console.log(stats);
                        res.json(200, meterReadToJson(winner));

                            fs.unlink(image, function (err) {
                                if (err){
                                    callback(err,null)
                                }
                            });
                        });

                    });
                }
            });
        }
    });
}

function preprocessImage(path, cropRect, callback) {
    textcleaner.process(path, cropRect,function(preprocessedImages) {
        callback(preprocessedImages);
    });
}

function preprocessImageWithParams(path, b, c,p, callback) {
    textcleaner.benchmark(path, b, c, p,function(err, isLcd, preprocessedImage) {
        if(err) {
            console.error(err);
            callback(err,isLcd,null, b, c, p);
        } else {
            callback(null,isLcd, preprocessedImage, b, c,p);
        }
    });
}

function meterReadFromData(data) {

    if (data.length == 0) return null;
    if (data[0].par.length == 0) return null;
    if (data[0].par[0].line.length == 0) return null;
    if (data[0].par[0].line[0].words.length == 0) return null;

    var words = data[0].par[0].line[0].words;
    console.log(words);

    var totalConfidence = 0;
    var totalCounter = 0;
    var fullWord = "";

    for (var index=0; index <words.length; index++) {

            var word = words[index];
            var singleWord = word.data;
            if (singleWord != null) {
                singleWord = singleWord.replace(/\n/g, '');
                singleWord = singleWord.replace(/ /g,'')
                singleWord = singleWord.replace(/\./g,'')
                singleWord = singleWord.replace('"', '');
                singleWord = singleWord.replace(/-/g, '');
            }

            if (singleWord != null) {
                fullWord = fullWord + singleWord;
            }

            if (word != null) {
                var params = word.infos.split("x_wconf");
                var confidence = params[1];
                confidence = confidence.replace(/ /g,'');
                totalConfidence += parseInt(confidence);
                totalCounter++;
            }
    }

    if (totalCounter > 0) {
        totalConfidence = totalConfidence / totalCounter;
    }

    var meterRead = new utils.MeterRead(fullWord,totalConfidence);
    return meterRead;
}

function meterReadToJson(meterRead) {
    return {"meter_read": meterRead.word, "confidence": meterRead.confidence.toFixed(2)};
}

function parseNextHocr(hocrs, index, results, callback) {

        if (index >= hocrs.length) {
            callback(results);
            return;
        }

        console.log("Current step of parsing: "+index);

        parseHocr(hocrs[index],function(err,image) {

            if (err == null) {
                results.push(image);
            }

            parseNextHocr(hocrs, index+1, results,callback);
        });
 }

 function parseHocr(hocr_value, callback) {

   var hocr2 = new hocr.Hocr(hocr_value, function(error, data) {
        if (error) {
          callback(error, null);
        } else {
          var meterRead = meterReadFromData(data);
          callback(null, meterRead);
        }
   });
}

function logArrayElements(element, index, array) {
    if (element == null) return;
    console.log('"'+element.word+'" = ' + element.confidence + '%');
}

function performOCR(path, isLcd, callback) {

    // Recognize text of any language in any format
    tesseract.process(path,isLcd,function(err, text) {
        if(err) {
            console.error(err);
        } else {
            callback(null, text);
        }
    });

}<|endoftext|>


<commit_before>/*global pulse, app, jQuery, require, document, esri, esriuk, Handlebars, console, $, mynearest, window, alert, unescape, define */

/*
 | Copyright 2015 ESRI (UK) Limited
 |
 | Licensed under the Apache License, Version 2.0 (the "License");
 | you may not use this file except in compliance with the License.
 | You may obtain a copy of the License at
 |
 |    http://www.apache.org/licenses/LICENSE-2.0
 |
 | Unless required by applicable law or agreed to in writing, software
 | distributed under the License is distributed on an "AS IS" BASIS,
 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | See the License for the specific language governing permissions and
 | limitations under the License.
 */
/* This task uses code from www.movable-type.co.uk                                                */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/*  Latitude/longitude spherical geodesy formulae & scripts           (c) Chris Veness 2002-2014  */
/*   - www.movable-type.co.uk/scripts/latlong.html                                   MIT Licence  */
/*  https://github.com/chrisveness/geodesy/blob/master/LICENSE                                    */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

define([
	"dojo/_base/declare",
	"dojo/_base/array",
	"dojo/Deferred",
	"esri/geometry/Point",
    "esri/geometry/Polygon",
    "esri/SpatialReference",
	"esri/geometry/mathUtils"],
	function (declare, array, Deferred, Point, Polygon, SpatialReference, mathUtils) {
		"use strict";

		return declare(null, {
		    constructor : function(options) {
			    this._maxFeature = options.maxFeatures || 10;
			    this._mode = options.mode || "planar"; // planar or geodesic
		    },

		    execute : function(params) {
			    var deferred = new Deferred();

			    try {
				    deferred.resolve(this._getNearestResult(params.point, params.featureSet));
			    } catch (err) {
				    deferred.reject(err);
			    }

			    return deferred.promise;
		    },

		    _getNearestResult : function(point, featureSet) {
		        // summary:
		        //		Calculates distances for features arounda point

		        var features = featureSet.features,
				    distance = this._mode === "geodesic" ? this._greatCircleDistance : this._euclidianDistance,
				    candidates = [], geometry, result, _this = this, polyCandidates = [], multiPCandidates = [];

			    // Supported in 10.1 or greater
			    if (featureSet.exceededTransferLimit && featureSet.exceededTransferLimit === true) {
			        console.warn("Feature limit reached, the result may not be accurate.");
			        result = { "limitExceeded": true };
			        return result;
			    }

			    // Calculate distance for each feature
			    if (featureSet.geometryType === "esriGeometryPoint") {
				    array.forEach(features, function(feature) {
					    candidates.push({
						    point: feature.geometry,
						    feature: feature,
						    distance: _this._convertDistanceTo("m", distance.call(_this, point, feature.geometry))
					    });
				    });

				    result = this._getMin(candidates);
			    }
			    else if (featureSet.geometryType === "esriGeometryPolygon") {
			        polyCandidates = [];
				    array.forEach(features, function(feature) {
					    geometry = feature.geometry;
					    array.forEach(geometry.rings, function (ring) {
                            // This returns candidates that have a mathmatical distance from the geometry based on the coordinate system
					        polyCandidates.push(this._getNearest(point, feature, ring));
					    }, this);
				    }, this);

			        // The coordinate based distance must be converted into a standard distance measurement i.e. Km or Miles
				    array.forEach(polyCandidates, function (candidate) {
				        candidate.distance = _this._convertDistanceTo("m", distance.call(_this, point, candidate.point));
				        // Once the final distance has been calculated check if the Point is inside the Polygon
				        var polyToCheck = new Polygon(candidate.feature.geometry);
				        if (polyToCheck.contains(point)) {
				            candidate.distance = 0;
				        }

				        candidates.push(candidate);
				    });

				    result = this._getMin(candidates);
			    }
			    else if (featureSet.geometryType === "esriGeometryPolyline") {
			        polyCandidates = [];
				    array.forEach(features, function(feature) {
					    geometry = feature.geometry;
					    array.forEach(geometry.paths, function(path) {
					        polyCandidates.push(this._getNearest(point, feature, path));
					    }, this);
				    }, this);

			        // The coordinate based distance must be converted into a standard distance measurement i.e. Km or Miles
				    array.forEach(polyCandidates, function (candidate) {
				        candidate.distance = _this._convertDistanceTo("m", distance.call(_this, point, candidate.point));
				        candidates.push(candidate);
				    });

				    result = this._getMin(candidates);
			    }
			    else if (featureSet.geometryType === "esriGeometryMultipoint") {
			        multiPCandidates = [];
			        array.forEach(features, function(feature) {
			            geometry = feature.geometry;

                        // If the geometery is Multipoint but only has a single point in it
			            if (geometry.points.length === 1) {
			                multiPCandidates.push({
			                    point: geometry.points[0],
			                    feature: feature,
			                    distance: _this._convertDistanceTo("m", distance.call(_this, point, new Point(geometry.points[0][0], geometry.points[0][1], new SpatialReference(4326))))
			                });
			            }
			            else {
			                multiPCandidates.push(this._getNearest(point, feature, geometry.points));
			            }
			        }, this);

		            // The coordinate based distance must be converted into a standard distance measurement i.e. Km or Miles
			        array.forEach(multiPCandidates, function (candidate) {
			            if (candidate.feature.geometry.points.length > 1) {
			                candidate.distance = _this._convertDistanceTo("m", distance.call(_this, point, candidate.point));
			            }
			            candidates.push(candidate);
			        });

			        result = this._getMin(candidates);
			    }
			    else {
				    // Not supported
				    result = null;
			    }

			    return result;
		    },

		    _getMin : function(features) {
			    var comparator = this._comparator;

			    // Sort the features
			    features.sort(comparator);

			    // Return the first 'n' features
			    return features.slice(0, this._maxFeature);
		    },

		    _getNearest: function (/*esri/geometry/Point*/point, /*esri/geometry/Geometry*/parentFeature, /*Number[]*/path) {
		        // summary:
		        //		Finds the nearest feature around a point

		        var minDistance = null, from, to, x, y, dx, dy, i, a, b, distance, n, len,
				    pathPoints, fromPoint, toPoint, length2, toEnd2, toStart2, distance2, calcLength2;

			    function square(num) {
				    // Faster than Math.pow to square a number
				    return num * num;
			    }

			    // Convert path to pathPoints
			    pathPoints = array.map(path, function (item) {
				    return { "x": item[0], "y": item[1] };
			    });

			    if (pathPoints.length > 1) {
				    for (n = 1, len = pathPoints.length; n < len; n++) {
					    // Get segment from points
					    fromPoint = pathPoints[n - 1];
					    toPoint = pathPoints[n];

					    if (toPoint.x !== fromPoint.x) {
						    a = (toPoint.y - fromPoint.y) / (toPoint.x - fromPoint.x);
						    b = toPoint.y - a * toPoint.x;
						    distance = Math.abs(a * point.x + b - point.y) / Math.sqrt(a * a + 1);
					    } else {
						    distance = Math.abs(point.x - toPoint.x);
					    }

					    // Length squared of the line segment
					    length2 = square(toPoint.y - fromPoint.y) + square(toPoint.x - fromPoint.x);
					    // Distance squared of point to the start of the line segment
					    toStart2 = square(fromPoint.y - point.y) + square(fromPoint.x - point.x);
					    // Distance squared of point to end of the line segment
					    toEnd2 = square(toPoint.y - point.y) + square(toPoint.x - point.x);
					    // Minimum distance squared of the point to the infinite line
					    distance2 = square(distance);
					    // Calculated length squared of the line segment
					    calcLength2 = toEnd2 - distance2 + toStart2 - distance2;

					    // Redefine minimum distance to line segment (not infinite line) if necessary
					    if (calcLength2 > length2) {
						    distance = Math.sqrt(Math.min(toEnd2, toStart2));
					    }

					    if ((minDistance === null) || (minDistance > distance)) {
						    if (calcLength2 > length2) {
							    if (toStart2 < toEnd2) {
								    to = 0; // Nearer to previous point
								    from = 1;
							    } else {
								    from = 0; // Nearer to current point
								    to = 1;
							    }
						    } else {
							    // perpendicular from point intersects line segment
							    to = ((Math.sqrt(toStart2 - distance2)) / Math.sqrt(length2));
							    from = ((Math.sqrt(toEnd2 - distance2)) / Math.sqrt(length2));
						    }

						    minDistance = distance;
						    i = n;
					    }
				    }

				    // Calculate coordinates
				    dx = pathPoints[i - 1].x - pathPoints[i].x;
				    dy = pathPoints[i - 1].y - pathPoints[i].y;
				    x = pathPoints[i - 1].x - (dx * to);
				    y = pathPoints[i - 1].y - (dy * to);
			    }

			    // Return feature
			    return {
				    point: new Point(x, y, point.spatialReference),
				    feature: parentFeature,
				    distance: minDistance
			    };
		    },

		    _euclidianDistance : function(p1, p2) {
			    return mathUtils.getLength(p1, p2);
		    },

		    _degToRad: function(deg) {
		        // summary:
		        //		Converts degrees to radians
		        return (deg * Math.PI) / 180;
	        },

		    _greatCircleDistance: function (p1, p2) {
		        // summary:
		        //		Returns the distance between two points in Km

			    // Haversine formula (http://www.movable-type.co.uk/scripts/latlong.html)
			    var toRad = this._toRad,
				    radius = 6378.1, // Earth's mean radius in km
				    lat1 = toRad.call(this, p1.y),
				    lon1 = toRad.call(this, p1.x),
				    lat2 = toRad.call(this, p2.y),
				    lon2 = toRad.call(this, p2.x),
				    dLat = lat2 - lat1,
				    dLon = lon2 - lon1,
				    a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2),
				    c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

			    return radius * c;
			    // Length in km
		    },

		    _toRad: function (number) {
		        // summary:
		        //		Converts a number to radians
		        return this._degToRad(number);
		    },

		    _convertDistanceTo: function (distanceUnits, distance) {
		        // summary:
		        //		Converts a distance from one unit type to another

		        var convertedDistance = null;

		        switch (distanceUnits) {
		            case "m":
		                convertedDistance = distance * 0.621371;
		                break;

		            default:
		                convertedDistance = distance;
		                break;
		        }

		        return convertedDistance;
		    },

		    _comparator: function (a, b) {
		        // summary:
		        //		Compares two numbers
			    if (a.distance < b.distance) {
				    return -1;
			    }

			    if (a.distance > b.distance) {
				    return 1;
			    }

			    return 0;
		    }
	});
});
<commit_msg>Support empty layers<commit_after>/*global pulse, app, jQuery, require, document, esri, esriuk, Handlebars, console, $, mynearest, window, alert, unescape, define */

/*
 | Copyright 2015 ESRI (UK) Limited
 |
 | Licensed under the Apache License, Version 2.0 (the "License");
 | you may not use this file except in compliance with the License.
 | You may obtain a copy of the License at
 |
 |    http://www.apache.org/licenses/LICENSE-2.0
 |
 | Unless required by applicable law or agreed to in writing, software
 | distributed under the License is distributed on an "AS IS" BASIS,
 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | See the License for the specific language governing permissions and
 | limitations under the License.
 */
/* This task uses code from www.movable-type.co.uk                                                */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/*  Latitude/longitude spherical geodesy formulae & scripts           (c) Chris Veness 2002-2014  */
/*   - www.movable-type.co.uk/scripts/latlong.html                                   MIT Licence  */
/*  https://github.com/chrisveness/geodesy/blob/master/LICENSE                                    */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

define([
	"dojo/_base/declare",
	"dojo/_base/array",
	"dojo/Deferred",
	"esri/geometry/Point",
    "esri/geometry/Polygon",
    "esri/SpatialReference",
	"esri/geometry/mathUtils"],
	function (declare, array, Deferred, Point, Polygon, SpatialReference, mathUtils) {
		"use strict";

		return declare(null, {
		    constructor : function(options) {
			    this._maxFeature = options.maxFeatures || 10;
			    this._mode = options.mode || "planar"; // planar or geodesic
		    },

		    execute : function(params) {
			    var deferred = new Deferred();

			    try {
				    deferred.resolve(this._getNearestResult(params.point, params.featureSet));
			    } catch (err) {
				    deferred.reject(err);
			    }

			    return deferred.promise;
		    },

		    _getNearestResult : function(point, featureSet) {
		        // summary:
		        //		Calculates distances for features arounda point

		        var features = featureSet.features,
				    distance = this._mode === "geodesic" ? this._greatCircleDistance : this._euclidianDistance,
				    candidates = [], geometry, result, _this = this, polyCandidates = [], multiPCandidates = [];

			    // Supported in 10.1 or greater
			    if (featureSet.exceededTransferLimit && featureSet.exceededTransferLimit === true) {
			        console.warn("Feature limit reached, the result may not be accurate.");
			        result = { "limitExceeded": true };
			        return result;
			    }

			    // Calculate distance for each feature
			    if (featureSet.geometryType === "esriGeometryPoint") {
				    array.forEach(features, function(feature) {
					    candidates.push({
						    point: feature.geometry,
						    feature: feature,
						    distance: _this._convertDistanceTo("m", distance.call(_this, point, feature.geometry))
					    });
				    });

				    result = this._getMin(candidates);
			    }
			    else if (featureSet.geometryType === "esriGeometryPolygon") {
			        polyCandidates = [];
				    array.forEach(features, function(feature) {
					    geometry = feature.geometry;
					    array.forEach(geometry.rings, function (ring) {
                            // This returns candidates that have a mathmatical distance from the geometry based on the coordinate system
					        polyCandidates.push(this._getNearest(point, feature, ring));
					    }, this);
				    }, this);

			        // The coordinate based distance must be converted into a standard distance measurement i.e. Km or Miles
				    array.forEach(polyCandidates, function (candidate) {
				        candidate.distance = _this._convertDistanceTo("m", distance.call(_this, point, candidate.point));
				        // Once the final distance has been calculated check if the Point is inside the Polygon
				        var polyToCheck = new Polygon(candidate.feature.geometry);
				        if (polyToCheck.contains(point)) {
				            candidate.distance = 0;
				        }

				        candidates.push(candidate);
				    });

				    result = this._getMin(candidates);
			    }
			    else if (featureSet.geometryType === "esriGeometryPolyline") {
			        polyCandidates = [];
				    array.forEach(features, function(feature) {
					    geometry = feature.geometry;
					    array.forEach(geometry.paths, function(path) {
					        polyCandidates.push(this._getNearest(point, feature, path));
					    }, this);
				    }, this);

			        // The coordinate based distance must be converted into a standard distance measurement i.e. Km or Miles
				    array.forEach(polyCandidates, function (candidate) {
				        candidate.distance = _this._convertDistanceTo("m", distance.call(_this, point, candidate.point));
				        candidates.push(candidate);
				    });

				    result = this._getMin(candidates);
			    }
			    else if (featureSet.geometryType === "esriGeometryMultipoint") {
			        multiPCandidates = [];
			        array.forEach(features, function(feature) {
			            geometry = feature.geometry;

                        // If the geometery is Multipoint but only has a single point in it
			            if (geometry.points.length === 1) {
			                multiPCandidates.push({
			                    point: geometry.points[0],
			                    feature: feature,
			                    distance: _this._convertDistanceTo("m", distance.call(_this, point, new Point(geometry.points[0][0], geometry.points[0][1], new SpatialReference(4326))))
			                });
			            }
			            else {
			                multiPCandidates.push(this._getNearest(point, feature, geometry.points));
			            }
			        }, this);

		            // The coordinate based distance must be converted into a standard distance measurement i.e. Km or Miles
			        array.forEach(multiPCandidates, function (candidate) {
			            if (candidate.feature.geometry.points.length > 1) {
			                candidate.distance = _this._convertDistanceTo("m", distance.call(_this, point, candidate.point));
			            }
			            candidates.push(candidate);
			        });

			        result = this._getMin(candidates);
			    }
			    else {
				    // Not supported
				    result = [];
			    }

			    return result;
		    },

		    _getMin : function(features) {
			    var comparator = this._comparator;

			    // Sort the features
			    features.sort(comparator);

			    // Return the first 'n' features
			    return features.slice(0, this._maxFeature);
		    },

		    _getNearest: function (/*esri/geometry/Point*/point, /*esri/geometry/Geometry*/parentFeature, /*Number[]*/path) {
		        // summary:
		        //		Finds the nearest feature around a point

		        var minDistance = null, from, to, x, y, dx, dy, i, a, b, distance, n, len,
				    pathPoints, fromPoint, toPoint, length2, toEnd2, toStart2, distance2, calcLength2;

			    function square(num) {
				    // Faster than Math.pow to square a number
				    return num * num;
			    }

			    // Convert path to pathPoints
			    pathPoints = array.map(path, function (item) {
				    return { "x": item[0], "y": item[1] };
			    });

			    if (pathPoints.length > 1) {
				    for (n = 1, len = pathPoints.length; n < len; n++) {
					    // Get segment from points
					    fromPoint = pathPoints[n - 1];
					    toPoint = pathPoints[n];

					    if (toPoint.x !== fromPoint.x) {
						    a = (toPoint.y - fromPoint.y) / (toPoint.x - fromPoint.x);
						    b = toPoint.y - a * toPoint.x;
						    distance = Math.abs(a * point.x + b - point.y) / Math.sqrt(a * a + 1);
					    } else {
						    distance = Math.abs(point.x - toPoint.x);
					    }

					    // Length squared of the line segment
					    length2 = square(toPoint.y - fromPoint.y) + square(toPoint.x - fromPoint.x);
					    // Distance squared of point to the start of the line segment
					    toStart2 = square(fromPoint.y - point.y) + square(fromPoint.x - point.x);
					    // Distance squared of point to end of the line segment
					    toEnd2 = square(toPoint.y - point.y) + square(toPoint.x - point.x);
					    // Minimum distance squared of the point to the infinite line
					    distance2 = square(distance);
					    // Calculated length squared of the line segment
					    calcLength2 = toEnd2 - distance2 + toStart2 - distance2;

					    // Redefine minimum distance to line segment (not infinite line) if necessary
					    if (calcLength2 > length2) {
						    distance = Math.sqrt(Math.min(toEnd2, toStart2));
					    }

					    if ((minDistance === null) || (minDistance > distance)) {
						    if (calcLength2 > length2) {
							    if (toStart2 < toEnd2) {
								    to = 0; // Nearer to previous point
								    from = 1;
							    } else {
								    from = 0; // Nearer to current point
								    to = 1;
							    }
						    } else {
							    // perpendicular from point intersects line segment
							    to = ((Math.sqrt(toStart2 - distance2)) / Math.sqrt(length2));
							    from = ((Math.sqrt(toEnd2 - distance2)) / Math.sqrt(length2));
						    }

						    minDistance = distance;
						    i = n;
					    }
				    }

				    // Calculate coordinates
				    dx = pathPoints[i - 1].x - pathPoints[i].x;
				    dy = pathPoints[i - 1].y - pathPoints[i].y;
				    x = pathPoints[i - 1].x - (dx * to);
				    y = pathPoints[i - 1].y - (dy * to);
			    }

			    // Return feature
			    return {
				    point: new Point(x, y, point.spatialReference),
				    feature: parentFeature,
				    distance: minDistance
			    };
		    },

		    _euclidianDistance : function(p1, p2) {
			    return mathUtils.getLength(p1, p2);
		    },

		    _degToRad: function(deg) {
		        // summary:
		        //		Converts degrees to radians
		        return (deg * Math.PI) / 180;
	        },

		    _greatCircleDistance: function (p1, p2) {
		        // summary:
		        //		Returns the distance between two points in Km

			    // Haversine formula (http://www.movable-type.co.uk/scripts/latlong.html)
			    var toRad = this._toRad,
				    radius = 6378.1, // Earth's mean radius in km
				    lat1 = toRad.call(this, p1.y),
				    lon1 = toRad.call(this, p1.x),
				    lat2 = toRad.call(this, p2.y),
				    lon2 = toRad.call(this, p2.x),
				    dLat = lat2 - lat1,
				    dLon = lon2 - lon1,
				    a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2),
				    c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

			    return radius * c;
			    // Length in km
		    },

		    _toRad: function (number) {
		        // summary:
		        //		Converts a number to radians
		        return this._degToRad(number);
		    },

		    _convertDistanceTo: function (distanceUnits, distance) {
		        // summary:
		        //		Converts a distance from one unit type to another

		        var convertedDistance = null;

		        switch (distanceUnits) {
		            case "m":
		                convertedDistance = distance * 0.621371;
		                break;

		            default:
		                convertedDistance = distance;
		                break;
		        }

		        return convertedDistance;
		    },

		    _comparator: function (a, b) {
		        // summary:
		        //		Compares two numbers
			    if (a.distance < b.distance) {
				    return -1;
			    }

			    if (a.distance > b.distance) {
				    return 1;
			    }

			    return 0;
		    }
	});
});
<|endoftext|>


<commit_before>#!/usr/bin/env node
'use strict';
var fs = require('fs');
var objectAssign = require('object-assign');
var parseArgs = require('minimist');
var getStdin = require('get-stdin');
var errors = require('./messages').errors;
var message = require('./messages');
var messages = require('./messages').messages;
var summary = require('./messages').summary;
var prettyprint = require('./messages').prettyPrintEntry;
var pkg = require('./package.json');
var eachAsync = require('each-async');
var Promise = require('pinkie-promise');
var duration = require('pendel');
var multiline = require('multiline');
var inquirer = require('inquirer');

var options = parseArgs(process.argv.slice(2));

var args = options._;

delete options._;

/**
 * The timecard object used by the cli.
 *
 * @note: This value is assigned in `init()`
 */

var cliTimecard;

/**
 * Initialize a new TimeCard
 *
 * @param {Object} options
 * @constructor
 */

function TimeCard(options) {
	if (!(this instanceof TimeCard)) {
		return new TimeCard(options);
	}

	this.options = objectAssign({}, options);

	this.filepath = this.options.filepath || process.cwd() + '/.timecard.json';
	this.hours = [];
	this.clockoutIsPending = false;
	this.pendingClockoutIndex = null;
	this.totalSeconds = 0;
}

/**
 * Clockin.
 *
 * @param {function} cb
 */

TimeCard.prototype.clockin = function (cb) {
	cb = cb || function () {};

	var self = this;

	var TC = getTimeCardData(this.filepath).then(function (timeCardData) {
		return self.processTimeCardData(timeCardData);
	}).then(function (timeCardData) {
		if (self.clockoutIsPending) {
			cb(new Error(errors.clockOutIsPending));
			return;
		}
		var date = new Date().toString();

		var tc = {};
		tc.date = date.slice(0, 15);
		tc.startTime = date.slice(16, 24);
		tc.id = self.hours.length;

		timeCardData.push(tc);

		self.writeTimeCard(JSON.stringify(timeCardData), reportSuccessfulClockIn);
	});
	TC.catch(function (err) {
		if (err) {
			cb(err);
		}
	});
};

/**
 * Clockout.
 *
 * @param {function} cb - the callback function
 */

TimeCard.prototype.clockout = function (cb) {
	cb = cb || function () {};

	var self = this;

	var TC = getTimeCardData(this.filepath).then(function (timeCardData) {
		return self.processTimeCardData(timeCardData);
	}).then(function () {
		if (self.clockoutIsPending) {
			var date = new Date().toString();

			self.hours[self.pendingClockoutIndex].endTime = date.slice(16, 24);
			self.writeTimeCard(JSON.stringify(self.hours), reportSuccessfulClockOut);
		} else {
			cb(new Error(errors.noClockInFound));
		}
	});
	TC.catch(function (err) {
		if (err) {
			cb(err);
		}
	});
};

/**
 * Create a blank timecard on `timecard new`
 *
 */

TimeCard.prototype.createBlankTimeCard = function () {
	var self = this;

	// First check to see if the file already exists
	fs.stat(self.filepath, function (err, stats) {
		if (err && err.code === 'ENOENT') {
			self.writeTimeCard('[]', reportSuccessfulNewTimeCard);
		} else if (err) {
			throw err;
		}
		if (stats && stats.isFile()) {
			var question = [{
				type: 'confirm',
				name: 'eraseCard',
				message: 'A timecard file already exists. Do you want to erase it and start over?',
				default: false
			}];

			inquirer.prompt(question, function (answer) {
				if (answer.eraseCard === true) {
					self.writeTimeCard('[]', reportSuccessfulNewTimeCard);
				} else {
					process.exit();
				}
			});
		}
	});
};

/**
 * Get the hour array recorded by Timecard.
 *
 */

TimeCard.prototype.hours = function () {
	return this.hours;
};

/**
 * Record the times and look for conditions on the timecard data.
 *
 * @param timeCardData
 * @returns {*} Promise
 */

TimeCard.prototype.processTimeCardData = function (timeCardData) {
	var self = this;

	return new Promise(function (resolve, reject) {
		eachAsync(timeCardData, function (item, index, next) {
			if (item.hasOwnProperty('startTime') && !item.hasOwnProperty('endTime')) {
				self.clockoutIsPending = true;
				self.pendingClockoutIndex = index;
			}

			// get the number of minutes for all completed timecard sessions
			if (item.hasOwnProperty('startTime') && item.hasOwnProperty('endTime')) {
				self.totalSeconds += duration(item.startTime, item.endTime).totalSeconds;
			}

			self.hours.push(item);

			next();
		}, function (error) {
			if (error) {
				reject(error);
			}
			resolve(timeCardData);
		});
	});
};

/**
 * Print the timecard data to the console.
 *
 * @param {function} cb - the callback
 */

TimeCard.prototype.print = function (cb) {
	cb = cb || function () {};

	var self = this;

	var TC = getTimeCardData(this.filepath).then(function (timeCardData) {
		return self.processTimeCardData(timeCardData);
	}).then(function (timeCardData) {
		console.log(messages.prettyPrintHeader);

		eachAsync(timeCardData, function (item, index, next) {
			prettyprint(item);

			next();
		}, function (error) {
			if (error) {
				throw error;
			}

			console.log(messages.prettyPrintBorder);
			summary(self.totalSeconds);

			console.log();
		});
	});
	TC.catch(function (err) {
		if (err) {
			cb(err);
		}
	});
};

/**
 * Read the json file with the timecard data.
 *
 * @param filepath - the filepath to search for the timecard data
 * @returns {*} Promise
 */

function getTimeCardData(filepath) {
	return new Promise(function (resolve, reject) {
		fs.readFile(filepath, 'utf8', function (err, data) {
			if (err) {
				reject(err);
			} else {
				resolve(JSON.parse(data));
			}
		});
	});
}

/**
 * Physically write the timecard data to the json file.
 *
 * @param {object} data - the json data to write
 * @param {function} cb - the callback
 */

TimeCard.prototype.writeTimeCard = function (data, cb) {
	fs.writeFile(this.filepath, data, function (err) {
		if (err) {
			throw err;
		}
		cb();
	});
};

/**
 * Pretty-print a successful 'timecard new' message.
 *
 * @note: This gets called after a new timecard
 * has been created with `timecard new`
 */

function reportSuccessfulNewTimeCard() {
	message.print(cliTimecard.filepath);
	console.log(messages.createdNewTimeCard);
}

/**
 * Pretty-print a successful clockin message.
 *
 */

function reportSuccessfulClockIn() {
	console.log(messages.successfulClockin);
}

/**
 * Pretty-print a successful clockout message.
 *
 */

function reportSuccessfulClockOut() {
	console.log(messages.successfulClockout);
}

/**
 * Timecard's point of entry.
 *
 * @note: This function processes the arguments and options passed in
 * from the command line and determines what the app should do next.
 *
 * @param {Array} args - cli args
 * @param {Object} options - cli options
 */

function init(args, options) {
	if (options.version || options.v) {
		console.log(pkg.version);
		process.exit();
	}

	if (options.help || options.h) {
		showHelp();
		process.exit();
	}

	if (args.length === 0) {
		showHelp();
		process.exit(1);
	}

	cliTimecard = new TimeCard();

	if (args.indexOf('new') > -1) {
		cliTimecard.createBlankTimeCard();
	} else if (args.indexOf('clockin') > -1) {
		cliTimecard.clockin(function (err) {
			if (err) {
				console.log(err.message);
			}
		});
	} else if (args.indexOf('clockout') > -1) {
		cliTimecard.clockout(function (err) {
			if (err) {
				console.log(err.message);
			}
		});
	} else if (args.indexOf('print') > -1) {
		cliTimecard.print(function (err) {
			if (err) {
				console.log(err.message);
			}
		});
	}
}

/**
 * Pretty-print Timecard's help information with `timecard --help`
 *
 */

function showHelp() {
	console.log(multiline(function () {
/*
    Record your project development time.

    Get timecard setup with the `new` command, then use the `clockin` and `clockout` commands
    to record your time. When you want to see a summary of your time, use the `print` command.

    Commands
    timecard new            setup a blank timecard for your project
    timecard clockin        set the start time
    timecard clockout       set the end time
    timecard print          print a summary of your time

    Options
    -h, --help              Show this help message
    -v, --version           Show the current timecard version
*/
	}));
}

/**
 * Collect the arguments and options from the command line.
 *
 */

if (process.stdin.isTTY) {
	init(args, options);
} else {
	getStdin(function (data) {
		[].push.apply(args, data.trim().split(/\r?\n/));
		init(args, options);
	});
}
<commit_msg>.timecard.json formatting<commit_after>#!/usr/bin/env node
'use strict';
var fs = require('fs');
var objectAssign = require('object-assign');
var parseArgs = require('minimist');
var getStdin = require('get-stdin');
var errors = require('./messages').errors;
var message = require('./messages');
var messages = require('./messages').messages;
var summary = require('./messages').summary;
var prettyprint = require('./messages').prettyPrintEntry;
var pkg = require('./package.json');
var eachAsync = require('each-async');
var Promise = require('pinkie-promise');
var duration = require('pendel');
var multiline = require('multiline');
var inquirer = require('inquirer');

var options = parseArgs(process.argv.slice(2));

var args = options._;

delete options._;

/**
 * The timecard object used by the cli.
 *
 * @note: This value is assigned in `init()`
 */

var cliTimecard;

/**
 * Initialize a new TimeCard
 *
 * @param {Object} options
 * @constructor
 */

function TimeCard(options) {
	if (!(this instanceof TimeCard)) {
		return new TimeCard(options);
	}

	this.options = objectAssign({}, options);

	this.filepath = this.options.filepath || process.cwd() + '/.timecard.json';
	this.hours = [];
	this.clockoutIsPending = false;
	this.pendingClockoutIndex = null;
	this.totalSeconds = 0;
}

/**
 * Clockin.
 *
 * @param {function} cb
 */

TimeCard.prototype.clockin = function (cb) {
	cb = cb || function () {};

	var self = this;

	var TC = getTimeCardData(this.filepath).then(function (timeCardData) {
		return self.processTimeCardData(timeCardData);
	}).then(function (timeCardData) {
		if (self.clockoutIsPending) {
			cb(new Error(errors.clockOutIsPending));
			return;
		}
		var date = new Date().toString();

		var tc = {};
		tc.id = self.hours.length;
		tc.date = date.slice(0, 15);
		tc.startTime = date.slice(16, 24);

		timeCardData.push(tc);

		self.writeTimeCard(JSON.stringify(timeCardData, null, 4), reportSuccessfulClockIn);
	});
	TC.catch(function (err) {
		if (err) {
			cb(err);
		}
	});
};

/**
 * Clockout.
 *
 * @param {function} cb - the callback function
 */

TimeCard.prototype.clockout = function (cb) {
	cb = cb || function () {};

	var self = this;

	var TC = getTimeCardData(this.filepath).then(function (timeCardData) {
		return self.processTimeCardData(timeCardData);
	}).then(function () {
		if (self.clockoutIsPending) {
			var date = new Date().toString();

			self.hours[self.pendingClockoutIndex].endTime = date.slice(16, 24);
			self.writeTimeCard(JSON.stringify(self.hours, null, 4), reportSuccessfulClockOut);
		} else {
			cb(new Error(errors.noClockInFound));
		}
	});
	TC.catch(function (err) {
		if (err) {
			cb(err);
		}
	});
};

/**
 * Create a blank timecard on `timecard new`
 *
 */

TimeCard.prototype.createBlankTimeCard = function () {
	var self = this;

	// First check to see if the file already exists
	fs.stat(self.filepath, function (err, stats) {
		if (err && err.code === 'ENOENT') {
			self.writeTimeCard('[]', reportSuccessfulNewTimeCard);
		} else if (err) {
			throw err;
		}
		if (stats && stats.isFile()) {
			var question = [{
				type: 'confirm',
				name: 'eraseCard',
				message: 'A timecard file already exists. Do you want to erase it and start over?',
				default: false
			}];

			inquirer.prompt(question, function (answer) {
				if (answer.eraseCard === true) {
					self.writeTimeCard('[]', reportSuccessfulNewTimeCard);
				} else {
					process.exit();
				}
			});
		}
	});
};

/**
 * Get the hour array recorded by Timecard.
 *
 */

TimeCard.prototype.hours = function () {
	return this.hours;
};

/**
 * Record the times and look for conditions on the timecard data.
 *
 * @param timeCardData
 * @returns {*} Promise
 */

TimeCard.prototype.processTimeCardData = function (timeCardData) {
	var self = this;

	return new Promise(function (resolve, reject) {
		eachAsync(timeCardData, function (item, index, next) {
			if (item.hasOwnProperty('startTime') && !item.hasOwnProperty('endTime')) {
				self.clockoutIsPending = true;
				self.pendingClockoutIndex = index;
			}

			// get the number of minutes for all completed timecard sessions
			if (item.hasOwnProperty('startTime') && item.hasOwnProperty('endTime')) {
				self.totalSeconds += duration(item.startTime, item.endTime).totalSeconds;
			}

			self.hours.push(item);

			next();
		}, function (error) {
			if (error) {
				reject(error);
			}
			resolve(timeCardData);
		});
	});
};

/**
 * Print the timecard data to the console.
 *
 * @param {function} cb - the callback
 */

TimeCard.prototype.print = function (cb) {
	cb = cb || function () {};

	var self = this;

	var TC = getTimeCardData(this.filepath).then(function (timeCardData) {
		return self.processTimeCardData(timeCardData);
	}).then(function (timeCardData) {
		console.log(messages.prettyPrintHeader);

		eachAsync(timeCardData, function (item, index, next) {
			prettyprint(item);

			next();
		}, function (error) {
			if (error) {
				throw error;
			}

			console.log(messages.prettyPrintBorder);
			summary(self.totalSeconds);

			console.log();
		});
	});
	TC.catch(function (err) {
		if (err) {
			cb(err);
		}
	});
};

/**
 * Read the json file with the timecard data.
 *
 * @param filepath - the filepath to search for the timecard data
 * @returns {*} Promise
 */

function getTimeCardData(filepath) {
	return new Promise(function (resolve, reject) {
		fs.readFile(filepath, 'utf8', function (err, data) {
			if (err) {
				reject(err);
			} else {
				resolve(JSON.parse(data));
			}
		});
	});
}

/**
 * Physically write the timecard data to the json file.
 *
 * @param {object} data - the json data to write
 * @param {function} cb - the callback
 */

TimeCard.prototype.writeTimeCard = function (data, cb) {
	fs.writeFile(this.filepath, data, function (err) {
		if (err) {
			throw err;
		}
		cb();
	});
};

/**
 * Pretty-print a successful 'timecard new' message.
 *
 * @note: This gets called after a new timecard
 * has been created with `timecard new`
 */

function reportSuccessfulNewTimeCard() {
	message.print(cliTimecard.filepath);
	console.log(messages.createdNewTimeCard);
}

/**
 * Pretty-print a successful clockin message.
 *
 */

function reportSuccessfulClockIn() {
	console.log(messages.successfulClockin);
}

/**
 * Pretty-print a successful clockout message.
 *
 */

function reportSuccessfulClockOut() {
	console.log(messages.successfulClockout);
}

/**
 * Timecard's point of entry.
 *
 * @note: This function processes the arguments and options passed in
 * from the command line and determines what the app should do next.
 *
 * @param {Array} args - cli args
 * @param {Object} options - cli options
 */

function init(args, options) {
	if (options.version || options.v) {
		console.log(pkg.version);
		process.exit();
	}

	if (options.help || options.h) {
		showHelp();
		process.exit();
	}

	if (args.length === 0) {
		showHelp();
		process.exit(1);
	}

	cliTimecard = new TimeCard();

	if (args.indexOf('new') > -1) {
		cliTimecard.createBlankTimeCard();
	} else if (args.indexOf('clockin') > -1) {
		cliTimecard.clockin(function (err) {
			if (err) {
				console.log(err.message);
			}
		});
	} else if (args.indexOf('clockout') > -1) {
		cliTimecard.clockout(function (err) {
			if (err) {
				console.log(err.message);
			}
		});
	} else if (args.indexOf('print') > -1) {
		cliTimecard.print(function (err) {
			if (err) {
				console.log(err.message);
			}
		});
	}
}

/**
 * Pretty-print Timecard's help information with `timecard --help`
 *
 */

function showHelp() {
	console.log(multiline(function () {
/*
    Record your project development time.

    Get timecard setup with the `new` command, then use the `clockin` and `clockout` commands
    to record your time. When you want to see a summary of your time, use the `print` command.

    Commands
    timecard new            setup a blank timecard for your project
    timecard clockin        set the start time
    timecard clockout       set the end time
    timecard print          print a summary of your time

    Options
    -h, --help              Show this help message
    -v, --version           Show the current timecard version
*/
	}));
}

/**
 * Collect the arguments and options from the command line.
 *
 */

if (process.stdin.isTTY) {
	init(args, options);
} else {
	getStdin(function (data) {
		[].push.apply(args, data.trim().split(/\r?\n/));
		init(args, options);
	});
}
<|endoftext|>