<commit_before>(function(root) {

    var hyve = typeof exports != 'undefined'?  exports : root.hyve = { }
    var get = typeof require == 'function' && require('request')

    // ECMA-262 compatible Array#forEach polyfills
    Array.prototype.forEach = Array.prototype.forEach || function(fn, ctx) {
        var len = this.length >>> 0
        for (var i = 0; i < len; ++i){
            if (i in this){
                fn.call(ctx, this[i], i, this)
            }
        }
    }

    //ECMA-262 standard indexOf from Mozilla Developer Network
    if (!Array.prototype.indexOf) {
        Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) {
            "use strict";
            if (this === null) {
                throw new TypeError();
            }
            var t = Object(this);
            var len = t.length >>> 0;
            if (len === 0) {
                return -1;
            }
            var n = 0;
            if (arguments.length > 0) {
                n = Number(arguments[1]);
                if (isNaN(n)) { // shortcut for verifying if it's NaN
                    n = 0;
                } else if (n !== 0 && n !== Infinity && n !== -Infinity) {
                    n = (n > 0 || -1) * Math.floor(Math.abs(n));
                }
            }
            if (n >= len) {
                return -1;
            }
            var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
            for (; k < len; k++) {
                if (k in t && t[k] === searchElement) {
                    return k;
                }
            }
            return -1;
        }
    }

    // Converts an object to an array
    function oc(a){
       var obj = {}
       for(var i=0;i<a.length;i++){
            obj[a[i]]=''
       }
       return obj
    }

    // Fills a template with data from an object
    function format(string, data) {
        "use strict"
        return string.replace(
            /\{\{(?:#(.+?)#)?\s*(.+?)\s*\}\}/g,
            function(m, cond, id) {
                var rv = data[id]
                if (rv === false){
                    return ''
                } else {
                    return rv? (cond || '') + rv : cond? m : ''
                }
            }
       )
    }

    // Pulls data from several streams and handles all with given callback
    function stream(query, callback, custom_services) {
        callback = callback || function(){}
        method = hyve.method

        // use services that contain proper method
        services = []
        check_services = custom_services || Object.keys(hyve.feeds)
        check_services.forEach(function(service){
            if (method in oc(hyve.feeds[service.toLowerCase()].methods)){
               services.push(service.toLowerCase())
           }
        })

        if (services.length === 0) throw "cannot stream; services is empty"

        services.forEach(function(service){
            // set the orig_url to the services feed_url for this method
            if (!hyve.feeds[service].orig_url){
                hyve.feeds[service].orig_url = hyve.feeds[service].feed_urls[method]
            }

            var options = hyve.feeds[service]

            if (options.token_update){
                options.token_update()
                options.token_update_lock = setInterval(function(){
                    options.token_update
                },options.token_timeout)
            }

            var runFetch = function(){
                var feed_url

                if (options.format_url){
                    feed_url = format( options.feed_urls[method]
                                    , hyve.feeds[service].format_url(query)
                                    )
                } else {
                    feed_url = format( options.feed_urls[method]
                                    ,{ query:  query
                                    ,  url_suffix: options.url_suffix
                                    ,  result_type: options.result_type
                                    ,  access_token: options.access_token
                                    ,  api_key: options.api_key
                                    ,  auth_user: options.auth_user
                                    ,  auth_signature: options.auth_signature
                                    })
                }

                if (hyve.feeds[service].fetch_url){
                    hyve.feeds[service].fetch_url(service, query, callback)
                } else if (hyve.feeds[service].oauth_version == '1.0'
                            && hyve.method == 'friends') {
                    feed_url = feed_url.substr(0,feed_url.indexOf("?"));
                    hyve.fetch_proxy(feed_url,service)
                } else {
                    fetch(feed_url, service, query, callback)
                }
            }
            runFetch()

            var interval = options.interval
            if (hyve.method == 'friends' && options.interval_friends){
                if (options.interval_friends){
                    interval = options.interval_friends
                }
            }

            hyve.feeds[service].lock = setInterval(function(){
                runFetch()
            }, interval)
        })
    }

    // specific external wrappers for search/stream functionality
    var friends = {
        stream: function(callback, custom_services) {
            hyve.method = 'friends'
            return stream('', callback, custom_services)
        }
    }
    var search = {
        stream: function(query, callback, custom_services) {
            hyve.method = 'search'
            return stream(query, callback, custom_services)
        },
        popular: function(query, callback, custom_services) {
            hyve.method = 'popular'
            return stream(query, callback, custom_services)
        }
    }

    // Stops any running streams for given services
    function stop(custom_services) {
        var services
        services = custom_services || Object.keys(hyve.feeds)
        services.forEach(function(service){
            if (hyve.feeds[service].lock) {
                if (hyve.feeds[service].orig_url){
                    hyve.feeds[service].feed_url = hyve.feeds[service].orig_url
                }
                clearInterval(hyve.feeds[service].lock)
            }
            if (hyve.feeds[service].token_update_lock){
                clearInterval(hyve.feeds[service].token_update_lock)
            }
        })
    }

    // Gives some feeds the chance to claim an item as its own, then returns
    // list of claimed/reformatted items, or the unaltered original
    function claim(item,callback){
        var new_items = []
        var services = Object.keys(hyve.feeds)
        item.links.forEach(function(link){
            if (!hyve.links[link]){
                hyve.links[link] = true
                services.forEach(function(service){
                    if (hyve.feeds[service].claim){
                        var new_item = hyve.feeds[service].claim( link
                                                                , item
                                                                , callback
                                                                )
                        if (new_item){
                            new_items.push(new_item)
                        }
                    }
                })
                if (link.search(/.jpg|.png|.gif/i) != -1){
                    var new_item = item
                    new_item.links = []
                    new_item.type = 'image'
                    if (!new_item.source_img){
                        new_item.source_img = link
                    }
                    if (!new_item.thumbnail){
                        new_item.thumbnail = link
                    }
                    new_items.push(new_item)
                }
            } else {
                new_items.push(item)
            }
        })
        if (new_items.length > 0){
            return new_items
        } else {
            return false
        }
    }

    // Place an item in an appropriate queue depending on its defined 'type'
    function enqueue(item){
        if(item){
            hyve.queue[item.type].sort(function(a,b){
                return b['date'] - a['date']
            })
            if (hyve.recall_enable === true){
                var check_id_key = item.service+':'+item.query+':'+item.id
                if (localStorage.getItem(check_id_key) != 'true'){
                    hyve.queue[item.type].unshift(item)
                    store(item)
                }
            } else {
                hyve.queue[item.type].unshift(item)
            }
        } else {
            throw('enqueue: an undefined item was inputted')
        }
    }

    // Removes an item from hyves queue
    function dequeue(item) {
        if (item) {
            idx = hyve.queue[item.type].indexOf(item)
            // use splice instead of delete as delete
            // leaves undefined element in array
            if (idx != -1) hyve.queue[item.type].splice(idx, 1)
        } else {
            throw('dequeue: an undefined item was inputted')
        }
    }

    // Persistantly stores an item in the browser via localStorage
    function store(item){
        var items_key = item.type+':'+item.query
        var items = localStorage.getItem(items_key)
        if (items){
            items = JSON.parse(items)
        } else {
            items = []
        }
        var check_id_key = item.service+':'+item.query+':'+item.id
        if (localStorage.getItem(check_id_key) != 'true'){
            items.unshift(item)
            localStorage.setItem(check_id_key,true)
        }
        trunc_items = items.splice(0,200)
        try {
            localStorage.setItem(items_key,JSON.stringify(trunc_items))
        } catch(e) {
            console.error('store: localStorage quota exceeded. Emptying', e)
            localStorage.clear()
        }
    }

    // Recall previously saved items from localStorage
    function recall(type,query){
        var itemskey = type+':'+query
        var items = JSON.parse(localStorage.getItem(itemskey)) || []
        items.sort(function(a,b){
            return b['date'] - a['date']
        })
        return items
    }

    // Reset queue and refill it with any previously stored data if any exists
    function replenish(query,types){
        if (hyve.recall_enable === true){
            hyve.queue = { 'text':[]
                         , 'link':[]
                         , 'video':[]
                         , 'image':[]
                         , 'checkin':[]
            }
            types = types || Object.keys(hyve.queue)
            types.forEach(function(type){
                hyve.queue[type] = recall(type,query)
            })
        }
    }

    // similiar to java's hashCode function (32bit)
    function string_hash(s) {
        var hash = 0
        if (s.length === 0) {
            return hash
        }
        for (i = 0 ; i < s.length ; i ++ ) {
            chr  = s.charCodeAt(i)
            hash = ((hash << 5) - hash) + chr
            hash = hash & hash
        }
        return hash
    }

    function processable(item) {
        if (item.text) {
            var hash = string_hash(item.text)

            if (hash) {
                if (hyve.items_seen.indexOf(hash) > -1) {
                    return false
                } else {
                    // if list length limit is reached pop the last item and push to the top
                    if (hyve.items_seen.length > hyve.items_seen_size) {
                        hyve.items_seen.shift()
                    }
                    hyve.items_seen.push(hash)

                    // if hash isn't seen process item
                    return true
                }
            }
        }
        // if no hash do not process
        return false
    }

    // Manually re-classify items as needed, check for dupes, send to callback
    function process(item, callback){
        if (item.date != parseInt(item.date,10)){
            var date_obj = new Date(item.date)
            item.date = date_obj.getTime()/1000
        }

        items = [item];
        item.links = item.links || []
        if (item.links.length > 0) {
            items = claim(item,callback)
        }
        if (items){
            items.forEach(function(item){
                // check if item is processable, i.e not a dupe
                if (processable(item)) {
                    if (hyve.queue_enable){
                        enqueue(item)
                    }
                    try {
                        callback(item)
                    } catch(e) {
                        console.error('process:', e.message, item.service, item.id, item)
                    }
                }
            })
        }
    }

    // Fetches a JSON stream
    var fetch = function() {
        var counter   = 0
        var callbacks = { }
        var head      = !get && document.getElementsByTagName('head')[0]

        // Returns a qualified identifier pointing to a callback
        function get_callback() {
            return format('hyve.callbacks.f{{id}}', { id: ++counter })
        }

        // Cleanup script leftovers from DOM
        function cleanup(script){
            head.removeChild(script)
            for (var property in script){
                delete script[property]
            }
        }

        // Requires a URI using JSONP
        function jsonp(url, callback) {
            var s = document.createElement('script')
            s.type = 'text/javascript'
            s.async = true
            s.src = url
            var wrap_callback = function(){
                cleanup(s)
                return callback.apply(this,arguments)
            }
            hyve.callbacks['f' + counter] = wrap_callback
            var x = document.getElementsByTagName('script')[0]
            x.parentNode.insertBefore(s,x)
        }

        // Requires a URI using the Node.js request library
        function request(url, callback) {
            get(url, function(error, res, data) {
                try {
                    callback(JSON.parse(data))
                } catch(e){
                    console.error('request: fetch failed - ',url, e)
                    callback({ }, e)
                }
            })
        }

        // Abstracts fetching URIs.
        function fetch(url, service, query, callback, item) {
            var fn = pass(service, query, callback, item)
            var cb = !get && get_callback()
            url    = format(url, { callback: cb })
            var fetcher = get? request : jsonp
            script = fetcher(url, fn)
        }

        // Higher-order function to process the fetched data
        function pass(service, query, callback, item) {
            return function(data) {
                // if service supports multiple parsers use that or fallback to
                // parse
                if (hyve.feeds[service].parsers) {
                    if (hyve.method in hyve.feeds[service].parsers) {
                        hyve.feeds[service].parsers[hyve.method](data, query, callback, item)
                    }
                } else {
                    hyve.feeds[service].parse(data, query, callback, item)
                }
            }
        }

        // Export the `fetch` function
        return fetch
    }()

    // A default echo function used when no real request proxy function exists
    function fetch_proxy(feed_url,service){
        console.log('hyve_request_proxy',feed_url,service)
    }

    // Exports data to the outside world
    hyve.friends = friends
    hyve.search = search
    hyve.method = '' // set by the calling stream
    hyve.stop = stop
    hyve.process = process
    hyve.format = format
    hyve.fetch = fetch
    hyve.fetch_proxy = fetch_proxy // override with your own proxy function
    hyve.recall = recall
    hyve.recall_enable = false
    hyve.replenish = replenish
    hyve.queue = {'text':[],'link':[],'video':[],'image':[],'checkin':[]}
    hyve.queue_enable = false; // enables queuing; no queue by default
    hyve.dequeue = dequeue
    hyve.items_seen = []
    hyve.items_seen_size = 5000 // length of buffer before rolling begins
    hyve.callbacks = []
    hyve.links = {}
    hyve.feeds = {}

    // Export hyve for node/browser compatibilty
    if (typeof module !== 'undefined' && module.exports) {
        module.exports = hyve;
    } else {
        root.hyve = hyve;
    }

})(this)
<commit_msg>fix token_update setInterval<commit_after>(function(root) {

    var hyve = typeof exports != 'undefined'?  exports : root.hyve = { }
    var get = typeof require == 'function' && require('request')

    // ECMA-262 compatible Array#forEach polyfills
    Array.prototype.forEach = Array.prototype.forEach || function(fn, ctx) {
        var len = this.length >>> 0
        for (var i = 0; i < len; ++i){
            if (i in this){
                fn.call(ctx, this[i], i, this)
            }
        }
    }

    //ECMA-262 standard indexOf from Mozilla Developer Network
    if (!Array.prototype.indexOf) {
        Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) {
            "use strict";
            if (this === null) {
                throw new TypeError();
            }
            var t = Object(this);
            var len = t.length >>> 0;
            if (len === 0) {
                return -1;
            }
            var n = 0;
            if (arguments.length > 0) {
                n = Number(arguments[1]);
                if (isNaN(n)) { // shortcut for verifying if it's NaN
                    n = 0;
                } else if (n !== 0 && n !== Infinity && n !== -Infinity) {
                    n = (n > 0 || -1) * Math.floor(Math.abs(n));
                }
            }
            if (n >= len) {
                return -1;
            }
            var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
            for (; k < len; k++) {
                if (k in t && t[k] === searchElement) {
                    return k;
                }
            }
            return -1;
        }
    }

    // Converts an object to an array
    function oc(a){
       var obj = {}
       for(var i=0;i<a.length;i++){
            obj[a[i]]=''
       }
       return obj
    }

    // Fills a template with data from an object
    function format(string, data) {
        "use strict"
        return string.replace(
            /\{\{(?:#(.+?)#)?\s*(.+?)\s*\}\}/g,
            function(m, cond, id) {
                var rv = data[id]
                if (rv === false){
                    return ''
                } else {
                    return rv? (cond || '') + rv : cond? m : ''
                }
            }
       )
    }

    // Pulls data from several streams and handles all with given callback
    function stream(query, callback, custom_services) {
        callback = callback || function(){}
        method = hyve.method

        // use services that contain proper method
        services = []
        check_services = custom_services || Object.keys(hyve.feeds)
        check_services.forEach(function(service){
            if (method in oc(hyve.feeds[service.toLowerCase()].methods)){
               services.push(service.toLowerCase())
           }
        })

        if (services.length === 0) throw "cannot stream; services is empty"

        services.forEach(function(service){
            // set the orig_url to the services feed_url for this method
            if (!hyve.feeds[service].orig_url){
                hyve.feeds[service].orig_url = hyve.feeds[service].feed_urls[method]
            }

            var options = hyve.feeds[service]

            if (options.token_update){
                options.token_update()
                options.token_update_lock = setInterval( options.token_update
                                                       , options.token_timeout
                                                       )
            }

            var runFetch = function(){
                var feed_url

                if (options.format_url){
                    feed_url = format( options.feed_urls[method]
                                    , hyve.feeds[service].format_url(query)
                                    )
                } else {
                    feed_url = format( options.feed_urls[method]
                                    ,{ query:  query
                                    ,  url_suffix: options.url_suffix
                                    ,  result_type: options.result_type
                                    ,  access_token: options.access_token
                                    ,  api_key: options.api_key
                                    ,  auth_user: options.auth_user
                                    ,  auth_signature: options.auth_signature
                                    })
                }

                if (hyve.feeds[service].fetch_url){
                    hyve.feeds[service].fetch_url(service, query, callback)
                } else if (hyve.feeds[service].oauth_version == '1.0'
                            && hyve.method == 'friends') {
                    feed_url = feed_url.substr(0,feed_url.indexOf("?"));
                    hyve.fetch_proxy(feed_url,service)
                } else {
                    fetch(feed_url, service, query, callback)
                }
            }
            runFetch()

            var interval = options.interval
            if (hyve.method == 'friends' && options.interval_friends){
                if (options.interval_friends){
                    interval = options.interval_friends
                }
            }

            hyve.feeds[service].lock = setInterval(function(){
                runFetch()
            }, interval)
        })
    }

    // specific external wrappers for search/stream functionality
    var friends = {
        stream: function(callback, custom_services) {
            hyve.method = 'friends'
            return stream('', callback, custom_services)
        }
    }
    var search = {
        stream: function(query, callback, custom_services) {
            hyve.method = 'search'
            return stream(query, callback, custom_services)
        },
        popular: function(query, callback, custom_services) {
            hyve.method = 'popular'
            return stream(query, callback, custom_services)
        }
    }

    // Stops any running streams for given services
    function stop(custom_services) {
        var services
        services = custom_services || Object.keys(hyve.feeds)
        services.forEach(function(service){
            if (hyve.feeds[service].lock) {
                if (hyve.feeds[service].orig_url){
                    hyve.feeds[service].feed_url = hyve.feeds[service].orig_url
                }
                clearInterval(hyve.feeds[service].lock)
            }
            if (hyve.feeds[service].token_update_lock){
                clearInterval(hyve.feeds[service].token_update_lock)
            }
        })
    }

    // Gives some feeds the chance to claim an item as its own, then returns
    // list of claimed/reformatted items, or the unaltered original
    function claim(item,callback){
        var new_items = []
        var services = Object.keys(hyve.feeds)
        item.links.forEach(function(link){
            if (!hyve.links[link]){
                hyve.links[link] = true
                services.forEach(function(service){
                    if (hyve.feeds[service].claim){
                        var new_item = hyve.feeds[service].claim( link
                                                                , item
                                                                , callback
                                                                )
                        if (new_item){
                            new_items.push(new_item)
                        }
                    }
                })
                if (link.search(/.jpg|.png|.gif/i) != -1){
                    var new_item = item
                    new_item.links = []
                    new_item.type = 'image'
                    if (!new_item.source_img){
                        new_item.source_img = link
                    }
                    if (!new_item.thumbnail){
                        new_item.thumbnail = link
                    }
                    new_items.push(new_item)
                }
            } else {
                new_items.push(item)
            }
        })
        if (new_items.length > 0){
            return new_items
        } else {
            return false
        }
    }

    // Place an item in an appropriate queue depending on its defined 'type'
    function enqueue(item){
        if(item){
            hyve.queue[item.type].sort(function(a,b){
                return b['date'] - a['date']
            })
            if (hyve.recall_enable === true){
                var check_id_key = item.service+':'+item.query+':'+item.id
                if (localStorage.getItem(check_id_key) != 'true'){
                    hyve.queue[item.type].unshift(item)
                    store(item)
                }
            } else {
                hyve.queue[item.type].unshift(item)
            }
        } else {
            throw('enqueue: an undefined item was inputted')
        }
    }

    // Removes an item from hyves queue
    function dequeue(item) {
        if (item) {
            idx = hyve.queue[item.type].indexOf(item)
            // use splice instead of delete as delete
            // leaves undefined element in array
            if (idx != -1) hyve.queue[item.type].splice(idx, 1)
        } else {
            throw('dequeue: an undefined item was inputted')
        }
    }

    // Persistantly stores an item in the browser via localStorage
    function store(item){
        var items_key = item.type+':'+item.query
        var items = localStorage.getItem(items_key)
        if (items){
            items = JSON.parse(items)
        } else {
            items = []
        }
        var check_id_key = item.service+':'+item.query+':'+item.id
        if (localStorage.getItem(check_id_key) != 'true'){
            items.unshift(item)
            localStorage.setItem(check_id_key,true)
        }
        trunc_items = items.splice(0,200)
        try {
            localStorage.setItem(items_key,JSON.stringify(trunc_items))
        } catch(e) {
            console.error('store: localStorage quota exceeded. Emptying', e)
            localStorage.clear()
        }
    }

    // Recall previously saved items from localStorage
    function recall(type,query){
        var itemskey = type+':'+query
        var items = JSON.parse(localStorage.getItem(itemskey)) || []
        items.sort(function(a,b){
            return b['date'] - a['date']
        })
        return items
    }

    // Reset queue and refill it with any previously stored data if any exists
    function replenish(query,types){
        if (hyve.recall_enable === true){
            hyve.queue = { 'text':[]
                         , 'link':[]
                         , 'video':[]
                         , 'image':[]
                         , 'checkin':[]
            }
            types = types || Object.keys(hyve.queue)
            types.forEach(function(type){
                hyve.queue[type] = recall(type,query)
            })
        }
    }

    // similiar to java's hashCode function (32bit)
    function string_hash(s) {
        var hash = 0
        if (s.length === 0) {
            return hash
        }
        for (i = 0 ; i < s.length ; i ++ ) {
            chr  = s.charCodeAt(i)
            hash = ((hash << 5) - hash) + chr
            hash = hash & hash
        }
        return hash
    }

    function processable(item) {
        if (item.text) {
            var hash = string_hash(item.text)

            if (hash) {
                if (hyve.items_seen.indexOf(hash) > -1) {
                    return false
                } else {
                    // if list length limit is reached pop the last item and push to the top
                    if (hyve.items_seen.length > hyve.items_seen_size) {
                        hyve.items_seen.shift()
                    }
                    hyve.items_seen.push(hash)

                    // if hash isn't seen process item
                    return true
                }
            }
        }
        // if no hash do not process
        return false
    }

    // Manually re-classify items as needed, check for dupes, send to callback
    function process(item, callback){
        if (item.date != parseInt(item.date,10)){
            var date_obj = new Date(item.date)
            item.date = date_obj.getTime()/1000
        }

        items = [item];
        item.links = item.links || []
        if (item.links.length > 0) {
            items = claim(item,callback)
        }
        if (items){
            items.forEach(function(item){
                // check if item is processable, i.e not a dupe
                if (processable(item)) {
                    if (hyve.queue_enable){
                        enqueue(item)
                    }
                    try {
                        callback(item)
                    } catch(e) {
                        console.error('process:', e.message, item.service, item.id, item)
                    }
                }
            })
        }
    }

    // Fetches a JSON stream
    var fetch = function() {
        var counter   = 0
        var callbacks = { }
        var head      = !get && document.getElementsByTagName('head')[0]

        // Returns a qualified identifier pointing to a callback
        function get_callback() {
            return format('hyve.callbacks.f{{id}}', { id: ++counter })
        }

        // Cleanup script leftovers from DOM
        function cleanup(script){
            head.removeChild(script)
            for (var property in script){
                delete script[property]
            }
        }

        // Requires a URI using JSONP
        function jsonp(url, callback) {
            var s = document.createElement('script')
            s.type = 'text/javascript'
            s.async = true
            s.src = url
            var wrap_callback = function(){
                cleanup(s)
                return callback.apply(this,arguments)
            }
            hyve.callbacks['f' + counter] = wrap_callback
            var x = document.getElementsByTagName('script')[0]
            x.parentNode.insertBefore(s,x)
        }

        // Requires a URI using the Node.js request library
        function request(url, callback) {
            get(url, function(error, res, data) {
                try {
                    callback(JSON.parse(data))
                } catch(e){
                    console.error('request: fetch failed - ',url, e)
                    callback({ }, e)
                }
            })
        }

        // Abstracts fetching URIs.
        function fetch(url, service, query, callback, item) {
            var fn = pass(service, query, callback, item)
            var cb = !get && get_callback()
            url    = format(url, { callback: cb })
            var fetcher = get? request : jsonp
            script = fetcher(url, fn)
        }

        // Higher-order function to process the fetched data
        function pass(service, query, callback, item) {
            return function(data) {
                // if service supports multiple parsers use that or fallback to
                // parse
                if (hyve.feeds[service].parsers) {
                    if (hyve.method in hyve.feeds[service].parsers) {
                        hyve.feeds[service].parsers[hyve.method](data, query, callback, item)
                    }
                } else {
                    hyve.feeds[service].parse(data, query, callback, item)
                }
            }
        }

        // Export the `fetch` function
        return fetch
    }()

    // A default echo function used when no real request proxy function exists
    function fetch_proxy(feed_url,service){
        console.log('hyve_request_proxy',feed_url,service)
    }

    // Exports data to the outside world
    hyve.friends = friends
    hyve.search = search
    hyve.method = '' // set by the calling stream
    hyve.stop = stop
    hyve.process = process
    hyve.format = format
    hyve.fetch = fetch
    hyve.fetch_proxy = fetch_proxy // override with your own proxy function
    hyve.recall = recall
    hyve.recall_enable = false
    hyve.replenish = replenish
    hyve.queue = {'text':[],'link':[],'video':[],'image':[],'checkin':[]}
    hyve.queue_enable = false; // enables queuing; no queue by default
    hyve.dequeue = dequeue
    hyve.items_seen = []
    hyve.items_seen_size = 5000 // length of buffer before rolling begins
    hyve.callbacks = []
    hyve.links = {}
    hyve.feeds = {}

    // Export hyve for node/browser compatibilty
    if (typeof module !== 'undefined' && module.exports) {
        module.exports = hyve;
    } else {
        root.hyve = hyve;
    }

})(this)
<|endoftext|>


<commit_before>/* VCO.ConfigFactory.js
 * Build TimelineConfig objects from other data sources
 */
;(function(VCO){
    function extractSpreadsheetKey(url) {
        var pat = /\bkey=([-_A-Za-z0-9]+)&?/i;
        if (url.match(pat)) {
            return url.match(pat)[1];
        }
        var key = null;
        if (url.match("docs.google.com/spreadsheets/d/")) {
            var pos = url.indexOf("docs.google.com/spreadsheets/d/") + "docs.google.com/spreadsheets/d/".length;
            var tail = url.substr(pos);
            key = tail.split('/')[0]
        }
        if (!key) { key = url}
        return key;
    }

    function extractGoogleEntryData_V1(item) {
        var item_data = {}
        for (k in item) {
            if (k.indexOf('gsx$') == 0) {
                item_data[k.substr(4)] = item[k].$t;
            }
        }
        if (!item_data.startdate) {
            throw("All items must have a start date column.")
        }
        var d = {
            media: {
                caption: item_data.mediacaption || '',
                credit: item_data.mediacredit || '',
                url: item_data.media || '',
                thumb: item_data.mediathumbnail || ''
            },
            text: {
                headline: item_data.headline || '',
                text: item_data.text || ''
            },
            group: item_data.tag || '',
            type: item_data.type || ''
        }
        d['start_date'] = VCO.Date.parseDate(item_data.startdate);
        if (item.enddate) {
            d['end_date'] = VCO.Date.parseDate(item.enddate);
        }

        return d;
    }

    function extractGoogleEntryData_V3(item) {

        var item_data = {}
        for (k in item) {
            if (k.indexOf('gsx$') == 0) {
                item_data[k.substr(4)] = item[k].$t;
            }
        }
        var d = {
            media: {
                caption: item_data.mediacaption || '',
                credit: item_data.mediacredit || '',
                url: item_data.media || '',
                thumb: item_data.mediathumbnail || ''
            },
            text: {
                headline: item_data.headline || '',
                text: item_data.text || ''
            },
            start_date: {
                year: item_data.year,
                month: item_data.month || '',
                day: item_data.day || ''
            },
            end_date: {
                year: item_data.endyear || '',
                month: item_data.endmonth || '',
                day: item_data.endday || ''
            },
            display_date: item_data.displaydate || '',

            type: item_data.type || ''
        }

        if (item_data.time) {
            VCO.Util.extend(d.start_date,VCO.DateUtil.parseTime(item_data.time));
        }

        if (item_data.endtime) {
            VCO.Util.extend(d.end_date,VCO.DateUtil.parseTime(item_data.endtime));
        }

        if (item_data.group) {
            d.group = item_data.group;
        }

        if (d.end_date.year == '') {
            var bad_date = d.end_date;
            delete d.end_date;
            if (bad_date.month != '' || bad_date.day != '' || bad_date.time != '') {
                var label = d.text.headline ||
                trace("Invalid end date for spreadsheet row. Must have a year if any other date fields are specified.");
                trace(item);
            }
        }

        if (item_data.background) {
            if (item_data.background.match(/^(https?:)?\/\/?/)) { // support http, https, protocol relative, site relative
                d['background'] = { 'url': item_data.background }
            } else { // for now we'll trust it's a color
                d['background'] = { 'color': item_data.background }
            }
        }

        return d;
    }

    var getGoogleItemExtractor = function(data) {
        if (typeof data.feed.entry === 'undefined'
                || data.feed.entry.length == 0) {
            throw('No data entries found.');
        }
        var entry = data.feed.entry[0];
        if (typeof entry.gsx$startdate !== 'undefined') {
            return extractGoogleEntryData_V1;
        } else if (typeof entry.gsx$year !== 'undefined') {
            return extractGoogleEntryData_V3;
        } else {
            throw('Invalid data format.');
        }
    }

    VCO.ConfigFactory = {

        extractSpreadsheetKey: extractSpreadsheetKey,

        fromGoogle: function(url) {
            var key = extractSpreadsheetKey(url);
            // TODO: maybe get specific worksheets?
            var worksheet = '1';
            url = "https://spreadsheets.google.com/feeds/list/" + key + "/" + worksheet + "/public/values?alt=json";
            return this.fromFeed(url);
        },

        fromFeed: function(url) {
            var timeline_config = { 'events': [] };
            var data = VCO.ajax({
                url: url,
                async: false
            });
            data = JSON.parse(data.responseText);
            window.google_data = data;
            var extract = getGoogleItemExtractor(data);
            for (var i = 0; i < data.feed.entry.length; i++) {
                var event = extract(data.feed.entry[i]);
                var row_type = 'event';
                if (typeof(event.type) != 'undefined') {
                    row_type = event.type;
                    delete event.type;
                }
                if (row_type == 'title') {
                    timeline_config.title = event;
                } else {
                    timeline_config.events.push(event);
                }
            };
            return timeline_config;
        }
    }
})(VCO)
<commit_msg>trim whitespace when reading from spreadsheets<commit_after>/* VCO.ConfigFactory.js
 * Build TimelineConfig objects from other data sources
 */
;(function(VCO){
    function extractSpreadsheetKey(url) {
        var pat = /\bkey=([-_A-Za-z0-9]+)&?/i;
        if (url.match(pat)) {
            return url.match(pat)[1];
        }
        var key = null;
        if (url.match("docs.google.com/spreadsheets/d/")) {
            var pos = url.indexOf("docs.google.com/spreadsheets/d/") + "docs.google.com/spreadsheets/d/".length;
            var tail = url.substr(pos);
            key = tail.split('/')[0]
        }
        if (!key) { key = url}
        return key;
    }

    function extractGoogleEntryData_V1(item) {
        var item_data = {}
        for (k in item) {
            if (k.indexOf('gsx$') == 0) {
                item_data[k.substr(4)] = item[k].$t;
            }
        }
        if (!item_data.startdate) {
            throw("All items must have a start date column.")
        }
        var d = {
            media: {
                caption: item_data.mediacaption || '',
                credit: item_data.mediacredit || '',
                url: item_data.media || '',
                thumb: item_data.mediathumbnail || ''
            },
            text: {
                headline: item_data.headline || '',
                text: item_data.text || ''
            },
            group: item_data.tag || '',
            type: item_data.type || ''
        }
        d['start_date'] = VCO.Date.parseDate(item_data.startdate);
        if (item.enddate) {
            d['end_date'] = VCO.Date.parseDate(item.enddate);
        }

        return d;
    }

    function extractGoogleEntryData_V3(item) {

        var item_data = {}
        for (k in item) {
            if (k.indexOf('gsx$') == 0) {
                item_data[k.substr(4)] = VCO.Util.trim(item[k].$t);
            }
        }
        var d = {
            media: {
                caption: item_data.mediacaption || '',
                credit: item_data.mediacredit || '',
                url: item_data.media || '',
                thumb: item_data.mediathumbnail || ''
            },
            text: {
                headline: item_data.headline || '',
                text: item_data.text || ''
            },
            start_date: {
                year: item_data.year,
                month: item_data.month || '',
                day: item_data.day || ''
            },
            end_date: {
                year: item_data.endyear || '',
                month: item_data.endmonth || '',
                day: item_data.endday || ''
            },
            display_date: item_data.displaydate || '',

            type: item_data.type || ''
        }

        if (item_data.time) {
            VCO.Util.extend(d.start_date,VCO.DateUtil.parseTime(item_data.time));
        }

        if (item_data.endtime) {
            VCO.Util.extend(d.end_date,VCO.DateUtil.parseTime(item_data.endtime));
        }

        if (item_data.group) {
            d.group = item_data.group;
        }

        if (d.end_date.year == '') {
            var bad_date = d.end_date;
            delete d.end_date;
            if (bad_date.month != '' || bad_date.day != '' || bad_date.time != '') {
                var label = d.text.headline ||
                trace("Invalid end date for spreadsheet row. Must have a year if any other date fields are specified.");
                trace(item);
            }
        }

        if (item_data.background) {
            if (item_data.background.match(/^(https?:)?\/\/?/)) { // support http, https, protocol relative, site relative
                d['background'] = { 'url': item_data.background }
            } else { // for now we'll trust it's a color
                d['background'] = { 'color': item_data.background }
            }
        }

        return d;
    }

    var getGoogleItemExtractor = function(data) {
        if (typeof data.feed.entry === 'undefined'
                || data.feed.entry.length == 0) {
            throw('No data entries found.');
        }
        var entry = data.feed.entry[0];
        if (typeof entry.gsx$startdate !== 'undefined') {
            return extractGoogleEntryData_V1;
        } else if (typeof entry.gsx$year !== 'undefined') {
            return extractGoogleEntryData_V3;
        } else {
            throw('Invalid data format.');
        }
    }

    VCO.ConfigFactory = {

        extractSpreadsheetKey: extractSpreadsheetKey,

        fromGoogle: function(url) {
            var key = extractSpreadsheetKey(url);
            // TODO: maybe get specific worksheets?
            var worksheet = '1';
            url = "https://spreadsheets.google.com/feeds/list/" + key + "/" + worksheet + "/public/values?alt=json";
            return this.fromFeed(url);
        },

        fromFeed: function(url) {
            var timeline_config = { 'events': [] };
            var data = VCO.ajax({
                url: url,
                async: false
            });
            data = JSON.parse(data.responseText);
            window.google_data = data;
            var extract = getGoogleItemExtractor(data);
            for (var i = 0; i < data.feed.entry.length; i++) {
                var event = extract(data.feed.entry[i]);
                var row_type = 'event';
                if (typeof(event.type) != 'undefined') {
                    row_type = event.type;
                    delete event.type;
                }
                if (row_type == 'title') {
                    timeline_config.title = event;
                } else {
                    timeline_config.events.push(event);
                }
            };
            return timeline_config;
        }
    }
})(VCO)
<|endoftext|>


<commit_before>'use strict';

const clamp = require('lodash/clamp');
const defaultTo = require('lodash/defaultTo');
const EventEmitter = require('events');
const EventType = require('../event/EventType');
const FrameEvent = require('../event/FrameEvent');
const ResizeEvent = require('../event/ResizeEvent');
const RenderBuffer = require('../render/webgl/texture/RenderBuffer');
const Request = require('./Request');
const Viewport = require('./Viewport');
const ClickHandler = require('./handler/ClickHandler');
const MouseHandler = require('./handler/MouseHandler');
const PanHandler = require('./handler/PanHandler');
const ZoomHandler = require('./handler/ZoomHandler');

// Private Methods

const resize = function(plot) {
	const current = {
		width: plot.container.offsetWidth,
		height: plot.container.offsetHeight
	};
	const prev = {
		width: plot.viewport.width,
		height: plot.viewport.height
	};
	const center = plot.viewport.getCenter();

	if (prev.width !== current.width ||
		prev.height !== current.height ||
		plot.pixelRatio !== window.devicePixelRatio) {
		// store device pixel ratio
		plot.pixelRatio = window.devicePixelRatio;
		// resize canvas
		plot.canvas.style.width = current.width + 'px';
		plot.canvas.style.height = current.height + 'px';
		plot.canvas.width = current.width * plot.pixelRatio;
		plot.canvas.height = current.height * plot.pixelRatio;
		// resize render target
		plot.renderBuffer.resize(
			current.width * plot.pixelRatio,
			current.height * plot.pixelRatio);
		// update viewport
		plot.viewport.width = current.width;
		plot.viewport.height = current.height;
		// re-center viewport
		plot.viewport.centerOn(center);
		// request tiles
		Request.requestTiles(plot);
		// emit resize
		plot.emit(EventType.RESIZE, new ResizeEvent(plot, prev, current));
	}
};

const reset = function(plot) {
	if (!plot.wraparound) {
		// if there is no wraparound, do not reset
		return;
	}
	// resets the position of the viewport relative to the layer such that
	// the layer native coordinate range is within the viewports bounds.
	// NOTE: This does not have any observable effect.
	const scale = Math.pow(2, plot.zoom);
	const layerWidth = scale * plot.tileSize;
	const layerSpans = Math.ceil(plot.viewport.width / layerWidth);
	const layerLeft = 0;
	const layerRight = layerWidth - 1;
	// layer is past the left bound of the viewport
	if (plot.viewport.x > layerRight) {
		plot.viewport.x -= layerWidth * layerSpans;
		if (plot.panAnimation) {
			plot.panAnimation.start.x -= layerWidth * layerSpans;
		}
	}
	// layer is past the right bound of the viewport
	if (plot.viewport.x + plot.viewport.width < layerLeft) {
		plot.viewport.x += layerWidth * layerSpans;
		if (plot.panAnimation) {
			plot.panAnimation.start.x += layerWidth * layerSpans;
		}
	}
};

const frame = function(plot) {

	// get frame timestamp
	const timestamp = Date.now();

	// emit start frame
	plot.emit(EventType.FRAME, new FrameEvent(timestamp));

	// update size
	resize(plot);

	const gl = plot.gl;

	// clear the backbuffer
	gl.clearColor(0, 0, 0, 0);
	gl.clear(gl.COLOR_BUFFER_BIT);

	// set the viewport
	gl.viewport(
		0, 0,
		plot.viewport.width * window.devicePixelRatio,
		plot.viewport.height * window.devicePixelRatio);

	// apply the zoom animation
	if (plot.zoomAnimation) {
		plot.zoomAnimation.updatePlot(plot, timestamp);
	}

	// apply the pan animation
	if (plot.panAnimation) {
		plot.panAnimation.updatePlot(plot, timestamp);
		Request.panRequest(plot);
	}

	// reset viewport / plot
	reset(plot);

	// render each layer
	plot.layers.forEach(layer => {
		layer.draw(timestamp);
	});

	// request next frame
	plot.frameRequest = requestAnimationFrame(() => {
		frame(plot);
	});
};

/**
 * Class representing a plot.
 */
class Plot extends EventEmitter {

	/**
	 * Instantiates a new Plot object.
	 *
	 * @param {String} selector - The selector for the canvas element.
	 * @param {Object} options - The plot options.
	 * @param {Number} options.tileSize - The dimension in pixels of a tile.
	 * @param {Number} options.zoom - The zoom of the plot.
	 * @param {Number} options.minZoom - The minimum zoom of the plot.
	 * @param {Number} options.maxZoom - The maximum zoom of the plot.
	 * @param {Number} options.center - The center of the plot, in plot pixels.
	 * @param {boolean} options.wraparound - Whether or not the plot wraps around.
	 *
	 * @param {Number} options.inertia - Whether or not pan inertia is enabled.
	 * @param {Number} options.inertiaEasing - The inertia easing factor.
	 * @param {Number} options.inertiaDeceleration - The inertia deceleration factor.
	 *
	 * @param {Number} options.continuousZoom - Whether or not continuous zoom is enabled.
	 * @param {Number} options.zoomDuration - The duration of the zoom animation.
	 * @param {Number} options.maxConcurrentZooms - The maximum concurrent zooms in a single batch.
	 * @param {Number} options.deltaPerZoom - The scroll delta required per zoom level.
	 * @param {Number} options.zoomDebounce - The debounce duration of the zoom in ms.
	 */
	constructor(selector, options = {}) {
		super();
		this.container = document.querySelector(selector);
		if (!this.container) {
			throw `Element could not be found for selector ${selector}`;
		}

		// create canvas element
		this.canvas = document.createElement('canvas');
		this.canvas.style.width = this.container.offsetWidth + 'px';
		this.canvas.style.height = this.container.offsetHeight + 'px';
		this.canvas.width = this.container.offsetWidth * window.devicePixelRatio;
		this.canvas.height = this.container.offsetHeight * window.devicePixelRatio;
		this.container.appendChild(this.canvas);

		// get WebGL context
		try {
			this.gl = this.canvas.getContext('webgl', options);
		} catch(err) {
			throw 'Unable to create a WebGLRenderingContext, please ensure your browser supports WebGL';
		}

		// create renderbuffer
		this.renderBuffer = new RenderBuffer(
			this.gl,
			this.canvas.width,
			this.canvas.height);

		// set viewport
		this.viewport = new Viewport({
			width: this.canvas.offsetWidth,
			height: this.canvas.offsetHeight
		});

		// set pixel ratio
		this.pixelRatio = window.devicePixelRatio;

		// tile size in pixels
		this.tileSize = defaultTo(options.tileSize, 256);

		// min and max zoom of the plot
		this.minZoom = defaultTo(options.minZoom, 0);
		this.maxZoom = defaultTo(options.maxZoom, 30);

		// current zoom of the plot
		this.zoom = defaultTo(options.zoom, 0);
		this.zoom = clamp(this.zoom, this.minZoom, this.maxZoom);

		// center the plot
		const center = defaultTo(
			options.center,
			Math.pow(2, this.zoom) * this.tileSize / 2);
		this.viewport.centerOn({
			x: center,
			y: center
		});

		// wraparound
		this.wraparound = defaultTo(options.wraparound, false);

		// create and enable handlers
		this.handlers = new Map();
		this.handlers.set('click', new ClickHandler(this, options));
		this.handlers.set('mouse', new MouseHandler(this, options));
		this.handlers.set('pan', new PanHandler(this, options));
		this.handlers.set('zoom', new ZoomHandler(this, options));
		this.handlers.forEach(handler => {
			handler.enable();
		});

		// layers
		this.layers = [];

		// frame request
		this.frameRequest = null;

		// being frame loop
		frame(this);
	}

	/**
	 * Destroys the plots association with the underlying canvas element and
	 * disables all event handlers.
	 *
	 * @returns {Plot} The plot object, for chaining.
	 */
	destroy() {
		// stop animation loop
		cancelAnimationFrame(this.frameRequest);
		this.frameRequest = null;
		// destroy context
		this.gl = null;
		this.canvas = null;
		this.container = null;
		this.renderBuffer = null;
		// disable handlers
		this.handlers.forEach(handler => {
			handler.disable();
		});
		// remove layers
		this.layers.forEach(layer => {
			this.removeLayer(layer);
		});
		return this;
	}

	/**
	 * Adds a layer to the plot.
	 *
	 * @param {Layer} layer - The layer to add to the plot.
	 *
	 * @returns {Plot} The plot object, for chaining.
	 */
	addLayer(layer) {
		if (!layer) {
			throw 'No layer argument provided';
		}
		if (this.layers.indexOf(layer) !== -1) {
			throw 'Provided layer is already attached to the plot';
		}
		this.layers.push(layer);
		layer.onAdd(this);
		return this;
	}

	/**
	 * Removes a layer from the plot.
	 *
	 * @param {Layer} layer - The layer to remove from the plot.
	 *
	 * @returns {Plot} The plot object, for chaining.
	 */
	removeLayer(layer) {
		if (!layer) {
			throw 'No layer argument provided';
		}
		const index = this.layers.indexOf(layer);
		if (index === -1) {
			throw 'Provided layer is not attached to the plot';
		}
		this.layers.splice(index, 1);
		layer.onRemove(this);
		return this;
	}

	/**
	 * Takes a mouse event and returns the corresponding viewport pixel
	 * position. Coordinate [0, 0] is bottom-left of the viewport.
	 *
	 * @param {Event} event - The mouse event.
	 *
	 * @returns {Object} The viewport pixel position.
	 */
	mouseToViewPx(event) {
		return {
			x: event.clientX,
			y: this.viewport.height - event.clientY
		};
	}

	/**
	 * Takes a mouse event and returns the corresponding plot pixel
	 * position. Coordinate [0, 0] is bottom-left of the plot.
	 *
	 * @param {Event} event - The mouse event.
	 *
	 * @returns {Object} The plot pixel position.
	 */
	mouseToPlotPx(event) {
		return this.viewPxToPlotPx(this.mouseToViewPx(event));
	}

	/**
	 * Takes a viewport pixel position and returns the corresponding plot
	 * pixel position. Coordinate [0, 0] is bottom-left of the plot.
	 *
	 * @param {Object} px - The viewport pixel position.
	 *
	 * @returns {Object} The plot pixel position.
	 */
	viewPxToPlotPx(px) {
		return {
			x: this.viewport.x + px.x,
			y: this.viewport.y + px.y
		};
	}

	/**
	 * Takes a plot pixel position and returns the corresponding viewport
	 * pixel position. Coordinate [0, 0] is bottom-left of the viewport.
	 *
	 * @param {Object} px - The plot pixel position.
	 *
	 * @returns {Object} The viewport pixel position.
	 */
	plotPxToViewPx(px) {
		return {
			x: px.x - this.viewport.x,
			y: px.y - this.viewport.y
		};
	}

	/**
	 * Returns the target zoom of the plot. If the plot is actively zooming, it
	 * will return the destination zoom. If the plot is not actively zooming, it
	 * will return the current zoom.
	 *
	 * @returns {Number} The target zoom of the plot.
	 */
	getTargetZoom() {
		if (this.zoomAnimation) {
			// if zooming, use the target level
			return this.zoomAnimation.targetZoom;
		}
		// if not zooming, use the current level
		return this.zoom;
	}

	/**
	 * Returns the target center of the plot in plot pixel coordinates. If the
	 * plot is actively zooming or panning, it will return the  destination
	 * center. If the plot is not actively zooming or panning, it will return
	 * the current center in plot pixel coordinates.
	 *
	 * @returns {Object} The target center in plot pixel coordinates.
	 */
	getTargetCenter() {
		if (this.zoomAnimation) {
			// if zooming, use the target level
			return this.zoomAnimation.targetViewport.getCenter();
		}
		// if not zooming, use the current level
		return this.viewport.getCenter();
	}

	/**
	 * Returns the target viewport of the plot. If the plot is actively zooming,
	 * it will return the target viewport. If the plot is not actively zooming,
	 * it will return the current viewport.
	 *
	 * @returns {Viewport} The target viewport of the plot.
	 */
	getTargetViewport() {
		if (this.zoomAnimation) {
			// if zooming, use the target level
			return this.zoomAnimation.targetViewport;
		}
		// if not zooming, use the current level
		return this.viewport;
	}
}

module.exports = Plot;
<commit_msg>Fix comments.<commit_after>'use strict';

const clamp = require('lodash/clamp');
const defaultTo = require('lodash/defaultTo');
const EventEmitter = require('events');
const EventType = require('../event/EventType');
const FrameEvent = require('../event/FrameEvent');
const ResizeEvent = require('../event/ResizeEvent');
const RenderBuffer = require('../render/webgl/texture/RenderBuffer');
const Request = require('./Request');
const Viewport = require('./Viewport');
const ClickHandler = require('./handler/ClickHandler');
const MouseHandler = require('./handler/MouseHandler');
const PanHandler = require('./handler/PanHandler');
const ZoomHandler = require('./handler/ZoomHandler');

// Private Methods

const resize = function(plot) {
	const current = {
		width: plot.container.offsetWidth,
		height: plot.container.offsetHeight
	};
	const prev = {
		width: plot.viewport.width,
		height: plot.viewport.height
	};
	const center = plot.viewport.getCenter();

	if (prev.width !== current.width ||
		prev.height !== current.height ||
		plot.pixelRatio !== window.devicePixelRatio) {
		// store device pixel ratio
		plot.pixelRatio = window.devicePixelRatio;
		// resize canvas
		plot.canvas.style.width = current.width + 'px';
		plot.canvas.style.height = current.height + 'px';
		plot.canvas.width = current.width * plot.pixelRatio;
		plot.canvas.height = current.height * plot.pixelRatio;
		// resize render target
		plot.renderBuffer.resize(
			current.width * plot.pixelRatio,
			current.height * plot.pixelRatio);
		// update viewport
		plot.viewport.width = current.width;
		plot.viewport.height = current.height;
		// re-center viewport
		plot.viewport.centerOn(center);
		// request tiles
		Request.requestTiles(plot);
		// emit resize
		plot.emit(EventType.RESIZE, new ResizeEvent(plot, prev, current));
	}
};

const reset = function(plot) {
	if (!plot.wraparound) {
		// if there is no wraparound, do not reset
		return;
	}
	// resets the position of the viewport relative to the layer such that
	// the layer native coordinate range is within the viewports bounds.
	// NOTE: This does not have any observable effect.
	const scale = Math.pow(2, plot.zoom);
	const layerWidth = scale * plot.tileSize;
	const layerSpans = Math.ceil(plot.viewport.width / layerWidth);
	const layerLeft = 0;
	const layerRight = layerWidth - 1;
	// layer is past the left bound of the viewport
	if (plot.viewport.x > layerRight) {
		plot.viewport.x -= layerWidth * layerSpans;
		if (plot.panAnimation) {
			plot.panAnimation.start.x -= layerWidth * layerSpans;
		}
	}
	// layer is past the right bound of the viewport
	if (plot.viewport.x + plot.viewport.width < layerLeft) {
		plot.viewport.x += layerWidth * layerSpans;
		if (plot.panAnimation) {
			plot.panAnimation.start.x += layerWidth * layerSpans;
		}
	}
};

const frame = function(plot) {

	// get frame timestamp
	const timestamp = Date.now();

	// emit start frame
	plot.emit(EventType.FRAME, new FrameEvent(timestamp));

	// update size
	resize(plot);

	const gl = plot.gl;

	// clear the backbuffer
	gl.clearColor(0, 0, 0, 0);
	gl.clear(gl.COLOR_BUFFER_BIT);

	// set the viewport
	gl.viewport(
		0, 0,
		plot.viewport.width * window.devicePixelRatio,
		plot.viewport.height * window.devicePixelRatio);

	// apply the zoom animation
	if (plot.zoomAnimation) {
		plot.zoomAnimation.updatePlot(plot, timestamp);
	}

	// apply the pan animation
	if (plot.panAnimation) {
		plot.panAnimation.updatePlot(plot, timestamp);
		Request.panRequest(plot);
	}

	// reset viewport / plot
	reset(plot);

	// render each layer
	plot.layers.forEach(layer => {
		layer.draw(timestamp);
	});

	// request next frame
	plot.frameRequest = requestAnimationFrame(() => {
		frame(plot);
	});
};

/**
 * Class representing a plot.
 */
class Plot extends EventEmitter {

	/**
	 * Instantiates a new Plot object.
	 *
	 * @param {String} selector - The selector for the canvas element.
	 * @param {Object} options - The plot options.
	 * @param {Number} options.tileSize - The dimension in pixels of a tile.
	 * @param {Number} options.zoom - The zoom of the plot.
	 * @param {Number} options.minZoom - The minimum zoom of the plot.
	 * @param {Number} options.maxZoom - The maximum zoom of the plot.
	 * @param {Number} options.center - The center of the plot, in plot pixels.
	 * @param {boolean} options.wraparound - Whether or not the plot wraps around.
	 *
	 * @param {Number} options.inertia - Whether or not pan inertia is enabled.
	 * @param {Number} options.inertiaEasing - The inertia easing factor.
	 * @param {Number} options.inertiaDeceleration - The inertia deceleration factor.
	 *
	 * @param {Number} options.continuousZoom - Whether or not continuous zoom is enabled.
	 * @param {Number} options.zoomDuration - The duration of the zoom animation.
	 * @param {Number} options.maxConcurrentZooms - The maximum concurrent zooms in a single batch.
	 * @param {Number} options.deltaPerZoom - The scroll delta required per zoom level.
	 * @param {Number} options.zoomDebounce - The debounce duration of the zoom in ms.
	 */
	constructor(selector, options = {}) {
		super();
		this.container = document.querySelector(selector);
		if (!this.container) {
			throw `Element could not be found for selector ${selector}`;
		}

		// create canvas element
		this.canvas = document.createElement('canvas');
		this.canvas.style.width = this.container.offsetWidth + 'px';
		this.canvas.style.height = this.container.offsetHeight + 'px';
		this.canvas.width = this.container.offsetWidth * window.devicePixelRatio;
		this.canvas.height = this.container.offsetHeight * window.devicePixelRatio;
		this.container.appendChild(this.canvas);

		// get WebGL context
		try {
			this.gl = this.canvas.getContext('webgl', options);
		} catch(err) {
			throw 'Unable to create a WebGLRenderingContext, please ensure your browser supports WebGL';
		}

		// create renderbuffer
		this.renderBuffer = new RenderBuffer(
			this.gl,
			this.canvas.width,
			this.canvas.height);

		// set viewport
		this.viewport = new Viewport({
			width: this.canvas.offsetWidth,
			height: this.canvas.offsetHeight
		});

		// set pixel ratio
		this.pixelRatio = window.devicePixelRatio;

		// tile size in pixels
		this.tileSize = defaultTo(options.tileSize, 256);

		// min and max zoom of the plot
		this.minZoom = defaultTo(options.minZoom, 0);
		this.maxZoom = defaultTo(options.maxZoom, 30);

		// current zoom of the plot
		this.zoom = defaultTo(options.zoom, 0);
		this.zoom = clamp(this.zoom, this.minZoom, this.maxZoom);

		// center the plot
		const center = defaultTo(
			options.center,
			Math.pow(2, this.zoom) * this.tileSize / 2);
		this.viewport.centerOn({
			x: center,
			y: center
		});

		// wraparound
		this.wraparound = defaultTo(options.wraparound, false);

		// create and enable handlers
		this.handlers = new Map();
		this.handlers.set('click', new ClickHandler(this, options));
		this.handlers.set('mouse', new MouseHandler(this, options));
		this.handlers.set('pan', new PanHandler(this, options));
		this.handlers.set('zoom', new ZoomHandler(this, options));
		this.handlers.forEach(handler => {
			handler.enable();
		});

		// layers
		this.layers = [];

		// frame request
		this.frameRequest = null;

		// being frame loop
		frame(this);
	}

	/**
	 * Destroys the plots association with the underlying canvas element and
	 * disables all event handlers.
	 *
	 * @returns {Plot} The plot object, for chaining.
	 */
	destroy() {
		// stop animation loop
		cancelAnimationFrame(this.frameRequest);
		this.frameRequest = null;
		// destroy context
		this.gl = null;
		this.canvas = null;
		this.container = null;
		this.renderBuffer = null;
		// disable handlers
		this.handlers.forEach(handler => {
			handler.disable();
		});
		// remove layers
		this.layers.forEach(layer => {
			this.removeLayer(layer);
		});
		return this;
	}

	/**
	 * Adds a layer to the plot.
	 *
	 * @param {Layer} layer - The layer to add to the plot.
	 *
	 * @returns {Plot} The plot object, for chaining.
	 */
	addLayer(layer) {
		if (!layer) {
			throw 'No layer argument provided';
		}
		if (this.layers.indexOf(layer) !== -1) {
			throw 'Provided layer is already attached to the plot';
		}
		this.layers.push(layer);
		layer.onAdd(this);
		return this;
	}

	/**
	 * Removes a layer from the plot.
	 *
	 * @param {Layer} layer - The layer to remove from the plot.
	 *
	 * @returns {Plot} The plot object, for chaining.
	 */
	removeLayer(layer) {
		if (!layer) {
			throw 'No layer argument provided';
		}
		const index = this.layers.indexOf(layer);
		if (index === -1) {
			throw 'Provided layer is not attached to the plot';
		}
		this.layers.splice(index, 1);
		layer.onRemove(this);
		return this;
	}

	/**
	 * Takes a mouse event and returns the corresponding viewport pixel
	 * position. Coordinate [0, 0] is bottom-left of the viewport.
	 *
	 * @param {Event} event - The mouse event.
	 *
	 * @returns {Object} The viewport pixel position.
	 */
	mouseToViewPx(event) {
		return {
			x: event.clientX,
			y: this.viewport.height - event.clientY
		};
	}

	/**
	 * Takes a mouse event and returns the corresponding plot pixel
	 * position. Coordinate [0, 0] is bottom-left of the plot.
	 *
	 * @param {Event} event - The mouse event.
	 *
	 * @returns {Object} The plot pixel position.
	 */
	mouseToPlotPx(event) {
		return this.viewPxToPlotPx(this.mouseToViewPx(event));
	}

	/**
	 * Takes a viewport pixel position and returns the corresponding plot
	 * pixel position. Coordinate [0, 0] is bottom-left of the plot.
	 *
	 * @param {Object} px - The viewport pixel position.
	 *
	 * @returns {Object} The plot pixel position.
	 */
	viewPxToPlotPx(px) {
		return {
			x: this.viewport.x + px.x,
			y: this.viewport.y + px.y
		};
	}

	/**
	 * Takes a plot pixel position and returns the corresponding viewport
	 * pixel position. Coordinate [0, 0] is bottom-left of the viewport.
	 *
	 * @param {Object} px - The plot pixel position.
	 *
	 * @returns {Object} The viewport pixel position.
	 */
	plotPxToViewPx(px) {
		return {
			x: px.x - this.viewport.x,
			y: px.y - this.viewport.y
		};
	}

	/**
	 * Returns the target zoom of the plot. If the plot is actively zooming, it
	 * will return the destination zoom. If the plot is not actively zooming, it
	 * will return the current zoom.
	 *
	 * @returns {Number} The target zoom of the plot.
	 */
	getTargetZoom() {
		if (this.zoomAnimation) {
			// if zooming, use the target level
			return this.zoomAnimation.targetZoom;
		}
		// if not zooming, use the current level
		return this.zoom;
	}

	/**
	 * Returns the target center of the plot in plot pixel coordinates. If the
	 * plot is actively zooming or panning, it will return the  destination
	 * center. If the plot is not actively zooming or panning, it will return
	 * the current center in plot pixel coordinates.
	 *
	 * @returns {Object} The target center in plot pixel coordinates.
	 */
	getTargetCenter() {
		if (this.zoomAnimation) {
			// if zooming, use the target center
			return this.zoomAnimation.targetViewport.getCenter();
		}
		// if not zooming, use the current center
		return this.viewport.getCenter();
	}

	/**
	 * Returns the target viewport of the plot. If the plot is actively zooming,
	 * it will return the target viewport. If the plot is not actively zooming,
	 * it will return the current viewport.
	 *
	 * @returns {Viewport} The target viewport of the plot.
	 */
	getTargetViewport() {
		if (this.zoomAnimation) {
			// if zooming, use the target viewport
			return this.zoomAnimation.targetViewport;
		}
		// if not zooming, use the current viewport
		return this.viewport;
	}
}

module.exports = Plot;
<|endoftext|>


<commit_before>(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define([], factory);
  } else if (typeof exports === 'object') {
    // CommonJS
    module.exports = factory();
  } else {
    // Browser globals
    root.IncrementalIndexedDBAdapter = factory();
  }
}(this, function () {
  return (function () {
    'use strict';

    function IncrementalIndexedDBAdapter() {
      this.mode = 'incremental'
      this.chunkSize = 100
      this.idb = null // will be lazily loaded on first operation that needs it
    }

    // chunkId - index of the data chunk - e.g. chunk 0 will be lokiIds 0-99
    IncrementalIndexedDBAdapter.prototype._getChunk = function(collection, chunkId) {
      // 0-99, 100-199, etc.
      const minId = chunkId * this.chunkSize
      const maxId = minId + this.chunkSize - 1

      // use idIndex to find first collection.data position within the $loki range
      const idIndex = collection.idIndex

      let firstDataPosition = null

      var max = idIndex.length - 1,
          min = 0,
          mid;

      while (idIndex[min] < idIndex[max]) {
        mid = (min + max) >> 1;

        if (idIndex[mid] < minId) {
          min = mid + 1;
        } else {
          max = mid;
        }
      }

      if (max === min && idIndex[min] >= minId) {
        firstDataPosition = min
      }

      if (firstDataPosition === null) {
        // no elements in this chunk
        return []
      }

      // find last position
      // if loki IDs are contiguous (no removed elements)
      // last position will be first + chunk - 1
      // (and we look back in case there are missing pieces)
      // TODO: Binary search (not as important as first position, worst case scanario is chunkSize steps)
      let lastDataPosition = null
      for (let i = firstDataPosition + this.chunkSize - 1; i >= firstDataPosition; i--) {
        if (idIndex[i] <= maxId) {
          lastDataPosition = i
          break
        }
      }

      // sanity check
      // TODO: remove me
      const firstElement = collection.data[firstDataPosition]
      if (!(firstElement && firstElement.$loki >= minId && firstElement.$loki <= maxId)) {
        throw new Error('broken invariant firstelement')
      }

      const lastElement = collection.data[lastDataPosition]
      if (!(lastElement && lastElement.$loki >= minId && lastElement.$loki <= maxId)) {
        throw new Error('broken invariant lastElement')
      }

      // this will have *up to* `this.chunkSize` elements (might have less, because $loki ids
      // will have holes when data is deleted)
      const chunkData = collection.data.slice(firstDataPosition, lastDataPosition + 1)

      // TODO: Remove sanity check
      if (chunkData.length > this.chunkSize) {
        throw new Error('broken invariant - chunk size')
      }

      return chunkData
    }

    IncrementalIndexedDBAdapter.prototype.saveDatabase = function(dbname, loki, callback) {
      console.log(`-- exportDatabase - begin`)
      console.time('exportDatabase')
      // console.log(loki.collections[0].lokiIdChanges)

      let chunksToSave = []

      console.time('makeChunks')
      loki.collections.forEach(collection => {

        console.time('get dirty chunk ids')
        const dirtyChunks = new Set()
        collection.dirtyIds.forEach(lokiId => {
          const chunkId = lokiId / this.chunkSize | 0
          dirtyChunks.add(chunkId)
        })
        collection.dirtyIds = null
        console.timeEnd('get dirty chunk ids')

        console.time('get chunks&serialize')
        dirtyChunks.forEach(chunkId => {
          const chunkData = this._getChunk(collection, chunkId)
          // we must stringify, because IDB is asynchronous, and underlying objects are mutable
          const chunkJSON = JSON.stringify(chunkData)
          chunksToSave.push({ key: collection.name + '.chunk.' + chunkId, value: chunkJSON})
        })
        console.timeEnd('get chunks&serialize')

        // clear out data as we won't be saving it
        collection.data = []
      })
      console.timeEnd('makeChunks')

      const serializedMetadata = JSON.stringify(loki)
      loki = null // allow GC of the DB copy

      // console.log(chunksToSave)
      // console.log(chunkIdsToRemove)
      // console.log(JSON.parse(serializedMetadata))

      chunksToSave.push({ key: 'loki', value: serializedMetadata })

      // TODO: Clear out lokiChangedIds flags on original database

      this._saveChunks(chunksToSave, callback)

      // if (success) {
      //   callback(null);
      // }
      // else {
      //   callback(new Error("some error occurred."));
      // }
    }

    IncrementalIndexedDBAdapter.prototype.loadDatabase = function(dbname, callback) {
      console.log(`-- loadDatabase - begin`)
      console.time('loadDatabase')
      this._getAllChunks(chunks => {

        if (!chunks.length) {
          console.log(`No chunks`)
          callback(null)
          return
        }

        console.log(`Found chunks:`, chunks.length)
        // console.log(`Found chunks:`, chunks)

        // sort chunks in place to load data in the right order (ascending loki ids)
        // on both Safari and Chrome, we'll get chunks in order like this: 0, 1, 10, 100...
        // console.time('sort')
        const getSortKey = function({ key }) {
          if (key.includes('.')) {
            const segments = key.split('.')
            if (segments.length === 3) {
              return parseInt(segments[2], 10)
            }
          }

          return key
        }
        chunks.sort(function(a, b) {
          const aKey = getSortKey(a), bKey =getSortKey(b);
          if(aKey < bKey) return -1;
          if(aKey > bKey) return 1;
          return 0;
        });
        // console.timeEnd('sort')
        // console.log(`Sorted chunks`, chunks)

        // repack chunks into a map
        let loki
        let collections = {}

        // console.time('repack')
        chunks.forEach(({ key, value }) => {
          if (key === 'loki') {
            loki = value
          } else if (key.includes('.')) {
            const keySegments = key.split('.')
            if (keySegments.length === 3 && keySegments[1] === 'chunk') {
              const colName = keySegments[0]
              if (collections[colName]) {
                collections[colName].push(value)
              } else {
                collections[colName] = [value]
              }
            } else {
              throw new Error('unknown chunk')
            }
          } else {
            throw new Error('unknown chunk')
          }
        })
        chunks = null
        // console.timeEnd('repack')
        // console.log(`Collections`, collections)

        // TODO: Validate collection chunks?
        if (!loki) {
          throw new Error('missing loki…')
        }

        // parse Loki object
        // console.time('parse')
        loki = JSON.parse(loki)
        // console.timeEnd('parse')
        // console.log(`Parsed loki object`, loki)

        // populate collections with data
        console.time('populate')
        this._populate(loki, collections)
        collections = null
        console.timeEnd('populate')

        // instantiate actual Loki object
        console.timeEnd('loadDatabase')
        // console.log(`Loaded Loki database!`, loki)
        callback(loki)
      })

      // if (success) {
      //   callback(newSerialized);
      // }
      // else {
      //   callback(new Error("some error"));
      // }
    }

    IncrementalIndexedDBAdapter.prototype._populate = function(loki, collections) {
      loki.collections.forEach(collection => {
        const dataChunks = collections[collection.name]

        if (dataChunks) {
          dataChunks.forEach((chunkObj, i) => {
            const chunk = JSON.parse(chunkObj)
            chunkObj = null
            dataChunks[i] = null

            chunk.forEach(doc => {
              collection.data.push(doc)
            })
          })
        } else {
          // console.log(`No chunks available for ${collection.name}`)
        }
      })
    }

    IncrementalIndexedDBAdapter.prototype._initializeIDB = function(callback) {
      const openRequest = indexedDB.open('IncrementalAdapterIDB', 1);

      openRequest.onupgradeneeded = e => {
        const db = e.target.result
        if (db.objectStoreNames.contains('Store')) {
          throw new Error('todo')
          // TODO: Finish this
        }

        const store = db.createObjectStore('Store', { keyPath: 'key' })
      }

      openRequest.onsuccess = e => {
        this.idb = e.target.result
        callback()
      }

      openRequest.onerror = e => {
        throw e
      }
    }

    IncrementalIndexedDBAdapter.prototype._saveChunks = function(chunks, callback) {
      if (!this.idb) {
        this._initializeIDB(() => {
          this._saveChunks(chunks, callback)
        })
        return
      }

      console.time('save chunks to idb')

      let tx = this.idb.transaction(['Store'], 'readwrite')
      tx.oncomplete = () => {
        console.timeEnd('save chunks to idb')
        console.timeEnd('exportDatabase')
        callback()
      }
      // TODO: Error handling

      let store = tx.objectStore('Store')

      console.time('put')
      chunks.forEach(object => {
        store.put(object)
      })
      console.timeEnd('put')
    }

    IncrementalIndexedDBAdapter.prototype._getAllChunks = function(callback) {
      if (!this.idb) {
        this._initializeIDB(() => {
          this._getAllChunks(callback)
        })
        return
      }

      console.time('getChunks')

      let tx = this.idb.transaction(['Store'], 'readonly')

      // TODO: Error handling

      const request = tx.objectStore('Store').getAll()
      request.onsuccess = e => {
        let chunks = e.target.result
        console.timeEnd('getChunks')
        callback(chunks)
      }
    }
    return IncrementalIndexedDBAdapter
  }())
}))
<commit_msg>some fixes & debug facilities<commit_after>(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define([], factory);
  } else if (typeof exports === 'object') {
    // CommonJS
    module.exports = factory();
  } else {
    // Browser globals
    root.IncrementalIndexedDBAdapter = factory();
  }
}(this, function () {
  return (function () {
    'use strict';

    function IncrementalIndexedDBAdapter() {
      this.mode = 'incremental'
      this.chunkSize = 100
      this.idb = null // will be lazily loaded on first operation that needs it
    }

    // chunkId - index of the data chunk - e.g. chunk 0 will be lokiIds 0-99
    IncrementalIndexedDBAdapter.prototype._getChunk = function(collection, chunkId) {
      // 0-99, 100-199, etc.
      const minId = chunkId * this.chunkSize
      const maxId = minId + this.chunkSize - 1

      // use idIndex to find first collection.data position within the $loki range
      const idIndex = collection.idIndex

      let firstDataPosition = null

      var max = idIndex.length - 1,
          min = 0,
          mid;

      while (idIndex[min] < idIndex[max]) {
        mid = (min + max) >> 1;

        if (idIndex[mid] < minId) {
          min = mid + 1;
        } else {
          max = mid;
        }
      }

      if (max === min && idIndex[min] >= minId) {
        firstDataPosition = min
      }

      if (firstDataPosition === null) {
        // no elements in this chunk
        return []
      }

      // find last position
      // if loki IDs are contiguous (no removed elements)
      // last position will be first + chunk - 1
      // (and we look back in case there are missing pieces)
      // TODO: Binary search (not as important as first position, worst case scanario is chunkSize steps)
      let lastDataPosition = null
      for (let i = firstDataPosition + this.chunkSize - 1; i >= firstDataPosition; i--) {
        if (idIndex[i] <= maxId) {
          lastDataPosition = i
          break
        }
      }

      // sanity check
      // TODO: remove me
      const firstElement = collection.data[firstDataPosition]
      if (!(firstElement && firstElement.$loki >= minId && firstElement.$loki <= maxId)) {
        throw new Error('broken invariant firstelement')
      }

      const lastElement = collection.data[lastDataPosition]
      if (!(lastElement && lastElement.$loki >= minId && lastElement.$loki <= maxId)) {
        throw new Error('broken invariant lastElement')
      }

      // this will have *up to* `this.chunkSize` elements (might have less, because $loki ids
      // will have holes when data is deleted)
      const chunkData = collection.data.slice(firstDataPosition, lastDataPosition + 1)

      // TODO: Remove sanity check
      if (chunkData.length > this.chunkSize) {
        throw new Error('broken invariant - chunk size')
      }

      return chunkData
    }

    IncrementalIndexedDBAdapter.prototype.saveDatabase = function(dbname, loki, callback) {
      console.log(`-- exportDatabase - begin`)
      console.time('exportDatabase')
      // console.log(loki.collections[0].lokiIdChanges)

      let chunksToSave = []

      console.time('makeChunks')
      loki.collections.forEach(collection => {

        console.time('get dirty chunk ids')
        const dirtyChunks = new Set()
        collection.dirtyIds.forEach(lokiId => {
          const chunkId = lokiId / this.chunkSize | 0
          dirtyChunks.add(chunkId)
        })
        collection.dirtyIds = []
        console.timeEnd('get dirty chunk ids')

        console.time('get chunks&serialize')
        dirtyChunks.forEach(chunkId => {
          const chunkData = this._getChunk(collection, chunkId)
          // we must stringify, because IDB is asynchronous, and underlying objects are mutable
          const chunkJSON = JSON.stringify(chunkData)
          chunksToSave.push({ key: collection.name + '.chunk.' + chunkId, value: chunkJSON})
        })
        console.timeEnd('get chunks&serialize')

        // clear out data as we won't be saving it
        collection.data = []
      })
      console.timeEnd('makeChunks')

      const serializedMetadata = JSON.stringify(loki)
      loki = null // allow GC of the DB copy

      // console.log(chunksToSave)
      // console.log(chunkIdsToRemove)
      // console.log(JSON.parse(serializedMetadata))

      chunksToSave.push({ key: 'loki', value: serializedMetadata })

      // TODO: Clear out lokiChangedIds flags on original database

      this._saveChunks(chunksToSave, callback)

      // if (success) {
      //   callback(null);
      // }
      // else {
      //   callback(new Error("some error occurred."));
      // }
    }

    IncrementalIndexedDBAdapter.prototype.loadDatabase = function(dbname, callback) {
      console.log(`-- loadDatabase - begin`)
      console.time('loadDatabase')
      this._getAllChunks(chunks => {

        if (!chunks.length) {
          console.log(`No chunks`)
          callback(null)
          return
        }

        console.log(`Found chunks:`, chunks.length)
        // console.log(`Found chunks:`, chunks)

        // sort chunks in place to load data in the right order (ascending loki ids)
        // on both Safari and Chrome, we'll get chunks in order like this: 0, 1, 10, 100...
        // console.time('sort')
        const getSortKey = function({ key }) {
          if (key.includes('.')) {
            const segments = key.split('.')
            if (segments.length === 3) {
              return parseInt(segments[2], 10)
            }
          }

          return key
        }
        chunks.sort(function(a, b) {
          const aKey = getSortKey(a), bKey =getSortKey(b);
          if(aKey < bKey) return -1;
          if(aKey > bKey) return 1;
          return 0;
        });
        // console.timeEnd('sort')
        // console.log(`Sorted chunks`, chunks)

        // repack chunks into a map
        let loki
        let collections = {}

        // console.time('repack')
        chunks.forEach(({ key, value }) => {
          if (key === 'loki') {
            loki = value
          } else if (key.includes('.')) {
            const keySegments = key.split('.')
            if (keySegments.length === 3 && keySegments[1] === 'chunk') {
              const colName = keySegments[0]
              if (collections[colName]) {
                collections[colName].push(value)
              } else {
                collections[colName] = [value]
              }
            } else {
              throw new Error('unknown chunk')
            }
          } else {
            throw new Error('unknown chunk')
          }
        })
        chunks = null
        // console.timeEnd('repack')
        // console.log(`Collections`, collections)

        // TODO: Validate collection chunks?
        if (!loki) {
          throw new Error('missing loki…')
        }

        // parse Loki object
        // console.time('parse')
        loki = JSON.parse(loki)
        // console.timeEnd('parse')
        // console.log(`Parsed loki object`, loki)

        // populate collections with data
        console.time('populate')
        this._populate(loki, collections)
        collections = null
        console.timeEnd('populate')

        // instantiate actual Loki object
        console.timeEnd('loadDatabase')
        // console.log(`Loaded Loki database!`, loki)
        callback(loki)
      })

      // if (success) {
      //   callback(newSerialized);
      // }
      // else {
      //   callback(new Error("some error"));
      // }
    }

    IncrementalIndexedDBAdapter.prototype._populate = function(loki, collections) {
      loki.collections.forEach(collection => {
        const dataChunks = collections[collection.name]

        if (dataChunks) {
          dataChunks.forEach((chunkObj, i) => {
            const chunk = JSON.parse(chunkObj)
            chunkObj = null
            dataChunks[i] = null

            chunk.forEach(doc => {
              collection.data.push(doc)
            })
          })
        } else {
          // console.log(`No chunks available for ${collection.name}`)
        }
      })
    }

    IncrementalIndexedDBAdapter.prototype._initializeIDB = function(callback) {
      console.log(`initializing idb`)
      const openRequest = indexedDB.open('IncrementalAdapterIDB', 1);

      openRequest.onupgradeneeded = e => {
        console.log('onupgradeneeded')
        const db = e.target.result
        if (db.objectStoreNames.contains('Store')) {
          throw new Error('todo')
          // TODO: Finish this
        }

        const store = db.createObjectStore('Store', { keyPath: 'key' })
      }

      openRequest.onblocked = e => {
        debugger
      }

      openRequest.onsuccess = e => {
        console.log('init success')
        this.idb = e.target.result
        callback()
      }

      openRequest.onerror = e => {
        throw e
      }
    }

    IncrementalIndexedDBAdapter.prototype._saveChunks = function(chunks, callback) {
      if (!this.idb) {
        this._initializeIDB(() => {
          this._saveChunks(chunks, callback)
        })
        return
      }

      console.time('save chunks to idb')

      let tx = this.idb.transaction(['Store'], 'readwrite')
      tx.oncomplete = () => {
        console.timeEnd('save chunks to idb')
        console.timeEnd('exportDatabase')
        callback()
      }
      // TODO: Error handling

      let store = tx.objectStore('Store')

      console.time('put')
      console.log(chunks)
      chunks.forEach(object => {
        store.put(object)
      })
      console.timeEnd('put')
    }

    IncrementalIndexedDBAdapter.prototype._getAllChunks = function(callback) {
      if (!this.idb) {
        this._initializeIDB(() => {
          this._getAllChunks(callback)
        })
        return
      }
      console.log('getting all chunks')
      console.time('getChunks')

      let tx = this.idb.transaction(['Store'], 'readonly')

      // TODO: Error handling

      const request = tx.objectStore('Store').getAll()
      request.onsuccess = e => {
        let chunks = e.target.result
        console.timeEnd('getChunks')
        callback(chunks)
      }

      request.onerror = () => {
        debugger
      }
    }

    IncrementalIndexedDBAdapter.prototype.deleteDatabase = function(dbname, callback) {
      console.log(`deleteDatabase`)
      console.time('deleteDatabase')
      this.idb && this.idb.close()
      this.idb = null

      const request = indexedDB.deleteDatabase('IncrementalAdapterIDB')

      // TODO: Error handling

      request.onsuccess = () => {
        console.timeEnd('deleteDatabase')
        console.log(`deleteDatabase done`)
        callback({ success: true })
      }

      request.onerror = () => {
        debugger
      }

      console.log(`deleteDatabase - exit fn`)
    }

    return IncrementalIndexedDBAdapter
  }())
}))
<|endoftext|>


