<commit_before>var http = require('http');
var querystring = require('querystring');
var crypto = require('crypto');

var base64 = require('./extern/base64');
var sprintf = require('./extern/sprintf').sprintf;

var OTP = require('./otp').OTP;
var constants = require('./constants');
var exceptions = require('./exceptions');
var utils = require('./utils');

/**
 *
 * @constructor
 * @param {Number} client_id Client ID
 * @param {String} key API key
 * @param {Boolean} use_https True to use HTTPS (defaults to true)
 *
 * Note: You can get client_id and API key at https://upgrade.yubico.com/getapikey/
 */
function Yubico(client_id, key, use_https) {
    this._client_id = client_id;
    this._key = (key !== null) ? base64.decode(key) : null;
    this._use_https = use_https || true;
}

/**
 * Verify the OTP token.
 *
 * @param {Number} timeout Connection timeout in seconds (defaults to 15
 *                         seconds)
 * @param {Function} callback Callback which is called with a possible error
 *                            as the first argument and true as the second
 *                            one if the provided OTP is valid.
 */
Yubico.prototype.verify = function(otp, timeout, callback) {
    var timeout_, callback_, nonce;

    if (typeof timeout === 'function') {
        timeout_ = constants.TIMEOUT * 1000;
        callback_ = timeout;
    }
    else {
        timeout_ = timeout * 1000;
        callback_ = callback;
    }

    this._verify_token(otp, timeout_, false, function(err, success) {
        if (err) {
            callback_(err, false);
            return;
        }

        callback_(null, true);
    });
};

/**
 * Verify multiple OTP tokens.
 *
 * @param {Array} otp_list Array of OTP tokens.
 * @param {Number} max_time_window How many seconds can pass between the first
 *                 and the last OTP generation.
 * @param {Number} timeout Connection timeout in seconds (defaults to 15
 *                         seconds)
 * @param {Function} callback Callback which is called with a possible error
 *                            as the first argument and true as the second
 *                            one if all the provided OTPs are valid.
 */
Yubico.prototype.verify_multi = function(otp_list, max_time_window, timeout, callback) {
    var self = this;
    var max_time_window_, timeout_, callback_;
    var i, otp_list_length, otp, otp_object, device_id, otps = [], device_ids = [];
    var callback_called = false, result_count = 0;

    if (typeof max_time_window === 'function') {
        max_time_window_ = constants.MAX_TIME_WINDOW;
        timeout_ = (constants.TIMEOUT * 1000);
        callback_ = max_time_window;
    }
    else {
        max_time_window_ = max_time_window || constants.MAX_TIME_WINDOW;
        timeout_ = (timeout * 1000) || (constants.TIMEOUT * 1000);
        callback_ = callback;
    }

    var call_callback = function(err, success) {
        if (callback_called) {
            return;
        }

        callback_called = true;
        callback_(err, success);
    };

    var handle_got_result = function(err, result) {
        if (err) {
            call_callback(err, false);
            return;
        }

        result_count++;
        if (result_count === otp_list_length) {
            call_callback(null, true);
        }
        else {
            self._verify_token(otps.shift(), timeout_, handle_got_result);
        }
    };

    otp_list_length = otp_list.length;
    for (i = 0; i < otp_list_length; i++) {
        otp = otp_list[i];
        otp_object = new OTP(otp);
        device_id = otp_object.get_device_id();

        if (i > 0 && device_ids.indexOf(device_id) === -1) {
            callback_(new Error('OTPs contain different device IDs'), false);
            return;
        }

        device_ids.push(device_id);
        otps.push(otp);
    }

    this._verify_token(otps.shift(), timeout_, handle_got_result);
};

/**
 * Verify the provided token.
 *
 * @param {String} otp OTP token.
 * @param {Number} timeout Number of seconds to wait for sync responses; if
 *                         absent, let the server decides.
 * @param {Boolean} return_response true to call callback with the response
 *                                  string as the last argument.
 * @param {Function} callback Callback which is called with a possible error
 *                            as the first argument, true as the second one
 *                            if the provided OTP is valid and the response
 *                            string as the third one if return_response equals
 *                            true.
 */
Yubico.prototype._verify_token = function(otp, timeout, return_response, callback) {
    var self = this;
    var otp_, nonce, query_string;

    var i, client, request, host, port, path, error;
    var timeout_ids = [], client_objects = [], request_objects = [];
    var got_response = false, callback_called = false;
    var client_timed_out_count = 0;

    otp_ = new OTP(otp).get_otp();
    nonce = base64.encode(utils.randstr(40));
    nonce = nonce.substr(0, 30);
    query_string = this._generate_query_string(otp_, nonce, true, 75, (timeout / 1000));

    var call_callback = function(err, success, response) {
        if (callback_called) {
            return;
        }

        if (!return_response) {
            response = undefined;
        }

        callback_called = true;
        callback(err, success, response);
    };

    var handle_got_response = function() {
        var i, client_object, request_object, timeout_id;
        got_response = true;

        for (i = 0; i < client_objects.length; i++) {
            client_object = client_objects[i];
            client_object.removeAllListeners('error');

            if (client_object.writable) {
                client_object.destroy();
            }
        }

        for (i = 0; i < request_objects.length; i++) {
            request_object = request_objects[i];
            request_object.removeAllListeners('response');
        }

        for (i = 0; i < timeout_ids.length; i++) {
            timeout_id = timeout_ids[i];
            clearTimeout(timeout_id);
        }
    };

    var clear_connect_timeout = function(timeout_id) {
        clearTimeout(timeout_id);
    };

    var handle_response = function(response, client, timeout_id) {
        var data_buffer = [];

        if (got_response || callback_called) {
            return;
        }

        if (!client.verifyPeer()) {
            // Invalid SSL certificate
            error = exceptions.InvalidSSLCertificateError(client.getPeerCertificate());
            call_callback(error, false, null);
            return;
        }

        clear_connect_timeout(timeout_id);

        response.on('data', function(chunk) {
            data_buffer.push(chunk);
        });

        response.on('end', function() {
            var response, split, parsed_response, signature, parameters;
            var status, response_otp, generated_signature, error;
            response = data_buffer.join('');

            status = response.match('status=([a-zA-Z0-9_]+)');
            response_otp = response.match('otp=([a-zA-Z0-9]+)');

            if (!status) {
                handle_got_response();

                call_callback(new Error('Missing status attribute'), false, response);
                return;
            }

            status = status[1].trim().toLowerCase();
            response_otp = response_otp[1].trim();
            if (otp !== response_otp) {
                handle_got_response();

                call_callback(new Error('OTP in the response does not match the' +
                                        ' provided OTP'), false, response);
                return;
            }

            if (self._key) {
                // Verify the response signature
                parsed_response = self._parse_parameters_from_response(response);
                signature = parsed_response[0];
                parameters = parsed_response[1];
                generated_signature = self._generate_message_signature(parameters, self._key);

                if (signature !== generated_signature) {
                    handle_got_response();

                    error = new exceptions.SignatureVerificationError(generated_signature, signature);
                    call_callback(error, false, response);
                    return;
                }
            }

            if (status.toLowerCase() === 'ok') {
                handle_got_response();

                call_callback(null, true, response);
                return;
            }
            else if (status === 'no_such_client') {
                handle_got_response();

                error = new exceptions.InvaliClientError(self._client_id);
                call_callback(error, false, response);
                return;
            }
            else if (status === 'replayed_otp' || status === 'bad_otp' ||
                     status === 'bad_signature') {
                handle_got_response();

                error = new exceptions.StatusCodeError(status);
                call_callback(error, false, response);
                return;
            }
            else if (status === 'replayed_request') {
                return;
            }

            handle_got_response();
            error = new exceptions.StatusCodeError(status);
            call_callback(error, false, response);
        });
    };

    var handle_error = function(err) {
        client.removeAllListeners('response');
        call_callback(err, false);
    };

    var handle_timeout = function(client) {
        if (got_response || callback_called) {
            return;
        }

        if (client.writable) {
            client.destroy();
        }

        client_timed_out_count++;
        client.removeAllListeners('response');

        if (client_timed_out_count === client_objects.length) {
            call_callback(new exceptions.ConnectionTimeoutError(timeout));
        }
    };

    port = (this._use_https) ? 443 : 80;
    for (i = 0; i < constants.API_HOSTS.length ; i++) {
        host = constants.API_HOSTS[i];
        path = sprintf('%s?%s', constants.API_PATH, query_string);

        client = http.createClient(port, host, this._use_https);
        request = client.request('GET', path, {
            'host': host,
            'User-Agent': sprintf('NodeJS Yubico library v%s.%s.%s',
                                  constants.VERSION[0], constants.VERSION[1],
                                  constants.VERSION[2])
        });

        request.end();
        client.on('error', handle_error);

        (function(client) {
            var timeout_id;

            timeout_id = setTimeout(function() {
                handle_timeout(client);
            }, timeout);

            request.on('response', function(response) {
                handle_response(response, client, timeout_id);
            });

            timeout_ids.push(timeout_id);
        })(client);

        client_objects.push(client);
        request_objects.push(request);
    }
};

/**
 * Generate a query string which is sent to the validation servers.
 *
 * @param {String} otp Yubikey token.
 * @param {String} nonce  16 to 40 characters long string with random data.
 * @param {String} sl A value 0 to 100 indicating percentage of syncing required
 *                    by client, or strings "fast" or "secure" to use
 *                    server-configured values; if
 *                    absent, let the server decides.
 * @param {Number} timeout Number of seconds to wait for sync responses; if
 *                         absent, let the server decide.
 * @return {String} Generated query string.
 */
Yubico.prototype._generate_query_string = function(otp, nonce, timestamp, sl, timeout) {
    var data, query_string, hmac_signature;

    data = { 'id': this._client_id,
             'otp': otp,
             'nonce': nonce
    };

    if (timestamp) {
        data.timestamp = 1;
    }

    if (sl) {
        if (sl < 0 || sl > 100 && [ 'fast', 'secure' ].indexOf(sl) === -1) {
            throw new Error('sl parameter value must be between 0 and 100 or string "fast" or "secure"');
        }

        data.sl = sl;
    }

    if (timeout) {
        data.timeout = timeout;
    }

    query_string = querystring.encode(data);

    if (this._key) {
        hmac_signature = this._generate_message_signature(query_string, this._key);
        hmac_signature = hmac_signature.replace(/\+/g, '%2B');
        query_string = sprintf('%s&h=%s', query_string, hmac_signature);
    }

    return query_string;
};

