<commit_before>var socketio = require("socket.io"),
    crypto = require("crypto"),
    uuid = require("uuid");

/**
 * Utility
 */
function safeCb(cb) {
    if (typeof cb === 'function') {
        return cb;
    } else {
        return function () {};
    }
}

module.exports = function Signaling(server, config) {
    var io = socketio.listen(server);

    io.sockets.on("connection", function (client) {

        client.resources = {
            profile: {},
            video: true,
            audio: false,
            screen: false
        }

        // send private message to another id
        client.on("message", function (msg) {
            if (!msg) return;

            var toClient = io.to(msg.to);
            if (!toClient) {
                // TODO: send msg to a room ?
                return;
            }

            msg.from = client.id;
            toClient.emit("message", msg);

        });

        client.on("shareScreen", function () {
            client.resources.screen = true;
        });

        client.on("unshareScreen", function (type) {
            client.resources.screen = false;
            removeFeed("screen");
        });

        function removeFeed(type) {
            if (client.room) {
                // remove resources type in the room
                io.sockets.in(client.room).emit("remove", {
                    id: client.id,
                    type: type
                });
                // leave the room
                if (!type) {
                    client.leave(client.room);
                    delete client.room;
                }
            }
        }

        function joinRoom(name, cb) {
            // sanity check
            if (typeof name !== "string") return; // do nothing

            // check  max clients in the room
            var current = clientsInRoom(name);
            if (config.rooms && config.rooms.maxClients > 0 && current >= config.rooms.maxClients) {
                safeCb(cb)("full");
                return;
            }
            // leave all rooms
            removeFeed();
            safeCb(cb)(null, describeRoom(name));
            client.join(name);
            client.room = name;
        }

        /**
         * Event: join, leave, disconnect
         */
        client.on("join", joinRoom);
        client.on("leave", function () {
            removeFeed();
        });

        // we don't want to pass "leave" directly because the
        // event type string of "socket end" gets passed too.
        client.on('disconnect', function () {
            removeFeed();
        });

        client.on("create", function (name, cb) {
            name = name || uuid.v4();

            // check room is exists
            var room = io.nsps["/"].adapter.rooms[name];
            if (room && room.length) {
                safeCb(cb)("taken");
            } else {
                joinRoom(name);
                safeCb(cb)(null, name);
            }
        });

        // create shared secret nonces for TURN authentication
        // the process is described in draft-uberti-behave-turn-rest
        var credentials = [];
        // allow selectively vending turn credentials based on origin.
        var origin = client.handshake.headers.origin;
        if (!config.turnorigins || config.turnorigins.indexOf(origin) !== -1) {
            config.turnservers.forEach(function (server) {
                var hmac = crypto.createHmac('sha1', server.secret);
                // default to 86400 seconds timeout unless specified
                var username = Math.floor(new Date().getTime() / 1000) + (server.expiry || 86400) + "";
                hmac.update(username);
                credentials.push({
                    username: username,
                    credential: hmac.digest('base64'),
                    urls: server.urls || server.url
                });
            });
        }

        var iceInfo = {
            stunservers: config.stunservers || [],
            turnservers: credentials
        }

        // notify client about stun and turn servers
        client.emit("iceservers", iceInfo);

    });

    function clientsInRoom(name) {
        // return io.sockets.clients(name).length;
        var adapter = io.nsps["/"].adapter;
        var clients = adapter.rooms[name] || {};
        return Object.keys(clients).length;
    }

    function describeRoom(name) {
        var adapter = io.nsps["/"].adapter;
        var room = adapter.rooms[name] || {};
        var sockets = room.sockets || {};
        var current = Object.keys(sockets).length;
        var result = {
            roomName: name,
            roomCount: current,
            clients: {}
        }

        Object.keys(sockets).forEach(function (id) {
            var client = adapter.nsp.connected[id];
            if (client) {
                result.clients[id] = client.resources;
            }
        });
        return result;
    }
}<commit_msg>set default empty turnservers if not available in config<commit_after>var socketio = require("socket.io"),
    crypto = require("crypto"),
    uuid = require("uuid");

/**
 * Utility
 */
function safeCb(cb) {
    if (typeof cb === 'function') {
        return cb;
    } else {
        return function () {};
    }
}

module.exports = function Signaling(server, config) {
    var io = socketio.listen(server);

    io.sockets.on("connection", function (client) {

        client.resources = {
            profile: {},
            video: true,
            audio: false,
            screen: false
        }

        // send private message to another id
        client.on("message", function (msg) {
            if (!msg) return;

            var toClient = io.to(msg.to);
            if (!toClient) {
                // TODO: send msg to a room ?
                return;
            }

            msg.from = client.id;
            toClient.emit("message", msg);

        });

        client.on("shareScreen", function () {
            client.resources.screen = true;
        });

        client.on("unshareScreen", function (type) {
            client.resources.screen = false;
            removeFeed("screen");
        });

        function removeFeed(type) {
            if (client.room) {
                // remove resources type in the room
                io.sockets.in(client.room).emit("remove", {
                    id: client.id,
                    type: type
                });
                // leave the room
                if (!type) {
                    client.leave(client.room);
                    delete client.room;
                }
            }
        }

        function joinRoom(name, cb) {
            // sanity check
            if (typeof name !== "string") return; // do nothing

            // check  max clients in the room
            var current = clientsInRoom(name);
            if (config.rooms && config.rooms.maxClients > 0 && current >= config.rooms.maxClients) {
                safeCb(cb)("full");
                return;
            }
            // leave all rooms
            removeFeed();
            safeCb(cb)(null, describeRoom(name));
            client.join(name);
            client.room = name;
        }

        /**
         * Event: join, leave, disconnect
         */
        client.on("join", joinRoom);
        client.on("leave", function () {
            removeFeed();
        });

        // we don't want to pass "leave" directly because the
        // event type string of "socket end" gets passed too.
        client.on('disconnect', function () {
            removeFeed();
        });

        client.on("create", function (name, cb) {
            name = name || uuid.v4();

            // check room is exists
            var room = io.nsps["/"].adapter.rooms[name];
            if (room && room.length) {
                safeCb(cb)("taken");
            } else {
                joinRoom(name);
                safeCb(cb)(null, name);
            }
        });

        // create shared secret nonces for TURN authentication
        // the process is described in draft-uberti-behave-turn-rest
        var credentials = [];
        // allow selectively vending turn credentials based on origin.
        var origin = client.handshake.headers.origin;
        if (!config.turnorigins || config.turnorigins.indexOf(origin) !== -1) {
            var turnservers = config.turnservers || [];
            turnservers.forEach(function (server) {
                var hmac = crypto.createHmac('sha1', server.secret);
                // default to 86400 seconds timeout unless specified
                var username = Math.floor(new Date().getTime() / 1000) + (server.expiry || 86400) + "";
                hmac.update(username);
                credentials.push({
                    username: username,
                    credential: hmac.digest('base64'),
                    urls: server.urls || server.url
                });
            });
        }

        var iceInfo = {
            stunservers: config.stunservers || [],
            turnservers: credentials
        }

        // notify client about stun and turn servers
        client.emit("iceservers", iceInfo);

    });

    function clientsInRoom(name) {
        // return io.sockets.clients(name).length;
        var adapter = io.nsps["/"].adapter;
        var clients = adapter.rooms[name] || {};
        return Object.keys(clients).length;
    }

    function describeRoom(name) {
        var adapter = io.nsps["/"].adapter;
        var room = adapter.rooms[name] || {};
        var sockets = room.sockets || {};
        var current = Object.keys(sockets).length;
        var result = {
            roomName: name,
            roomCount: current,
            clients: {}
        }

        Object.keys(sockets).forEach(function (id) {
            var client = adapter.nsp.connected[id];
            if (client) {
                result.clients[id] = client.resources;
            }
        });
        return result;
    }
}<|endoftext|>


<commit_before>import kotoAssert from './assert.js';
import Layer from './layer.js';

/**
 * Create a koto chart
 *
 * @constructor
 *
 * @param {d3.selection} selection The chart's "base" DOM node. This should
 *        contain any nodes that the chart generates.
 */
class Chart {
  constructor(selection) {
    this.base = selection; // Container for chart @type {d3.selection}.
      this.hasDrawn = false; // Has this chart been drawn at lease once?

      function baseExtend(dst, maps) {
        var setDst = function (value, key) {
            dst.set(key, value);
        };
        for (var i = 0, ii = maps.length; i < ii; ++i) {
          var map = maps[i];
          map.forEach(setDst);
        }
        return dst;
      }

      this.merge = {
        configs: function(){
          var merged = baseExtend(this.configs, arguments);
          return merged;
        }.bind(this),
        accessors: function(){
          var merged = baseExtend(this.accessors, arguments);
          return merged;
        }.bind(this)
      };

      // exposed properties
      this.configs = new Map();
      this.accessors = new Map();

      // private
      this._layers = new Map();
      this._attached = new Map();
      this._events = new Map();
  }

  /**
   * A "hook" method that you may define to modify input data before it is used
   * to draw the chart's layers and attachments. This method will be used by all
   * sub-classes.
   *
   * Note: you will most likely never call this method directly, but rather
   * include it as part of a chart definition, and then rely on d3.chart to
   * invoke it when you draw the chart with {@link Chart#draw}.
   *
   * @param {Array} data Input data provided to @link Chart#draw}.
   * @returns {mixed} Data to be used in drawing the chart's layers and
   *                  attachments.
   */
  transform(data) { return data; }

  /**
   * A "hook" method that you may define to choose which mutation of the input
   * data is sent to which of the attached charts (by name). This method will
   * be used by all sub-classes. This only applies to charts that use the
   * {@link Chart#attach} method.
   *
   * Note: you will most likely never call this method directly, but rather
   * include it as part of a chart definition, and then rely on d3.chart to
   * invoke it when you draw the chart with {@link Chart#draw}.
   *
   * @param {String} data Name of attached chart defined in {@link Chart#attach}.
   * @param {Array} data Input data provided to {@link Chart#draw}.
   * @returns {mixed} Data to be used in drawing the chart's layers and
   *                  attachments.
   */
  demux(name, data) { return data; }