<commit_before>/**
 * @module ios-driver
 */

UIAApplication.prototype.keyboard_original = UIAApplication.prototype.keyboard;

/**
 * Returns the keyboard the  user will use to interact with the UIElement currenty having the focus.
 * If the keyboard isn't there yet, wait for it to appear up to implicit timeout.
 *
 * @return {UIAKeyboard} the keyboard used to sendkeys.
 *
 */
UIAApplication.prototype.keyboard = function () {
    var keyboard = this.keyboard_original();
    log("keyboard : " + keyboard);
    if (keyboard.toString() === "[object UIAElementNil]") {
        UIATarget.localTarget().delay(2);
        keyboard = this.keyboard_original();
        if (keyboard.toString() == "[object UIAElementNil]") {
            throw new UIAutomationException("cannot find keyboard", 7);
        } else {
            return keyboard;
        }

    } else {
        return keyboard;
    }
}
<commit_msg>removing debug logs.<commit_after>/**
 * @module ios-driver
 */

UIAApplication.prototype.keyboard_original = UIAApplication.prototype.keyboard;

/**
 * Returns the keyboard the  user will use to interact with the UIElement currenty having the focus.
 * If the keyboard isn't there yet, wait for it to appear up to implicit timeout.
 *
 * @return {UIAKeyboard} the keyboard used to sendkeys.
 *
 */