/**
 * Parse parameters from the response.
 *
 * @param {String} response Response string
 * @return {Array} Array where the first member is parsed signature and the
 *                second one if the rest of the parsed response returned as
 *                a query string.
 */
Yubico.prototype._parse_parameters_from_response = function(response) {
    var i, split = [], pairs, pair, signature, query_string;

    pairs = response.split('\n');
    for (i = 0; i < pairs.length; i++) {
        pair = pairs[i].trim();

        if (pair === '') {
            continue;
        }

        split.push(pair);
    }

    signature = split[0].replace('h=', '');
    query_string = split.slice(1).join('&');

    return [ signature, query_string ];
};

/**
  * Convert a query string to an object.
 *
 * @param {String} query_string Query string (e.g. foo=bar&bar=baz)
 * @return {Object} Object where the key is a parameter name and the value is
 *                  is the parameter value.
 */
Yubico.prototype._query_string_to_object = function(query_string) {
    var i, pairs, pair, parameters = {};

    pairs = query_string.split('&');
    for (i = 0; i < pairs.length; i++) {
        pair = pairs[i].trim();

        if (pair === '') {
            continue;
        }

        pair = pair.split('=');
        parameters[pair[0]] = pair[1];
    }

    return parameters;
};

/**
 * Generate HMAC-SHA1 signature for the given query string using the
 * provided key.
 *
 * @param {String} query_string Query string
 * @param {String} key Cryptographic key.
 * @return {String} base64 encoded signature.
 */
Yubico.prototype._generate_message_signature = function(query_string, key) {
    var i, pairs, pair, pairs_sorted, pairs_string, hmac, signature;
    pairs = query_string.split('&');

    pairs_sorted = [];
    for (i = 0; i < pairs.length; i++) {
        pair = pairs[i];
        pair = pair.split('=');
        pairs_sorted.push(pair);
    }

    pairs_sorted.sort();

    pairs = [];
    for (i = 0; i < pairs_sorted.length; i++) {
        pair = pairs_sorted[i];
        pairs[i] = pair.join('=');
    }

    pairs_string = pairs.join('&');

    hmac = crypto.createHmac('sha1', key);
    hmac.update(pairs_string);
    signature = hmac.digest('base64');

    return signature;
};

exports.Yubico = Yubico;
<commit_msg>Throw exception if none or only a single token is provided in multi mode.<commit_after>var http = require('http');
var querystring = require('querystring');
var crypto = require('crypto');

var base64 = require('./extern/base64');
var sprintf = require('./extern/sprintf').sprintf;

var OTP = require('./otp').OTP;
var constants = require('./constants');
var exceptions = require('./exceptions');
var utils = require('./utils');

/**
 *
 * @constructor
 * @param {Number} client_id Client ID
 * @param {String} key API key
 * @param {Boolean} use_https True to use HTTPS (defaults to true)
 *
 * Note: You can get client_id and API key at https://upgrade.yubico.com/getapikey/
 */
function Yubico(client_id, key, use_https) {
    this._client_id = client_id;
    this._key = (key !== null) ? base64.decode(key) : null;
    this._use_https = use_https || true;
}

/**
 * Verify the OTP token.
 *
 * @param {Number} timeout Connection timeout in seconds (defaults to 15
 *                         seconds)
 * @param {Function} callback Callback which is called with a possible error
 *                            as the first argument and true as the second
 *                            one if the provided OTP is valid.
 */
Yubico.prototype.verify = function(otp, timeout, callback) {
    var timeout_, callback_, nonce;

    if (typeof timeout === 'function') {
        timeout_ = constants.TIMEOUT * 1000;
        callback_ = timeout;
    }
    else {
        timeout_ = timeout * 1000;
        callback_ = callback;
    }

    this._verify_token(otp, timeout_, false, function(err, success) {
        if (err) {
            callback_(err, false);
            return;
        }

        callback_(null, true);
    });
};

/**
 * Verify multiple OTP tokens.
 *
 * @param {Array} otp_list Array of OTP tokens.
 * @param {Number} max_time_window How many seconds can pass between the first
 *                 and the last OTP generation.
 * @param {Number} timeout Connection timeout in seconds (defaults to 15
 *                         seconds)
 * @param {Function} callback Callback which is called with a possible error
 *                            as the first argument and true as the second
 *                            one if all the provided OTPs are valid.
 */
Yubico.prototype.verify_multi = function(otp_list, max_time_window, timeout, callback) {
    var self = this;
    var max_time_window_, timeout_, callback_;
    var i, otp_list_length, otp, otp_object, device_id, otps = [], device_ids = [];
    var callback_called = false, result_count = 0;

    if (typeof max_time_window === 'function') {
        max_time_window_ = constants.MAX_TIME_WINDOW;
        timeout_ = (constants.TIMEOUT * 1000);
        callback_ = max_time_window;
    }
    else {
        max_time_window_ = max_time_window || constants.MAX_TIME_WINDOW;
        timeout_ = (timeout * 1000) || (constants.TIMEOUT * 1000);
        callback_ = callback;
    }

    if (otp_list.length <= 1) {
        callback_(new Error('otp_list array must contain at least two tokens.'), false);
        return;
    }

    var call_callback = function(err, success) {
        if (callback_called) {
            return;
        }

        callback_called = true;
        callback_(err, success);
    };

    var handle_got_result = function(err, result) {
        if (err) {
            call_callback(err, false);
            return;
        }

        result_count++;
        if (result_count === otp_list_length) {
            call_callback(null, true);
        }
        else {
            self._verify_token(otps.shift(), timeout_, handle_got_result);
        }
    };

    otp_list_length = otp_list.length;
    for (i = 0; i < otp_list_length; i++) {
        otp = otp_list[i];
        otp_object = new OTP(otp);
        device_id = otp_object.get_device_id();

        if (i > 0 && device_ids.indexOf(device_id) === -1) {
            callback_(new Error('OTPs contain different device IDs'), false);
            return;
        }

        device_ids.push(device_id);
        otps.push(otp);
    }

    this._verify_token(otps.shift(), timeout_, handle_got_result);
};

/**
 * Verify the provided token.
 *
 * @param {String} otp OTP token.
 * @param {Number} timeout Number of seconds to wait for sync responses; if
 *                         absent, let the server decides.
 * @param {Boolean} return_response true to call callback with the response
 *                                  string as the last argument.
 * @param {Function} callback Callback which is called with a possible error
 *                            as the first argument, true as the second one
 *                            if the provided OTP is valid and the response
 *                            string as the third one if return_response equals
 *                            true.
 */
Yubico.prototype._verify_token = function(otp, timeout, return_response, callback) {
    var self = this;
    var otp_, nonce, query_string;

    var i, client, request, host, port, path, error;
    var timeout_ids = [], client_objects = [], request_objects = [];
    var got_response = false, callback_called = false;
    var client_timed_out_count = 0;

    otp_ = new OTP(otp).get_otp();
    nonce = base64.encode(utils.randstr(40));
    nonce = nonce.substr(0, 30);
    query_string = this._generate_query_string(otp_, nonce, true, 75, (timeout / 1000));

    var call_callback = function(err, success, response) {
        if (callback_called) {
            return;
        }

        if (!return_response) {
            response = undefined;
        }

        callback_called = true;
        callback(err, success, response);
    };

    var handle_got_response = function() {
        var i, client_object, request_object, timeout_id;
        got_response = true;

        for (i = 0; i < client_objects.length; i++) {
            client_object = client_objects[i];
            client_object.removeAllListeners('error');

            if (client_object.writable) {
                client_object.destroy();
            }
        }

        for (i = 0; i < request_objects.length; i++) {
            request_object = request_objects[i];
            request_object.removeAllListeners('response');
        }

        for (i = 0; i < timeout_ids.length; i++) {
            timeout_id = timeout_ids[i];
            clearTimeout(timeout_id);
        }
    };

    var clear_connect_timeout = function(timeout_id) {
        clearTimeout(timeout_id);
    };

    var handle_response = function(response, client, timeout_id) {
        var data_buffer = [];

        if (got_response || callback_called) {
            return;
        }

        if (!client.verifyPeer()) {
            // Invalid SSL certificate
            error = exceptions.InvalidSSLCertificateError(client.getPeerCertificate());
            call_callback(error, false, null);
            return;
        }

        clear_connect_timeout(timeout_id);

        response.on('data', function(chunk) {
            data_buffer.push(chunk);
        });

        response.on('end', function() {
            var response, split, parsed_response, signature, parameters;
            var status, response_otp, generated_signature, error;
            response = data_buffer.join('');

            status = response.match('status=([a-zA-Z0-9_]+)');
            response_otp = response.match('otp=([a-zA-Z0-9]+)');

            if (!status) {
                handle_got_response();

                call_callback(new Error('Missing status attribute'), false, response);
                return;
            }

            status = status[1].trim().toLowerCase();
            response_otp = response_otp[1].trim();
            if (otp !== response_otp) {
                handle_got_response();

                call_callback(new Error('OTP in the response does not match the' +
                                        ' provided OTP'), false, response);
                return;
            }

            if (self._key) {
                // Verify the response signature
                parsed_response = self._parse_parameters_from_response(response);
                signature = parsed_response[0];
                parameters = parsed_response[1];
                generated_signature = self._generate_message_signature(parameters, self._key);

                if (signature !== generated_signature) {
                    handle_got_response();

                    error = new exceptions.SignatureVerificationError(generated_signature, signature);
                    call_callback(error, false, response);
                    return;
                }
            }

            if (status.toLowerCase() === 'ok') {
                handle_got_response();

                call_callback(null, true, response);
                return;
            }
            else if (status === 'no_such_client') {
                handle_got_response();

                error = new exceptions.InvaliClientError(self._client_id);
                call_callback(error, false, response);
                return;
            }
            else if (status === 'replayed_otp' || status === 'bad_otp' ||
                     status === 'bad_signature') {
                handle_got_response();

                error = new exceptions.StatusCodeError(status);
                call_callback(error, false, response);
                return;
            }
            else if (status === 'replayed_request') {
                return;
            }

            handle_got_response();
            error = new exceptions.StatusCodeError(status);
            call_callback(error, false, response);
        });
    };

    var handle_error = function(err) {
        client.removeAllListeners('response');
        call_callback(err, false);
    };

    var handle_timeout = function(client) {
        if (got_response || callback_called) {
            return;
        }

        if (client.writable) {
            client.destroy();
        }

        client_timed_out_count++;
        client.removeAllListeners('response');

        if (client_timed_out_count === client_objects.length) {
            call_callback(new exceptions.ConnectionTimeoutError(timeout));
        }
    };

    port = (this._use_https) ? 443 : 80;
    for (i = 0; i < constants.API_HOSTS.length ; i++) {
        host = constants.API_HOSTS[i];
        path = sprintf('%s?%s', constants.API_PATH, query_string);

        client = http.createClient(port, host, this._use_https);
        request = client.request('GET', path, {
            'host': host,
            'User-Agent': sprintf('NodeJS Yubico library v%s.%s.%s',
                                  constants.VERSION[0], constants.VERSION[1],
                                  constants.VERSION[2])
        });

        request.end();
        client.on('error', handle_error);

        (function(client) {
            var timeout_id;

            timeout_id = setTimeout(function() {
                handle_timeout(client);
            }, timeout);

            request.on('response', function(response) {
                handle_response(response, client, timeout_id);
            });

            timeout_ids.push(timeout_id);
        })(client);

        client_objects.push(client);
        request_objects.push(request);
    }
};