  /**
   * A "hook" method that will allow you to run some arbitrary code before
   * {@link Chart#draw}. This will run everytime {@link Chart#draw} is called.
   *
   * Note: you will most likely never call this method directly, but rather
   * include it as part of a chart definition, and then rely on d3.chart to
   * invoke it when you draw the chart with {@link Chart#draw}.
   *
   * @param  {[type]} data [description]
   * @return {[type]}      [description]
   */
  preDraw() {}

  /**
   * A "hook" method that will allow you to run some arbitrary code after
   * {@link Chart#draw}. This will run everytime {@link Chart#draw} is called.
   *
   * Note: you will most likely never call this method directly, but rather
   * include it as part of a chart definition, and then rely on d3.chart to
   * invoke it when you draw the chart with {@link Chart#draw}.
   *
   * @param  {[type]} data [description]
   * @return {[type]}      [description]
   */
  postDraw() {}

  /**
   * Remove a layer from the chart.
   *
   * @param {String} name The name of the layer to remove.
   * @returns {Layer} The layer removed by this operation.
   */
  unlayer(name) {
    var layer = this.layer(name);

    this._layers.delete(name);
    delete layer._chart;

    return layer;
  }

  /**
   * Interact with the chart's {@link Layer|layers}.
   *
   * If only a `name` is provided, simply return the layer registered to that
   * name (if any).
   *
   * If a `name` and `selection` are provided, treat the `selection` as a
   * previously-created layer and attach it to the chart with the specified
   * `name`.
   *
   * If all three arguments are specified, initialize a new {@link Layer} using
   * the specified `selection` as a base passing along the specified `options`.
   *
   * The {@link Layer.draw} method of attached layers will be invoked
   * whenever this chart's {@link Chart#draw} is invoked and will receive the
   * data (optionally modified by the chart's {@link Chart#transform} method.
   *
   * @param {String} name Name of the layer to attach or retrieve.
   * @param {d3.selection|Layer} [selection] The layer's base or a
   *        previously-created {@link Layer}.
   * @param {Object} [options] Options to be forwarded to {@link Layer|the Layer
   *        constructor}
   *
   * @returns {Layer}
   */
  layer(name, selection, options) {
    var _Chart = this;
    var _layer;

    if (arguments.length === 1) {
      return this._layers.get(name);
    }

    // we are reattaching a previous layer, which the
    // selection argument is now set to.
    if (arguments.length === 2) {

      if (typeof selection.draw === 'function') {
        selection._chart = this;
        this._layers.set(name, selection);
        return this._layers.get(name);

      } else {
        kotoAssert(false, 'When reattaching a layer, the second argument '+
          'must be a d3.chart layer');
      }
    }

    _layer = new Layer(selection, options);

    this._layers.set(name, _layer);

    selection._chart = this;

    _layer.remove = function () {
      _Chart._layers.delete(name);
      return this;
    };

    return _layer;
  }

  /**
   * Register or retrieve an "attachment" Chart. The "attachment" chart's `draw`
   * method will be invoked whenever the containing chart's `draw` method is
   * invoked.
   *
   * @param {String} attachmentName Name of the attachment
   * @param {Chart} [chart] koto to register as a mix in of this chart. When
   *        unspecified, this method will return the attachment previously
   *        registered with the specified `attachmentName` (if any).
   *
   * @returns {Chart} Reference to this chart (chainable).
   */
  attach(attachmentName, chart) {
    if (arguments.length === 1) {
      return this._attached.get(attachmentName);
    }

    this._attached.set(attachmentName, chart);
    return chart;
  }

  /**
   * Update the chart's representation in the DOM, drawing all of its layers and
   * any "attachment" charts (as attached via {@link Chart#attach}).
   *
   * Note: The first time you call this method, the property `hasDrawn` will be
   * set to true. This is helpful if you want to only run some code on the first
   * time the chart is drawn.
   *
   * @param {Object} data Data to pass to the {@link Layer#draw|draw method} of
   *        this cart's {@link Layer|layers} (if any) and the {@link
   *        Chart#draw|draw method} of this chart's attachments (if any).
   */
  draw(rawData) {
    var layer, attachmentData;

    var data = this.transform(rawData);

    this.preDraw(data);

    for (layer of this._layers.values()) {
      layer.draw(data);
    }

    for (let [attachmentName, attachment] of this._attached.entries()) {
      attachmentData = this.demux ? this.demux(attachmentName, data) : data;
      attachment.draw(attachmentData);
    }

    this.hasDrawn = true;

    this.postDraw(data);
  }

  /**
   * Function invoked with the context specified when the handler was bound (via
   * {@link Chart#on} {@link Chart#once}).
   *
   * @callback ChartEventHandler
   * @param {...*} arguments Invoked with the arguments passed to {@link
   *         Chart#trigger}
   */

  /**
   * Subscribe a callback function to an event triggered on the chart. See {@link
   * Chart#once} to subscribe a callback function to an event for one occurence.
   *
   * @externalExample {runnable} chart-on
   *
   * @param {String} name Name of the event
   * @param {ChartEventHandler} callback Function to be invoked when the event
   *        occurs
   * @param {Object} [context] Value to set as `this` when invoking the
   *        `callback`. Defaults to the chart instance.
   *
   * @returns {Chart} A reference to this chart (chainable).
   */
  on(name, callback, context) {
    var events;
    if (this._events.has(name)) {
      events = this._events.get(name);
    } else {
      events = new Set();
    }

    events.add({
      callback: callback,
      context: context || this,
      _chart: this
    });

    this._events.set(name, events);
    return this;
  }

  /**
   * Subscribe a callback function to an event triggered on the chart. This
   * function will be invoked at the next occurance of the event and immediately
   * unsubscribed. See {@link Chart#on} to subscribe a callback function to an
   * event indefinitely.
   *
   * @externalExample {runnable} chart-once
   *
   * @param {String} name Name of the event
   * @param {ChartEventHandler} callback Function to be invoked when the event
   *        occurs
   * @param {Object} [context] Value to set as `this` when invoking the
   *        `callback`. Defaults to the chart instance
   *
   * @returns {Chart} A reference to this chart (chainable)
   */
  once(name, callback, context) {
    var self = this;
    var _once = function() {
      self.off(name, _once);
      callback.apply(this, arguments);
    };
    return this.on(name, _once, context);
  }

  /**
   * Unsubscribe one or more callback functions from an event triggered on the
   * chart. When no arguments are specified, *all* handlers will be unsubscribed.
   * When only a `name` is specified, all handlers subscribed to that event will
   * be unsubscribed. When a `name` and `callback` are specified, only that
   * function will be unsubscribed from that event. When a `name` and `context`
   * are specified (but `callback` is omitted), all events bound to the given
   * event with the given context will be unsubscribed.
   *
   * @externalExample {runnable} chart-off
   *
   * @param {String} [name] Name of the event to be unsubscribed
   * @param {ChartEventHandler} [callback] Function to be unsubscribed
   * @param {Object} [context] Contexts to be unsubscribe
   *
   * @returns {Chart} A reference to this chart (chainable).
   */
  off(name, callback, context) {

    // remove all events
    if (arguments.length === 0) {
      this._events.clear();
      return this;
    }

    // remove all events for a specific name
    if (arguments.length === 1) {
      if (this._events.has(name)) {
        this._events.get(name).clear();
      }
      return this;
    }

    // remove all events that match whatever combination of name, context
    // and callback.

    this._events.get(name).forEach((event, clone, map) => {
      if ((callback && callback === clone.callback) ||
          (context && context === clone.context)) {
        map.delete(event);
      }
    });

    return this;
  }

  /**
   * Publish an event on this chart with the given `name`.
   *
   * @externalExample {runnable} chart-trigger
   *
   * @param {String} name Name of the event to publish
   * @param {...*} arguments Values with which to invoke the registered
   *        callbacks.
   *
   * @returns {Chart} A reference to this chart (chainable).
   */
  trigger(name, ...args) {
    if (this._events.has(name)) {
      this._events.get(name).forEach((event) => {
        event.callback.call(event.context, ...args);
      });
    }
    return this;
  }
  /**
   * Get and set chart options (or configs)
   *
   * @param  {mixed} nameOrObject name of item getting or setting
   *                              or its an object with key value pairs.
   * @param  {mixed} value the value for config item witha that name.
   * @return {mixed} if getting, its the value. if setting it is the chart instance.
   */
  config(nameOrObject, value) {
    var key;
    var definition;

    if (arguments.length === 0) {
      return this.configs;
    }

    if (arguments.length === 1) {
      if (typeof nameOrObject === 'object') {
        for (key in nameOrObject) {
          if(this.configs.has(key)) {
            definition = this.configs.get(key);
            definition.value = nameOrObject[key];
            this.configs.set(key, definition); // redundant?
          } else {
            console.warn(`config with name ${nameOrObject} is not defined.`);
          }
        }
        return this;
      }
      kotoAssert(this.configs.has(nameOrObject), `${nameOrObject} is not a valid option.`);
      return this.configs.get(nameOrObject).value;
    }

    if(arguments.length === 2) {
      if(this.configs.has(nameOrObject)) {
        definition = this.configs.get(nameOrObject);
        definition.value = value;
        this.configs.set(nameOrObject, definition);
      } else {
        console.warn(`config with name ${nameOrObject} is not defined.`);
      }
      return this;
    }
  }

  /**
   * This will get or set any of the chart's accessors.
   *
   * @param  {String or Object} item If string, it will return the function for that accessor item.
   *                                 If object, it will update that accessor with set function.
   * @param  {function} [value] The function to update accessor item with.
   * @return {object} The chart to preserve chainability.
   */
  accessor (item, value) {
    var key;
    if (arguments.length === 0) {
      return this.accessors;
    }

    if (arguments.length === 1) {
      if (typeof item === 'string') {
        kotoAssert(this.accessors.has(item), `${item} is not a valid accessor.`);
        return this.accessors.get(item);
      } else {
        for (key in item) {
          this.accessors.set(key, item[key]);
        }
      }
    } else {
      this.accessors.set(item, value);
    }
    return this;
  }
}

export default Chart;
<commit_msg>fix(chart): the chart's merge functionality is much better<commit_after>import kotoAssert from './assert.js';
import Layer from './layer.js';

/**
 * Create a koto chart
 *
 * @constructor
 *
 * @param {d3.selection} selection The chart's "base" DOM node. This should
 *        contain any nodes that the chart generates.
 */