UIAApplication.prototype.keyboard = function (opt_implicit_wait) {
    var wait = 2;
    if (!(typeof opt_implicit_wait === "undefined")) {
        wait = opt_implicit_wait;
    }
    var keyboard = this.keyboard_original();
    if (keyboard.toString() === "[object UIAElementNil]") {
        UIATarget.localTarget().delay(wait);
        keyboard = this.keyboard_original();
        if (keyboard.toString() == "[object UIAElementNil]") {
            throw new UIAutomationException("cannot find keyboard", 7);
        } else {
            return keyboard;
        }

    } else {
        return keyboard;
    }
}
<|endoftext|>


<commit_before>/*global __DEVELOPMENT__*/
'use strict'
import { google as storageConfig } from '../conf/storage'
import { SITE_META } from '../constants/index'

function replaceStorageUrlPrefix(url='', isDev = __DEVELOPMENT__) {
  if (isDev || typeof url !== 'string') {
    return url
  }
  const { schema, hostname, bucket } = storageConfig
  let toBeReplaced = `${schema}://${hostname}/${bucket}`
  let toReplace = `${SITE_META.URL}storage`

  return url.replace(toBeReplaced, toReplace)
}

export { replaceStorageUrlPrefix }
<commit_msg>[Update] Update src/utils/replace-storage-url-prefix.js<commit_after>/*global __DEVELOPMENT__*/
'use strict'
import { google as storageConfig } from '../conf/storage'
import { SITE_META } from '../constants/index'