/**
 * Generate a query string which is sent to the validation servers.
 *
 * @param {String} otp Yubikey token.
 * @param {String} nonce  16 to 40 characters long string with random data.
 * @param {String} sl A value 0 to 100 indicating percentage of syncing required
 *                    by client, or strings "fast" or "secure" to use
 *                    server-configured values; if
 *                    absent, let the server decides.
 * @param {Number} timeout Number of seconds to wait for sync responses; if
 *                         absent, let the server decide.
 * @return {String} Generated query string.
 */
Yubico.prototype._generate_query_string = function(otp, nonce, timestamp, sl, timeout) {
    var data, query_string, hmac_signature;

    data = { 'id': this._client_id,
             'otp': otp,
             'nonce': nonce
    };

    if (timestamp) {
        data.timestamp = 1;
    }

    if (sl) {
        if (sl < 0 || sl > 100 && [ 'fast', 'secure' ].indexOf(sl) === -1) {
            throw new Error('sl parameter value must be between 0 and 100 or string "fast" or "secure"');
        }

        data.sl = sl;
    }

    if (timeout) {
        data.timeout = timeout;
    }

    query_string = querystring.encode(data);

    if (this._key) {
        hmac_signature = this._generate_message_signature(query_string, this._key);
        hmac_signature = hmac_signature.replace(/\+/g, '%2B');
        query_string = sprintf('%s&h=%s', query_string, hmac_signature);
    }

    return query_string;
};

/**
 * Parse parameters from the response.
 *
 * @param {String} response Response string
 * @return {Array} Array where the first member is parsed signature and the
 *                second one if the rest of the parsed response returned as
 *                a query string.
 */
Yubico.prototype._parse_parameters_from_response = function(response) {
    var i, split = [], pairs, pair, signature, query_string;

    pairs = response.split('\n');
    for (i = 0; i < pairs.length; i++) {
        pair = pairs[i].trim();

        if (pair === '') {
            continue;
        }

        split.push(pair);
    }

    signature = split[0].replace('h=', '');
    query_string = split.slice(1).join('&');

    return [ signature, query_string ];
};

/**
  * Convert a query string to an object.
 *
 * @param {String} query_string Query string (e.g. foo=bar&bar=baz)
 * @return {Object} Object where the key is a parameter name and the value is
 *                  is the parameter value.
 */
Yubico.prototype._query_string_to_object = function(query_string) {
    var i, pairs, pair, parameters = {};

    pairs = query_string.split('&');
    for (i = 0; i < pairs.length; i++) {
        pair = pairs[i].trim();

        if (pair === '') {
            continue;
        }

        pair = pair.split('=');
        parameters[pair[0]] = pair[1];
    }

    return parameters;
};

/**
 * Generate HMAC-SHA1 signature for the given query string using the
 * provided key.
 *
 * @param {String} query_string Query string
 * @param {String} key Cryptographic key.
 * @return {String} base64 encoded signature.
 */
Yubico.prototype._generate_message_signature = function(query_string, key) {
    var i, pairs, pair, pairs_sorted, pairs_string, hmac, signature;
    pairs = query_string.split('&');

    pairs_sorted = [];
    for (i = 0; i < pairs.length; i++) {
        pair = pairs[i];
        pair = pair.split('=');
        pairs_sorted.push(pair);
    }

    pairs_sorted.sort();

    pairs = [];
    for (i = 0; i < pairs_sorted.length; i++) {
        pair = pairs_sorted[i];
        pairs[i] = pair.join('=');
    }

    pairs_string = pairs.join('&');

    hmac = crypto.createHmac('sha1', key);
    hmac.update(pairs_string);
    signature = hmac.digest('base64');

    return signature;
};

exports.Yubico = Yubico;
<|endoftext|>


<commit_before>// @flow

import ForerunnerDB from 'forerunnerdb';

import { parseMatch, parsePerson } from 'utils/parsers';

import type { MessageType } from 'types/message';
import type { MatchType } from 'types/match';
import type { PersonType } from 'types/person';
import type { ActionType } from 'types/action';

const fdb = new ForerunnerDB();
const db = fdb.db('tind3r');
db.debug(false);

export const collections = {
  matches: db.collection('matches'),
  persons: db.collection('persons'),
  messages: db.collection('messages'),
  actions: db.collection('actions'),
};

export function load() {
  let loaded = 0;

  window.db = db;

  return new Promise((resolve, reject) => {
    const checkIfLoaded = () => (loaded === 4 ? resolve() : null);
    const loadedCallback = err => {
      if (err) {
        reject(err);
      }

      loaded += 1;
      checkIfLoaded();
    };

    collections.matches.load(loadedCallback);
    collections.persons.load(loadedCallback);
    collections.messages.load(loadedCallback);
    collections.actions.load(loadedCallback);
  });
}

export function matchCollection(): Array<MatchType> {
  const matches: Array<MatchType> = collections.matches.find({}, {
    $join: [{
      persons: {
        _id: 'person_id',
        $as: 'person',
        // $require: true,
        $multi: false,
      },
    }],
  });

  matches.forEach(match => {
    const lastMessage: MessageType = collections.messages.find({
      match_id: match._id,
    }, {
      $limit: 1,
      $orderBy: {
        date: -1,
      },
    })[0];

    match.last_msg_from_db = lastMessage || {}; // eslint-disable-line
  });

  return matches;
}

export function getMessages(matchId: string): Array<MessageType> {
  const messages = collections.messages.find({
    match_id: matchId,
  });

  return messages;
}

export function getMatch(matchId: string): MatchType {
  return collections.matches.find({
    _id: matchId,
  })[0];
}

export function getMatchByPerson(personId: string): MatchType {
  return collections.matches.find({
    person_id: personId,
  })[0];
}

export function getPerson(personId: string): PersonType {
  return collections.persons.find({
    _id: personId,
  })[0];
}

export function createAction(payload: ActionType) {
  collections.actions.insert(payload);
  collections.actions.save();
}

export function getActions(personId?: string): Array<ActionType> {
  let query = {};

  if (personId) {
    query = { person_id: personId };
  }

  return collections.actions.find(query);
}

export function removeActions() {
  collections.actions.remove();
  collections.actions.save();
}

export function removeAction(personId: string) {
  collections.actions.remove({ person_id: personId });
  collections.actions.save();
}

export function createPersons(persons: Array<PersonType>) {
  const collection = collections.persons;
  collection.insert(persons);
  collection.save();
}

export function createMatches(data: Array<MatchType>) {
  const collection = collections.matches;
  collection.insert(data);
  collection.save();
}

export function createMatch(match: Object) {
  const parsedMatch: MatchType = parseMatch(match, true);
  const parsedPerson: PersonType = parsePerson(match.person);

  createMatches([parsedMatch]);
  createPersons([parsedPerson]);
}

export function updateMatch(matchIds: string[], payload: Object) {
  collections.matches.update({ _id: { $in: matchIds } }, payload);
  collections.matches.save();
}

export function updatePerson(personId: string, payload: Object) {
  collections.persons.updateById(personId, parsePerson(payload));
  collections.persons.save();
}

export function removeMessage(_id: string) {
  collections.messages.remove({ _id });
  collections.messages.save();
}

export function removeMatch(_id: string) {
  const removedMatch = collections.matches.remove({ _id });
  collections.messages.remove({ match_id: _id });
  collections.persons.remove({ _id: removedMatch.person_id });

  collections.matches.save();
  collections.messages.save();
  collections.persons.save();
}

export function purge() {
  collections.matches.remove();
  collections.messages.remove();
  collections.persons.remove();

  collections.matches.save();
  collections.messages.save();
  collections.persons.save();

  removeActions();
}
<commit_msg>Revert require person<commit_after>// @flow

import ForerunnerDB from 'forerunnerdb';

import { parseMatch, parsePerson } from 'utils/parsers';

import type { MessageType } from 'types/message';
import type { MatchType } from 'types/match';
import type { PersonType } from 'types/person';
import type { ActionType } from 'types/action';

const fdb = new ForerunnerDB();
const db = fdb.db('tind3r');
db.debug(false);

export const collections = {
  matches: db.collection('matches'),
  persons: db.collection('persons'),
  messages: db.collection('messages'),
  actions: db.collection('actions'),
};

export function load() {
  let loaded = 0;

  window.db = db;

  return new Promise((resolve, reject) => {
    const checkIfLoaded = () => (loaded === 4 ? resolve() : null);
    const loadedCallback = err => {
      if (err) {
        reject(err);
      }

      loaded += 1;
      checkIfLoaded();
    };

    collections.matches.load(loadedCallback);
    collections.persons.load(loadedCallback);
    collections.messages.load(loadedCallback);
    collections.actions.load(loadedCallback);
  });
}

export function matchCollection(): Array<MatchType> {
  const matches: Array<MatchType> = collections.matches.find({}, {
    $join: [{
      persons: {
        _id: 'person_id',
        $as: 'person',
        $require: true,
        $multi: false,
      },
    }],
  });

  matches.forEach(match => {
    const lastMessage: MessageType = collections.messages.find({
      match_id: match._id,
    }, {
      $limit: 1,
      $orderBy: {
        date: -1,
      },
    })[0];

    match.last_msg_from_db = lastMessage || {}; // eslint-disable-line
  });

  return matches;
}

export function getMessages(matchId: string): Array<MessageType> {
  const messages = collections.messages.find({
    match_id: matchId,
  });

  return messages;
}

export function getMatch(matchId: string): MatchType {
  return collections.matches.find({
    _id: matchId,
  })[0];
}