class Chart {
  constructor(selection) {
    this.base = selection; // Container for chart @type {d3.selection}.
      this.hasDrawn = false; // Has this chart been drawn at lease once?

      function baseExtend(dst, maps) {
        var setDst = function (value, key) {
            dst.set(key, value);
        };
        for (var i = 0, ii = maps.length; i < ii; ++i) {
          var map = maps[i];
          map.forEach(setDst);
        }
        return dst;
      }

      this.merge = {
        configs: function(){
          var merged = baseExtend(this.configs, arguments);
          return merged;
        }.bind(this),
        accessors: function(){
          var merged = baseExtend(this.accessors, arguments);
          return merged;
        }.bind(this)
      };

      // exposed properties
      this.configs = new Map();
      this.accessors = new Map();

      // private
      this._layers = new Map();
      this._attached = new Map();
      this._events = new Map();
  }

  /**
   * A "hook" method that you may define to modify input data before it is used
   * to draw the chart's layers and attachments. This method will be used by all
   * sub-classes.
   *
   * Note: you will most likely never call this method directly, but rather
   * include it as part of a chart definition, and then rely on d3.chart to
   * invoke it when you draw the chart with {@link Chart#draw}.
   *
   * @param {Array} data Input data provided to @link Chart#draw}.
   * @returns {mixed} Data to be used in drawing the chart's layers and
   *                  attachments.
   */
  transform(data) { return data; }

  /**
   * A "hook" method that you may define to choose which mutation of the input
   * data is sent to which of the attached charts (by name). This method will
   * be used by all sub-classes. This only applies to charts that use the
   * {@link Chart#attach} method.
   *
   * Note: you will most likely never call this method directly, but rather
   * include it as part of a chart definition, and then rely on d3.chart to
   * invoke it when you draw the chart with {@link Chart#draw}.
   *
   * @param {String} data Name of attached chart defined in {@link Chart#attach}.
   * @param {Array} data Input data provided to {@link Chart#draw}.
   * @returns {mixed} Data to be used in drawing the chart's layers and
   *                  attachments.
   */
  demux(name, data) { return data; }

  /**
   * A "hook" method that will allow you to run some arbitrary code before
   * {@link Chart#draw}. This will run everytime {@link Chart#draw} is called.
   *
   * Note: you will most likely never call this method directly, but rather
   * include it as part of a chart definition, and then rely on d3.chart to
   * invoke it when you draw the chart with {@link Chart#draw}.
   *
   * @param  {[type]} data [description]
   * @return {[type]}      [description]
   */
  preDraw() {}

  /**
   * A "hook" method that will allow you to run some arbitrary code after
   * {@link Chart#draw}. This will run everytime {@link Chart#draw} is called.
   *
   * Note: you will most likely never call this method directly, but rather
   * include it as part of a chart definition, and then rely on d3.chart to
   * invoke it when you draw the chart with {@link Chart#draw}.
   *
   * @param  {[type]} data [description]
   * @return {[type]}      [description]
   */
  postDraw() {}

  /**
   * Remove a layer from the chart.
   *
   * @param {String} name The name of the layer to remove.
   * @returns {Layer} The layer removed by this operation.
   */
  unlayer(name) {
    var layer = this.layer(name);

    this._layers.delete(name);
    delete layer._chart;

    return layer;
  }

  /**
   * Interact with the chart's {@link Layer|layers}.
   *
   * If only a `name` is provided, simply return the layer registered to that
   * name (if any).
   *
   * If a `name` and `selection` are provided, treat the `selection` as a
   * previously-created layer and attach it to the chart with the specified
   * `name`.
   *
   * If all three arguments are specified, initialize a new {@link Layer} using
   * the specified `selection` as a base passing along the specified `options`.
   *
   * The {@link Layer.draw} method of attached layers will be invoked
   * whenever this chart's {@link Chart#draw} is invoked and will receive the
   * data (optionally modified by the chart's {@link Chart#transform} method.
   *
   * @param {String} name Name of the layer to attach or retrieve.
   * @param {d3.selection|Layer} [selection] The layer's base or a
   *        previously-created {@link Layer}.
   * @param {Object} [options] Options to be forwarded to {@link Layer|the Layer
   *        constructor}
   *
   * @returns {Layer}
   */
  layer(name, selection, options) {
    var _Chart = this;
    var _layer;

    if (arguments.length === 1) {
      return this._layers.get(name);
    }

    // we are reattaching a previous layer, which the
    // selection argument is now set to.
    if (arguments.length === 2) {

      if (typeof selection.draw === 'function') {
        selection._chart = this;
        this._layers.set(name, selection);
        return this._layers.get(name);

      } else {
        kotoAssert(false, 'When reattaching a layer, the second argument '+
          'must be a d3.chart layer');
      }
    }

    _layer = new Layer(selection, options);

    this._layers.set(name, _layer);

    selection._chart = this;

    _layer.remove = function () {
      _Chart._layers.delete(name);
      return this;
    };

    return _layer;
  }

  /**
   * Register or retrieve an "attachment" Chart. The "attachment" chart's `draw`
   * method will be invoked whenever the containing chart's `draw` method is
   * invoked.
   *
   * @param {String} attachmentName Name of the attachment
   * @param {Chart} [chart] koto to register as a mix in of this chart. When
   *        unspecified, this method will return the attachment previously
   *        registered with the specified `attachmentName` (if any).
   *
   * @returns {Chart} Reference to this chart (chainable).
   */
  attach(attachmentName, chart) {
    if (arguments.length === 1) {
      return this._attached.get(attachmentName);
    }

    this._attached.set(attachmentName, chart);
    return chart;
  }

  /**
   * Update the chart's representation in the DOM, drawing all of its layers and
   * any "attachment" charts (as attached via {@link Chart#attach}).
   *
   * Note: The first time you call this method, the property `hasDrawn` will be
   * set to true. This is helpful if you want to only run some code on the first
   * time the chart is drawn.
   *
   * @param {Object} data Data to pass to the {@link Layer#draw|draw method} of
   *        this cart's {@link Layer|layers} (if any) and the {@link
   *        Chart#draw|draw method} of this chart's attachments (if any).
   */
  draw(rawData) {
    var layer, attachmentData;

    var data = this.transform(rawData);

    this.preDraw(data);

    for (layer of this._layers.values()) {
      layer.draw(data);
    }

    for (let [attachmentName, attachment] of this._attached.entries()) {
      attachmentData = this.demux ? this.demux(attachmentName, data) : data;
      attachment.draw(attachmentData);
    }

    this.hasDrawn = true;

    this.postDraw(data);
  }

  /**
   * Function invoked with the context specified when the handler was bound (via
   * {@link Chart#on} {@link Chart#once}).
   *
   * @callback ChartEventHandler
   * @param {...*} arguments Invoked with the arguments passed to {@link
   *         Chart#trigger}
   */

  /**
   * Subscribe a callback function to an event triggered on the chart. See {@link
   * Chart#once} to subscribe a callback function to an event for one occurence.
   *
   * @externalExample {runnable} chart-on
   *
   * @param {String} name Name of the event
   * @param {ChartEventHandler} callback Function to be invoked when the event
   *        occurs
   * @param {Object} [context] Value to set as `this` when invoking the
   *        `callback`. Defaults to the chart instance.
   *
   * @returns {Chart} A reference to this chart (chainable).
   */
  on(name, callback, context) {
    var events;
    if (this._events.has(name)) {
      events = this._events.get(name);
    } else {
      events = new Set();
    }

    events.add({
      callback: callback,
      context: context || this,
      _chart: this
    });

    this._events.set(name, events);
    return this;
  }

  /**
   * Subscribe a callback function to an event triggered on the chart. This
   * function will be invoked at the next occurance of the event and immediately
   * unsubscribed. See {@link Chart#on} to subscribe a callback function to an
   * event indefinitely.
   *
   * @externalExample {runnable} chart-once
   *
   * @param {String} name Name of the event
   * @param {ChartEventHandler} callback Function to be invoked when the event
   *        occurs
   * @param {Object} [context] Value to set as `this` when invoking the
   *        `callback`. Defaults to the chart instance
   *
   * @returns {Chart} A reference to this chart (chainable)
   */
  once(name, callback, context) {
    var self = this;
    var _once = function() {
      self.off(name, _once);
      callback.apply(this, arguments);
    };
    return this.on(name, _once, context);
  }

  /**
   * Unsubscribe one or more callback functions from an event triggered on the
   * chart. When no arguments are specified, *all* handlers will be unsubscribed.
   * When only a `name` is specified, all handlers subscribed to that event will
   * be unsubscribed. When a `name` and `callback` are specified, only that
   * function will be unsubscribed from that event. When a `name` and `context`
   * are specified (but `callback` is omitted), all events bound to the given
   * event with the given context will be unsubscribed.
   *
   * @externalExample {runnable} chart-off
   *
   * @param {String} [name] Name of the event to be unsubscribed
   * @param {ChartEventHandler} [callback] Function to be unsubscribed
   * @param {Object} [context] Contexts to be unsubscribe
   *
   * @returns {Chart} A reference to this chart (chainable).
   */
  off(name, callback, context) {

    // remove all events
    if (arguments.length === 0) {
      this._events.clear();
      return this;
    }

    // remove all events for a specific name
    if (arguments.length === 1) {
      if (this._events.has(name)) {
        this._events.get(name).clear();
      }
      return this;
    }

    // remove all events that match whatever combination of name, context
    // and callback.

    this._events.get(name).forEach((event, clone, map) => {
      if ((callback && callback === clone.callback) ||
          (context && context === clone.context)) {
        map.delete(event);
      }
    });

    return this;
  }