function replaceStorageUrlPrefix(url='', isDev = __DEVELOPMENT__) {
  if (isDev || typeof url !== 'string') {
    return url
  }
  const { schema, hostname, bucket } = storageConfig
  let toBeReplaced = `${schema}://${hostname}/${bucket}`
  let toReplace = `${SITE_META.URL_NO_SLASH}`

  return url.replace(toBeReplaced, toReplace)
}

export { replaceStorageUrlPrefix }
<|endoftext|>


<commit_before>var vec2 = require('../math/vec2')
,   Utils = require('../utils/Utils');

module.exports = AABB;

/**
 * Axis aligned bounding box class.
 * @class AABB
 * @constructor
 * @param {Object}  [options]
 * @param {Array}   [options.upperBound]
 * @param {Array}   [options.lowerBound]
 */
function AABB(options){

    /**
     * The lower bound of the bounding box.
     * @property lowerBound
     * @type {Array}
     */
    this.lowerBound = vec2.create();
    if(options && options.lowerBound){
        vec2.copy(this.lowerBound, options.lowerBound);
    }

    /**
     * The upper bound of the bounding box.
     * @property upperBound
     * @type {Array}
     */
    this.upperBound = vec2.create();
    if(options && options.upperBound){
        vec2.copy(this.upperBound, options.upperBound);
    }
}