export function getMatchByPerson(personId: string): MatchType {
  return collections.matches.find({
    person_id: personId,
  })[0];
}

export function getPerson(personId: string): PersonType {
  return collections.persons.find({
    _id: personId,
  })[0];
}

export function createAction(payload: ActionType) {
  collections.actions.insert(payload);
  collections.actions.save();
}

export function getActions(personId?: string): Array<ActionType> {
  let query = {};

  if (personId) {
    query = { person_id: personId };
  }

  return collections.actions.find(query);
}

export function removeActions() {
  collections.actions.remove();
  collections.actions.save();
}

export function removeAction(personId: string) {
  collections.actions.remove({ person_id: personId });
  collections.actions.save();
}

export function createPersons(persons: Array<PersonType>) {
  const collection = collections.persons;
  collection.insert(persons);
  collection.save();
}

export function createMatches(data: Array<MatchType>) {
  const collection = collections.matches;
  collection.insert(data);
  collection.save();
}

export function createMatch(match: Object) {
  const parsedMatch: MatchType = parseMatch(match, true);
  const parsedPerson: PersonType = parsePerson(match.person);

  createMatches([parsedMatch]);
  createPersons([parsedPerson]);
}

export function updateMatch(matchIds: string[], payload: Object) {
  collections.matches.update({ _id: { $in: matchIds } }, payload);
  collections.matches.save();
}

export function updatePerson(personId: string, payload: Object) {
  collections.persons.updateById(personId, parsePerson(payload));
  collections.persons.save();
}

export function removeMessage(_id: string) {
  collections.messages.remove({ _id });
  collections.messages.save();
}

export function removeMatch(_id: string) {
  const removedMatch = collections.matches.remove({ _id });
  collections.messages.remove({ match_id: _id });
  collections.persons.remove({ _id: removedMatch.person_id });

  collections.matches.save();
  collections.messages.save();
  collections.persons.save();
}

export function purge() {
  collections.matches.remove();
  collections.messages.remove();
  collections.persons.remove();

  collections.matches.save();
  collections.messages.save();
  collections.persons.save();

  removeActions();
}
<|endoftext|>


<commit_before>import picoModal from 'picoModal'
import parseGPX from './gpx'

const AVAILABLE_THEMES = [
    'CartoDB.DarkMatter',
    'CartoDB.DarkMatterNoLabels',
    'CartoDB.Positron',
    'CartoDB.PositronNoLabels',
    'Esri.WorldImagery',
    'OpenStreetMap.Mapnik',
    'OpenStreetMap.BlackAndWhite',
    'Stamen.TerrainBackground',
    'Stamen.Toner',
    'Stamen.TonerLite',
    'Stamen.TonerBackground',
    'Stamen.Watercolor',
    'Thunderforest.OpenCycleMap',
    'Thunderforest.SpinalMap',
    'Thunderforest.Pioneer',
]

const MODAL_CONTENT = {
    help: `
dérive

Drag and drop one or more GPX files here

If you use Strava, you can obtain a ZIP file of your activity data
in GPX format on your profile
page and clicking "Download all your activities."

All processing happens in your browser. Your files will not be uploaded or
stored anywhere.

In a dérive one or more persons during a certain period drop their
relations, their work and leisure activities, and all their other
usual motives for movement and action, and let themselves be drawn by
the attractions of the terrain and the encounters they find there.

1

Code is available on GitHub.

`,

    export: `

Export Image

<form id="settings">

    Format:
    <select name="format">
      <option selected value="png">PNG</option>
      <option value="svg">SVG (no background map)</option>
    </select>

    <input id="render-export" type="button" value="Render">

</form>

`
}

// Adapted from: http://www.html5rocks.com/en/tutorials/file/dndfiles/
function handleFileSelect(map, evt) {
    evt.stopPropagation()
    evt.preventDefault()

    let tracks = []
    let files = evt.dataTransfer.files
    let modal = buildUploadModal(files.length)

    modal.show()

    let fileIndex = 0

    function loadNextFile() {
        if (fileIndex >= files.length) {
            tracks.forEach(t => {
                console.log('Adding track:', t.name)
                map.addTrack(t.points)
            })
            return modal.destroy()
        }

        let reader = new FileReader()
        reader.onload = (event) => {
            parseGPX(event.target.result, (err, track) => {
                // TODO: Make an error modal
                if (err) return window.alert(err)

                tracks.push(track)
                modal.progress(fileIndex)

                // do the next file, but give the UI time to update.
                window.setTimeout(loadNextFile, 1)
            })
        }

        reader.readAsBinaryString(files[fileIndex++])
    }

    loadNextFile()
}

function handleDragOver(evt) {
    evt.dataTransfer.dropEffect = 'copy'
    evt.stopPropagation()
    evt.preventDefault()
}

function buildUploadModal(numFiles) {
    function getModalContent(numLoaded) {
        return `
Reading GPX files...

${numLoaded} loaded of ${numFiles}`
    }

    let modal = picoModal({
        content: getModalContent(0),
        closeButton: false,
        escCloses: false,
        overlayClose: false,
        overlayStyles: (styles) => { styles.opacity = 0.1 }
    })

    modal.progress = (loaded) => {
        modal.modalElem().innerHTML = getModalContent(loaded)
    }

    return modal
}

export function buildSettingsModal(opts, finishCallback) {
    let themes = AVAILABLE_THEMES.map(t => {
        let selected = t == opts.theme ? 'selected' : ''

        return `<option ${selected} value="${t}">${t}</option>`
    })

    let modalContent = `

Options

<form id="settings">

    Theme
    <select name="theme">
       ${themes}
    </select>

    Line color
    <input name="color" type="color" value=${opts.lineOptions.color}>

    Line opacity
    <input name="opacity" type="range" min=0 max=1 step=0.01
           value=${opts.lineOptions.opacity}>

    Line width
    <input name="weight" type="number" min=1 max=100
           value=${opts.lineOptions.weight}>

</form>
`
    let modal = picoModal({
        content: modalContent,
        closeButton: true,
        escCloses: true,
        overlayClose: true,
        overlayStyles: (styles) => { styles.opacity = 0.1 }
    })

    modal.afterClose((modal) => {
        let elements = document.getElementById('settings').elements
        let options = Object.assign({}, opts)

        let _ = ['theme'].forEach(n => {
            options[n] = elements[n].value
        })

        _ = ['color', 'weight', 'opacity'].forEach(n => {
            options.lineOptions[n] = elements[n].value
        })

        finishCallback(options)
        modal.destroy()
    })

    return modal
}

export function showModal(type) {
    let modal = picoModal({
        content: MODAL_CONTENT[type],
        overlayStyles: (styles) => { styles.opacity = 0.01 }
    }).afterClose(() => modal.destroy())

    modal.show()

    return modal
}

export function initialize(map) {
    let modal = showModal('help')

    window.addEventListener('dragover', handleDragOver, false)

    window.addEventListener('drop', e => {
        if (!modal.destroyed) {
            modal.destroy()
            modal.destroyed = true
        }
        handleFileSelect(map, e)
    }, false)
}
<commit_msg>Remove maps needing API keys<commit_after>import picoModal from 'picoModal'
import parseGPX from './gpx'

const AVAILABLE_THEMES = [
    'CartoDB.DarkMatter',
    'CartoDB.DarkMatterNoLabels',
    'CartoDB.Positron',
    'CartoDB.PositronNoLabels',
    'Esri.WorldImagery',
    'OpenStreetMap.Mapnik',
    'OpenStreetMap.BlackAndWhite',
    'Stamen.TerrainBackground',
    'Stamen.Toner',
    'Stamen.TonerLite',
    'Stamen.TonerBackground',
    'Stamen.Watercolor',
]

const MODAL_CONTENT = {
    help: `
dérive

Drag and drop one or more GPX files here

If you use Strava, you can obtain a ZIP file of your activity data
in GPX format on your profile
page and clicking "Download all your activities."

All processing happens in your browser. Your files will not be uploaded or
stored anywhere.

In a dérive one or more persons during a certain period drop their
relations, their work and leisure activities, and all their other
usual motives for movement and action, and let themselves be drawn by
the attractions of the terrain and the encounters they find there.

1

Code is available on GitHub.

`,

    export: `

Export Image

<form id="settings">

    Format:
    <select name="format">
      <option selected value="png">PNG</option>
      <option value="svg">SVG (no background map)</option>
    </select>

    <input id="render-export" type="button" value="Render">

</form>

`
}

// Adapted from: http://www.html5rocks.com/en/tutorials/file/dndfiles/
function handleFileSelect(map, evt) {
    evt.stopPropagation()
    evt.preventDefault()

    let tracks = []
    let files = evt.dataTransfer.files
    let modal = buildUploadModal(files.length)

    modal.show()

    let fileIndex = 0

    function loadNextFile() {
        if (fileIndex >= files.length) {
            tracks.forEach(t => {
                console.log('Adding track:', t.name)
                map.addTrack(t.points)
            })
            return modal.destroy()
        }

        let reader = new FileReader()
        reader.onload = (event) => {
            parseGPX(event.target.result, (err, track) => {
                // TODO: Make an error modal
                if (err) return window.alert(err)

                tracks.push(track)
                modal.progress(fileIndex)

                // do the next file, but give the UI time to update.
                window.setTimeout(loadNextFile, 1)
            })
        }

        reader.readAsBinaryString(files[fileIndex++])
    }

    loadNextFile()
}

function handleDragOver(evt) {
    evt.dataTransfer.dropEffect = 'copy'
    evt.stopPropagation()
    evt.preventDefault()
}

function buildUploadModal(numFiles) {
    function getModalContent(numLoaded) {
        return `
Reading GPX files...

${numLoaded} loaded of ${numFiles}`
    }

    let modal = picoModal({
        content: getModalContent(0),
        closeButton: false,
        escCloses: false,
        overlayClose: false,
        overlayStyles: (styles) => { styles.opacity = 0.1 }
    })

    modal.progress = (loaded) => {
        modal.modalElem().innerHTML = getModalContent(loaded)
    }

    return modal
}