  /**
   * Publish an event on this chart with the given `name`.
   *
   * @externalExample {runnable} chart-trigger
   *
   * @param {String} name Name of the event to publish
   * @param {...*} arguments Values with which to invoke the registered
   *        callbacks.
   *
   * @returns {Chart} A reference to this chart (chainable).
   */
  trigger(name, ...args) {
    if (this._events.has(name)) {
      this._events.get(name).forEach((event) => {
        event.callback.call(event.context, ...args);
      });
    }
    return this;
  }
  /**
   * Get and set chart options (or configs)
   *
   * @param  {mixed} nameOrObject name of item getting or setting
   *                              or its an object with key value pairs.
   * @param  {mixed} value the value for config item witha that name.
   * @return {mixed} if getting, its the value. if setting it is the chart instance.
   */
  config(nameOrObject, value) {
    var key;
    var definition;

    if (arguments.length === 0) {
      return this.configs;
    }

    if (arguments.length === 1) {
      if (typeof nameOrObject === 'object') {
        for (key in nameOrObject) {
          if(this.configs.has(key)) {
            definition = this.configs.get(key);
            if (definition.hasOwnProperty('setter')) {
              definition.value = definition.setter.call(definition, nameOrObject[key]);
            } else {
              definition.value = nameOrObject[key];
            }
            this.configs.set(key, definition); // redundant?
          } else {
            console.warn(`config with name ${nameOrObject} is not defined.`);
          }
        }
        return this;
      }
      kotoAssert(this.configs.has(nameOrObject), `${nameOrObject} is not a valid option.`);
      var definition = this.configs.get(nameOrObject);
      if (definition.hasOwnProperty('getter')) {
        return definition.getter.call(definition);
      }
      return definition.value;
    }

    if(arguments.length === 2) {
      if(this.configs.has(nameOrObject)) {
        definition = this.configs.get(nameOrObject);
        if (definition.hasOwnProperty('setter')) {
          definition.value = definition.setter.call(definition, value);
        } else {
          definition.value = value;
        }
        this.configs.set(nameOrObject, definition);
      } else {
        console.warn(`config with name ${nameOrObject} is not defined.`);
      }
      return this;
    }
  }

  /**
   * This will get or set any of the chart's accessors.
   *
   * @param  {String or Object} item If string, it will return the function for that accessor item.
   *                                 If object, it will update that accessor with set function.
   * @param  {function} [value] The function to update accessor item with.
   * @return {object} The chart to preserve chainability.
   */
  accessor (item, value) {
    var key;
    if (arguments.length === 0) {
      return this.accessors;
    }

    if (arguments.length === 1) {
      if (typeof item === 'string') {
        kotoAssert(this.accessors.has(item), `${item} is not a valid accessor.`);
        return this.accessors.get(item);
      } else {
        for (key in item) {
          this.accessors.set(key, item[key]);
        }
      }
    } else {
      this.accessors.set(item, value);
    }
    return this;
  }
}

export default Chart;
<|endoftext|>


<commit_before>import React from 'react';
import moment from 'moment';
import { Link } from 'react-router';

class TimelineItem extends React.Component {
  getUpdateContent() {
    const { update } = this.props;
    const when = moment(update.updated_at).fromNow();
    if (update.assignment) {
      const book = update.assignment.book;
      const wasMarkedAsRead = update.mark_book_read === 1;
      const numPagesRead = update.num_pages;

      if (wasMarkedAsRead) {
        return (

              {book.title} by {book.author} was marked as read

            ({when})

        );
      }

      return (

            Read {numPagesRead} pages of {book.title} by {book.author}

          ({when})

      );
    }

    const circleColor = this.getCircleColor(update);
    const _className = `circle ${circleColor}`;
    const linkContent = `/app/responses/student-response/${update.id}`;
    return (

          Submitted a response for {update.book.title} by {update.book.author}

        ({when})

            <Link className='link-response' to={linkContent}>View response</Link>

    );
  }

  render() {
    return (

        {this.getUpdateContent()}

    );
  }

  getCircleColor(update) {
    return (update.currentReview) ? update.currentReview.decision_type_name
      : 'no-decision';
  }
}

export default TimelineItem;
<commit_msg>make sure we show num_pages_read in timeline<commit_after>import React from 'react';
import moment from 'moment';
import { Link } from 'react-router';

class TimelineItem extends React.Component {
  getUpdateContent() {
    const { update } = this.props;
    const when = moment(update.updated_at).fromNow();
    if (update.assignment) {
      const book = update.assignment.book;
      const wasMarkedAsRead = update.mark_book_read === 1;
      const numPagesRead = update.num_pages_read;

      if (wasMarkedAsRead) {
        return (

              {book.title} by {book.author} was marked as read

            ({when})

        );
      }

      return (

            Read {numPagesRead} pages of {book.title} by {book.author}

          ({when})

      );
    }

    const circleColor = this.getCircleColor(update);
    const _className = `circle ${circleColor}`;
    const linkContent = `/app/responses/student-response/${update.id}`;
    return (

          Submitted a response for {update.book.title} by {update.book.author}

        ({when})

            <Link className='link-response' to={linkContent}>View response</Link>

    );
  }

  render() {
    return (

        {this.getUpdateContent()}

    );
  }

  getCircleColor(update) {
    return (update.currentReview) ? update.currentReview.decision_type_name
      : 'no-decision';
  }
}

export default TimelineItem;
<|endoftext|>


<commit_before>var mathjs = require("mathjs");
var AStarGraph = require("./AStarGraph");

(function(exports) {

    var _PathNode = class PathNode {
        constructor(position, velocity, acceleration) {
            this.s = position;
            this.v = velocity || Array(position.length).fill(0);
            this.a = acceleration || Array(position.length).fill(0);
        }
    }
    class Trajectory extends AStarGraph {
        constructor(options={}) {
            super(options);
            this.dimensions = options.dimensions || 1;
            this.vMax = options.maxVelocity || Array(this.dimensions).fill(10);
            this.aMax = options.maxAcceleration || Array(this.dimensions).fill(1);
            this.jMax = options.maxJerk || Array(this.dimensions).fill(1);
            this.a0 = Array(this.dimensions).fill(0);
            this.nodeMap = {};
        }
        getNode(position, velocity, acceleration) {
            var node = new _PathNode(position, velocity, acceleration);
            var key = JSON.stringify(node);
            return (this.nodeMap[key] = this.nodeMap[key] || node);
        }
        axisAccelerations(node, i) {
            var av = [];
            var a = node.a[i];
            var v = node.v[i];
            var vMax = this.vMax[i];
            var aMax = this.aMax[i];
            var jMax = this.jMax[i];
            var aplus = a + jMax;
            var aminus = a - jMax;
            if (a < aMax && -vMax <= v+aplus && v+aplus <= vMax) {
                av.push(mathjs.min(aMax, a+jMax));
            }
            if (-vMax <= a+v && -vMax <= a+v && a+v <= vMax) {
                av.push(a);
            }
            if (-aMax < a && -vMax <= v+aminus && v+aminus <= vMax) {
                av.push(mathjs.max(-aMax, a-jMax));
            }
            return av;
        }
        neighborsOf(node, goal) {
            var da = node.a.map((a,i) => this.axisAccelerations(node, i));
            var apermutations = Trajectory.permutations(da).map((dai) => {
                var avariation = mathjs.add(node.a,dai);
                var vvariation = mathjs.add(node.v,avariation);
                var svariation = mathjs.add(node.s,vvariation);
                return this.getNode(svariation, vvariation, avariation);
            });
            return apermutations;
        }
        estimateCost(n1, n2) {
            return mathjs.subtract(n1.s, n2.s).reduce((sum, diff) => sum + diff * diff, 0);
        }
        static get PathNode() {
            return _PathNode;
        }
        static permutations(vv) {
            var vvelts = vv.reduce((acc, e) => e == null || !e.length ? 0:(acc+1), 0);
            if (vv.length !== vvelts) {
                return [];
            }
            function * variations() {
                var vi = Array(vv.length).fill(0);
                for(;;) {
                    yield vv.map((v,i) => v[vi[i]]);
                    for (var i = 0; i < vv.length; i++) {
                        if (++vi[i] < vv[i].length) {
                            break;
                        }
                        vi[i] = 0;
                        if (i+1 >= vv.length) {
                            return;
                        }
                    }
                }
            }
            return Array.from(variations());
        }
    }

    module.exports = exports.Trajectory = Trajectory;
})(typeof exports === "object" ? exports : (exports = {}));

(typeof describe === 'function') && describe("Trajectory", function() {
    var should = require("should");
    var Trajectory = exports.Trajectory;
    var PathNode = Trajectory.PathNode;

    it("Trajectory(options) is the constructor", function() {
    })
    it("PathNode(name, pos, velocity) is the constructor", function() {
        var n1 = new PathNode([1,1,1]);
        should.deepEqual(n1.v, [0,0,0]); // default velocity is zero
        var n2 = new PathNode([1,1,1], [1,2,3]);
        should.deepEqual(n2.v, [1,2,3]);
    });
    it("estimateCost(n1,n2) estimates the cost to move between the given nodes", function() {
        var trajectory = new Trajectory();
        var n1 = new PathNode([1,3,2]);
        var n2 = new PathNode([3,1,4]);
        trajectory.estimateCost(n1,n2).should.equal(12);
    })
    it("getNode(s,v,a) returns unique node with given attributes", function() {
        var trj = new Trajectory({
            dimensions: 2,
        });
        var n1 = trj.getNode([1,2],[3,4],[5,6]);
        var n2 = trj.getNode([1,2],[3,4],[5,6]);
        n1.should.equal(n2);
        n1.should.instanceOf(PathNode);
    })
    it("axisAccelerations(node, i) returns possible neighbor accelerations", function() {
        var trj = new Trajectory({
            dimensions: 2,
            maxVelocity: [10, 100],
            maxAcceleration: [4, 5],
            maxJerk: [1, 2],
        });
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[0,0],[0,1]), 0), [1,0,-1]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[0,0],[1,1]), 0), [2,1,0]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[0,0],[4,1]), 0), [4,3]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[0,0],[-4,1]), 0), [-3,-4]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[10,0],[0,0]), 0), [0,-1]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[-10,0],[0,0]), 0), [1,0]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[10,0],[1,0]), 0), [0]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[-10,0],[-2,0]), 0), []);
    })
    it("permutations(vv) generates permutations", function() {
        should.deepEqual(Trajectory.permutations([[1,2],[3,4,5]]), [
            [1,3],
            [2,3],
            [1,4],
            [2,4],
            [1,5],
            [2,5],
        ]);
        should.deepEqual(Trajectory.permutations([[1,2],[],[3,4,5]]), []);
    })
    it("neighborsOf(node, goal) generates node neighbors towards goal", function() {
        var trj = new Trajectory({
            dimensions: 2,
        });
        var start = new PathNode([1,1]);
        var neighbors = trj.neighborsOf(start);
        neighbors.length.should.equal(9);
        neighbors[0].should.equal(trj.getNode([2,2],[1,1],[1,1]));
        neighbors[1].should.equal(trj.getNode([1,2],[0,1],[0,1]));
        neighbors[2].should.equal(trj.getNode([0,2],[-1,1],[-1,1]));
        neighbors[8].should.equal(trj.getNode([0,0],[-1,-1],[-1,-1]));
    })
})
<commit_msg>trajectory<commit_after>var mathjs = require("mathjs");
var AStarGraph = require("./AStarGraph");