var tmp = vec2.create();

/**
 * Set the AABB bounds from a set of points.
 * @method setFromPoints
 * @param {Array} points An array of vec2's.
 */
AABB.prototype.setFromPoints = function(points,position,angle){
    var l = this.lowerBound,
        u = this.upperBound;
    vec2.set(l,  Number.MAX_VALUE,  Number.MAX_VALUE);
    vec2.set(u, -Number.MAX_VALUE, -Number.MAX_VALUE);
    for(var i=0; i<points.length; i++){
        var p = points[i];

        if(typeof(angle) === "number"){
            vec2.rotate(tmp,p,angle);
            p = tmp;
        }

        for(var j=0; j<2; j++){
            if(p[j] > u[j]){
                u[j] = p[j];
            }
            if(p[j] < l[j]){
                l[j] = p[j];
            }
        }
    }

    // Add offset
    if(position){
        vec2.add(this.lowerBound, this.lowerBound, position);
        vec2.add(this.upperBound, this.upperBound, position);
    }
};

/**
 * Copy bounds from an AABB to this AABB
 * @method copy
 * @param  {AABB} aabb
 */
AABB.prototype.copy = function(aabb){
    vec2.copy(this.lowerBound, aabb.lowerBound);
    vec2.copy(this.upperBound, aabb.upperBound);
};