export function buildSettingsModal(opts, finishCallback) {
    let themes = AVAILABLE_THEMES.map(t => {
        let selected = t == opts.theme ? 'selected' : ''

        return `<option ${selected} value="${t}">${t}</option>`
    })

    let modalContent = `

Options

<form id="settings">

    Theme
    <select name="theme">
       ${themes}
    </select>

    Line color
    <input name="color" type="color" value=${opts.lineOptions.color}>

    Line opacity
    <input name="opacity" type="range" min=0 max=1 step=0.01
           value=${opts.lineOptions.opacity}>

    Line width
    <input name="weight" type="number" min=1 max=100
           value=${opts.lineOptions.weight}>

</form>
`
    let modal = picoModal({
        content: modalContent,
        closeButton: true,
        escCloses: true,
        overlayClose: true,
        overlayStyles: (styles) => { styles.opacity = 0.1 }
    })

    modal.afterClose((modal) => {
        let elements = document.getElementById('settings').elements
        let options = Object.assign({}, opts)

        let _ = ['theme'].forEach(n => {
            options[n] = elements[n].value
        })

        _ = ['color', 'weight', 'opacity'].forEach(n => {
            options.lineOptions[n] = elements[n].value
        })

        finishCallback(options)
        modal.destroy()
    })

    return modal
}

export function showModal(type) {
    let modal = picoModal({
        content: MODAL_CONTENT[type],
        overlayStyles: (styles) => { styles.opacity = 0.01 }
    }).afterClose(() => modal.destroy())

    modal.show()

    return modal
}

export function initialize(map) {
    let modal = showModal('help')

    window.addEventListener('dragover', handleDragOver, false)

    window.addEventListener('drop', e => {
        if (!modal.destroyed) {
            modal.destroy()
            modal.destroyed = true
        }
        handleFileSelect(map, e)
    }, false)
}
<|endoftext|>


<commit_before>const { denodeify } = require('./util')
const bindLate = require('bind-late')
const chalk = require('chalk')
const read = denodeify(require('read'))
const Table = require('cli-table')

export default bindLate({
  color: false,

  chalk: _ => _.color != null
    ? new chalk.constructor({ enabled: _.color })
    : chalk,

  chevron: '>>',

  greyParens: _ => text =>
    _.chalk.enabled
      ? text
          .replace('(', chalk.styles.grey.open + '(')
          .replace(')', ')' + chalk.styles.grey.close)
      : text,

  ask: _ => question =>
    `${_.chalk.green(_.chevron)} ${_.greyParens(question)}`
      + (_.chalk.enabled ? chalk.styles.cyan.open : ''),

  prompt: _ => opts =>
    read(Object.assign({}, opts, { prompt: _.ask(opts.prompt) })),

  error: _ => (...args) =>
    console.error(_.chalk.red(_.chevron), ...args),

  info: _ => (...args) =>
    console.log(_.chalk.blue(_.chevron), ...args),

  date: date =>
    date.toISOString()
      .substr(0, 10),

  time: date =>
    date.toISOString()
      .substr(11, 9),

  dateTime: date =>
    date.toISOString()
      .replace('T', ' ')
      .substr(0, 19),

  title: _ => title =>
    title + '\n'
      + _.chalk.gray(String.repeat('─', _.chalk.stripColor(title).length)),

  post: _ => post =>
    _.chalk.enabled
      ? post.replace(/:[\w\d]+:/g, _.chalk.yellow('$&'))
      : post,

  table: (opts, rows) => {
    const table = new Table(opts)
    table.push(...rows)
    return table.toString()
  },
})
<commit_msg>Standard style<commit_after>const { denodeify } = require('./util')
const bindLate = require('bind-late')
const chalk = require('chalk')
const read = denodeify(require('read'))
const Table = require('cli-table')

export default bindLate({
  color: false,

  chalk: _ => _.color != null
    ? new chalk.constructor({ enabled: _.color })
    : chalk,

  chevron: '>>',

  greyParens: _ => text =>
    _.chalk.enabled
      ? text
          .replace('(', chalk.styles.grey.open + '(')
          .replace(')', ')' + chalk.styles.grey.close)
      : text,

  ask: _ => question =>
    `${_.chalk.green(_.chevron)} ${_.greyParens(question)}` +
      (_.chalk.enabled ? chalk.styles.cyan.open : ''),

  prompt: _ => opts =>
    read(Object.assign({}, opts, { prompt: _.ask(opts.prompt) })),

  error: _ => (...args) =>
    console.error(_.chalk.red(_.chevron), ...args),

  info: _ => (...args) =>
    console.log(_.chalk.blue(_.chevron), ...args),

  date: date =>
    date.toISOString()
      .substr(0, 10),

  time: date =>
    date.toISOString()
      .substr(11, 9),

  dateTime: date =>
    date.toISOString()
      .replace('T', ' ')
      .substr(0, 19),

  title: _ => title =>
    title + '\n' +
      _.chalk.gray(String.repeat('─', _.chalk.stripColor(title).length)),

  post: _ => post =>
    _.chalk.enabled
      ? post.replace(/:[\w\d]+:/g, _.chalk.yellow('$&'))
      : post,

  table: (opts, rows) => {
    const table = new Table(opts)
    table.push(...rows)
    return table.toString()
  },
})
<|endoftext|>


<commit_before>import picoModal from 'picomodal';
import parseGPX from './gpx';
import Image from './image';

const AVAILABLE_THEMES = [
    'CartoDB.DarkMatter',
    'CartoDB.DarkMatterNoLabels',
    'CartoDB.Positron',
    'CartoDB.PositronNoLabels',
    'Esri.WorldImagery',
    'OpenStreetMap.Mapnik',
    'OpenStreetMap.BlackAndWhite',
    'OpenTopoMap',
    'Stamen.Terrain',
    'Stamen.TerrainBackground',
    'Stamen.Toner',
    'Stamen.TonerLite',
    'Stamen.TonerBackground',
    'Stamen.Watercolor',
];

const MODAL_CONTENT = {
    help: `

dérive

Drag and drop one or more GPX or JPG files here

If you use Strava, you can obtain a ZIP file of your activity data
in GPX format on your profile
page and clicking "Download all your activities."

All processing happens in your browser. Your files will not be uploaded or
stored anywhere.

In a dérive one or more persons during a certain period drop their
relations, their work and leisure activities, and all their other
usual motives for movement and action, and let themselves be drawn by
the attractions of the terrain and the encounters they find there.

1

Code is available on GitHub.

`,

    exportImage: `

Export Image

<form id="export-settings">

        Format:
        <select name="format">
            <option selected value="png">PNG</option>
            <option value="svg">SVG (no background map)</option>
        </select>

        <input id="render-export" type="button" value="Render">

</form>

`
};

// Adapted from: http://www.html5rocks.com/en/tutorials/file/dndfiles/
function handleFileSelect(map, evt) {
    evt.stopPropagation();
    evt.preventDefault();

    let tracks = [];
    let files = Array.from(evt.dataTransfer.files);
    let modal = buildUploadModal(files.length);

    modal.show();

    let parseGpx = file => new Promise(resolve => {
        parseGPX(file)
            .then(parsedTracks => {
                parsedTracks.forEach(track => {
                    track.filename = file.name;
                    tracks.push(track);
                    map.addTrack(track);
                    modal.addSuccess();
                });
            })
            .then(resolve);
    });

    let handleGpx = file => new Promise(resolve => {
        let reader = new FileReader();
        reader.onload = () => {
            parseGpx(reader.result);
            resolve();
        };
        reader.readAsText(file, 'UTF-8');
    });

    let handleImage = file => new Promise(resolve => {
        let image = new Image(file);

        image.extractExifData().then((image) => {
            if (!image) {
                modal.addFailure({name: file.name, error: 'No geolocation data'});
                return resolve();
            }

            map.addImage(image);
            modal.addSuccess();
            resolve();
        });
    });

    let detectFileType = file => new Promise(resolve => {
        let extension = file.name.split('.').pop().toLowerCase();

        switch (extension) {
            case 'gpx':
                handleGpx(file)
                    .then(resolve);
                break;
            case 'jpg':
                handleImage(file)
                    .then(resolve);
                break;
            default:
                console.log(`File ${file.name} is an unsupported format.`);
                modal.addFailure({name: file.name, error: 'Unsupported file format'});
                resolve();
        }
    });

    let loadFile = file => detectFileType(file)
        .catch(err => {
            modal.addFailure({name: file.name, error: err});
        });

    Promise.all(files.map(loadFile)).then(() => {
        map.center();

        modal.finished();
    });
}

function handleDragOver(evt) {
    evt.dataTransfer.dropEffect = 'copy';
    evt.stopPropagation();
    evt.preventDefault();
}

function buildUploadModal(numFiles) {
    let numLoaded = 0;
    let failures = [];
    let getModalContent = () => `

Reading files...

${numLoaded} loaded${
            failures.length ? `, <span class='failures'>${failures.length} failed</span>` : ``
        } of ${numFiles}
`;

    let modal = picoModal({
        content: getModalContent(),
        escCloses: false,
        overlayClose: false,
        overlayStyles: styles => {
            styles.opacity = 0.1;
        },
    });

    modal.afterCreate(() => {
        // Do not allow the modal to be closed before loading is complete.
        // PicoModal does not allow for native toggling
        modal.closeElem().style.display = 'none';
    });

    modal.afterClose(() => modal.destroy());

    // Override the content of the modal, without removing the close button.
    // PicoModal does not allow for native content overwriting.
    modal.setContent = body => {
        Array.from(modal.modalElem().childNodes).forEach(child => {
            if (child !== modal.closeElem()) {
                modal.modalElem().removeChild(child);
            }
        });

        modal.modalElem().insertAdjacentHTML('afterbegin', body);
    };

    modal.addFailure = failure => {
        failures.push(failure);
        modal.setContent(getModalContent());
    };

    modal.addSuccess = () => {
        numLoaded++;
        modal.setContent(getModalContent());
    };

    // Show any errors, or close modal if no errors occurred
    modal.finished = () => {
        if (failures.length === 0) {
            return modal.close();
        }

        let failedItems = failures.map(failure => `<li>${failure.name}</li>`);
        modal.setContent(`

Files loaded

                Loaded ${numLoaded},

                    ${failures.length} failure${failures.length === 1 ? '' : 's'}:

${failedItems.join('')}
`);
        // enable all the methods of closing the window
        modal.closeElem().style.display = '';
        modal.options({
            escCloses: true,
            overlayClose: true,
        });
    };

    return modal;
}