(function(exports) {

    class PathNode {
        constructor(position, velocity, acceleration) {
            this.s = position;
            this.v = velocity || Array(position.length).fill(0);
            this.a = acceleration || Array(position.length).fill(0);
        }
    }

    class Trajectory extends AStarGraph {
        constructor(options={}) {
            super(options);
            this.dimensions = options.dimensions || 1;
            this.vMax = options.maxVelocity || Array(this.dimensions).fill(10);
            this.aMax = options.maxAcceleration || Array(this.dimensions).fill(2);
            this.jMax = options.maxJerk || Array(this.dimensions).fill(1);
            this.a0 = Array(this.dimensions).fill(0);
            this.goalDistSquared = this.jMax.reduce((sum, j) => sum + j*j, 0);
            this.nodeMap = {};
        }
        getNode(position, velocity, acceleration) {
            var node = new PathNode(position, velocity, acceleration);
            var key = JSON.stringify(node);
            return (this.nodeMap[key] = this.nodeMap[key] || node);
        }
        axisAccelerations(node, i) {
            Trajectory.validateNode(node);
            var av = [];
            var a = node.a[i];
            var v = node.v[i];
            var vMax = this.vMax[i];
            var aMax = this.aMax[i];
            var jMax = this.jMax[i];
            var aplus = a + jMax;
            var aminus = a - jMax;
            if (a < aMax && -vMax <= v+aplus && v+aplus <= vMax) {
                av.push(mathjs.min(aMax, a+jMax));
            }
            if (-vMax <= a+v && -vMax <= a+v && a+v <= vMax) {
                av.push(a);
            }
            if (-aMax < a && -vMax <= v+aminus && v+aminus <= vMax) {
                av.push(mathjs.max(-aMax, a-jMax));
            }
            return av;
        }
        static validateNode(node) {
            if (!(node instanceof PathNode)) {
                throw new Error("Expected neigbhorsOf(?PathNode?,...)");
            }
            return node;
        }
        isNearGoal(node, goal) { // a goal is zero-velocity
            for (var i = 0; i < this.dimensions; i++) {
                var jMax = this.jMax[i];
                var ds = goal.s[i] - node.s[i];
                var v = node.v[i];
                var dv = 0 - v;
                var a = node.a[i];

                if (ds < -jMax || jMax < ds) {
                    return false; // too far
                }
                if (v < -jMax || jMax < v) {
                    return false; // too fast
                }
                if (a < -jMax || jMax < a) {
                    return false; // too jerky
                }
                if (v < 0 && ds > 0 || v > 0 && ds < 0) {
                    return false; // wrong way velocity
                }
                if (dv > 0 && a < 0 || dv < 0 && a > 0) {
                    return false; // wrong way acceleration
                }
            }
            return true;
        }
        neighborsOf(node, goal) {
            Trajectory.validateNode(node);
            if (goal && this.isNearGoal(node, goal)) {
                return [goal];
            }
            var da = node.a.map((a,i) => this.axisAccelerations(node, i));
            var apermutations = Trajectory.permutations(da).map((dai) => {
                var avariation = mathjs.add(node.a,dai);
                var vvariation = mathjs.add(node.v,avariation);
                var svariation = mathjs.add(node.s,vvariation);
                return this.getNode(svariation, vvariation, avariation);
            });
            return apermutations;
        }
        distSquared(n1, n2) {
            return mathjs.subtract(n1.s, n2.s).reduce((sum, diff) => sum + diff * diff, 0);
        }
        estimateCost(n1, n2) {
            return this.distSquared(n1, n2);
        }
        static get PathNode() {
            return PathNode;
        }
        static permutations(vv) {
            var vvelts = vv.reduce((acc, e) => e == null || !e.length ? 0:(acc+1), 0);
            if (vv.length !== vvelts) {
                return [];
            }
            function * variations() {
                var vi = Array(vv.length).fill(0);
                for(;;) {
                    yield vv.map((v,i) => v[vi[i]]);
                    for (var i = 0; i < vv.length; i++) {
                        if (++vi[i] < vv[i].length) {
                            break;
                        }
                        vi[i] = 0;
                        if (i+1 >= vv.length) {
                            return;
                        }
                    }
                }
            }
            return Array.from(variations());
        }
    }

    module.exports = exports.Trajectory = Trajectory;
})(typeof exports === "object" ? exports : (exports = {}));

(typeof describe === 'function') && describe("Trajectory", function() {
    var should = require("should");
    var Trajectory = exports.Trajectory;
    var PathNode = Trajectory.PathNode;

    it("Trajectory(options) is the constructor", function() {
    })
    it("PathNode(name, pos, velocity) is the constructor", function() {
        var n1 = new PathNode([1,1,1]);
        should.deepEqual(n1.v, [0,0,0]); // default velocity is zero
        var n2 = new PathNode([1,1,1], [1,2,3]);
        should.deepEqual(n2.v, [1,2,3]);
    });
    it("estimateCost(n1,n2) estimates the cost to move between the given nodes", function() {
        var trajectory = new Trajectory();
        var n1 = new PathNode([1,3,2]);
        var n2 = new PathNode([3,1,4]);
        trajectory.estimateCost(n1,n2).should.equal(12);
    })
    it("getNode(s,v,a) returns unique node with given attributes", function() {
        var trj = new Trajectory({
            dimensions: 2,
        });
        var n1 = trj.getNode([1,2],[3,4],[5,6]);
        var n2 = trj.getNode([1,2],[3,4],[5,6]);
        n1.should.equal(n2);
        n1.should.instanceOf(PathNode);
    })
    it("axisAccelerations(node, i) returns possible neighbor accelerations", function() {
        var trj = new Trajectory({
            dimensions: 2,
            maxVelocity: [10, 100],
            maxAcceleration: [4, 5],
            maxJerk: [1, 2],
        });
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[0,0],[0,1]), 0), [1,0,-1]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[0,0],[1,1]), 0), [2,1,0]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[0,0],[4,1]), 0), [4,3]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[0,0],[-4,1]), 0), [-3,-4]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[10,0],[0,0]), 0), [0,-1]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[-10,0],[0,0]), 0), [1,0]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[10,0],[1,0]), 0), [0]);
        should.deepEqual(trj.axisAccelerations(trj.getNode([0,0],[-10,0],[-2,0]), 0), []);
    })
    it("permutations(vv) generates permutations", function() {
        should.deepEqual(Trajectory.permutations([[1,2],[3,4,5]]), [
            [1,3],
            [2,3],
            [1,4],
            [2,4],
            [1,5],
            [2,5],
        ]);
        should.deepEqual(Trajectory.permutations([[1,2],[],[3,4,5]]), []);
    })
    it("neighborsOf(node) generates node neighbors", function() {
        var trj = new Trajectory({
            dimensions: 2,
        });
        var start = new PathNode([1,1]);
        var neighbors = trj.neighborsOf(start);
        neighbors.length.should.equal(9);
        neighbors[0].should.equal(trj.getNode([2,2],[1,1],[1,1]));
        neighbors[1].should.equal(trj.getNode([1,2],[0,1],[0,1]));
        neighbors[2].should.equal(trj.getNode([0,2],[-1,1],[-1,1]));
        neighbors[8].should.equal(trj.getNode([0,0],[-1,-1],[-1,-1]));
    })
    it("TESTTESTisNearGoal(node, goal) returns true if goal is reachable in one step from node", function() {
        var trj = new Trajectory({
            dimensions: 2,
            maxVelocity: [100,100],
            maxAcceleration: [2,2],
            maxJerk: [1,1],
        });
        var goal = new PathNode([1,1]);
        trj.isNearGoal(goal, goal).should.equal(true);

        trj.isNearGoal(trj.getNode([0,0]), goal).should.equal(true);
        trj.isNearGoal(trj.getNode([2,0]), goal).should.equal(true);
        trj.isNearGoal(trj.getNode([2,2]), goal).should.equal(true);
        trj.isNearGoal(trj.getNode([0,2]), goal).should.equal(true);

        trj.isNearGoal(trj.getNode([0,0],[1,1]), goal).should.equal(true); // can stop
        trj.isNearGoal(trj.getNode([2,2],[-1,-1]), goal).should.equal(true); // can stop
        trj.isNearGoal(trj.getNode([0,0],[1.1,1.1]), goal).should.equal(false); // too fast
        trj.isNearGoal(trj.getNode([0,0],[-1.1,-1.1]), goal).should.equal(false); // too fast
        trj.isNearGoal(trj.getNode([0,0],[1,1],[-1,-1]), goal).should.equal(true); // can decelerate
        trj.isNearGoal(trj.getNode([2,2],[-1,-1],[1,1]), goal).should.equal(true); // can decelerate
        trj.isNearGoal(trj.getNode([0,0],[1,1],[-1.1,-1.1]), goal).should.equal(false); // too jerky
        trj.isNearGoal(trj.getNode([2,2],[1,1],[1.1,1.1]), goal).should.equal(false); // too jerky
        trj.isNearGoal(trj.getNode([0,0],[-1,-1]), goal).should.equal(false); // wrong velocity direction
        trj.isNearGoal(trj.getNode([2,2],[1,1]), goal).should.equal(false); // wrong velocity direction
        trj.isNearGoal(trj.getNode([0,0],[1,1],[1,1]), goal).should.equal(false); // wrong acceleration direction
        trj.isNearGoal(trj.getNode([2,2],[-1,-1],[-1,-1]), goal).should.equal(false); // wrong acceleration direction
    })
    it("neighborsOf(node, goal) generates goal node if near", function() {
        var trj = new Trajectory({
            dimensions: 2,
        });
        var goal = new PathNode([1,1]);
        var neighbors = trj.neighborsOf(goal);
        neighbors.forEach((n) => {
            var vinverse = mathjs.multiply(-1, n.v);
            var ninverse = trj.getNode(n.s, vinverse);
            var nn = trj.neighborsOf(ninverse, goal);
            nn.length.should.equal(1, JSON.stringify(n));
            nn[0].should.equal(goal);
        });
        var close = trj.getNode(mathjs.add([0.1,0.1], goal.s), trj.vMax, trj.aMax);
    })
})
<|endoftext|>