/**
 * Extend this AABB so that it covers the given AABB too.
 * @method extend
 * @param  {AABB} aabb
 */
AABB.prototype.extend = function(aabb){
    // Loop over x and y
    for(var i=0; i<2; i++){
        // Extend lower bound
        if(aabb.lowerBound[i] < this.lowerBound[i]){
            this.lowerBound[i] = aabb.lowerBound[i];
        }

        // Upper
        if(aabb.upperBound[i] > this.upperBound[i]){
            this.upperBound[i] = aabb.upperBound[i];
        }
    }
};

/**
 * Returns true if the given AABB overlaps this AABB.
 * @method overlaps
 * @param  {AABB} aabb
 * @return {Boolean}
 */
AABB.prototype.overlaps = function(aabb){
    var l1 = this.lowerBound,
        u1 = this.upperBound,
        l2 = aabb.lowerBound,
        u2 = aabb.upperBound;

    //      l2        u2
    //      |---------|
    // |--------|
    // l1       u1

    return ((l2[0] <= u1[0] && u1[0] <= u2[0]) || (l1[0] <= u2[0] && u2[0] <= u1[0])) &&
           ((l2[1] <= u1[1] && u1[1] <= u2[1]) || (l1[1] <= u2[1] && u2[1] <= u1[1]));
};
<commit_msg>AABB optimization<commit_after>var vec2 = require('../math/vec2')
,   Utils = require('../utils/Utils');