export function buildSettingsModal(tracks, opts, finishCallback) {
    let overrideExisting = opts.lineOptions.overrideExisting ? 'checked' : '';

    if (tracks.length > 0) {
        let allSameColor = tracks.every(trk => {
            return trk.options.color === tracks[0].options.color;
        });

        if (!allSameColor) {
            overrideExisting = false;
        } else {
            opts.lineOptions.color = tracks[0].options.color;
        }
    }

    let detect = opts.lineOptions.detectColors ? 'checked' : '';
    let themes = AVAILABLE_THEMES.map(t => {
        let selected = (t === opts.theme) ? 'selected' : '';

        return `<option ${selected} value="${t}">${t}</option>`;
    });

    let modalContent = `

Options

<form id="settings">

        Theme
        <select name="theme">
            ${themes}
        </select>

        Line color
        <input name="color" type="color" value=${opts.lineOptions.color}>

        Line opacity
        <input name="opacity" type="range" min=0 max=1 step=0.01
            value=${opts.lineOptions.opacity}>

        Line width
        <input name="weight" type="number" min=1 max=100
            value=${opts.lineOptions.weight}>

        Marker color
        <input name="markerColor" type="color" value=${opts.markerOptions.color}>

        Marker opacity
        <input name="markerOpacity" type="range" min=0 max=1 step=0.01
            value=${opts.markerOptions.opacity}>

        Marker width
        <input name="markerWeight" type="number" min=1 max=100
            value=${opts.markerOptions.weight}>

        Marker radius
        <input name="markerRadius" type="number" min=1 max=100
            value=${opts.markerOptions.radius}>

        Override existing tracks
        <input name="overrideExisting" type="checkbox" ${overrideExisting}>

        Detect color from Strava bulk export
        <input name="detectColors" type="checkbox" ${detect}>

</form>`;

    let modal = picoModal({
        content: modalContent,
        closeButton: true,
        escCloses: true,
        overlayClose: true,
        overlayStyles: (styles) => {
            styles.opacity = 0.1;
        },
    });

    modal.afterClose((modal) => {
        let elements = document.getElementById('settings').elements;
        let options = Object.assign({}, opts);

        for (let opt of ['theme']) {
            options[opt] = elements[opt].value;
        }

        for (let opt of ['color', 'weight', 'opacity']) {
            options.lineOptions[opt] = elements[opt].value;
        }

        for (let opt of ['markerColor', 'markerWeight', 'markerOpacity', 'markerRadius']) {
            let optionName = opt.replace('marker', '').toLowerCase();
            options.markerOptions[optionName] = elements[opt].value;
        }

        for (let opt of ['overrideExisting', 'detectColors']) {
            options.lineOptions[opt] = elements[opt].checked;
        }

        finishCallback(options);
        modal.destroy();
    });

    return modal;
}

export function showModal(type) {
    let modal = picoModal({
        content: MODAL_CONTENT[type],
        overlayStyles: (styles) => {
            styles.opacity = 0.01;
        },
    });

    modal.show();

    return modal;
}

export function initialize(map) {
    let modal = showModal('help');

    window.addEventListener('dragover', handleDragOver, false);

    window.addEventListener('drop', e => {
        if (!modal.destroyed) {
            modal.destroy();
            modal.destroyed = true;
        }
        handleFileSelect(map, e);
    }, false);
}
<commit_msg>Simplify the GPX handler<commit_after>import picoModal from 'picomodal';
import parseGPX from './gpx';
import Image from './image';

const AVAILABLE_THEMES = [
    'CartoDB.DarkMatter',
    'CartoDB.DarkMatterNoLabels',
    'CartoDB.Positron',
    'CartoDB.PositronNoLabels',
    'Esri.WorldImagery',
    'OpenStreetMap.Mapnik',
    'OpenStreetMap.BlackAndWhite',
    'OpenTopoMap',
    'Stamen.Terrain',
    'Stamen.TerrainBackground',
    'Stamen.Toner',
    'Stamen.TonerLite',
    'Stamen.TonerBackground',
    'Stamen.Watercolor',
];

const MODAL_CONTENT = {
    help: `

dérive

Drag and drop one or more GPX or JPG files here

If you use Strava, you can obtain a ZIP file of your activity data
in GPX format on your profile
page and clicking "Download all your activities."

All processing happens in your browser. Your files will not be uploaded or
stored anywhere.

In a dérive one or more persons during a certain period drop their
relations, their work and leisure activities, and all their other
usual motives for movement and action, and let themselves be drawn by
the attractions of the terrain and the encounters they find there.

1

Code is available on GitHub.

`,

    exportImage: `

Export Image

<form id="export-settings">

        Format:
        <select name="format">
            <option selected value="png">PNG</option>
            <option value="svg">SVG (no background map)</option>
        </select>

        <input id="render-export" type="button" value="Render">

</form>

`
};

// Adapted from: http://www.html5rocks.com/en/tutorials/file/dndfiles/
function handleFileSelect(map, evt) {
    evt.stopPropagation();
    evt.preventDefault();

    let tracks = [];
    let files = Array.from(evt.dataTransfer.files);
    let modal = buildUploadModal(files.length);

    modal.show();

    let handleGpx = file => new Promise(resolve => {
        let reader = new FileReader();
        reader.onload = () => {
            parseGPX(reader.result)
                .then(parsedTracks => {
                    parsedTracks.forEach(track => {
                        track.filename = file.name;
                        tracks.push(track);
                        map.addTrack(track);
                        modal.addSuccess();
                    });
                })
                .then(resolve);
        };
        reader.readAsText(file, 'UTF-8');
    });

    let handleImage = file => new Promise(resolve => {
        let image = new Image(file);

        image.extractExifData().then((image) => {
            if (!image) {
                modal.addFailure({name: file.name, error: 'No geolocation data'});
                return resolve();
            }

            map.addImage(image);
            modal.addSuccess();
            resolve();
        });
    });

    let detectFileType = file => new Promise(resolve => {
        let extension = file.name.split('.').pop().toLowerCase();

        switch (extension) {
            case 'gpx':
                handleGpx(file)
                    .then(resolve);
                break;
            case 'jpg':
                handleImage(file)
                    .then(resolve);
                break;
            default:
                console.log(`File ${file.name} is an unsupported format.`);
                modal.addFailure({name: file.name, error: 'Unsupported file format'});
                resolve();
        }
    });

    let loadFile = file => detectFileType(file)
        .catch(err => {
            modal.addFailure({name: file.name, error: err});
        });

    Promise.all(files.map(loadFile)).then(() => {
        map.center();

        modal.finished();
    });
}

function handleDragOver(evt) {
    evt.dataTransfer.dropEffect = 'copy';
    evt.stopPropagation();
    evt.preventDefault();
}

function buildUploadModal(numFiles) {
    let numLoaded = 0;
    let failures = [];
    let getModalContent = () => `

Reading files...

${numLoaded} loaded${
            failures.length ? `, <span class='failures'>${failures.length} failed</span>` : ``
        } of ${numFiles}
`;

    let modal = picoModal({
        content: getModalContent(),
        escCloses: false,
        overlayClose: false,
        overlayStyles: styles => {
            styles.opacity = 0.1;
        },
    });

    modal.afterCreate(() => {
        // Do not allow the modal to be closed before loading is complete.
        // PicoModal does not allow for native toggling
        modal.closeElem().style.display = 'none';
    });

    modal.afterClose(() => modal.destroy());

    // Override the content of the modal, without removing the close button.
    // PicoModal does not allow for native content overwriting.
    modal.setContent = body => {
        Array.from(modal.modalElem().childNodes).forEach(child => {
            if (child !== modal.closeElem()) {
                modal.modalElem().removeChild(child);
            }
        });

        modal.modalElem().insertAdjacentHTML('afterbegin', body);
    };

    modal.addFailure = failure => {
        failures.push(failure);
        modal.setContent(getModalContent());
    };

    modal.addSuccess = () => {
        numLoaded++;
        modal.setContent(getModalContent());
    };

    // Show any errors, or close modal if no errors occurred
    modal.finished = () => {
        if (failures.length === 0) {
            return modal.close();
        }

        let failedItems = failures.map(failure => `<li>${failure.name}</li>`);
        modal.setContent(`

Files loaded

                Loaded ${numLoaded},

                    ${failures.length} failure${failures.length === 1 ? '' : 's'}:

${failedItems.join('')}
`);
        // enable all the methods of closing the window
        modal.closeElem().style.display = '';
        modal.options({
            escCloses: true,
            overlayClose: true,
        });
    };

    return modal;
}

export function buildSettingsModal(tracks, opts, finishCallback) {
    let overrideExisting = opts.lineOptions.overrideExisting ? 'checked' : '';

    if (tracks.length > 0) {
        let allSameColor = tracks.every(trk => {
            return trk.options.color === tracks[0].options.color;
        });

        if (!allSameColor) {
            overrideExisting = false;
        } else {
            opts.lineOptions.color = tracks[0].options.color;
        }
    }

    let detect = opts.lineOptions.detectColors ? 'checked' : '';
    let themes = AVAILABLE_THEMES.map(t => {
        let selected = (t === opts.theme) ? 'selected' : '';

        return `<option ${selected} value="${t}">${t}</option>`;
    });

    let modalContent = `

Options

<form id="settings">

        Theme
        <select name="theme">
            ${themes}
        </select>

        Line color
        <input name="color" type="color" value=${opts.lineOptions.color}>

        Line opacity
        <input name="opacity" type="range" min=0 max=1 step=0.01
            value=${opts.lineOptions.opacity}>

        Line width
        <input name="weight" type="number" min=1 max=100
            value=${opts.lineOptions.weight}>

        Marker color
        <input name="markerColor" type="color" value=${opts.markerOptions.color}>

        Marker opacity
        <input name="markerOpacity" type="range" min=0 max=1 step=0.01
            value=${opts.markerOptions.opacity}>

        Marker width
        <input name="markerWeight" type="number" min=1 max=100
            value=${opts.markerOptions.weight}>

        Marker radius
        <input name="markerRadius" type="number" min=1 max=100
            value=${opts.markerOptions.radius}>

        Override existing tracks
        <input name="overrideExisting" type="checkbox" ${overrideExisting}>

        Detect color from Strava bulk export
        <input name="detectColors" type="checkbox" ${detect}>

</form>`;

    let modal = picoModal({
        content: modalContent,
        closeButton: true,
        escCloses: true,
        overlayClose: true,
        overlayStyles: (styles) => {
            styles.opacity = 0.1;
        },
    });

    modal.afterClose((modal) => {
        let elements = document.getElementById('settings').elements;
        let options = Object.assign({}, opts);

        for (let opt of ['theme']) {
            options[opt] = elements[opt].value;
        }

        for (let opt of ['color', 'weight', 'opacity']) {
            options.lineOptions[opt] = elements[opt].value;
        }

        for (let opt of ['markerColor', 'markerWeight', 'markerOpacity', 'markerRadius']) {
            let optionName = opt.replace('marker', '').toLowerCase();
            options.markerOptions[optionName] = elements[opt].value;
        }

        for (let opt of ['overrideExisting', 'detectColors']) {
            options.lineOptions[opt] = elements[opt].checked;
        }

        finishCallback(options);
        modal.destroy();
    });

    return modal;
}