<commit_before>/*
 *  Bullet.js
 *  2014/07/16
 *  @auther minimo
 *  This Program is MIT license.
 */

phina.define("pbr.Bullet", {
    superClass: "phina.display.CanvasElement",
    layer: LAYER_BULLET,

    _member: {
        param: null,
        id: -1,

        vx: 0,
        vy: 1,

        isVanish: false,
        isVanishEffect: true,

        rollAngle: 5,
        rolling: true,
    },

    init: function() {
        this.superInit();
        this.$extend(this._member);

        this.boundingType = "circle";
        this.radius = 2;

        this.on("enterframe", function(){
            if (this.rolling) this.rotation += this.rollAngle;

            this.x += this.vx;
            this.y += this.vy;

            //自機との当り判定チェック
            var player = this.parentScene.player;
            if (player.isCollision) {
                if (this.isHitElement(player) ) {
                    player.damage();
                    this.isVanish = true;
                }
            }

            //画面範囲外
            if (this.x<-32 || this.x>SC_W+32 || this.y<-32 || this.y>SC_H+32) {
                this.isVanish = true;
                this.isVanishEffect = false;
            }

            if (this.isVanish) this.remove();
        }.bind(this) );

        //リムーブ時
        this.on("removed", function(){
            if (this.isVanishEffect) pbr.Effect.BulletVanish(this).addChildTo(this.parentScene);
            this.bulletLayer.pool.push(this);
        }.bind(this));
    },

    setup: function(param) {
        param = param.$safe({
            id: -1,
            x: SC_W*0.5,
            y: SC_H*0.5,
            direction: 180,
            velocity: 1,
            type: "RS",
        });

        this.x = param.x;
        this.y = param.y;

        //当り判定設定
        this.boundingType = "circle";
        this.radius = 2;

        this.id = param.id;
        this.vx = Math.cos(param.direction) * param.velocity;
        this.vy = Math.sin(param.direction) * param.velocity;

        this.direction = param.direction;

        //弾種別グラフィック
        var type = 1, size = 1, index = 0;
        switch (param.type) {
            case "RS":  type = 1; size = 0.6; index = 0; break;
            case "BS":  type = 1; size = 0.6; index = 1; break;
            case "RM":  type = 1; size = 0.8; index = 0; break;
            case "BM":  type = 1; size = 0.8; index = 1; break;
            case "RL":  type = 1; size = 1.0; index = 0; break;
            case "BL":  type = 1; size = 1.0; index = 1; break;

            case "RES": type = 2; size = 0.6; index = 0; break;
            case "BES": type = 2; size = 0.6; index =16; break;
            case "REM": type = 2; size = 1.0; index = 0; break;
            case "BEM": type = 2; size = 1.0; index =16; break;
            case "THIN":type = 2; size = 1.0; index =24; this.rolling = false; this.rotation = this.direction*toDeg-90; break;
        }
        if (this.sprite) this.sprite.remove();
        this.sprite = phina.display.Sprite("bullet"+type, 24, 24)
            .addChildTo(this)
            .setFrameIndex(index)
            .setScale(size);
        return this;
    },

    erace: function() {
    },
});

<commit_msg>update bullet<commit_after>/*
 *  Bullet.js
 *  2014/07/16
 *  @auther minimo
 *  This Program is MIT license.
 */

phina.define("pbr.Bullet", {
    superClass: "phina.display.CanvasElement",
    layer: LAYER_BULLET,

    _member: {
        param: null,
        id: -1,

        vx: 0,
        vy: 1,

        rollAngle: 5,
        rolling: true,
    },

    init: function() {
        this.superInit();
        this.$extend(this._member);

        this.boundingType = "circle";
        this.radius = 2;

        this.on("enterframe", function(){
            if (this.rolling) this.rotation += this.rollAngle;

            this.x += this.vx;
            this.y += this.vy;

            //自機との当り判定チェック
            var player = this.parentScene.player;
            if (player.isCollision) {
                if (this.isHitElement(player) ) {
                    player.damage();
                    this.remove();
                    return;
                }
            }

            //画面範囲外
            if (this.x<-32 || this.x>SC_W+32 || this.y<-32 || this.y>SC_H+32) {
                this.remove();
                return;
            }
        }.bind(this));

        //リムーブ時
        this.on("removed", function(){
            this.bulletLayer.pool.push(this);
        }.bind(this));
    },

    setup: function(param) {
        param = param.$safe({
            id: -1,
            x: SC_W*0.5,
            y: SC_H*0.5,
            direction: 180,
            velocity: 1,
            type: "RS",
        });

        this.x = param.x;
        this.y = param.y;

        //当り判定設定
        this.boundingType = "circle";
        this.radius = 2;

        this.id = param.id;
        this.vx = Math.cos(param.direction) * param.velocity;
        this.vy = Math.sin(param.direction) * param.velocity;

        this.direction = param.direction;

        //弾種別グラフィック
        var type = 1, size = 1, index = 0;
        switch (param.type) {
            case "RS":  type = 1; size = 0.6; index = 0; break;
            case "BS":  type = 1; size = 0.6; index = 1; break;
            case "RM":  type = 1; size = 0.8; index = 0; break;
            case "BM":  type = 1; size = 0.8; index = 1; break;
            case "RL":  type = 1; size = 1.0; index = 0; break;
            case "BL":  type = 1; size = 1.0; index = 1; break;

            case "RES": type = 2; size = 0.6; index = 0; break;
            case "BES": type = 2; size = 0.6; index =16; break;
            case "REM": type = 2; size = 1.0; index = 0; break;
            case "BEM": type = 2; size = 1.0; index =16; break;
            case "THIN":type = 2; size = 1.0; index =24; this.rolling = false; this.rotation = this.direction*toDeg-90; break;
        }
        if (this.sprite) this.sprite.remove();
        this.sprite = phina.display.Sprite("bullet"+type, 24, 24)
            .addChildTo(this)
            .setFrameIndex(index)
            .setScale(size);
        return this;
    },

    erace: function() {
        pbr.Effect.BulletVanish(this).addChildTo(this.parentScene);
    },
});

<|endoftext|>


<commit_before>function pageViewModel(gvm) {
    // projecttitle
    gvm.projecttitle = ko.observable("");
    gvm.projectId = $("#projectHeader").data('value');
    gvm.lastIdFromDb = -1;
    gvm.lastId = -1;

    // Page specific i18n bindings
    gvm.title = ko.computed(function (){i18n.setLocale(gvm.lang()); return gvm.app() + ' - ' + i18n.__("ProjectTitle") + ": " + gvm.projecttitle();}, gvm);
    gvm.projectRules = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("ProjectRulesTitle");}, gvm);
    gvm.projectname = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("ProjectName");}, gvm);
    gvm.savePage = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("SaveBtn");}, gvm);
    gvm.addRuleName = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("AddRule");}, gvm);
    gvm.ruleName = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("RuleName");}, gvm);
    gvm.ruleAction = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("RuleAction");}, gvm);
    gvm.ruleActionDropdown = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("RuleActionDropdown");}, gvm);
    gvm.ruleNotOK = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("RuleNotOK");}, gvm);

    gvm.getProjectInfo = function() {
        $.getJSON('/api/project/' + gvm.projectId, function(data) {
            gvm.pageHeader(data[0].code + ' - ' + data[0].name);
        });
    };

    gvm.projectRules = ko.observableArray([]);
    gvm.projectActions = ko.observableArray([]);

    gvm.addRule = function() {
        gvm.projectRules.push(new Rule(this));
    }

    gvm.removeRule = function(rule) {
        gvm.projectRules.remove(rule);
    }

    gvm.clearActionsStructure = function() {
        gvm.projectActions.destroyAll();
    }
}

function initPage() {
    fetchProjectStructure();

    $(".addRuleBtn").click(function() {
        viewModel.addRule();
    });
}

function fetchProjectStructure() {
    viewModel.clearActionsStructure();

    $.getJSON("/api/projectstructure/" + viewModel.projectId, function(data){
        $.each(data, function(i, item){
            //var competence = viewModel.updateCompetence(item.id, item.code, item.description, item.max, item.weight);
            viewModel.projectActions.push(item.description);

            $.each(item.subcompetences, function(i, subcomp){
                //var subcompetence = new SubCompetence(competence, subcomp.id, subcomp.code, subcomp.description, subcomp.weight);
                //competence.subcompetences.push(subcompetence);
                viewModel.projectActions.push(subcomp.description);

                $.each(subcomp.indicators, function(i, indic){
                    //subcompetence.indicators.push(new Indicator(subcompetence, indic.id, indic.name, indic.description));
                    viewModel.projectActions.push(indic.description);
                    console.log(indic.description);
                });
            });
        })
    });
    console.log(viewModel.projectActions);
}

/**
 * Rule class
 */
function Rule(viewmodel, id, code, name, weight, locked, subcompetences) {

}<commit_msg>Edit Rulespage<commit_after>function pageViewModel(gvm) {
    // projecttitle
    gvm.projecttitle = ko.observable("");
    gvm.projectId = $("#projectHeader").data('value');
    gvm.lastIdFromDb = -1;
    gvm.lastId = -1;

    // Page specific i18n bindings
    gvm.title = ko.computed(function (){i18n.setLocale(gvm.lang()); return gvm.app() + ' - ' + i18n.__("ProjectTitle") + ": " + gvm.projecttitle();}, gvm);
    gvm.projectRules = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("ProjectRulesTitle");}, gvm);
    gvm.projectname = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("ProjectName");}, gvm);
    gvm.savePage = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("SaveBtn");}, gvm);
    gvm.addRuleName = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("AddRule");}, gvm);
    gvm.ruleName = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("RuleName");}, gvm);
    gvm.ruleAction = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("RuleAction");}, gvm);
    gvm.ruleActionDropdown = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("RuleActionDropdown");}, gvm);
    gvm.ruleNotOK = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("RuleNotOK");}, gvm);

    gvm.getProjectInfo = function() {
        $.getJSON('/api/project/' + gvm.projectId, function(data) {
            gvm.pageHeader(data[0].code + ' - ' + data[0].name);
        });
    };

    gvm.projectRules = ko.observableArray([]);
    gvm.projectActions = ko.observableArray([]);

    gvm.addProjectActions = function(data) {
        gvm.projectActions.push(data);
    }

    gvm.addRule = function() {
        gvm.projectRules.push(new Rule(this));
    }

    gvm.removeRule = function(rule) {
        gvm.projectRules.remove(rule);
    }

    gvm.clearActionsStructure = function() {
        gvm.projectActions.destroyAll();
    }
}