module.exports = AABB;

/**
 * Axis aligned bounding box class.
 * @class AABB
 * @constructor
 * @param {Object}  [options]
 * @param {Array}   [options.upperBound]
 * @param {Array}   [options.lowerBound]
 */
function AABB(options){

    /**
     * The lower bound of the bounding box.
     * @property lowerBound
     * @type {Array}
     */
    this.lowerBound = vec2.create();
    if(options && options.lowerBound){
        vec2.copy(this.lowerBound, options.lowerBound);
    }

    /**
     * The upper bound of the bounding box.
     * @property upperBound
     * @type {Array}
     */
    this.upperBound = vec2.create();
    if(options && options.upperBound){
        vec2.copy(this.upperBound, options.upperBound);
    }
}

var tmp = vec2.create();

/**
 * Set the AABB bounds from a set of points.
 * @method setFromPoints
 * @param {Array} points An array of vec2's.
 */
AABB.prototype.setFromPoints = function(points,position,angle){
    var l = this.lowerBound,
        u = this.upperBound;
    vec2.set(l,  Number.MAX_VALUE,  Number.MAX_VALUE);
    vec2.set(u, -Number.MAX_VALUE, -Number.MAX_VALUE);
    for(var i=0; i<points.length; i++){
        var p = points[i];

        if(typeof(angle) === "number"){
            vec2.rotate(tmp,p,angle);
            p = tmp;
        }

        for(var j=0; j<2; j++){
            if(p[j] > u[j]){
                u[j] = p[j];
            }
            if(p[j] < l[j]){
                l[j] = p[j];
            }
        }
    }

    // Add offset
    if(position){
        vec2.add(this.lowerBound, this.lowerBound, position);
        vec2.add(this.upperBound, this.upperBound, position);
    }
};