export function showModal(type) {
    let modal = picoModal({
        content: MODAL_CONTENT[type],
        overlayStyles: (styles) => {
            styles.opacity = 0.01;
        },
    });

    modal.show();

    return modal;
}

export function initialize(map) {
    let modal = showModal('help');

    window.addEventListener('dragover', handleDragOver, false);

    window.addEventListener('drop', e => {
        if (!modal.destroyed) {
            modal.destroy();
            modal.destroyed = true;
        }
        handleFileSelect(map, e);
    }, false);
}
<|endoftext|>


<commit_before>module.exports = RigidBody;

var Shape = require('../shapes/Shape')
,   Vec3 = require('../math/Vec3')
,   Mat3 = require('../math/Mat3')
,   Quaternion = require('../math/Quaternion')
,   Particle = require('./Particle')
,   Material = require('../material/Material')

/**
 * Rigid body base class
 * @class RigidBody
 * @constructor
 * @param {Number} mass
 * @param {Shape} shape
 * @param {Material} material
 */
function RigidBody(mass,shape,material){

    // Check input
    if(typeof(mass)!=="number"){
        throw new Error("Argument 1 (mass) must be a number.");
    }
    if(typeof(material)!=="undefined" && !(material instanceof(Material))){
        throw new Error("Argument 3 (material) must be an instance of Material.");
    }

    Particle.call(this,mass,material);

    var that = this;

    /**
     * Rotational force on the body, around center of mass
     * @property Vec3 tau
     * @todo should be renamed to .angularForce
     */
    this.tau = new Vec3();

    /**
     * Orientation of the body
     * @property quaternion
     * @type {Quaternion}
     */
    this.quaternion = new Quaternion();

    /**
     * @property initQuaternion
     * @type {Quaternion}
     */
    this.initQuaternion = new Quaternion();

    /**
     * @property angularVelocity
     * @type {Vec3}
     */
    this.angularVelocity = new Vec3();

    /**
     * @property initAngularVelocity
     * @type {Vec3}
     */
    this.initAngularVelocity = new Vec3();

    /**
     * @property shape
     * @type {Shape}
     */
    this.shape = shape;

    /**
     * @property inertia
     * @type {Vec3}
     */
    this.inertia = new Vec3();
    shape.calculateLocalInertia(mass,this.inertia);

    this.inertiaWorld = new Vec3();
    this.inertia.copy(this.inertiaWorld);

    /**
     * @property invInertia
     * @type {Vec3}
     */
    this.invInertia = new Vec3(this.inertia.x>0 ? 1.0/this.inertia.x : 0,
                               this.inertia.y>0 ? 1.0/this.inertia.y : 0,
                               this.inertia.z>0 ? 1.0/this.inertia.z : 0);
    this.invInertiaWorld = new Mat3();

    /**
     * @property angularDamping
     * @type {Number}
     */
    this.angularDamping = 0.01; // Perhaps default should be zero here?

    /**
     * @property aabbmin
     * @type {Vec3}
     */
    this.aabbmin = new Vec3();

    /**
     * @property aabbmax
     * @type {Vec3}
     */
    this.aabbmax = new Vec3();

    /**
     * Indicates if the AABB needs to be updated before use.
     * @property aabbNeedsUpdate
     * @type {Boolean}
     */
    this.aabbNeedsUpdate = true;

    this.wlambda = new Vec3();

    this.updateInertiaWorld(true);
};

RigidBody.prototype = new Particle(0);
RigidBody.prototype.constructor = RigidBody;

/**
 * Convert a world point to local body frame.
 * @method pointToLocalFrame
 * @param  {Vec3} worldPoint
 * @param  {Vec3} result
 * @return {Vec3}
 */
RigidBody.prototype.pointToLocalFrame = function(worldPoint,result){
    var result = result || new Vec3;
    worldPoint.vsub(this.position,result);
    this.quaternion.conjugate().vmult(result,result);
    return result;
};

/**
 * Convert a local body point to world frame.
 * @method pointToWorldFrame
 * @param  {Vec3} worldPoint
 * @param  {Vec3} result
 * @return {Vec3}
 */
RigidBody.prototype.pointToWorldFrame = function(localPoint,result){
    var result = result || new Vec3;
    this.quaternion.vmult(localPoint,result);
    result.vadd(this.position,result);
    return result;
};

/**
 * Updates the .aabbmin and .aabbmax properties
 * @method computeAABB
 */
RigidBody.prototype.computeAABB = function(){
    this.shape.calculateWorldAABB(this.position,
                                  this.quaternion,
                                  this.aabbmin,
                                  this.aabbmax);
    this.aabbNeedsUpdate = false;
};

var uiw_m1 = new Mat3(),
    uiw_m2 = new Mat3(),
    uiw_m3 = new Mat3();

/**
 * Update .inertiaWorld and .invInertiaWorld
 * @method updateInertiaWorld
 */
RigidBody.prototype.updateInertiaWorld = function(force){
    var I = this.invInertia;
    if(I.x == I.y && I.y == I.z && !force){
        // If inertia M = s*I, where I is identity and s a scalar, then
        //    R*M*R' = R*(s*I)*R' = s*R*I*R' = s*R*R' = s*I = M
        // where R is the rotation matrix.
        // In other words, we don't have to transform the inertia if all
        // inertia diagonal entries are equal.
    } else {
        var m1 = uiw_m1,
            m2 = uiw_m2,
            m3 = uiw_m3;
        m1.setRotationFromQuaternion(this.quaternion);
        m1.transpose(m2);
        m1.scale(I,m1);
        m1.mmult(m2,this.invInertiaWorld);
        //m3.getTrace(this.invInertiaWorld);
    }

    /*
    this.quaternion.vmult(this.inertia,this.inertiaWorld);
    this.quaternion.vmult(this.invInertia,this.invInertiaWorld);
    */
};

/**
 * Apply force to a world point. This could for example be a point on the RigidBody surface. Applying force this way will add to Body.force and Body.tau.
 * @method applyForce
 * @param  {Vec3} force The amount of force to add.
 * @param  {Vec3} worldPoint A world point to apply the force on.
 */
var RigidBody_applyForce_r = new Vec3();
var RigidBody_applyForce_rotForce = new Vec3();
RigidBody.prototype.applyForce = function(force,worldPoint){
    // Compute point position relative to the body center
    var r = RigidBody_applyForce_r;
    worldPoint.vsub(this.position,r);

    // Compute produced rotational force
    var rotForce = RigidBody_applyForce_rotForce;
    r.cross(force,rotForce);

    // Add linear force
    this.force.vadd(force,this.force);

    // Add rotational force
    this.tau.vadd(rotForce,this.tau);
};

/**
 * Apply impulse to a world point. This could for example be a point on the RigidBody surface. An impulse is a force added to a body during a short period of time (impulse = force * time). Impulses will be added to Body.velocity and Body.angularVelocity.
 * @method applyImpulse
 * @param  {Vec3} impulse The amount of impulse to add.
 * @param  {Vec3} worldPoint A world point to apply the force on.
 */
var RigidBody_applyImpulse_r = new Vec3();
var RigidBody_applyImpulse_velo = new Vec3();
var RigidBody_applyImpulse_rotVelo = new Vec3();
RigidBody.prototype.applyImpulse = function(impulse,worldPoint){
    // Compute point position relative to the body center
    var r = RigidBody_applyImpulse_r;
    worldPoint.vsub(this.position,r);

    // Compute produced central impulse velocity
    var velo = RigidBody_applyImpulse_velo;
    impulse.copy(velo);
    velo.mult(this.invMass,velo);

    // Add linear impulse
    this.velocity.vadd(velo, this.velocity);

    // Compute produced rotational impulse velocity
    var rotVelo = RigidBody_applyImpulse_rotVelo;
    r.cross(impulse,rotVelo);

    /*
    rotVelo.x *= this.invInertia.x;
    rotVelo.y *= this.invInertia.y;
    rotVelo.z *= this.invInertia.z;
    */
    this.invInertiaWorld.vmult(rotVelo,rotVelo);

    // Add rotational Impulse
    this.angularVelocity.vadd(rotVelo, this.angularVelocity);
};
<commit_msg>added method .updateMassProperties to RigidBody<commit_after>module.exports = RigidBody;

var Shape = require('../shapes/Shape')
,   Vec3 = require('../math/Vec3')
,   Mat3 = require('../math/Mat3')
,   Quaternion = require('../math/Quaternion')
,   Particle = require('./Particle')
,   Material = require('../material/Material')

/**
 * Rigid body base class
 * @class RigidBody
 * @constructor
 * @param {Number} mass
 * @param {Shape} shape
 * @param {Material} material
 */
function RigidBody(mass,shape,material){

    // Check input
    if(typeof(mass)!=="number"){
        throw new Error("Argument 1 (mass) must be a number.");
    }
    if(typeof(material)!=="undefined" && !(material instanceof(Material))){
        throw new Error("Argument 3 (material) must be an instance of Material.");
    }

    Particle.call(this,mass,material);

    var that = this;

    /**
     * Rotational force on the body, around center of mass
     * @property Vec3 tau
     * @todo should be renamed to .angularForce
     */
    this.tau = new Vec3();

    /**
     * Orientation of the body
     * @property quaternion
     * @type {Quaternion}
     */
    this.quaternion = new Quaternion();

    /**
     * @property initQuaternion
     * @type {Quaternion}
     */
    this.initQuaternion = new Quaternion();

    /**
     * @property angularVelocity
     * @type {Vec3}
     */
    this.angularVelocity = new Vec3();

    /**
     * @property initAngularVelocity
     * @type {Vec3}
     */
    this.initAngularVelocity = new Vec3();

    /**
     * @property shape
     * @type {Shape}
     */
    this.shape = shape;

    /**
     * @property inertia
     * @type {Vec3}
     */
    this.inertia = new Vec3();

    /**
     * @property {Vec3} invInertia
     */
    this.invInertia = new Vec3();

    /**
     * @property {Mat3} invInertiaWorld
     */
    this.invInertiaWorld = new Mat3();

    /**
     * @property {Number} angularDamping
     */
    this.angularDamping = 0.01; // Perhaps default should be zero here?

    /**
     * @property aabbmin
     * @type {Vec3}
     */
    this.aabbmin = new Vec3();

    /**
     * @property aabbmax
     * @type {Vec3}
     */
    this.aabbmax = new Vec3();

    /**
     * Indicates if the AABB needs to be updated before use.
     * @property aabbNeedsUpdate
     * @type {Boolean}
     */
    this.aabbNeedsUpdate = true;

    this.wlambda = new Vec3();

    this.updateMassProperties();
};

