<commit_before>var _ = require('lodash');
var url_utils = require('url');

var utils = require('../utils');
var BaseError = utils.BaseError;
var Extendable = utils.Extendable;

var events = require('../events');
var Eventable = events.Eventable;

var HttpApiError = BaseError.extend(function(self) {
    /**class:HttpApiError()

    Thrown when an error occurs while making and checking an HTTP request and
    the corresponding API reply.
    */
    self.name = "HttpApiError";
});

var HttpRequestError = BaseError.extend(function(self, request, reason) {
    /**class:HttpRequestError(request, reason)

    Thrown when an error occurs while making and checking an HTTP request.

    :param HttpRequest request: the request.
    :param string reason: the reason for the failure. Optional.
    */
    self.name = "HttpRequestError";
    self.request = request;
    self.reason = reason || null;

    if (self.reason) {
        self.message = [self.reason, self.request].join(' ');
    } else {
        self.message = self.request.toString();
    }
});

var HttpResponseError = BaseError.extend(function(self, response, reason) {
    /**class:HttpResponseError(response, reason)

    Thrown when an error response is returned by an HTTP request or if the HTTP
    response body cannot be parsed.

    :param HttpResponse response: the response.
    :param string reason: the reason for the failure. Optional.
    */
    self.name = "HttpResponseError";
    self.response = response;
    self.reason = reason || null;

    if (self.reason) {
        self.message = [self.reason, self.response].join(' ');
    } else {
        self.message = self.response.toString();
    }
});

var HttpRequest = Extendable.extend(function(self, method, url, opts) {
    /**class:HttpRequest(request, code, opts)

    Encapsulates information about an HTTP request made by the
    :class:`HttpApi`. Once :meth:`HttpRequest.encode` has been invoked,
    the request's data is encoded and made available as the request's body.

    :param string method:
        the HTTP request method.
    :param string url:
        the url to send the request to.
    :param string opts.data:
        the request's data to be encoded as the request's body. Optional.
    :param opts.params:
        An object of key-value pairs to append to the URL as query
        parameters. Can be in any form accepted by node.js's querystring
        module
    :param string opts.verify_options:
        A list of options to verify when doing an HTTPS requesty. Optional.
    */
    Extendable.call(self);

    opts = _.defaults(opts || {}, {
        headers: null,
        body: null,
        data: null,
        params: {},
        encoder: _.identity,
        verify_options: null
    });

    self.url = url;
    if (_.isString(self.url)) {
        self.url = url_utils.format(url_utils.parse(url));
    }

    self.method = method.toUpperCase();
    self.headers = opts.headers;
    self.data = opts.data;
    self.body = opts.body;
    self.params = opts.params;
    self.encoder = opts.encoder;
    self.verify_options = opts.verify_options;

    self.encode = function() {
        /**:HttpRequest.encode()

        Encodes the request data (if available).
        */
        if (self.data !== null) {
            try {
                self.body = self.encoder(self.data);
            }
            catch (e) {
                throw new HttpRequestError(
                    self, "Could not parse request (" + e.toString() + ")");
            }
        }
    };

    self.to_cmd = function() {
        /**:HttpRequest.to_cmd()

        Returns a sandbox API command that can be used to initiate this request
        via the sandbox API.
        */
        var cmd = {};
        cmd.data = {};
        cmd.name = 'http.' + self.method.toLowerCase();

        var url_data = url_utils.parse(self.url);
        url_data.query = self.params;
        cmd.data.url = url_utils.format(url_data);

        if (self.headers !== null) {
            cmd.data.headers = self.headers;
        }

        if (self.body !== null) {
            cmd.data.data = self.body;
        }

        if (self.verify_options !== null) {
            cmd.data.verify_options = self.verify_options;
        }

        return cmd;
    };

    self.toString = function() {
        var parts = [
            'HttpRequest:',
            self.method,
            self.url];

        if (self.body !== null) {
            parts.push('(body: ' + self.body + ')');
        }

        if (_.size(self.params)) {
            parts.push('(params: ' + JSON.stringify(self.params) + ')');
        }

        return '[' + parts.join(' ') + ']';
    };

});

var HttpResponse = Extendable.extend(function(self, request, code, opts) {
    /**class:HttpResponse(request, code, opts)

    Encapsulates information about an HTTP response given to the
    :class:`HttpApi`. Once :meth:`HttpResponse.decode` has been invoked,
    the response's body is decoded and made available as the response's data.

    :param HttpRequest request:
        the request that caused the response.
    :param integer code:
        the status code for the HTTP response.
    :param string opts.body:
        the response's body to be decoded as the response's data. Optional.
    */
    Extendable.call(self);

    opts = _.defaults(opts || {}, {
        data: null,
        body: null,
        decoder: _.identity
    });

    self.request = request;
    self.code = code;
    self.data = opts.data;
    self.body = opts.body;
    self.decoder = opts.decoder;

    self.decode = function() {
        /**:HttpResponse.decode()

        Decodes the responses body (if available).
        */
        if (self.body !== null) {

            try {
                self.data = self.decoder(self.body);
            }
            catch (e) {
                throw new HttpResponseError(
                    self, "Could not parse response (" + e.toString() + ")");
            }
        }
    };

    self.toString = function() {
        var parts = [
            'HttpResponse:',
            self.code];

        if (self.body !== null) {
            parts.push('(body: ' + self.body + ')');
        }

        parts.push('(request: ' + self.request + ')');
        return parts.join(' ');
    };
});