/**
 * Copy bounds from an AABB to this AABB
 * @method copy
 * @param  {AABB} aabb
 */
AABB.prototype.copy = function(aabb){
    vec2.copy(this.lowerBound, aabb.lowerBound);
    vec2.copy(this.upperBound, aabb.upperBound);
};

/**
 * Extend this AABB so that it covers the given AABB too.
 * @method extend
 * @param  {AABB} aabb
 */
AABB.prototype.extend = function(aabb){
    // Loop over x and y
    var i = 2;
    while(i--){
        // Extend lower bound
        var l = aabb.lowerBound[i];
        if(this.lowerBound[i] > l){
            this.lowerBound[i] = l;
        }

        // Upper
        var u = aabb.upperBound[i];
        if(this.upperBound[i] < u){
            this.upperBound[i] = u;
        }
    }
};

/**
 * Returns true if the given AABB overlaps this AABB.
 * @method overlaps
 * @param  {AABB} aabb
 * @return {Boolean}
 */
AABB.prototype.overlaps = function(aabb){
    var l1 = this.lowerBound,
        u1 = this.upperBound,
        l2 = aabb.lowerBound,
        u2 = aabb.upperBound;

    //      l2        u2
    //      |---------|
    // |--------|
    // l1       u1

    return ((l2[0] <= u1[0] && u1[0] <= u2[0]) || (l1[0] <= u2[0] && u2[0] <= u1[0])) &&
           ((l2[1] <= u1[1] && u1[1] <= u2[1]) || (l1[1] <= u2[1] && u2[1] <= u1[1]));
};
<|endoftext|>