RigidBody.prototype = new Particle(0);
RigidBody.prototype.constructor = RigidBody;

/**
 * Convert a world point to local body frame.
 * @method pointToLocalFrame
 * @param  {Vec3} worldPoint
 * @param  {Vec3} result
 * @return {Vec3}
 */
RigidBody.prototype.pointToLocalFrame = function(worldPoint,result){
    var result = result || new Vec3;
    worldPoint.vsub(this.position,result);
    this.quaternion.conjugate().vmult(result,result);
    return result;
};

/**
 * Convert a local body point to world frame.
 * @method pointToWorldFrame
 * @param  {Vec3} worldPoint
 * @param  {Vec3} result
 * @return {Vec3}
 */
RigidBody.prototype.pointToWorldFrame = function(localPoint,result){
    var result = result || new Vec3;
    this.quaternion.vmult(localPoint,result);
    result.vadd(this.position,result);
    return result;
};

/**
 * Updates the .aabbmin and .aabbmax properties
 * @method computeAABB
 */
RigidBody.prototype.computeAABB = function(){
    this.shape.calculateWorldAABB(this.position,
                                  this.quaternion,
                                  this.aabbmin,
                                  this.aabbmax);
    this.aabbNeedsUpdate = false;
};

var uiw_m1 = new Mat3(),
    uiw_m2 = new Mat3(),
    uiw_m3 = new Mat3();

/**
 * Update .inertiaWorld and .invInertiaWorld
 * @method updateInertiaWorld
 */
RigidBody.prototype.updateInertiaWorld = function(force){
    var I = this.invInertia;
    if(I.x == I.y && I.y == I.z && !force){
        // If inertia M = s*I, where I is identity and s a scalar, then
        //    R*M*R' = R*(s*I)*R' = s*R*I*R' = s*R*R' = s*I = M
        // where R is the rotation matrix.
        // In other words, we don't have to transform the inertia if all
        // inertia diagonal entries are equal.
    } else {
        var m1 = uiw_m1,
            m2 = uiw_m2,
            m3 = uiw_m3;
        m1.setRotationFromQuaternion(this.quaternion);
        m1.transpose(m2);
        m1.scale(I,m1);
        m1.mmult(m2,this.invInertiaWorld);
        //m3.getTrace(this.invInertiaWorld);
    }

    /*
    this.quaternion.vmult(this.inertia,this.inertiaWorld);
    this.quaternion.vmult(this.invInertia,this.invInertiaWorld);
    */
};

/**
 * Apply force to a world point. This could for example be a point on the RigidBody surface. Applying force this way will add to Body.force and Body.tau.
 * @method applyForce
 * @param  {Vec3} force The amount of force to add.
 * @param  {Vec3} worldPoint A world point to apply the force on.
 */
var RigidBody_applyForce_r = new Vec3();
var RigidBody_applyForce_rotForce = new Vec3();
RigidBody.prototype.applyForce = function(force,worldPoint){
    // Compute point position relative to the body center
    var r = RigidBody_applyForce_r;
    worldPoint.vsub(this.position,r);

    // Compute produced rotational force
    var rotForce = RigidBody_applyForce_rotForce;
    r.cross(force,rotForce);

    // Add linear force
    this.force.vadd(force,this.force);

    // Add rotational force
    this.tau.vadd(rotForce,this.tau);
};

/**
 * Apply impulse to a world point. This could for example be a point on the RigidBody surface. An impulse is a force added to a body during a short period of time (impulse = force * time). Impulses will be added to Body.velocity and Body.angularVelocity.
 * @method applyImpulse
 * @param  {Vec3} impulse The amount of impulse to add.
 * @param  {Vec3} worldPoint A world point to apply the force on.
 */
var RigidBody_applyImpulse_r = new Vec3();
var RigidBody_applyImpulse_velo = new Vec3();
var RigidBody_applyImpulse_rotVelo = new Vec3();
RigidBody.prototype.applyImpulse = function(impulse,worldPoint){
    // Compute point position relative to the body center
    var r = RigidBody_applyImpulse_r;
    worldPoint.vsub(this.position,r);

    // Compute produced central impulse velocity
    var velo = RigidBody_applyImpulse_velo;
    impulse.copy(velo);
    velo.mult(this.invMass,velo);

    // Add linear impulse
    this.velocity.vadd(velo, this.velocity);

    // Compute produced rotational impulse velocity
    var rotVelo = RigidBody_applyImpulse_rotVelo;
    r.cross(impulse,rotVelo);

    /*
    rotVelo.x *= this.invInertia.x;
    rotVelo.y *= this.invInertia.y;
    rotVelo.z *= this.invInertia.z;
    */
    this.invInertiaWorld.vmult(rotVelo,rotVelo);

    // Add rotational Impulse
    this.angularVelocity.vadd(rotVelo, this.angularVelocity);
};

/**
 * Should be called whenever you change the body mass.
 * @method updateMassProperties
 */
RigidBody.prototype.updateMassProperties = function(){
    this.invMass = this.mass>0 ? 1.0/this.mass : 0;
    this.shape.calculateLocalInertia(this.mass, this.inertia);
    this.invInertia.set(this.inertia.x>0 ? 1.0/this.inertia.x : 0,
                        this.inertia.y>0 ? 1.0/this.inertia.y : 0,
                        this.inertia.z>0 ? 1.0/this.inertia.z : 0);
    this.updateInertiaWorld(true);
};
<|endoftext|>


<commit_before>goog.provide('og.layer.XYZ');

goog.require('og.layer.Layer');
goog.require('og.quadTree');

og.layer.XYZ = function (name, options) {
    og.base(this, name, options);
};

og.extend(og.layer.XYZ, og.layer.Layer);

og.layer.XYZ.prototype.handleSegmentTile = function (material) {
    if (og.layer.requestsCounter >= og.layer.MAX_REQUESTS && this.counter) {
        this.pendingsQueue.push(material);
    } else {
        this.loadSegmentTileImage(material);
    }
};

og.layer.XYZ.prototype.GetHTTPRequestString = function (segment) {
    return og.layer.replaceTemplate(this.url, {
        "tilex": segment.tileX.toString(),
        "tiley": segment.tileY.toString(),
        "zoom": segment.zoomIndex.toString()
    });
};

og.layer.XYZ.prototype.loadSegmentTileImage = function (material) {
    var that = this;
    this.counter++;
    og.layer.requestsCounter++;
    var img = new Image();
    img.crossOrigin = '';
    img.onload = function () {
        that.events.callEvents(that.events.onload, {
            "image": this,
            "segment": material.segment
        });
        material.applyTexture.call(material, this);
        that.dequeueRequest();
    };

    img.onerror = function () {
        if (material) {
            material.textureNotExists.call(material);
        }
        that.dequeueRequest();
    };

    if (material.segment.materials.length) {
        img.src = this.GetHTTPRequestString(material.segment);
    } else {
        //Have to be that segment was clearified
        this.dequeueRequest();
    }
};

og.layer.XYZ.prototype.dequeueRequest = function () {
    this.counter--;
    og.layer.requestsCounter--;
    if (this.pendingsQueue.length && og.layer.requestsCounter < og.layer.MAX_REQUESTS) {
        var pmat;
        if (pmat = this.whilePendings())
            this.loadSegmentTileImage.call(this, pmat);
    }
};

og.layer.XYZ.prototype.whilePendings = function () {
    while (this.pendingsQueue.length) {
        var pmat = this.pendingsQueue.pop();
        if (pmat) {
            if (pmat.segment.node.getState() != og.quadTree.NOTRENDERING) {
                return pmat;
            } else {
                pmat.imageIsLoading = false;
            }
        }
    }
    return null;
};<commit_msg>You shouldn't to call onload if there are no events.<commit_after>goog.provide('og.layer.XYZ');

goog.require('og.layer.Layer');
goog.require('og.quadTree');

og.layer.XYZ = function (name, options) {
    og.base(this, name, options);
};

og.extend(og.layer.XYZ, og.layer.Layer);

og.layer.XYZ.prototype.handleSegmentTile = function (material) {
    if (og.layer.requestsCounter >= og.layer.MAX_REQUESTS && this.counter) {
        this.pendingsQueue.push(material);
    } else {
        this.loadSegmentTileImage(material);
    }
};

og.layer.XYZ.prototype.GetHTTPRequestString = function (segment) {
    return og.layer.replaceTemplate(this.url, {
        "tilex": segment.tileX.toString(),
        "tiley": segment.tileY.toString(),
        "zoom": segment.zoomIndex.toString()
    });
};

og.layer.XYZ.prototype.loadSegmentTileImage = function (material) {
    var that = this;
    this.counter++;
    og.layer.requestsCounter++;
    var img = new Image();
    img.crossOrigin = '';
    img.onload = function () {
        var e = that.events.onload;
        if (e.length) {
            that.events.callEvents(e, {
                "image": this,
                "segment": material.segment
            });
        }
        material.applyTexture.call(material, this);
        that.dequeueRequest();
    };

    img.onerror = function () {
        if (material) {
            material.textureNotExists.call(material);
        }
        that.dequeueRequest();
    };

    if (material.segment.materials.length) {
        img.src = this.GetHTTPRequestString(material.segment);
    } else {
        //Have to be that segment was clearified
        this.dequeueRequest();
    }
};

og.layer.XYZ.prototype.dequeueRequest = function () {
    this.counter--;
    og.layer.requestsCounter--;
    if (this.pendingsQueue.length && og.layer.requestsCounter < og.layer.MAX_REQUESTS) {
        var pmat;
        if (pmat = this.whilePendings())
            this.loadSegmentTileImage.call(this, pmat);
    }
};

og.layer.XYZ.prototype.whilePendings = function () {
    while (this.pendingsQueue.length) {
        var pmat = this.pendingsQueue.pop();
        if (pmat) {
            if (pmat.segment.node.getState() != og.quadTree.NOTRENDERING) {
                return pmat;
            } else {
                pmat.imageIsLoading = false;
            }
        }
    }
    return null;
};<|endoftext|>