var HttpApi = Eventable.extend(function(self, im, opts) {
    /**class:HttpApi(im, opts)

    A helper class for making HTTP requests via the HTTP sandbox resource.

    :param InteractionMachine im:
        The interaction machine to use when making requests.
    :param object opts.headers:
        Default headers to use in HTTP requests.
    :param object opts.auth:
        Adds a HTTP Basic authentication to the default headers. Should
        contain ``username`` and ``password`` attributes.
    */
    opts = _.defaults(opts || {}, {
        headers: {},
    });

    self.im = im;
    self.default_headers = opts.headers;

    self.make_auth = utils.basic_auth;

    if (opts.auth) {
        var auth = self.make_auth(opts.auth.username, opts.auth.password);
        self.default_headers.Authorization = [auth];
    }

    self.decode_response_body = function(body) {
        /**:HttpApi.decode_response_body(body)

        :param string body:
            The body to decode.

        Sub-classes should override this to decode the response body and throw
        an exception if the body cannot be parsed. This base implementation
        returns the body as-is (i.e. decoding is left to the code calling the
        :class:`HttpApi`).
        */
        return body;
    };

    self.encode_request_data = function(data) {
        /**:HttpApi.encode_request_data(data)

        :param object data:
            The data to encode.

        Sub-classes should override this to encode the request body and throw
        an exception if the data cannot be encoded. This base implementation
        returns the data as-is (i.e. encoding is left to code calling the
        :class:`HttpApi`).
        */
        return data;
    };

    self.create_headers = function(headers) {
        /**:HttpApi.create_headers(headers)

        Combines a set of custom headers with the default headers passed to
        :class:`HttpApi`.

        :param object headers:
            Additional HTTP headers. Attributes are header names. Values are
            header values.

        Returns the complete set of HTTP headers.
        */
        var all_headers = {};
        _.extend(all_headers, self.default_headers);
        _.extend(all_headers, headers);
        return all_headers;
    };

    self.parse_reply = function(reply, request) {
        /**:HttpApi.parse_reply(reply, request)

        Check an HTTP reply and throw an :class:`HttpRequestError` if the
        sandbox API command was unsuccessful, or otherwise parse the sandbox's
        reply into a response.  If the response status code is not in the 200
        range or the reply body cannot be decoded, throw an
        :class:`HttpResponseError`.

        Logs an error via the sandbox logging resource in an error is raised.

        :param object reply:
            Raw response to the sandbox API command.
        :param HttpRequest request:
            The request that initiated the sandbox API command.

        Returns an :class:`HttpResponse` or throws an :class:`HttpApiError`
        (either the :class:`HttpRequestError` or :class:`HttpResponseError`
        derivative, depending on what error occured).
        */
        var response = new HttpResponse(request, reply.code, {
            body: reply.body,
            decoder: self.decode_response_body
        });

        response.decode();

        if (response.code < 200 || response.code >= 300) {
            throw new HttpResponseError(response);
        }

        return response;
    };

    self.request = function(method, url, opts) {
        /**:HttpApi.request(method, url, opts)

        :param string method:
            The HTTP method to use (e.g. `GET`, `POST`).
        :param string url:
            The URL to make the request to. If you pass in query parameters
            using ``opts.params``, don't include any in the URL itself.
        :param opts.params:
            An object of key-value pairs to append to the URL as query
            parameters. Can be in any form accepted by node.js's querystring
            module
        :param object opts.data:
            Data to pass as the request body. Will be encoded using
            :func:`HttpApi.encode_request_data` before being sent.
        :param object opts.headers:
            Additional headers to add to the default headers.

        Returns a :class:`HttpResponse` via a promise. Failures while making
        and checking the request will be thrown as :class:`HttpApiError`s, and
        can be caught with a Q errback. See :meth:`HttpApi.parse_reply` for
        more on the response parsing and error throwing.
        */
        opts = _.defaults(opts || {}, {headers: {}});
        opts.headers = self.create_headers(opts.headers);
        opts.encoder = self.encode_request_data;

        var request = new HttpRequest(method, url, opts);
        request.encode();

        var cmd = request.to_cmd();
        return self
            .im.api_request(cmd.name, cmd.data)
            .then(
                function(reply) {
                    return self.parse_reply(reply, request);
                }, function(e) {
                    throw new HttpRequestError(request, e.reply.reason);
                });

    };

    self.get = function(url, opts) {
        /**:HttpApi.get(url, opts)

        Make an HTTP GET request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('GET', url, opts);
    };

    self.head = function(url, opts) {
        /**:HttpApi.head(url, opts)

        Make an HTTP HEAD request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('HEAD', url, opts);
    };

    self.post = function(url, opts) {
        /**:HttpApi.post(url, opts)

        Make an HTTP POST request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('POST', url, opts);
    };

    self.put = function(url, opts) {
        /**:HttpApi.put(url, opts)

        Make an HTTP PUT request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('PUT', url, opts);
    };

    self.delete = function(url, opts) {
        /**:HttpApi.delete(url, opts)

        Make an HTTP DELETE request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('DELETE', url, opts);
    };
});

var JsonApi = HttpApi.extend(function(self, im, opts) {
    /**class:JsonApi(im, opts)

    A helper class for making HTTP requests that send and receive JSON
    encoded data.

    :param InteractionMachine im:
        The interaction machine to use when making requests.
    :param object opts.headers:
        Default headers to use in HTTP requests. The ``Content-Type``
        header is overridden to be ``application/json; charset=utf-8``.
    :param object opts.auth:
        Adds a HTTP Basic authentication to the default headers. Should
        contain ``username`` and ``password`` attributes.
    */
    HttpApi.call(self, im, opts);

    self.default_headers['Content-Type'] = ['application/json; charset=utf-8'];

    self.decode_response_body = function(body) {
        /**:JsonApi.decode_response_body(body)

        Decode an HTTP response body using ``JSON.parse()``.

        :param string body:
            Raw HTTP response body to parse.

        Returns the decoded response body.
        */
        return JSON.parse(body);
    };

    self.encode_request_data = function(data) {
        /**:JsonApi.encode_request_data(data)

        Encode an object as JSON using ``JSON.stringify()``.

        :param object data:
            Object to encode to JSON.

        Returns the serialized object as a string.
        */
        return JSON.stringify(data);
    };
});

this.HttpApi = HttpApi;
this.JsonApi = JsonApi;

this.HttpApiError = HttpApiError;
this.HttpRequestError = HttpRequestError;
this.HttpResponseError = HttpResponseError;
this.HttpRequest = HttpRequest;
this.HttpResponse = HttpResponse;
<commit_msg>Fix typo in docstring.<commit_after>var _ = require('lodash');
var url_utils = require('url');

var utils = require('../utils');
var BaseError = utils.BaseError;
var Extendable = utils.Extendable;

var events = require('../events');
var Eventable = events.Eventable;

var HttpApiError = BaseError.extend(function(self) {
    /**class:HttpApiError()

    Thrown when an error occurs while making and checking an HTTP request and
    the corresponding API reply.
    */
    self.name = "HttpApiError";
});

var HttpRequestError = BaseError.extend(function(self, request, reason) {
    /**class:HttpRequestError(request, reason)

    Thrown when an error occurs while making and checking an HTTP request.

    :param HttpRequest request: the request.
    :param string reason: the reason for the failure. Optional.
    */
    self.name = "HttpRequestError";
    self.request = request;
    self.reason = reason || null;

    if (self.reason) {
        self.message = [self.reason, self.request].join(' ');
    } else {
        self.message = self.request.toString();
    }
});

var HttpResponseError = BaseError.extend(function(self, response, reason) {
    /**class:HttpResponseError(response, reason)

    Thrown when an error response is returned by an HTTP request or if the HTTP
    response body cannot be parsed.

    :param HttpResponse response: the response.
    :param string reason: the reason for the failure. Optional.
    */
    self.name = "HttpResponseError";
    self.response = response;
    self.reason = reason || null;

    if (self.reason) {
        self.message = [self.reason, self.response].join(' ');
    } else {
        self.message = self.response.toString();
    }
});

var HttpRequest = Extendable.extend(function(self, method, url, opts) {
    /**class:HttpRequest(request, code, opts)

    Encapsulates information about an HTTP request made by the
    :class:`HttpApi`. Once :meth:`HttpRequest.encode` has been invoked,
    the request's data is encoded and made available as the request's body.

    :param string method:
        the HTTP request method.
    :param string url:
        the url to send the request to.
    :param string opts.data:
        the request's data to be encoded as the request's body. Optional.
    :param opts.params:
        An object of key-value pairs to append to the URL as query
        parameters. Can be in any form accepted by node.js's querystring
        module
    :param string opts.verify_options:
        A list of options to verify when doing an HTTPS request. Optional.
    */
    Extendable.call(self);

    opts = _.defaults(opts || {}, {
        headers: null,
        body: null,
        data: null,
        params: {},
        encoder: _.identity,
        verify_options: null
    });

    self.url = url;
    if (_.isString(self.url)) {
        self.url = url_utils.format(url_utils.parse(url));
    }

    self.method = method.toUpperCase();
    self.headers = opts.headers;
    self.data = opts.data;
    self.body = opts.body;
    self.params = opts.params;
    self.encoder = opts.encoder;
    self.verify_options = opts.verify_options;

    self.encode = function() {
        /**:HttpRequest.encode()

        Encodes the request data (if available).
        */
        if (self.data !== null) {
            try {
                self.body = self.encoder(self.data);
            }
            catch (e) {
                throw new HttpRequestError(
                    self, "Could not parse request (" + e.toString() + ")");
            }
        }
    };

    self.to_cmd = function() {
        /**:HttpRequest.to_cmd()

        Returns a sandbox API command that can be used to initiate this request
        via the sandbox API.
        */
        var cmd = {};
        cmd.data = {};
        cmd.name = 'http.' + self.method.toLowerCase();

        var url_data = url_utils.parse(self.url);
        url_data.query = self.params;
        cmd.data.url = url_utils.format(url_data);

        if (self.headers !== null) {
            cmd.data.headers = self.headers;
        }

        if (self.body !== null) {
            cmd.data.data = self.body;
        }

        if (self.verify_options !== null) {
            cmd.data.verify_options = self.verify_options;
        }

        return cmd;
    };

    self.toString = function() {
        var parts = [
            'HttpRequest:',
            self.method,
            self.url];

        if (self.body !== null) {
            parts.push('(body: ' + self.body + ')');
        }

        if (_.size(self.params)) {
            parts.push('(params: ' + JSON.stringify(self.params) + ')');
        }

        return '[' + parts.join(' ') + ']';
    };

});

var HttpResponse = Extendable.extend(function(self, request, code, opts) {
    /**class:HttpResponse(request, code, opts)

    Encapsulates information about an HTTP response given to the
    :class:`HttpApi`. Once :meth:`HttpResponse.decode` has been invoked,
    the response's body is decoded and made available as the response's data.

    :param HttpRequest request:
        the request that caused the response.
    :param integer code:
        the status code for the HTTP response.
    :param string opts.body:
        the response's body to be decoded as the response's data. Optional.
    */
    Extendable.call(self);

    opts = _.defaults(opts || {}, {
        data: null,
        body: null,
        decoder: _.identity
    });

    self.request = request;
    self.code = code;
    self.data = opts.data;
    self.body = opts.body;
    self.decoder = opts.decoder;

    self.decode = function() {
        /**:HttpResponse.decode()

        Decodes the responses body (if available).
        */
        if (self.body !== null) {

            try {
                self.data = self.decoder(self.body);
            }
            catch (e) {
                throw new HttpResponseError(
                    self, "Could not parse response (" + e.toString() + ")");
            }
        }
    };

    self.toString = function() {
        var parts = [
            'HttpResponse:',
            self.code];

        if (self.body !== null) {
            parts.push('(body: ' + self.body + ')');
        }

        parts.push('(request: ' + self.request + ')');
        return parts.join(' ');
    };
});

var HttpApi = Eventable.extend(function(self, im, opts) {
    /**class:HttpApi(im, opts)

    A helper class for making HTTP requests via the HTTP sandbox resource.

    :param InteractionMachine im:
        The interaction machine to use when making requests.
    :param object opts.headers:
        Default headers to use in HTTP requests.
    :param object opts.auth:
        Adds a HTTP Basic authentication to the default headers. Should
        contain ``username`` and ``password`` attributes.
    */
    opts = _.defaults(opts || {}, {
        headers: {},
    });

    self.im = im;
    self.default_headers = opts.headers;

    self.make_auth = utils.basic_auth;

    if (opts.auth) {
        var auth = self.make_auth(opts.auth.username, opts.auth.password);
        self.default_headers.Authorization = [auth];
    }

    self.decode_response_body = function(body) {
        /**:HttpApi.decode_response_body(body)

        :param string body:
            The body to decode.

        Sub-classes should override this to decode the response body and throw
        an exception if the body cannot be parsed. This base implementation
        returns the body as-is (i.e. decoding is left to the code calling the
        :class:`HttpApi`).
        */
        return body;
    };

    self.encode_request_data = function(data) {
        /**:HttpApi.encode_request_data(data)

        :param object data:
            The data to encode.

        Sub-classes should override this to encode the request body and throw
        an exception if the data cannot be encoded. This base implementation
        returns the data as-is (i.e. encoding is left to code calling the
        :class:`HttpApi`).
        */
        return data;
    };

    self.create_headers = function(headers) {
        /**:HttpApi.create_headers(headers)

        Combines a set of custom headers with the default headers passed to
        :class:`HttpApi`.

        :param object headers:
            Additional HTTP headers. Attributes are header names. Values are
            header values.

        Returns the complete set of HTTP headers.
        */
        var all_headers = {};
        _.extend(all_headers, self.default_headers);
        _.extend(all_headers, headers);
        return all_headers;
    };

    self.parse_reply = function(reply, request) {
        /**:HttpApi.parse_reply(reply, request)

        Check an HTTP reply and throw an :class:`HttpRequestError` if the
        sandbox API command was unsuccessful, or otherwise parse the sandbox's
        reply into a response.  If the response status code is not in the 200
        range or the reply body cannot be decoded, throw an
        :class:`HttpResponseError`.

        Logs an error via the sandbox logging resource in an error is raised.

        :param object reply:
            Raw response to the sandbox API command.
        :param HttpRequest request:
            The request that initiated the sandbox API command.

        Returns an :class:`HttpResponse` or throws an :class:`HttpApiError`
        (either the :class:`HttpRequestError` or :class:`HttpResponseError`
        derivative, depending on what error occured).
        */
        var response = new HttpResponse(request, reply.code, {
            body: reply.body,
            decoder: self.decode_response_body
        });

        response.decode();

        if (response.code < 200 || response.code >= 300) {
            throw new HttpResponseError(response);
        }

        return response;
    };

    self.request = function(method, url, opts) {
        /**:HttpApi.request(method, url, opts)

        :param string method:
            The HTTP method to use (e.g. `GET`, `POST`).
        :param string url:
            The URL to make the request to. If you pass in query parameters
            using ``opts.params``, don't include any in the URL itself.
        :param opts.params:
            An object of key-value pairs to append to the URL as query
            parameters. Can be in any form accepted by node.js's querystring
            module
        :param object opts.data:
            Data to pass as the request body. Will be encoded using
            :func:`HttpApi.encode_request_data` before being sent.
        :param object opts.headers:
            Additional headers to add to the default headers.

        Returns a :class:`HttpResponse` via a promise. Failures while making
        and checking the request will be thrown as :class:`HttpApiError`s, and
        can be caught with a Q errback. See :meth:`HttpApi.parse_reply` for
        more on the response parsing and error throwing.
        */
        opts = _.defaults(opts || {}, {headers: {}});
        opts.headers = self.create_headers(opts.headers);
        opts.encoder = self.encode_request_data;

        var request = new HttpRequest(method, url, opts);
        request.encode();

        var cmd = request.to_cmd();
        return self
            .im.api_request(cmd.name, cmd.data)
            .then(
                function(reply) {
                    return self.parse_reply(reply, request);
                }, function(e) {
                    throw new HttpRequestError(request, e.reply.reason);
                });

    };

    self.get = function(url, opts) {
        /**:HttpApi.get(url, opts)

        Make an HTTP GET request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('GET', url, opts);
    };

    self.head = function(url, opts) {
        /**:HttpApi.head(url, opts)

        Make an HTTP HEAD request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('HEAD', url, opts);
    };

    self.post = function(url, opts) {
        /**:HttpApi.post(url, opts)

        Make an HTTP POST request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('POST', url, opts);
    };

    self.put = function(url, opts) {
        /**:HttpApi.put(url, opts)

        Make an HTTP PUT request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('PUT', url, opts);
    };

    self.delete = function(url, opts) {
        /**:HttpApi.delete(url, opts)

        Make an HTTP DELETE request.

        :param string url:
            The URL to make the request to.
        :param object opts:
            Options to pass to :func:`HttpApi.request`.

        Returns a promise which fires with the decoded value of the
        response body or an object with an error attribute containing
        the error message.
        */
        return self.request('DELETE', url, opts);
    };
});

var JsonApi = HttpApi.extend(function(self, im, opts) {
    /**class:JsonApi(im, opts)

    A helper class for making HTTP requests that send and receive JSON
    encoded data.

    :param InteractionMachine im:
        The interaction machine to use when making requests.
    :param object opts.headers:
        Default headers to use in HTTP requests. The ``Content-Type``
        header is overridden to be ``application/json; charset=utf-8``.
    :param object opts.auth:
        Adds a HTTP Basic authentication to the default headers. Should
        contain ``username`` and ``password`` attributes.
    */
    HttpApi.call(self, im, opts);

    self.default_headers['Content-Type'] = ['application/json; charset=utf-8'];

    self.decode_response_body = function(body) {
        /**:JsonApi.decode_response_body(body)

        Decode an HTTP response body using ``JSON.parse()``.

        :param string body:
            Raw HTTP response body to parse.

        Returns the decoded response body.
        */
        return JSON.parse(body);
    };

    self.encode_request_data = function(data) {
        /**:JsonApi.encode_request_data(data)

        Encode an object as JSON using ``JSON.stringify()``.

        :param object data:
            Object to encode to JSON.

        Returns the serialized object as a string.
        */
        return JSON.stringify(data);
    };
});

this.HttpApi = HttpApi;
this.JsonApi = JsonApi;

this.HttpApiError = HttpApiError;
this.HttpRequestError = HttpRequestError;
this.HttpResponseError = HttpResponseError;
this.HttpRequest = HttpRequest;
this.HttpResponse = HttpResponse;
<|endoftext|>


<commit_before>var fs = require('fs'),
    sys = require('sys'),
    events = require('events'),
    buffer = require('buffer'),
    http = require('http'),
    url = require('url'),
    path = require('path');

this.version = [0, 5, 9];

var mime = require('./node-static/mime');
var util = require('./node-static/util');

var serverInfo = 'node-static/' + this.version.join('.');

// In-memory file store
this.store = {};
this.indexStore = {};

this.Server = function (root, options) {
    if (root && (typeof(root) === 'object')) { options = root, root = null }

    this.root    = path.resolve(root || '.');
    this.options = options || {};
    this.cache   = 3600;

    this.defaultHeaders  = {};
    this.options.headers = this.options.headers || {};

    if ('cache' in this.options) {
        if (typeof(this.options.cache) === 'number') {
            this.cache = this.options.cache;
        } else if (! this.options.cache) {
            this.cache = false;
        }
    }

    if (this.cache !== false) {
        this.defaultHeaders['Cache-Control'] = 'max-age=' + this.cache;
    }
    this.defaultHeaders['Server'] = serverInfo;

    for (var k in this.defaultHeaders) {
        this.options.headers[k] = this.options.headers[k] ||
                                  this.defaultHeaders[k];
    }
};

this.Server.prototype.serveDir = function (pathname, req, res, finish) {
    var htmlIndex = path.join(pathname, 'index.html'),
        that = this;

    fs.stat(htmlIndex, function (e, stat) {
        if (!e) {
            that.respond(null, 200, {}, [htmlIndex], stat, req, res, finish);
        } else {
            if (pathname in exports.store) {
                streamFiles(exports.indexStore[pathname].files);
            } else {
                // Stream a directory of files as a single file.
                fs.readFile(path.join(pathname, 'index.json'), function (e, contents) {
                    if (e) { return finish(404, {}) }
                    var index = JSON.parse(contents);
                    exports.indexStore[pathname] = index;
                    streamFiles(index.files);
                });
            }
        }
    });
    function streamFiles(files) {
        util.mstat(pathname, files, function (e, stat) {
            that.respond(pathname, 200, {}, files, stat, req, res, finish);
        });
    }
};
this.Server.prototype.serveFile = function (pathname, status, headers, req, res) {
    var that = this;
    var promise = new(events.EventEmitter);

    pathname = this.resolve(pathname);

    fs.stat(pathname, function (e, stat) {
        if (e) {
            return promise.emit('error', e);
        }
        that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) {
            that.finish(status, headers, req, res, promise);
        });
    });
    return promise;
};
this.Server.prototype.finish = function (status, headers, req, res, promise, callback) {
    var result = {
        status:  status,
        headers: headers,
        message: http.STATUS_CODES[status]
    };

    headers['Server'] = serverInfo;

    if (!status || status >= 400) {
        if (callback) {
            callback(result);
        } else {
            if (promise.listeners('error').length > 0) {
                promise.emit('error', result);
            }
            res.writeHead(status, headers);
            res.end();
        }
    } else {
        // Don't end the request here, if we're streaming;
        // it's taken care of in `prototype.stream`.
        if (status !== 200 || req.method !== 'GET') {
            res.writeHead(status, headers);
            res.end();
        }
        callback && callback(null, result);
        promise.emit('success', result);
    }
};