function initPage() {
    fetchProjectStructure();

    $(".addRuleBtn").click(function() {
        viewModel.addRule();
    });
}

function fetchProjectStructure() {
    viewModel.clearActionsStructure();

    $.getJSON("/api/projectstructure/" + viewModel.projectId, function(data){
        $.each(data, function(i, item){
            //var competence = viewModel.updateCompetence(item.id, item.code, item.description, item.max, item.weight);
            viewModel.addProjectActions(item.description);

            $.each(item.subcompetences, function(i, subcomp){
                //var subcompetence = new SubCompetence(competence, subcomp.id, subcomp.code, subcomp.description, subcomp.weight);
                //competence.subcompetences.push(subcompetence);
                viewModel.projectActions.push(subcomp.description);

                $.each(subcomp.indicators, function(i, indic){
                    //subcompetence.indicators.push(new Indicator(subcompetence, indic.id, indic.name, indic.description));
                    projectActions.push(indic.description);
                });
            });
        })
    });
    console.log(viewModel.projectActions);
}

/**
 * Rule class
 */
function Rule(viewmodel, id, code, name, weight, locked, subcompetences) {

}<|endoftext|>


<commit_before>/**
 * 优化 typeof 获取未知对象类型
 * @param  {mixed} mixed
 * @return {string}       Number|String|Object|Array|Function
 */
export const typeOf = mixed =>
  Object.prototype.toString.apply(mixed).match(/\[object (\w+)\]/)[1];

/**
 * 对象数据扩充
 * @param  {Object} obj 目标对象
 * @param  {object} ext 扩充对象
 * @return {object}
 */
export const assign = (obj = {}, ext = {}) => {
  for (let k in ext) {
    if (ext.hasOwnProperty(k)) {
      obj[k] = ext[k];
    }
  }
  return obj;
};

/**
 * object => serialize
 * @param  {Object} obj [description]
 * @return {[type]}     [description]
 */
export const serialize = (obj = {}) =>
  Object.keys(obj).map(k =>
    `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`
  ).join('&');

/**
 * hash search 转对象
 * ?a=a&b=b => {a:"a",b:"b"}
 * @param  {string} hash
 * @return {obj}
 */
export const search2obj = (hash = "") => {
  let ret = {},
    seg = decodeURIComponent(hash).replace(/^\?/, '').split('&'),
    len = seg.length,
    i = 0,
    s;
  for (; i < len; i++) {
    if (!seg[i]) {
      continue;
    }
    s = seg[i].split('=');
    ret[s[0]] = s[1];
  }
  return ret;
};

/**
 * xmlhttp 请求
 * @param  {string} url     请求地址
 * @param  {object} options 设置
 * @return {callback}
 */
export const xhr = (url, options = {}) => {
  let opts = assign({
      method: "GET",
      data: {},
      headers: {},
      cache: false,
      type: "json",
      done() {},
      fail() {},
      progress() {}
    }, options),
    xhr, progress = 0,
    send_data = [],
    has_q = url.split("/").pop().search(/\?/) !== -1;
  // 是否有缓存
  if (!opts.cache) {
    url += (has_q ? "&" : "?") + "_=" + Math.random();
    has_q = true;
  }
  // 整理发送数据
  if(serialize(opts.data) !== ""){
    send_data.push(serialize(opts.data));
  }
  // 如果是put /post 则用formdata
  if (/^put$|^post$/i.test(opts.method)) {
    opts.headers["Content-type"] = "application/x-www-form-urlencoded";
  } else if(send_data.length > 0) {
    url += (has_q ? "&" : "?") + send_data;
  }
  xhr = new XMLHttpRequest();
  xhr.open(opts.method, url, true);
  for (let k in opts.headers) {
    xhr.setRequestHeader(k, opts.headers[k]);
  }
  // 如果支持进度条
  xhr.upload.onprogress = xhr.onprogress = (e) => {
    if (e.lengthComputable) {
      progress = Math.round(e.loaded * 100 / e.total);
      opts.progress.call(e.target, progress);
    }
  };
  xhr.onload = (e) => {
    let res;
    if (e.target.status === 200 || e.target.status === 304) {
      res = e.target.responseText;
      if (opts.type === "json") {
        res = JSON.parse(res);
      }
      opts.done.call(e.target, res);
    }
    else{
      opts.fail.call(e.target, e.target.status);
    }
  };
  xhr.onerror = opts.fail;
  // done().fail().progress()
  xhr.done = fn => {
    opts.done = fn;
    return xhr;
  }
  xhr.fail = fn => {
    opts.fail = fn;
    return xhr;
  }
  xhr.progress = fn => {
    opts.progress = fn;
    return xhr;
  }
  xhr.send(send_data);
  return xhr;
};

/**
 * 清除字符串中指定的标签
 * @param  {string} tag 标签名称
 * @param  {string} str 字符串
 * @return {string}
 */
export const tagRemove = (tag, str) => {
  return str.replace(new RegExp(`<${tag}(.|\\s)*?\\/${tag}>`, "g"), "");
};

/**
 * 解析字符串中的标签内容
 * @param  {string} tag 标签名称
 * @param  {string} 解析字符串
 * @return {string}
 */
export const tagContent = function(tag, str) {
  let re = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, "gm"),
    text = "";
  for (let match = re.exec(str); match; match = re.exec(str)) {
    text += match[1];
  }
  return text;
};

export const cookie = {
  /**
   * 设置 cookie
   * @param  {string} name  项
   * @param  {String} value 值
   * @param  {Object} opts  扩展配置
   * @return {null}
   */
  set(name, value = "", options) {
    let data = {},
      cookies = [],
      opts = assign({
        path: "/",
        domain: ""
      }, options);
    if (!name ||
      /^(?:expires|max\-age|path|domain|secure)$/i.test(name)) {
      throw new Error("Cookie name is required and do not use cookie reserved keywords.");
    }
    data[encodeURIComponent(name)] = encodeURIComponent(value);
    if (opts["expires"]) {
      throw new Error("Please use max-age to replace expires");
    }
    for (let opt in opts) {
      data[opt] = opts[opt];
    }
    for (let key in data) {
      cookies.push(`${key}=${data[key]}`);
    }
    cookies = cookies.join("; ");
    document.cookie = `${cookies};`;
  },

  /**
   * 删除cookie某项
   * @param  {string} name
   * @return {null}
   */
  remove(name) {
    this.set(name, "", {
      "max-age": 0
    });
  },

  /**
   * 获取cookie某项
   * @param  {String} key [description]
   * @return {[type]}      [description]
   */
  get(key = "") {
    for (let _cookie of document.cookie.split("; ")) {
      let [name, value] = _cookie.split("=");
      if (key !== "" && key === name) {
        return decodeURIComponent(value);
      }
    }
  }

};

/**
 * 动态加载文件,建议私有化
 * @param  {String} type   加载远程文件类型 [script,link,img]
 * @param  {String} url    地址
 * @param  {Object} opts   附加配置
 * @return {Element}
 */
export const loadFile = (type = "script", url, options) => {
  let el = document.createElement(type),
    src = {
      script: "src",
      link: "href",
      img: "src"
    },
    opts = assign({
      position: "head",
      attrs: {},
      success() {},
      error() {}
    }, options);

  if (!src.hasOwnProperty(type)) {
    throw new Error(`File type:${type} is not support dynamic load.`);
  }
  if (typeof url === "undefined") {
    throw new Error("Load file url is required.");
  }
  // 扩展属性
  if (Object.keys(opts.attrs).length) {
    for (let _attr in opts.attrs) {
      el[_attr] = opts.attrs[_attr];
    }
  }
  el[src[type]] = url;
  if (type === "link") {
    el.rel = "stylesheet";
  }
  el.addEventListener("load", opts.success, false);
  el.addEventListener("error", opts.error, false);
  document[opts.position].appendChild(el);
  return el;
};

/**
 * 删除动态载入的文件标签，loadFile失败后可用，建议私有化
 * @param  {String} type [description]
 * @param  {String} rel  [description]
 * @return {null}      [description]
 */
export const removeFile = (type = "script", position = "head", rel) => {
  let i = 0,
    tags = document[position].getElementsByTagName(type);
  for (; i < tags.length; i++) {
    if (tags[i].rel === rel)
      tags[i].parentNode.removeChild(tags[i]);
  }
};

/**
 * 生成 hash
 * @param  {string} s
 * @return {hash}
 */
export const hashCode = s => {
  return s.split("").reduce((a, b) => {
    a = ((a << 5) - a) + b.charCodeAt(0);
    return a & a;
  }, 0);
};

/**
 * 在浏览器关闭之前缓存ajax获取的json数据
 * @param  {String} url           [description]
 * @param  {Object} options [description]
 * @return {Object}               [description]
 */
export const cacheJSON = (url, options={}) => {
  const name = `_hc_cache_json_${hashCode(url.replace(/^http[s]?:\/\//, ""))}`;
  let callback = assign({
    force: false, // true的时候强制ajax获取
    done(){}, fail(){}
  }, options);
  if ("localStorage" in window && cookie.get(`${name}`) && !callback.force) {
    setTimeout(() => {
      callback.done.call(null, JSON.parse(window.localStorage.getItem(name)));
    }, 0);
  }
  else{
    xhr(url).done(res => {
      // 关闭浏览器失效，保证下次浏览获取新的资源列表
      cookie.set(name, "y");
      window.localStorage.setItem(name, JSON.stringify(res));
      callback.done.call(null, res);
    })
    .fail(status => {
      callback.fail.call(null, status);
    });
  }
  // 支持 done().fail()
  callback.done = fn => {
    callback.done = fn;
    return callback;
  };
  callback.fail = fn => {
    callback.fail = fn;
    return callback;
  };
  return callback;
};
<commit_msg>xhr fail跟complete bugfix<commit_after>/**
 * 优化 typeof 获取未知对象类型
 * @param  {mixed} mixed
 * @return {string}       Number|String|Object|Array|Function
 */