this.Server.prototype.servePath = function (pathname, status, headers, req, res, finish) {
    var that = this,
        promise = new(events.EventEmitter);

    pathname = this.resolve(pathname);

    // Only allow GET and HEAD requests
    if (req.method !== 'GET' && req.method !== 'HEAD') {
        finish(405, { 'Allow': 'GET, HEAD' });
        return promise;
    }

    // Make sure we're not trying to access a
    // file outside of the root.
    if (pathname.indexOf(that.root) === 0) {
        fs.stat(pathname, function (e, stat) {
            if (e) {
                finish(404, {});
            } else if (stat.isFile()) {      // Stream a single file.
                that.respond(null, status, headers, [pathname], stat, req, res, finish);
            } else if (stat.isDirectory()) { // Stream a directory of files.
                that.serveDir(pathname, req, res, finish);
            } else {
                finish(400, {});
            }
        });
    } else {
        // Forbidden
        finish(403, {});
    }
    return promise;
};
this.Server.prototype.resolve = function (pathname) {
    return path.resolve(path.join(this.root, pathname));
};
this.Server.prototype.serve = function (req, res, callback) {
    var that = this,
        promise = new(events.EventEmitter);

    var pathname = decodeURI(url.parse(req.url).pathname);

    var finish = function (status, headers) {
        that.finish(status, headers, req, res, promise, callback);
    };

    process.nextTick(function () {
        that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) {
            promise.emit('success', result);
        }).on('error', function (err) {
            promise.emit('error');
        });
    });
    if (! callback) { return promise }
};

this.Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) {
    var mtime   = Date.parse(stat.mtime),
        key     = pathname || files[0],
        headers = {};

    // Copy default headers
    for (var k in this.options.headers) {  headers[k] = this.options.headers[k] }

    headers['Etag']          = JSON.stringify([stat.ino, stat.size, mtime].join('-'));
    headers['Date']          = new(Date)().toUTCString();
    headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString();

    // Conditional GET
    // If the "If-Modified-Since" or "If-None-Match" headers
    // match the conditions, send a 304 Not Modified.
    if (req.headers['if-none-match'] === headers['Etag'] ||
        Date.parse(req.headers['if-modified-since']) >= mtime) {
        finish(304, headers);
    } else if (req.method === 'HEAD') {
        finish(200, headers);
    } else {
        var fileExtension = path.extname(files[0]).slice(1).toLowerCase();
        headers['Content-Length'] = stat.size;
        headers['Content-Type']   = mime.contentTypes[fileExtension] ||
                                   'application/octet-stream';

        for (var k in _headers) { headers[k] = _headers[k] }

        res.writeHead(status, headers);

        // If the file was cached and it's not older
        // than what's on disk, serve the cached version.
        if (this.cache && (key in exports.store) &&
            exports.store[key].stat.mtime >= stat.mtime) {
            res.end(exports.store[key].buffer);
            finish(status, headers);
        } else {
            this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) {
                if (e) { return finish(500, {}) }
                exports.store[key] = {
                    stat:      stat,
                    buffer:    buffer,
                    timestamp: Date.now()
                };
                finish(status, headers);
            });
        }
    }
};

this.Server.prototype.stream = function (pathname, files, buffer, res, callback) {
    (function streamFile(files, offset) {
        var file = files.shift();

        if (file) {
            file = file[0] === '/' ? file : path.join(pathname || '.', file);

            // Stream the file to the client
            fs.createReadStream(file, {
                flags: 'r',
                mode: 0666
            }).on('data', function (chunk) {
                chunk.copy(buffer, offset);
                offset += chunk.length;
            }).on('close', function () {
                streamFile(files, offset);
            }).on('error', function (err) {
                callback(err);
                console.error(err);
            }).pipe(res, { end: false });
        } else {
            res.end();
            callback(null, buffer, offset);
        }
    })(files.slice(0), 0);
};
<commit_msg>removed call to 'sys', deprecated<commit_after>var fs = require('fs'),
    events = require('events'),
    buffer = require('buffer'),
    http = require('http'),
    url = require('url'),
    path = require('path');

this.version = [0, 5, 9];

var mime = require('./node-static/mime');
var util = require('./node-static/util');

var serverInfo = 'node-static/' + this.version.join('.');

// In-memory file store
this.store = {};
this.indexStore = {};

this.Server = function (root, options) {
    if (root && (typeof(root) === 'object')) { options = root, root = null }

    this.root    = path.resolve(root || '.');
    this.options = options || {};
    this.cache   = 3600;

    this.defaultHeaders  = {};
    this.options.headers = this.options.headers || {};

    if ('cache' in this.options) {
        if (typeof(this.options.cache) === 'number') {
            this.cache = this.options.cache;
        } else if (! this.options.cache) {
            this.cache = false;
        }
    }

    if (this.cache !== false) {
        this.defaultHeaders['Cache-Control'] = 'max-age=' + this.cache;
    }
    this.defaultHeaders['Server'] = serverInfo;

    for (var k in this.defaultHeaders) {
        this.options.headers[k] = this.options.headers[k] ||
                                  this.defaultHeaders[k];
    }
};