export const typeOf = mixed =>
  Object.prototype.toString.apply(mixed).match(/\[object (\w+)\]/)[1];

/**
 * 对象数据扩充
 * @param  {Object} obj 目标对象
 * @param  {object} ext 扩充对象
 * @return {object}
 */
export const assign = (obj = {}, ext = {}) => {
  for (let k in ext) {
    if (ext.hasOwnProperty(k)) {
      obj[k] = ext[k];
    }
  }
  return obj;
};

/**
 * object => serialize
 * @param  {Object} obj [description]
 * @return {[type]}     [description]
 */
export const serialize = (obj = {}) =>
  Object.keys(obj).map(k =>
    `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`
  ).join('&');

/**
 * hash search 转对象
 * ?a=a&b=b => {a:"a",b:"b"}
 * @param  {string} hash
 * @return {obj}
 */
export const search2obj = (hash = "") => {
  let ret = {},
    seg = decodeURIComponent(hash).replace(/^\?/, '').split('&'),
    len = seg.length,
    i = 0,
    s;
  for (; i < len; i++) {
    if (!seg[i]) {
      continue;
    }
    s = seg[i].split('=');
    ret[s[0]] = s[1];
  }
  return ret;
};

/**
 * xmlhttp 请求
 * @param  {string} url     请求地址
 * @param  {object} options 设置
 * @return {callback}
 */
export const xhr = (url, options = {}) => {
  let opts = assign({
      method: "GET",
      data: {},
      headers: {},
      cache: false,
      type: "json",
      done() {},
      fail() {},
      progress() {},
      complete() {}
    }, options),
    xhr, progress = 0,
    send_data = [],
    has_q = url.split("/").pop().search(/\?/) !== -1;
  // 是否有缓存
  if (!opts.cache) {
    url += (has_q ? "&" : "?") + "_=" + Math.random();
    has_q = true;
  }
  // 整理发送数据
  if(serialize(opts.data) !== ""){
    send_data.push(serialize(opts.data));
  }
  // 如果是put /post 则用formdata
  if (/^put$|^post$/i.test(opts.method)) {
    opts.headers["Content-type"] = "application/x-www-form-urlencoded";
  } else if(send_data.length > 0) {
    url += (has_q ? "&" : "?") + send_data;
  }
  xhr = new XMLHttpRequest();
  xhr.open(opts.method, url, true);
  for (let k in opts.headers) {
    xhr.setRequestHeader(k, opts.headers[k]);
  }
  // 如果支持进度条
  xhr.upload.onprogress = xhr.onprogress = (e) => {
    if (e.lengthComputable) {
      progress = Math.round(e.loaded * 100 / e.total);
      opts.progress.call(e.target, progress);
    }
  };
  xhr.addEventListener('load', (e) => {
    let res;
    if (e.target.status === 200 || e.target.status === 304) {
      res = e.target.responseText;
      if (opts.type === "json") {
        res = JSON.parse(res);
      }
      opts.done.call(e.target, res);
    }
    else{
      opts.fail.call(e.target, e.target.status);
    }
  }, false);
  xhr.addEventListener('error', () => {
    opts.fail();
  }, false);
  xhr.addEventListener('loadend', () => {
    opts.complete();
  }, false);
  // done().fail().progress()
  xhr.done = fn => {
    opts.done = fn;
    return xhr;
  }
  xhr.fail = fn => {
    opts.fail = fn;
    return xhr;
  }
  xhr.progress = fn => {
    opts.progress = fn;
    return xhr;
  }
  xhr.complete = fn => {
    opts.complete = fn;
    return xhr;
  }
  xhr.send(send_data);
  return xhr;
};

/**
 * 清除字符串中指定的标签
 * @param  {string} tag 标签名称
 * @param  {string} str 字符串
 * @return {string}
 */
export const tagRemove = (tag, str) => {
  return str.replace(new RegExp(`<${tag}(.|\\s)*?\\/${tag}>`, "g"), "");
};

/**
 * 解析字符串中的标签内容
 * @param  {string} tag 标签名称
 * @param  {string} 解析字符串
 * @return {string}
 */
export const tagContent = function(tag, str) {
  let re = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, "gm"),
    text = "";
  for (let match = re.exec(str); match; match = re.exec(str)) {
    text += match[1];
  }
  return text;
};

export const cookie = {
  /**
   * 设置 cookie
   * @param  {string} name  项
   * @param  {String} value 值
   * @param  {Object} opts  扩展配置
   * @return {null}
   */
  set(name, value = "", options) {
    let data = {},
      cookies = [],
      opts = assign({
        path: "/",
        domain: ""
      }, options);
    if (!name ||
      /^(?:expires|max\-age|path|domain|secure)$/i.test(name)) {
      throw new Error("Cookie name is required and do not use cookie reserved keywords.");
    }
    data[encodeURIComponent(name)] = encodeURIComponent(value);
    if (opts["expires"]) {
      throw new Error("Please use max-age to replace expires");
    }
    for (let opt in opts) {
      data[opt] = opts[opt];
    }
    for (let key in data) {
      cookies.push(`${key}=${data[key]}`);
    }
    cookies = cookies.join("; ");
    document.cookie = `${cookies};`;
  },

  /**
   * 删除cookie某项
   * @param  {string} name
   * @return {null}
   */
  remove(name) {
    this.set(name, "", {
      "max-age": 0
    });
  },

  /**
   * 获取cookie某项
   * @param  {String} key [description]
   * @return {[type]}      [description]
   */
  get(key = "") {
    for (let _cookie of document.cookie.split("; ")) {
      let [name, value] = _cookie.split("=");
      if (key !== "" && key === name) {
        return decodeURIComponent(value);
      }
    }
  }

};

/**
 * 动态加载文件,建议私有化
 * @param  {String} type   加载远程文件类型 [script,link,img]
 * @param  {String} url    地址
 * @param  {Object} opts   附加配置
 * @return {Element}
 */
export const loadFile = (type = "script", url, options) => {
  let el = document.createElement(type),
    src = {
      script: "src",
      link: "href",
      img: "src"
    },
    opts = assign({
      position: "head",
      attrs: {},
      success() {},
      error() {}
    }, options);

  if (!src.hasOwnProperty(type)) {
    throw new Error(`File type:${type} is not support dynamic load.`);
  }
  if (typeof url === "undefined") {
    throw new Error("Load file url is required.");
  }
  // 扩展属性
  if (Object.keys(opts.attrs).length) {
    for (let _attr in opts.attrs) {
      el[_attr] = opts.attrs[_attr];
    }
  }
  el[src[type]] = url;
  if (type === "link") {
    el.rel = "stylesheet";
  }
  el.addEventListener("load", opts.success, false);
  el.addEventListener("error", opts.error, false);
  document[opts.position].appendChild(el);
  return el;
};

/**
 * 删除动态载入的文件标签，loadFile失败后可用，建议私有化
 * @param  {String} type [description]
 * @param  {String} rel  [description]
 * @return {null}      [description]
 */
export const removeFile = (type = "script", position = "head", rel) => {
  let i = 0,
    tags = document[position].getElementsByTagName(type);
  for (; i < tags.length; i++) {
    if (tags[i].rel === rel)
      tags[i].parentNode.removeChild(tags[i]);
  }
};

/**
 * 生成 hash
 * @param  {string} s
 * @return {hash}
 */
export const hashCode = s => {
  return s.split("").reduce((a, b) => {
    a = ((a << 5) - a) + b.charCodeAt(0);
    return a & a;
  }, 0);
};

/**
 * 在浏览器关闭之前缓存ajax获取的json数据
 * @param  {String} url           [description]
 * @param  {Object} options [description]
 * @return {Object}               [description]
 */
export const cacheJSON = (url, options={}) => {
  const name = `_hc_cache_json_${hashCode(url.replace(/^http[s]?:\/\//, ""))}`;
  let callback = assign({
    force: false, // true的时候强制ajax获取
    done(){}, fail(){}
  }, options);
  if ("localStorage" in window && cookie.get(`${name}`) && !callback.force) {
    setTimeout(() => {
      callback.done.call(null, JSON.parse(window.localStorage.getItem(name)));
    }, 0);
  }
  else{
    xhr(url).done(res => {
      // 关闭浏览器失效，保证下次浏览获取新的资源列表
      cookie.set(name, "y");
      window.localStorage.setItem(name, JSON.stringify(res));
      callback.done.call(null, res);
    })
    .fail(status => {
      callback.fail.call(null, status);
    });
  }
  // 支持 done().fail()
  callback.done = fn => {
    callback.done = fn;
    return callback;
  };
  callback.fail = fn => {
    callback.fail = fn;
    return callback;
  };
  return callback;
};
<|endoftext|>


<commit_before>/*jslint node: true */
"use strict";
var ecdsa = require('secp256k1');

exports.sign = function(hash, priv_key){
	var res = ecdsa.sign(hash, priv_key);
	return res.signature.toString("base64");
};

exports.verify = function(hash, b64_sig, b64_pub_key){
	var signature = new Buffer(b64_sig, "base64"); // 64 bytes (32+32)
	return ecdsa.verify(hash, signature, new Buffer(b64_pub_key, "base64"));
};

<commit_msg>safer signature checking<commit_after>/*jslint node: true */
"use strict";
var ecdsa = require('secp256k1');

exports.sign = function(hash, priv_key){
	var res = ecdsa.sign(hash, priv_key);
	return res.signature.toString("base64");
};

exports.verify = function(hash, b64_sig, b64_pub_key){
	try{
		var signature = new Buffer(b64_sig, "base64"); // 64 bytes (32+32)
		return ecdsa.verify(hash, signature, new Buffer(b64_pub_key, "base64"));
	}
	catch(e){
		console.log('signature verification exception: '+e.toString());
		return false;
	}
};

<|endoftext|>