this.Server.prototype.serveDir = function (pathname, req, res, finish) {
    var htmlIndex = path.join(pathname, 'index.html'),
        that = this;

    fs.stat(htmlIndex, function (e, stat) {
        if (!e) {
            that.respond(null, 200, {}, [htmlIndex], stat, req, res, finish);
        } else {
            if (pathname in exports.store) {
                streamFiles(exports.indexStore[pathname].files);
            } else {
                // Stream a directory of files as a single file.
                fs.readFile(path.join(pathname, 'index.json'), function (e, contents) {
                    if (e) { return finish(404, {}) }
                    var index = JSON.parse(contents);
                    exports.indexStore[pathname] = index;
                    streamFiles(index.files);
                });
            }
        }
    });
    function streamFiles(files) {
        util.mstat(pathname, files, function (e, stat) {
            that.respond(pathname, 200, {}, files, stat, req, res, finish);
        });
    }
};
this.Server.prototype.serveFile = function (pathname, status, headers, req, res) {
    var that = this;
    var promise = new(events.EventEmitter);

    pathname = this.resolve(pathname);

    fs.stat(pathname, function (e, stat) {
        if (e) {
            return promise.emit('error', e);
        }
        that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) {
            that.finish(status, headers, req, res, promise);
        });
    });
    return promise;
};
this.Server.prototype.finish = function (status, headers, req, res, promise, callback) {
    var result = {
        status:  status,
        headers: headers,
        message: http.STATUS_CODES[status]
    };

    headers['Server'] = serverInfo;

    if (!status || status >= 400) {
        if (callback) {
            callback(result);
        } else {
            if (promise.listeners('error').length > 0) {
                promise.emit('error', result);
            }
            res.writeHead(status, headers);
            res.end();
        }
    } else {
        // Don't end the request here, if we're streaming;
        // it's taken care of in `prototype.stream`.
        if (status !== 200 || req.method !== 'GET') {
            res.writeHead(status, headers);
            res.end();
        }
        callback && callback(null, result);
        promise.emit('success', result);
    }
};

this.Server.prototype.servePath = function (pathname, status, headers, req, res, finish) {
    var that = this,
        promise = new(events.EventEmitter);

    pathname = this.resolve(pathname);

    // Only allow GET and HEAD requests
    if (req.method !== 'GET' && req.method !== 'HEAD') {
        finish(405, { 'Allow': 'GET, HEAD' });
        return promise;
    }

    // Make sure we're not trying to access a
    // file outside of the root.
    if (pathname.indexOf(that.root) === 0) {
        fs.stat(pathname, function (e, stat) {
            if (e) {
                finish(404, {});
            } else if (stat.isFile()) {      // Stream a single file.
                that.respond(null, status, headers, [pathname], stat, req, res, finish);
            } else if (stat.isDirectory()) { // Stream a directory of files.
                that.serveDir(pathname, req, res, finish);
            } else {
                finish(400, {});
            }
        });
    } else {
        // Forbidden
        finish(403, {});
    }
    return promise;
};
this.Server.prototype.resolve = function (pathname) {
    return path.resolve(path.join(this.root, pathname));
};
this.Server.prototype.serve = function (req, res, callback) {
    var that = this,
        promise = new(events.EventEmitter);

    var pathname = decodeURI(url.parse(req.url).pathname);

    var finish = function (status, headers) {
        that.finish(status, headers, req, res, promise, callback);
    };

    process.nextTick(function () {
        that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) {
            promise.emit('success', result);
        }).on('error', function (err) {
            promise.emit('error');
        });
    });
    if (! callback) { return promise }
};

this.Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) {
    var mtime   = Date.parse(stat.mtime),
        key     = pathname || files[0],
        headers = {};

    // Copy default headers
    for (var k in this.options.headers) {  headers[k] = this.options.headers[k] }

    headers['Etag']          = JSON.stringify([stat.ino, stat.size, mtime].join('-'));
    headers['Date']          = new(Date)().toUTCString();
    headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString();

    // Conditional GET
    // If the "If-Modified-Since" or "If-None-Match" headers
    // match the conditions, send a 304 Not Modified.
    if (req.headers['if-none-match'] === headers['Etag'] ||
        Date.parse(req.headers['if-modified-since']) >= mtime) {
        finish(304, headers);
    } else if (req.method === 'HEAD') {
        finish(200, headers);
    } else {
        var fileExtension = path.extname(files[0]).slice(1).toLowerCase();
        headers['Content-Length'] = stat.size;
        headers['Content-Type']   = mime.contentTypes[fileExtension] ||
                                   'application/octet-stream';

        for (var k in _headers) { headers[k] = _headers[k] }

        res.writeHead(status, headers);

        // If the file was cached and it's not older
        // than what's on disk, serve the cached version.
        if (this.cache && (key in exports.store) &&
            exports.store[key].stat.mtime >= stat.mtime) {
            res.end(exports.store[key].buffer);
            finish(status, headers);
        } else {
            this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) {
                if (e) { return finish(500, {}) }
                exports.store[key] = {
                    stat:      stat,
                    buffer:    buffer,
                    timestamp: Date.now()
                };
                finish(status, headers);
            });
        }
    }
};

this.Server.prototype.stream = function (pathname, files, buffer, res, callback) {
    (function streamFile(files, offset) {
        var file = files.shift();

        if (file) {
            file = file[0] === '/' ? file : path.join(pathname || '.', file);

            // Stream the file to the client
            fs.createReadStream(file, {
                flags: 'r',
                mode: 0666
            }).on('data', function (chunk) {
                chunk.copy(buffer, offset);
                offset += chunk.length;
            }).on('close', function () {
                streamFile(files, offset);
            }).on('error', function (err) {
                callback(err);
                console.error(err);
            }).pipe(res, { end: false });
        } else {
            res.end();
            callback(null, buffer, offset);
        }
    })(files.slice(0), 0);
};
<|endoftext|>


<commit_before>// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require foundation
//= require turbolinks
//= require_tree
//= require d3

$(function(){ $(document).foundation(); });

var loader = document.getElementById('loader');
function startLoading() {
  loader.className = '';
}
function finishedLoading() {
  loader.className = 'done';
  setTimeout(function() {
    loader.className = 'hide';
  }, 1000);
}

startLoading();<commit_msg>Add finishedLoading method call<commit_after>// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require foundation
//= require turbolinks
//= require_tree
//= require d3

$(function(){ $(document).foundation(); });

var loader = document.getElementById('loader');
startLoading();
finishedLoading();
function startLoading() {
  loader.className = '';
}
function finishedLoading() {
  loader.className = 'done';
  setTimeout(function() {
    loader.className = 'hide';
  }, 1000);
}

<|endoftext|>


<commit_before>var demos = [{"name": "Velocity addition",
              "steps": [{"caption": "Three stars. The left and right stars approach the center star at high speed - 0.9 times the speed of light.",
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Center", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -20, 0], "p": [2.1 * c, 0], "m": 1, "label": "Left", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [30, -30, 0], "p": [-2.1 * c, 0], "m": 1, "label": "Right", "shape": aSphere(5,64)}],
                         "frame": 0},
                        {"caption": "Let's see that again, but this time from the perspective of the far-left star. The rightmost star only approaches at 0.995c — not the 1.8c you'd expect.",
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Center", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -20, 0], "p": [2.1 * c, 0], "m": 1, "label": "Left", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [30, -30, 0], "p": [-2.1 * c, 0], "m": 1, "label": "Right", "shape": aSphere(5,64)}],
                         "frame": 1}
                       ]
             },
             {"name": "Relativistic Doppler shift",
              "steps": [{"caption": "First watch as Doppler shifting is turned off. A star shoots past.",
                         "options": {"showDoppler": false, "showGamma": false},
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Observer", "options": {"showVelocity": false}, "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -30, 0], "p": [1.5 * c, 0], "m": 1, "label": "Star", "shape": aSphere(5,64)}
                                    ],
                         "frame": 0},
                        {"caption": "Now watch the same motion with the relativistic Doppler effect visible.",
                         "options": {"showDoppler": true},
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Observer", "shape": aSphere(5,64), "options": {"showVelocity": false}},
                                     {"object": extendedObject, "x": [-30, -30, 0], "p": [1.5 * c, 0], "m": 1, "label": "Star", "shape": aSphere(5,64)}
                                    ],
                         "frame": 0}
                       ]
             },
             {"name": "Relativistic snake",
              "steps": [{"caption": "We have here two knives a fixed distance apart, moving downwards. A special breed of high-speed snake, Crotalus relativisticus, is moving at a speed close to the speed of light. It's just short enough to fit between the blades.",
                         "objects": [{"object": extendedObject, "x": [-27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[5, -10, 0, 0], [5, 10, 0, 0], [-5, -10, 0, 0], [5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[-5, -10, 0, 0], [-5, 10, 0, 0], [5, -10, 0, 0], [-5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [-68, 0, 0], "p": [8, 0, 0], "m": 1, "shape": linesPadder([[-35, 0, 0, 0], [35, 0, 0, 0]],3)}
                                    ],
                         "options": {"showFramePos": true, "showVisualPos": false, "showVelocity": true}},
                        {"caption": "But then consider the perspective of the snake. It's not length-contracted — the knives are. They're much closer together. How does the snake survive the knives?",
                         "objects": [{"object": extendedObject, "x": [-27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[5, -10, 0, 0], [5, 10, 0, 0], [-5, -10, 0, 0], [5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[-5, -10, 0, 0], [-5, 10, 0, 0], [5, -10, 0, 0], [-5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [-68, 0, 0], "p": [8, 0, 0], "m": 1, "shape": [[-35, 0, 0, 0], [35, 0, 0, 0]]}
                                    ],
                         "frame"  : 2,
                         "options": {"showFramePos": true, "showVisualPos": false}},
                       ]
             },
            {"name": "Test thingies.",
             "steps": [{"caption": "Test for a sphere",
                        "objects": [{"object": extendedObject, "x": [0, -32, 0], "p": [0,0,0], "m": 1, "shape": aSphere(10,100)},
                                    {"object": extendedObject, "x": [20, -20, -20], "p": [0,0,0], "m": 1,
                                            "options": {"temperature": 3000},
                                            "label": "A circle",   "shape": aCircle(10,30)}
                      ],
             "options": {"showFramePos": true, "showVisualPos": true}}]},
            {"name": "Photons",
             "steps": [{"caption": "Photons.",
                        "objects": [{"object": photon, "x": [0, 0, 0], "v": [1, 0, 0]},
                                    {"object": photon, "x": [0, 0, 0], "v": [-1, 0, 0]}]}]
            }
            ];
<commit_msg>Simple demo of the constant speed of light. Still broken due to photon bug.<commit_after>var demos = [{"name": "Velocity addition",
              "steps": [{"caption": "Three stars. The left and right stars approach the center star at high speed - 0.9 times the speed of light.",
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Center", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -20, 0], "p": [2.1 * c, 0], "m": 1, "label": "Left", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [30, -30, 0], "p": [-2.1 * c, 0], "m": 1, "label": "Right", "shape": aSphere(5,64)}],
                         "frame": 0},
                        {"caption": "Let's see that again, but this time from the perspective of the far-left star. The rightmost star only approaches at 0.995c — not the 1.8c you'd expect.",
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Center", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -20, 0], "p": [2.1 * c, 0], "m": 1, "label": "Left", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [30, -30, 0], "p": [-2.1 * c, 0], "m": 1, "label": "Right", "shape": aSphere(5,64)}],
                         "frame": 1}
                       ]
             },
             {"name": "Relativistic Doppler shift",
              "steps": [{"caption": "First watch as Doppler shifting is turned off. A star shoots past.",
                         "options": {"showDoppler": false, "showGamma": false},
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Observer", "options": {"showVelocity": false}, "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -30, 0], "p": [1.5 * c, 0], "m": 1, "label": "Star", "shape": aSphere(5,64)}
                                    ],
                         "frame": 0},
                        {"caption": "Now watch the same motion with the relativistic Doppler effect visible.",
                         "options": {"showDoppler": true},
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Observer", "shape": aSphere(5,64), "options": {"showVelocity": false}},
                                     {"object": extendedObject, "x": [-30, -30, 0], "p": [1.5 * c, 0], "m": 1, "label": "Star", "shape": aSphere(5,64)}
                                    ],
                         "frame": 0}
                       ]
             },
             {"name": "Relativistic snake",
              "steps": [{"caption": "We have here two knives a fixed distance apart, moving downwards. A special breed of high-speed snake, Crotalus relativisticus, is moving at a speed close to the speed of light. It's just short enough to fit between the blades.",
                         "objects": [{"object": extendedObject, "x": [-27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[5, -10, 0, 0], [5, 10, 0, 0], [-5, -10, 0, 0], [5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[-5, -10, 0, 0], [-5, 10, 0, 0], [5, -10, 0, 0], [-5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [-68, 0, 0], "p": [8, 0, 0], "m": 1, "shape": linesPadder([[-35, 0, 0, 0], [35, 0, 0, 0]],3)}
                                    ],
                         "options": {"showFramePos": true, "showVisualPos": false, "showVelocity": true}},
                        {"caption": "But then consider the perspective of the snake. It's not length-contracted — the knives are. They're much closer together. How does the snake survive the knives?",
                         "objects": [{"object": extendedObject, "x": [-27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[5, -10, 0, 0], [5, 10, 0, 0], [-5, -10, 0, 0], [5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[-5, -10, 0, 0], [-5, 10, 0, 0], [5, -10, 0, 0], [-5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [-68, 0, 0], "p": [8, 0, 0], "m": 1, "shape": [[-35, 0, 0, 0], [35, 0, 0, 0]]}
                                    ],
                         "frame"  : 2,
                         "options": {"showFramePos": true, "showVisualPos": false}},
                       ]
             },
            {"name": "Test thingies.",
             "steps": [{"caption": "Test for a sphere",
                        "objects": [{"object": extendedObject, "x": [0, -32, 0], "p": [0,0,0], "m": 1, "shape": aSphere(10,100)},
                                    {"object": extendedObject, "x": [20, -20, -20], "p": [0,0,0], "m": 1,
                                            "options": {"temperature": 3000},
                                            "label": "A circle",   "shape": aCircle(10,30)}
                      ],
             "options": {"showFramePos": true, "showVisualPos": true}}]},
            {"name": "Speed of light",
             "steps": [{"caption": "Two photons are emitted from the center of a ruler, heading outwards.",
                        "objects": [{"object": photon, "x": [0, 10, 0], "v": [1, 0, 0]},
                                    {"object": photon, "x": [0, 10, 0], "v": [-1, 0, 0]},
                                    {"object": extendedObject, "x": [0, 0, 0], "p": [0, 0, 0], "m": 1, "shape": linesPadder([[-40, 0, 0, 0], [40, 0, 0, 0]], 3)}
                                    ],
                        "options": {"showFramePos": true, "showVisualPos": false}},
                       {"caption": "Now, the same scene, from the perspective of an observe travelling past at a high speed.",
                        "objects": [{"object": photon, "x": [0, 10, 0], "v": [1, 0, 0]},
                                    {"object": photon, "x": [0, 10, 0], "v": [-1, 0, 0]},
                                    {"object": extendedObject, "x": [0, 0, 0], "p": [0, 0, 0], "m": 1, "shape": linesPadder([[-40, 0, 0, 0], [40, 0, 0, 0]], 3)},
                                    {"object": extendedObject, "x": [-40, -10, 0], "p": [5, 0, 0], "m": 1, "shape": aSphere(5, 50)}],
                        "frame": 3,
                        "options": {"showFramePos": true, "showVisualPos": false}}
                      ]
            }
            ];
<|endoftext|>


<commit_before>// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview A class that can be used to listen to font size changes.
 */

goog.provide('goog.dom.FontSizeMonitor');
goog.provide('goog.dom.FontSizeMonitor.EventType');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.userAgent');

// TODO(user): Move this to goog.events instead.

/**
 * This class can be used to monitor changes in font size.  Instances will
 * dispatch a {@code goog.dom.FontSizeMonitor.EventType.CHANGE} event.
 * Example usage:
 *

 * var fms = new goog.dom.FontSizeMonitor();
 * goog.events.listen(fms, goog.dom.FontSizeMonitor.EventType.CHANGE,
 *     function(e) {
 *       alert('Font size was changed');
 *     });
 *

 * @param {goog.dom.DomHelper=} opt_domHelper DOM helper object that is used to
 *     determine where to insert the DOM nodes used to determine when the font
 *     size changes.
 * @constructor
 * @extends {goog.events.EventTarget}
 */
goog.dom.FontSizeMonitor = function(opt_domHelper) {
  goog.events.EventTarget.call(this);

  var dom = opt_domHelper || goog.dom.getDomHelper();

  /**
   * Offscreen iframe which we use to detect resize events.
   * @type {Element}
   * @private
   */
  this.sizeElement_ = dom.createDom(
      // The size of the iframe is expressed in em, which are font size relative
      // which will cause the iframe to be resized when the font size changes.
      // The actual values are not relevant as long as we can ensure that the
      // iframe has a non zero size and is completely off screen.
      goog.userAgent.IE ? 'div' : 'iframe',
      {'style': 'position:absolute;width:9em;height:9em;top:-99em;z-index:-9'});
  var p = dom.getDocument().body;
  p.insertBefore(this.sizeElement_, p.firstChild);

  /**
   * The object that we listen to resize events on.
   * @type {Element|Window}
   * @private
   */
  var resizeTarget = this.resizeTarget_ =
      goog.userAgent.IE ? this.sizeElement_ :
          goog.dom.getFrameContentWindow(
              /** @type {HTMLIFrameElement} */ (this.sizeElement_));

  // We need to open and close the document to get Firefox 2 to work.  We must
  // not do this for IE in case we are using HTTPS since accessing the document
  // on an about:blank iframe in IE using HTTPS raises a Permission Denied
  // error. Additionally, firefox shows this frame in tab order by default,
  // suppress it by using a tabindex of -1.
  if (goog.userAgent.GECKO) {
    this.sizeElement_.tabIndex = -1;
    var doc = resizeTarget.document;
    doc.open();
    doc.close();
  }

  // Listen to resize event on the window inside the iframe.
  goog.events.listen(resizeTarget, goog.events.EventType.RESIZE,
                     this.handleResize_, false, this);

  /**
   * Last measured width of the iframe element.
   * @type {number}
   * @private
   */
  this.lastWidth_ = this.sizeElement_.offsetWidth;
};
goog.inherits(goog.dom.FontSizeMonitor, goog.events.EventTarget);

/**
 * The event types that the FontSizeMonitor fires.
 * @enum {string}
 */
goog.dom.FontSizeMonitor.EventType = {
  // TODO(user): Change value to 'change' after updating the callers.
  CHANGE: 'fontsizechange'
};

/**
 * Constant for the change event.
 * @type {string}
 * @deprecated Use {@code goog.dom.FontSizeMonitor.EventType.CHANGE} instead.
 */
goog.dom.FontSizeMonitor.CHANGE_EVENT =
    goog.dom.FontSizeMonitor.EventType.CHANGE;

/**
 * Disposes of the font size monitor.
 */
goog.dom.FontSizeMonitor.prototype.disposeInternal = function() {
  goog.dom.FontSizeMonitor.superClass_.disposeInternal.call(this);

  goog.events.unlisten(this.resizeTarget_, goog.events.EventType.RESIZE,
                       this.handleResize_, false, this);
  this.resizeTarget_ = null;

  // Firefox 2 crashes if the iframe is removed during the unload phase.
  if (!goog.userAgent.GECKO || goog.userAgent.isVersion('1.9')) {
    goog.dom.removeNode(this.sizeElement_);
  }
  delete this.sizeElement_;
};

/**
 * Handles the onresize event of the iframe and dispatches a change event in
 * case its size really changed.
 * @param {goog.events.BrowserEvent} e The event object.
 * @private
 */
goog.dom.FontSizeMonitor.prototype.handleResize_ = function(e) {
  // Only dispatch the event if the size really changed.  Some newer browsers do
  // not really change the font-size,  instead they zoom the whole page.  This
  // does trigger window resize events on the iframe but the logical pixel size
  // remains the same (the device pixel size changes but that is irrelevant).
  var currentWidth = this.sizeElement_.offsetWidth;
  if (this.lastWidth_ != currentWidth) {
    this.lastWidth_ = currentWidth;
    this.dispatchEvent(goog.dom.FontSizeMonitor.EventType.CHANGE);
  }
};
<commit_msg><commit_after>// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview A class that can be used to listen to font size changes.
 */

goog.provide('goog.dom.FontSizeMonitor');
goog.provide('goog.dom.FontSizeMonitor.EventType');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.userAgent');

// TODO(user): Move this to goog.events instead.

/**
 * This class can be used to monitor changes in font size.  Instances will
 * dispatch a {@code goog.dom.FontSizeMonitor.EventType.CHANGE} event.
 * Example usage:
 *

 * var fms = new goog.dom.FontSizeMonitor();
 * goog.events.listen(fms, goog.dom.FontSizeMonitor.EventType.CHANGE,
 *     function(e) {
 *       alert('Font size was changed');
 *     });
 *

 * @param {goog.dom.DomHelper=} opt_domHelper DOM helper object that is used to
 *     determine where to insert the DOM nodes used to determine when the font
 *     size changes.
 * @constructor
 * @extends {goog.events.EventTarget}
 */
goog.dom.FontSizeMonitor = function(opt_domHelper) {
  goog.events.EventTarget.call(this);

  var dom = opt_domHelper || goog.dom.getDomHelper();

  /**
   * Offscreen iframe which we use to detect resize events.
   * @type {Element}
   * @private
   */
  this.sizeElement_ = dom.createDom(
      // The size of the iframe is expressed in em, which are font size relative
      // which will cause the iframe to be resized when the font size changes.
      // The actual values are not relevant as long as we can ensure that the
      // iframe has a non zero size and is completely off screen.
      goog.userAgent.IE ? 'div' : 'iframe',
      {'style': 'position:absolute;width:9em;height:9em;top:-99em'});
  var p = dom.getDocument().body;
  p.insertBefore(this.sizeElement_, p.firstChild);

  /**
   * The object that we listen to resize events on.
   * @type {Element|Window}
   * @private
   */
  var resizeTarget = this.resizeTarget_ =
      goog.userAgent.IE ? this.sizeElement_ :
          goog.dom.getFrameContentWindow(
              /** @type {HTMLIFrameElement} */ (this.sizeElement_));

  // We need to open and close the document to get Firefox 2 to work.  We must
  // not do this for IE in case we are using HTTPS since accessing the document
  // on an about:blank iframe in IE using HTTPS raises a Permission Denied
  // error. Additionally, firefox shows this frame in tab order by default,
  // suppress it by using a tabindex of -1.
  if (goog.userAgent.GECKO) {
    this.sizeElement_.tabIndex = -1;
    var doc = resizeTarget.document;
    doc.open();
    doc.close();
  }

  // Listen to resize event on the window inside the iframe.
  goog.events.listen(resizeTarget, goog.events.EventType.RESIZE,
                     this.handleResize_, false, this);

  /**
   * Last measured width of the iframe element.
   * @type {number}
   * @private
   */
  this.lastWidth_ = this.sizeElement_.offsetWidth;
};
goog.inherits(goog.dom.FontSizeMonitor, goog.events.EventTarget);

/**
 * The event types that the FontSizeMonitor fires.
 * @enum {string}
 */
goog.dom.FontSizeMonitor.EventType = {
  // TODO(user): Change value to 'change' after updating the callers.
  CHANGE: 'fontsizechange'
};

/**
 * Constant for the change event.
 * @type {string}
 * @deprecated Use {@code goog.dom.FontSizeMonitor.EventType.CHANGE} instead.
 */
goog.dom.FontSizeMonitor.CHANGE_EVENT =
    goog.dom.FontSizeMonitor.EventType.CHANGE;

/**
 * Disposes of the font size monitor.
 */
goog.dom.FontSizeMonitor.prototype.disposeInternal = function() {
  goog.dom.FontSizeMonitor.superClass_.disposeInternal.call(this);

  goog.events.unlisten(this.resizeTarget_, goog.events.EventType.RESIZE,
                       this.handleResize_, false, this);
  this.resizeTarget_ = null;

  // Firefox 2 crashes if the iframe is removed during the unload phase.
  if (!goog.userAgent.GECKO || goog.userAgent.isVersion('1.9')) {
    goog.dom.removeNode(this.sizeElement_);
  }
  delete this.sizeElement_;
};

/**
 * Handles the onresize event of the iframe and dispatches a change event in
 * case its size really changed.
 * @param {goog.events.BrowserEvent} e The event object.
 * @private
 */
goog.dom.FontSizeMonitor.prototype.handleResize_ = function(e) {
  // Only dispatch the event if the size really changed.  Some newer browsers do
  // not really change the font-size,  instead they zoom the whole page.  This
  // does trigger window resize events on the iframe but the logical pixel size
  // remains the same (the device pixel size changes but that is irrelevant).
  var currentWidth = this.sizeElement_.offsetWidth;
  if (this.lastWidth_ != currentWidth) {
    this.lastWidth_ = currentWidth;
    this.dispatchEvent(goog.dom.FontSizeMonitor.EventType.CHANGE);
  }
};
<|endoftext|>


<commit_before>import * as utility from './utility.js'

/**
 * Get the data table configuration based on selected orientation.
 * @returns {{scrollX: boolean, bSortCellsTop: boolean, dom: string, buttons: string[], fixedColumns: {leftColumns: number}, columnDefs: *[]}}
 * The desired data table configuration.
 */
function getDataTableConfig () {
  // Declare the variable that holds the number of fixed left columns.
  const numFixedColumns = $('#table-orientation-column').is(':checked') ? 3 : 1

  return {
    // Allow scroll horizontally.
    scrollX: true,
    // Do not sort headers.
    bSortCellsTop: true,
    // specify where the button is
    dom: `<'row'<'col-sm-6'B><'col-sm-6 text-right'l>>
          <'row'<'col-sm-12'tr>><'row'<'col-sm-8 text-left'p><'col-sm-4'i>>`,

    // specify all the button that is put on to the page
    buttons: ['copyHtml5', 'excelHtml5', 'csvHtml5', 'pdfHtml5', 'colvis'],

    // Set number of fixed columns on left of the data table.
    fixedColumns: {
      leftColumns: numFixedColumns
    },

    // Truncate the long words in most left column.
    columnDefs: [
      {
        targets: 0,
        render: $.fn.dataTable.render.ellipsis(15, true)
      }
    ]
  }
}

/**
 * Check if the table is too large, if too large, give user the option to choose.
 * @returns {void}.
 */
function checkTokenizerSize () {
  // convert form into an object map string to string.
  const form = utility.jsonifyForm()
  // Send the ajax request to get the tokenizer matrix size.
  utility.sendAjaxRequest('/tokenizerSize', form)
    .done(
      function (response) {
        // Cast the response to an integer.
        const size = Number(response)
        if (size < 50000) { // If less than 50000 data, render the table.
          generateTokenizerResult()
        } else { // Else give user the option to download or keep waiting.
          $('#decision-button').click()
        }
      }
    )
    .fail(
      function (jqXHR, textStatus, errorThrown) {
        console.log('textStatus: ' + textStatus)
        console.log('errorThrown: ' + errorThrown)
        utility.runModal('Error encountered while calculating the size of the tokenizer result.')
      }
    )
}

/**
 * Display the result of the tokenizer matrix on web page.
 * @returns {void}.
 */
function generateTokenizerResult () {
  // show loading icon
  $('#status-analyze').css({'visibility': 'visible'})

  // convert form into an object map string to string
  const form = utility.jsonifyForm()

  // send the ajax request
  utility.sendAjaxRequest('/tokenizerMatrix', form)
    .done(
      function (response) {
        const outerTableDivSelector = $('#tokenizerMatrix')
        // put the response onto the web page
        outerTableDivSelector.html(response)
        // initialize the data table
        outerTableDivSelector.children().DataTable(getDataTableConfig())
        // display everything in the tokenize div.
        $('#tokenizerResult').css({'display': 'block'})
      })
    .fail(
      function (jqXHR, textStatus, errorThrown) {
        console.log('textStatus: ' + textStatus)
        console.log('errorThrown: ' + errorThrown)
        utility.runModal('Error encountered while generating the tokenize table result.')
      })
    .always(
      function () {
        $('#status-analyze').css({'visibility': 'hidden'})
      })
}

$(function () {
  // The event handler for clicking on download matrix in the modal.
  $('#choose-download').click(function () {
    $('#download-tokenizer').click()
  })
  // The event handler for clicking on continue rendering in the modal.
  $('#choose-continue').click(function () {
    generateTokenizerResult()
  })

  // The event handler for download tokenize clicked.
  $('#download-tokenizer').click(function (download) {
    // On check possible submission error on click.
    const error = utility.submissionError(1)
    // If error found, run modal to display message and prevent the submission from happening.
    if (error !== null) {
      utility.runModal(error)
      download.preventDefault()
    }
  })

  // The event handler for generate tokenize clicked.
  $('#get-tokenizer').click(function () {
    // Get the possible error happened during submission the ajax call.
    const error = utility.submissionError(1)

    if (error === null) { // If there is no error, do the ajax call.
      checkTokenizerSize()
    } else { // If error found, do modal.
      utility.runModal(error)
    }
  })
})
<commit_msg>Formatting data table<commit_after>import * as utility from './utility.js'

/**
 * Get the data table configuration based on selected orientation.
 * @returns {{scrollX: boolean, bSortCellsTop: boolean, dom: string, buttons: string[], fixedColumns: {leftColumns: number}, columnDefs: *[]}}
 * The desired data table configuration.
 */
function getDataTableConfig () {
  // Declare the variable that holds the number of fixed left columns.
  const numFixedColumns = $('#table-orientation-column').is(':checked') ? 3 : 1

  return {
    // Allow scroll horizontally.
    scrollX: true,
    // Do not sort headers.
    bSortCellsTop: true,
    // specify where the button is
    dom: `<'row'<'col-sm-12 text-right'l>>
          <'row'<'col-sm-6'B><'col-sm-6 text-right'f>>
          <'row'<'col-sm-12'tr>>
          <'row'<'col-sm-6'i><'col-sm-7'p>>`,

    // specify all the button that is put on to the page
    buttons: ['copyHtml5', 'excelHtml5', 'csvHtml5', 'pdfHtml5', 'colvis'],

    // Set number of fixed columns on left of the data table.
    fixedColumns: {
      leftColumns: numFixedColumns
    },

    // Truncate the long words in most left column.
    columnDefs: [
      {
        targets: 0,
        render: $.fn.dataTable.render.ellipsis(15, true)
      }
    ]
  }
}

/**
 * Check if the table is too large, if too large, give user the option to choose.
 * @returns {void}.
 */
function checkTokenizerSize () {
  // convert form into an object map string to string.
  const form = utility.jsonifyForm()
  // Send the ajax request to get the tokenizer matrix size.
  utility.sendAjaxRequest('/tokenizerSize', form)
    .done(
      function (response) {
        // Cast the response to an integer.
        const size = Number(response)
        if (size < 50000) { // If less than 50000 data, render the table.
          generateTokenizerResult()
        } else { // Else give user the option to download or keep waiting.
          $('#decision-button').click()
        }
      }
    )
    .fail(
      function (jqXHR, textStatus, errorThrown) {
        console.log('textStatus: ' + textStatus)
        console.log('errorThrown: ' + errorThrown)
        utility.runModal('Error encountered while calculating the size of the tokenizer result.')
      }
    )
}

/**
 * Display the result of the tokenizer matrix on web page.
 * @returns {void}.
 */
function generateTokenizerResult () {
  // show loading icon
  $('#status-analyze').css({'visibility': 'visible'})

  // convert form into an object map string to string
  const form = utility.jsonifyForm()

  // send the ajax request
  utility.sendAjaxRequest('/tokenizerMatrix', form)
    .done(
      function (response) {
        const outerTableDivSelector = $('#tokenizerMatrix')
        // put the response onto the web page
        outerTableDivSelector.html(response)
        // initialize the data table
        outerTableDivSelector.children().DataTable(getDataTableConfig())
        // display everything in the tokenize div.
        $('#tokenizerResult').css({'display': 'block'})
      })
    .fail(
      function (jqXHR, textStatus, errorThrown) {
        console.log('textStatus: ' + textStatus)
        console.log('errorThrown: ' + errorThrown)
        utility.runModal('Error encountered while generating the tokenize table result.')
      })
    .always(
      function () {
        $('#status-analyze').css({'visibility': 'hidden'})
      })
}

$(function () {
  // The event handler for clicking on download matrix in the modal.
  $('#choose-download').click(function () {
    $('#download-tokenizer').click()
  })
  // The event handler for clicking on continue rendering in the modal.
  $('#choose-continue').click(function () {
    generateTokenizerResult()
  })

  // The event handler for download tokenize clicked.
  $('#download-tokenizer').click(function (download) {
    // On check possible submission error on click.
    const error = utility.submissionError(1)
    // If error found, run modal to display message and prevent the submission from happening.
    if (error !== null) {
      utility.runModal(error)
      download.preventDefault()
    }
  })

  // The event handler for generate tokenize clicked.
  $('#get-tokenizer').click(function () {
    // Get the possible error happened during submission the ajax call.
    const error = utility.submissionError(1)

    if (error === null) { // If there is no error, do the ajax call.
      checkTokenizerSize()
    } else { // If error found, do modal.
      utility.runModal(error)
    }
  })
})
<|endoftext|>