<commit_before>/**
 * @module proact-flow
 */

/**
 *

 *   Creates a queue of actions or action queue.
 *

 *

 *  The idea of the action queues is to decide the order of the actions pushed into them.
 *  For example if an action should be executed only once, but is pushed for a second time,
 *  it is moved in the end of the queue and its parameters are updated.
 *

 *

 *  The ProAct.Queue is a priority queue, meaning every action has a numeric priority.
 *  The actions with the numerically lowest priority are with highes prority when executed.
 *

 *

 *  The {{#crossLink "ProAct.Queue/go:method"}}{{/crossLink}} method deques all the actions from the queue and executes them in the right
 *  order, using their priorities.
 *

 *

 *  A ProAct.Queue can be used to setup the action flow - the order of the actions must be executed.
 *  ProAct.js uses it to create an action flow if something changes.
 *

 *
 * TODO Default name should be extracted to a constant. ~meddle@2014-07-10
 *
 * @class ProAct.Queue
 * @constructor
 * @param {String} name
 *    The name of the queue, every ProAct.Queue must have a name.
 *    The default value of the name is 'proq'. {{#crossLink "ProAct.Queues"}}{{/crossLink}} uses the names to manage its queues.
 * @param {Object} options
 *    Various options for the queue.
 *
Available options:

 *

 *
before - A callback called before each call of {{#crossLink "ProAct.Queue/go:method"}}{{/crossLink}}.

 *
after - A callback called after each call of {{#crossLink "ProAct.Queue/go:method"}}{{/crossLink}}.

 *
err - A callback called every time an error is thrown.

 *

 */
ProAct.Queue = P.Q = function (name, options) {
  this.name = name || 'proq';
  this.options = options || {};

  this._queue = [];
};

/**
 * Executes the passed action.
 *
 * @for ProAct.Queue
 * @method runAction
 * @static
 * @param {ProAct.Queue} queue
 *      The queue managing the action to execute.
 * @param {Object} context
 *      The context in which the action should be executed.
 *

 *        The action is a normal JavaScript function and the context is the object
 *        that should be bound to this when calling it.
 *

 *

 *        It can be null or undefined.
 *

 * @param {Function} action
 *      The action to execute.
 * @param {Array} args
 *      The parameters to be passed to the action.
 * @param {Function} errHandler
 *      It is called if an error is thrown when executing the action.
 *

 *        It can be null if the error should be catched from the outside.
 *

 */
ProAct.Queue.runAction = function (queue, context, action, args, errHandler) {
  if (args && args.length > 0) {
    if (errHandler) {
      try {
        action.apply(context, args);
      } catch (e) {
        if (!e.fromFlow) {
          e.fromFlow = true;
          errHandler(queue, e);
        } else {
          throw e;
        }
      }
    } else {
      action.apply(context, args);
    }
  } else {
    if (errHandler) {
      try {
        action.call(context);
      } catch(e) {
        if (!e.fromFlow) {
          e.fromFlow = true;
          errHandler(queue, e);
        } else {
          throw e;
        }
      }
    } else {
      action.call(context);
    }
  }
};

P.Q.prototype = {

  /**
   * Reference to the constructor of this object.
   *
   * @memberof ProAct.Queue
   * @instance
   * @constant
   * @type {Object}
   * @default ProAct.Queue
   */
  constructor: ProAct.Queue,

  /**
   * Retrieves the lenght of this ProAct.Queue.
   *
   * @memberof ProAct.Queue
   * @instance
   * @method length
   * @return {Number}
   *      The number of actions queued in this queue.
   */
  length: function () {
    return this._queue.length / 4;
  },

  /**
   * Checks if this ProAct.Queue is empty.
   *
   * @memberof ProAct.Queue
   * @instance
   * @method isEmpty
   * @return {Boolean}
   *      True if there are no actions in this queue.
   */
  isEmpty: function () {
    return this.length() === 0;
  },

  /**
   * Pushes an action to this queue.
   * This method can enque the same action multiple times and always with priority of '1'.
   *

   *  ProAct.Queue#defer, ProAct.Queue#enque and ProAct.Queue#add are aliases of this method.
   *

   *
   * @memberof ProAct.Queue
   * @instance
   * @method push
   * @param {Object} context
   *      The context of the action.
   *      It can be null.
   *

   *        If the method is called with a Function context, the context becomes the action.
   *        This way the method can be called with only one parameter for actions without context.
   *

   * @param {Function} action
   *      The action to enque.
   *

   *        If there is no context and the action is passed in place of the context,
   *        this parameter can hold the arguments of the action.
   *

   * @param {Array} args
   *      Arguments to be passed to the action when it is executed.
   */
  push: function (context, action, args) {
    if (context && (typeof(context) === 'function')) {
      args = action;
      action = context;
      context = null;
    }

    this._queue.push(context, action, args, 1);
  },

  /**
   * Pushes an action to this queue only once.
   *

   *  If the action is pushed for the second time using this method, instead of
   *  adding it to the queue, its priority goes up and its arguments are updated.
   *  This means that this action will be executed after all the other actions, pushed only once.
   *

   *

   *  ProAct.Queue#deferOnce, ProAct.Queue#enqueOnce and ProAct.Queue#addOnce are aliases of this method.
   *

   *
   * @memberof ProAct.Queue
   * @instance
   * @method pushOnce
   * @param {Object} context
   *      The context of the action.
   *      It can be null.
   *

   *        If the method is called with a Function context, the context becomes the action.
   *        This way the method can be called with only one parameter for actions without context.
   *

   * @param {Function} action
   *      The action to enque.
   *

   *        If there is no context and the action is passed in place of the context,
   *        this parameter can hold the arguments of the action.
   *

   * @param {Array} args
   *      Arguments to be passed to the action when it is executed.
   */
  pushOnce: function (context, action, args) {
    if (context && (typeof(context) === 'function')) {
      args = action;
      action = context;
      context = null;
    }

    var queue = this._queue, current, currentMethod,
        i, length = queue.length;

    for (i = 0; i < length; i += 4) {
      current = queue[i];
      currentMethod = queue[i + 1];

      if (current === context && currentMethod === action) {
        queue[i + 2] = args;
        queue[i + 3] = queue[i + 3] + 1;
        return;
      }
    }

    this.push(context, action, args);
  },

  /**
   * Starts the action flow.
   *

   *  Executes the actions in this queue in the order they were enqued, but also uses the priorities
   *  to execute these with numerically higher priority after these with numerically lower priority.
   *

   *

   *  If some of the actions enques new actions in this queue and the parameter once is set to false
   *  this method is recursively called executing the new actions.
   *

   *

   *  ProAct.Queue#run is alias of this method.
   *

   *
   * @memberof ProAct.Queue
   * @instance
   * @method go
   * @param {Boolean} once
   *      True if 'go' should not be called for actions generated by the executed ones.
   * @see {@link ProAct.Queue#push}
   * @see {@link ProAct.Queue#pushOnce}
   */
  go: function (once) {
    var queue = this._queue,
        options = this.options,
        runs = this.runs,
        length = queue.length,
        before = options && options.before,
        after = options && options.after,
        err = options && options.err,
        i, l = length, going = true, priority = 1,
        tl = l,
        obj, method, args, prio;

    if (length && before) {
      before(this);
    }

    while (going) {
      going = false;
      l = tl;
      for (i = 0; i < l; i += 4) {
        obj = queue[i];
        method = queue[i + 1];
        args = queue[i + 2];
        prio = queue[i + 3];

        if (prio === priority) {
          P.Q.runAction(this, obj, method, args, err);
        } else if (prio > priority) {
          going = true;
          tl = i + 4;
        }
      }
      priority = priority + 1;
    }

    if (length && after) {
      after(this);
    }

    if (queue.length > length) {
      this._queue = queue.slice(length);

      if (!once) {
        this.go();
      }
    } else {
      this._queue.length = 0;
    }
  }
};

P.Q.prototype.defer = P.Q.prototype.enque = P.Q.prototype.add = P.Q.prototype.push;
P.Q.prototype.deferOnce = P.Q.prototype.enqueOnce = P.Q.prototype.addOnce = P.Q.prototype.pushOnce;
P.Q.prototype.run = P.Q.prototype.go;
<commit_msg>ProAct.Queue docs.<commit_after>/**
 * @module proact-flow
 */

/**
 *

 *   Creates a queue of actions or action queue.
 *

 *

 *  The idea of the action queues is to decide the order of the actions pushed into them.
 *  For example if an action should be executed only once, but is pushed for a second time,
 *  it is moved in the end of the queue and its parameters are updated.
 *

 *

 *  The ProAct.Queue is a priority queue, meaning every action has a numeric priority.
 *  The actions with the numerically lowest priority are with highes prority when executed.
 *

 *

 *  The {{#crossLink "ProAct.Queue/go:method"}}{{/crossLink}} method deques all the actions from the queue and executes them in the right
 *  order, using their priorities.
 *

 *

 *  A ProAct.Queue can be used to setup the action flow - the order of the actions must be executed.
 *  ProAct.js uses it to create an action flow if something changes.
 *

 *
 * TODO Default name should be extracted to a constant. ~meddle@2014-07-10
 *
 * @class ProAct.Queue
 * @constructor
 * @param {String} name
 *    The name of the queue, every ProAct.Queue must have a name.
 *    The default value of the name is 'proq'. {{#crossLink "ProAct.Queues"}}{{/crossLink}} uses the names to manage its queues.
 * @param {Object} options
 *    Various options for the queue.
 *
Available options:

 *

 *
before - A callback called before each call of {{#crossLink "ProAct.Queue/go:method"}}{{/crossLink}}.

 *
after - A callback called after each call of {{#crossLink "ProAct.Queue/go:method"}}{{/crossLink}}.

 *
err - A callback called every time an error is thrown.

 *

 */
ProAct.Queue = P.Q = function (name, options) {
  this.name = name || 'proq';
  this.options = options || {};

  this._queue = [];
};

/**
 * Executes the passed action.
 *
 * @for ProAct.Queue
 * @method runAction
 * @static
 * @param {ProAct.Queue} queue
 *      The queue managing the action to execute.
 * @param {Object} context
 *      The context in which the action should be executed.
 *

 *        The action is a normal JavaScript function and the context is the object
 *        that should be bound to this when calling it.
 *

 *

 *        It can be null or undefined.
 *

 * @param {Function} action
 *      The action to execute.
 * @param {Array} args
 *      The parameters to be passed to the action.
 * @param {Function} errHandler
 *      It is called if an error is thrown when executing the action.
 *

 *        It can be null if the error should be catched from the outside.
 *

 */
ProAct.Queue.runAction = function (queue, context, action, args, errHandler) {
  if (args && args.length > 0) {
    if (errHandler) {
      try {
        action.apply(context, args);
      } catch (e) {
        if (!e.fromFlow) {
          e.fromFlow = true;
          errHandler(queue, e);
        } else {
          throw e;
        }
      }
    } else {
      action.apply(context, args);
    }
  } else {
    if (errHandler) {
      try {
        action.call(context);
      } catch(e) {
        if (!e.fromFlow) {
          e.fromFlow = true;
          errHandler(queue, e);
        } else {
          throw e;
        }
      }
    } else {
      action.call(context);
    }
  }
};

P.Q.prototype = {

  /**
   * Reference to the constructor of this object.
   *
   * @property constructor
   * @type ProAct.Queue
   * @final
   * @for ProAct.Queue
   */
  constructor: ProAct.Queue,

  /**
   * Retrieves the lenght of this ProAct.Queue.
   *
   * @memberof ProAct.Queue
   * @instance
   * @method length
   * @return {Number}
   *      The number of actions queued in this queue.
   */
  length: function () {
    return this._queue.length / 4;
  },

  /**
   * Checks if this ProAct.Queue is empty.
   *
   * @memberof ProAct.Queue
   * @instance
   * @method isEmpty
   * @return {Boolean}
   *      True if there are no actions in this queue.
   */
  isEmpty: function () {
    return this.length() === 0;
  },

  /**
   * Pushes an action to this queue.
   * This method can enque the same action multiple times and always with priority of '1'.
   *

   *  ProAct.Queue#defer, ProAct.Queue#enque and ProAct.Queue#add are aliases of this method.
   *

   *
   * @memberof ProAct.Queue
   * @instance
   * @method push
   * @param {Object} context
   *      The context of the action.
   *      It can be null.
   *

   *        If the method is called with a Function context, the context becomes the action.
   *        This way the method can be called with only one parameter for actions without context.
   *

   * @param {Function} action
   *      The action to enque.
   *

   *        If there is no context and the action is passed in place of the context,
   *        this parameter can hold the arguments of the action.
   *

   * @param {Array} args
   *      Arguments to be passed to the action when it is executed.
   */
  push: function (context, action, args) {
    if (context && (typeof(context) === 'function')) {
      args = action;
      action = context;
      context = null;
    }

    this._queue.push(context, action, args, 1);
  },

  /**
   * Pushes an action to this queue only once.
   *

   *  If the action is pushed for the second time using this method, instead of
   *  adding it to the queue, its priority goes up and its arguments are updated.
   *  This means that this action will be executed after all the other actions, pushed only once.
   *

   *

   *  ProAct.Queue#deferOnce, ProAct.Queue#enqueOnce and ProAct.Queue#addOnce are aliases of this method.
   *

   *
   * @memberof ProAct.Queue
   * @instance
   * @method pushOnce
   * @param {Object} context
   *      The context of the action.
   *      It can be null.
   *

   *        If the method is called with a Function context, the context becomes the action.
   *        This way the method can be called with only one parameter for actions without context.
   *

   * @param {Function} action
   *      The action to enque.
   *

   *        If there is no context and the action is passed in place of the context,
   *        this parameter can hold the arguments of the action.
   *

   * @param {Array} args
   *      Arguments to be passed to the action when it is executed.
   */
  pushOnce: function (context, action, args) {
    if (context && (typeof(context) === 'function')) {
      args = action;
      action = context;
      context = null;
    }

    var queue = this._queue, current, currentMethod,
        i, length = queue.length;

    for (i = 0; i < length; i += 4) {
      current = queue[i];
      currentMethod = queue[i + 1];

      if (current === context && currentMethod === action) {
        queue[i + 2] = args;
        queue[i + 3] = queue[i + 3] + 1;
        return;
      }
    }

    this.push(context, action, args);
  },

  /**
   * Starts the action flow.
   *

   *  Executes the actions in this queue in the order they were enqued, but also uses the priorities
   *  to execute these with numerically higher priority after these with numerically lower priority.
   *

   *

   *  If some of the actions enques new actions in this queue and the parameter once is set to false
   *  this method is recursively called executing the new actions.
   *

   *

   *  ProAct.Queue#run is alias of this method.
   *

   *
   * @memberof ProAct.Queue
   * @instance
   * @method go
   * @param {Boolean} once
   *      True if 'go' should not be called for actions generated by the executed ones.
   * @see {@link ProAct.Queue#push}
   * @see {@link ProAct.Queue#pushOnce}
   */
  go: function (once) {
    var queue = this._queue,
        options = this.options,
        runs = this.runs,
        length = queue.length,
        before = options && options.before,
        after = options && options.after,
        err = options && options.err,
        i, l = length, going = true, priority = 1,
        tl = l,
        obj, method, args, prio;

    if (length && before) {
      before(this);
    }

    while (going) {
      going = false;
      l = tl;
      for (i = 0; i < l; i += 4) {
        obj = queue[i];
        method = queue[i + 1];
        args = queue[i + 2];
        prio = queue[i + 3];

        if (prio === priority) {
          P.Q.runAction(this, obj, method, args, err);
        } else if (prio > priority) {
          going = true;
          tl = i + 4;
        }
      }
      priority = priority + 1;
    }

    if (length && after) {
      after(this);
    }

    if (queue.length > length) {
      this._queue = queue.slice(length);

      if (!once) {
        this.go();
      }
    } else {
      this._queue.length = 0;
    }
  }
};

P.Q.prototype.defer = P.Q.prototype.enque = P.Q.prototype.add = P.Q.prototype.push;
P.Q.prototype.deferOnce = P.Q.prototype.enqueOnce = P.Q.prototype.addOnce = P.Q.prototype.pushOnce;
P.Q.prototype.run = P.Q.prototype.go;
<|endoftext|>


<commit_before>/*!
 * LocalModel
 * Developer: Rick Craig
 * https://github.com/RickCraig/localmodel
 */

'use strict';

/**
 * Generates a random UUID
 * @private
 * @returns {String} random id
 */
var generateUUID = function(){
  var d = Date.now();
  var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
    .replace(/[xy]/g, function(c) {
      var r = (d + Math.random()*16)%16 | 0;
      d = Math.floor(d/16);
      return (c==='x' ? r : (r&0x3|0x8)).toString(16);
    });
  return uuid;
};

/**
 * Builds the key for the data
 * @private
 * @param {String} name - of the model
 * @param {String} id - of the entry
 * @returns {String} the merged key name
 */
var getKey = function(name, id) {
  return name + '::' + id;
};

/**
 * Adds the index for the new entry
 * @private
 * @param {String} name - the name of the model
 * @param {String} newIndex - the key for the new entry
 */
var addIndex = function(name, newIndex) {
  var indexName = name + '-index';
  var indexString = localStorage.getItem(indexName);
  var indices = indexString ? JSON.parse(indexString) : [];
  indices.push(newIndex);
  localStorage.setItem(indexName, JSON.stringify(indices));
};

/**
 * Returns the indices for a model
 * @private
 * @param {String} name - the model name
 * @returns {Array} the indices
 */
var getIndices = function(name) {
  var indices = localStorage.getItem(name + '-index');
  return JSON.parse(indices);
};

/**
 * Get a specific index from the indices
 * @private
 * @param {Array} indices
 * @param {String} term
 * @returns {String} matching index
 */
var getIndex = function(indices, term) {
  var regex = new RegExp('::' + term, 'g');
  for (var i = 0; i < indices.length; i++) {
    var index = indices[i];
    if (regex.test(index)) {
      return index;
    }
  }
  return;
};

/**
 * Checks if an object is empty
 * @private
 * @param {Object} obj
 * @returns {Boolean} true if empty
 */
var isEmpty = function(obj) {
  for(var prop in obj) {
    if(obj.hasOwnProperty(prop)) {
      return false;
    }
  }
  return true;
};

/**
 * Checks an array of booleans for a false
 * @private
 * @param {Array} arr - an array of booleans
 * @returns {Boolean} true if contains a false
 */
var containsFalse = function(arr) {
  for (var i = 0; i < arr.length; i++) {
    if (!arr[i]) {
      return true;
    }
  }
  return false;
};

/**
 * Takes in a data string and returns
 * true if query matches
 * @private
 * @param {String} data - the string to match
 * @param {Mixed} query
 * @returns {Boolean} true if the data matches the query
 */
var matchQuery = function(data, query) {
  // Query using regular expression
  if (query instanceof RegExp) {
    return query.test(data);
  }

  // Query using string or number
  if (typeof query === 'string' ||
    typeof query === 'number') {
      return data === query;
    }

  if (typeof query === 'object') {
    // Do the business in here for $gte, $gt, $lte, $lt
    // Remember to tag this 0.0.2

    if (typeof data === 'number') {
      var matches = [];
      if (query.$gte) {
        matches.push(query.$gte <= data);
      }

      if (query.$gt) {
        matches.push(query.$gt < data);
      }

      if (query.$lte) {
        matches.push(query.$lte >= data);
      }

      if (query.$lt) {
        matches.push(query.$lt > data);
      }

      return !containsFalse(matches);
    }

  }
};

/**
 * Local Document constructor
 * @public
 * @param {Object} data - the entry raw data
 */
var LocalDocument = function(data, schema) {
  this.schema = schema;
  this.data = {};
  this.indexKey = getKey(schema.name, data._id);

  // Try to force the schema type
  for (var key in data) {
    var type = schema.schema[key];
    var property = data[key];

    if (type === LocalSchema.SchemaTypes.Date) {
      property = new Date(property);
    }

    this.data[key] = property;
  }
};

/**
 * Used to save the document when updated
 * @public
 */
LocalDocument.prototype.save = function() {
  // Build the object to save
  var toBeSaved = {};
  for (var key in this.schema.schema) {
    toBeSaved[key] = this.data[key];
  }

  var itemKey = getKey(this.schema.name, this.data._id);
  localStorage.setItem(itemKey, JSON.stringify(toBeSaved));
};

/**
 * Local Schema constructor
 * @public
 * @param {Object} schema
 */
var LocalSchema = function(name, schema) {
  this.schema = schema;
  this.name = name;
};

/**
 * Create a new data object for this schema
 * @public
 * @param {Object} data
 * @returns {}
 */
LocalSchema.prototype.create = function(data) {
  var newEntry = {};
  newEntry._id = generateUUID();
  for (var key in this.schema) {
    newEntry[key] = data[key];
  }

  // Save to localstorage
  // At some point if there is an index, it can be added the the key for speed
  var index = getKey(this.name, newEntry._id);
  localStorage.setItem(index, JSON.stringify(newEntry));
  addIndex(this.name, index);

  // Clear indices
  this.indices = null;

  return new LocalDocument(newEntry, this);
};

/**
 * Returns all entries in storage
 * @public
 * @returns {Array} all entries
 */
LocalSchema.prototype.all = function() {
  this.indices = this.indices || getIndices(this.name);
  var results = [];

  // Check if the collection is empty
  if (!this.indices) {
    return results;
  }

  for (var i = 0; i < this.indices.length; i++) {
    var index = this.indices[i];
    var result = JSON.parse(localStorage.getItem(index));
    results.push(new LocalDocument(result, this));
  }
  return results;
};

/**
 * Find an entry by ID
 * @public
 * @param {String} id
 * @returns {Object} the object or null
 */
LocalSchema.prototype.findById = function(id) {
  this.indices = this.indices || getIndices(this.name);
  var match = getIndex(this.indices, id);
  if (!match) {
    return null;
  }
  return new LocalDocument(JSON.parse(localStorage.getItem(match)), this);
};

/**
 * Find entries matching a query
 * @public
 * @param {Object} query
 * @returns {Array} an array of matches
 */
LocalSchema.prototype.find = function(query) {
  if (!query || isEmpty(query)) {
    return this.all();
  }

  this.indices = this.indices || getIndices(this.name);
  var results = [];

  // Check if the collection is empty
  if (!this.indices) {
    return results;
  }

  for (var i = 0; i < this.indices.length; i++) {
    var entry = localStorage.getItem(this.indices[i]);
    var parsed = JSON.parse(entry);
    var matches = [];

    for (var key in query) {
      var queryItem = query[key];
      var isRegex = queryItem instanceof RegExp;
      if (!isRegex &&
        (queryItem === '' || isEmpty(queryItem))) {
        continue;
      }

      if (parsed[key]) {
        matches.push(matchQuery(parsed[key], queryItem));
      }
    }

    if (!containsFalse(matches)) {
      results.push(new LocalDocument(parsed, this));
    }
  }
  return results;
};

/**
 * LocalSchema Schema Types
 * For use in validation and return
 * @public
 */
LocalSchema.SchemaTypes = {
  String: 'string',
  Number: 'number',
  Boolean: 'boolean',
  Mixed: 'mixed',
  Date: 'date'
};

/**
 * LocalModel constructor
 * @public
 * @param {Object} options
 */

var LocalModel = function(options) {
  this.options = options || {};
  this.models = {};
};

/**
 * Adds a model schema to the list of models
 * @public
 * @param {String} name - the name of the model
 * @param {Object} schema - the schema for the model
 * @returns {Object} the schema;
 */
LocalModel.prototype.addModel = function(name, schema) {
  var model = new LocalSchema(name, schema);
  this.models[name] = model;
  return model;
};

/**
 * Returns the schema for the model by name
 * @public
 * @param {String} name - the name of the model
 * @returns {Object} the model schema
 */
LocalModel.prototype.model = function(name) {
  if (!this.models[name]) {
    console.error('The model with name "' + name + '" does not exist.');
    return null;
  }
  return this.models[name];
};
<commit_msg>CS Fix<commit_after>/*!
 * LocalModel
 * Developer: Rick Craig
 * https://github.com/RickCraig/localmodel
 */

'use strict';

/**
 * Generates a random UUID
 * @private
 * @returns {String} random id
 */
var generateUUID = function(){
  var d = Date.now();
  var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
    .replace(/[xy]/g, function(c) {
      var r = (d + Math.random()*16)%16 | 0;
      d = Math.floor(d/16);
      return (c==='x' ? r : (r&0x3|0x8)).toString(16);
    });
  return uuid;
};

/**
 * Builds the key for the data
 * @private
 * @param {String} name - of the model
 * @param {String} id - of the entry
 * @returns {String} the merged key name
 */
var getKey = function(name, id) {
  return name + '::' + id;
};

/**
 * Adds the index for the new entry
 * @private
 * @param {String} name - the name of the model
 * @param {String} newIndex - the key for the new entry
 */
var addIndex = function(name, newIndex) {
  var indexName = name + '-index';
  var indexString = localStorage.getItem(indexName);
  var indices = indexString ? JSON.parse(indexString) : [];
  indices.push(newIndex);
  localStorage.setItem(indexName, JSON.stringify(indices));
};

/**
 * Returns the indices for a model
 * @private
 * @param {String} name - the model name
 * @returns {Array} the indices
 */
var getIndices = function(name) {
  var indices = localStorage.getItem(name + '-index');
  return JSON.parse(indices);
};

/**
 * Get a specific index from the indices
 * @private
 * @param {Array} indices
 * @param {String} term
 * @returns {String} matching index
 */
var getIndex = function(indices, term) {
  var regex = new RegExp('::' + term, 'g');
  for (var i = 0; i < indices.length; i++) {
    var index = indices[i];
    if (regex.test(index)) {
      return index;
    }
  }
  return;
};

/**
 * Checks if an object is empty
 * @private
 * @param {Object} obj
 * @returns {Boolean} true if empty
 */
var isEmpty = function(obj) {
  for(var prop in obj) {
    if(obj.hasOwnProperty(prop)) {
      return false;
    }
  }
  return true;
};

/**
 * Checks an array of booleans for a false
 * @private
 * @param {Array} arr - an array of booleans
 * @returns {Boolean} true if contains a false
 */
var containsFalse = function(arr) {
  for (var i = 0; i < arr.length; i++) {
    if (!arr[i]) {
      return true;
    }
  }
  return false;
};

/**
 * Takes in a data string and returns
 * true if query matches
 * @private
 * @param {String} data - the string to match
 * @param {Mixed} query
 * @returns {Boolean} true if the data matches the query
 */
var matchQuery = function(data, query) {
  // Query using regular expression
  if (query instanceof RegExp) {
    return query.test(data);
  }

  // Query using string or number
  if (typeof query === 'string' ||
    typeof query === 'number') {
      return data === query;
    }

  if (typeof query === 'object') {
    // Do the business in here for $gte, $gt, $lte, $lt
    // Remember to tag this 0.0.2

    if (typeof data === 'number') {
      var matches = [];
      if (query.$gte) {
        matches.push(query.$gte <= data);
      }

      if (query.$gt) {
        matches.push(query.$gt < data);
      }

      if (query.$lte) {
        matches.push(query.$lte >= data);
      }

      if (query.$lt) {
        matches.push(query.$lt > data);
      }

      return !containsFalse(matches);
    }

  }
};

/**
 * Local Document constructor
 * @public
 * @param {Object} data - the entry raw data
 */
var LocalDocument = function(data, schema) {
  this.schema = schema;
  this.data = {};
  this.indexKey = getKey(schema.name, data._id);

  // Try to force the schema type
  for (var key in data) {
    var type = schema.schema[key];
    var property = data[key];

    if (type === LocalSchema.SchemaTypes.Date) {
      property = new Date(property);
    }

    this.data[key] = property;
  }
};

/**
 * Used to save the document when updated
 * @public
 */
LocalDocument.prototype.save = function() {
  // Build the object to save
  var toBeSaved = {};
  for (var key in this.schema.schema) {
    toBeSaved[key] = this.data[key];
  }

  var itemKey = getKey(this.schema.name, this.data._id);
  localStorage.setItem(itemKey, JSON.stringify(toBeSaved));
};

/**
 * Local Schema constructor
 * @public
 * @param {Object} schema
 */
var LocalSchema = function(name, schema) {
  this.schema = schema;
  this.name = name;
};

/**
 * Create a new data object for this schema
 * @public
 * @param {Object} data
 * @returns {}
 */
LocalSchema.prototype.create = function(data) {
  var newEntry = {};
  newEntry._id = generateUUID();
  for (var key in this.schema) {
    newEntry[key] = data[key];
  }

  // Save to localstorage
  // At some point if there is an index, it can be added the the key for speed
  var index = getKey(this.name, newEntry._id);
  localStorage.setItem(index, JSON.stringify(newEntry));
  addIndex(this.name, index);

  // Clear indices
  this.indices = null;

  return new LocalDocument(newEntry, this);
};

/**
 * Returns all entries in storage
 * @public
 * @returns {Array} all entries
 */
LocalSchema.prototype.all = function() {
  this.indices = this.indices || getIndices(this.name);
  var results = [];

  // Check if the collection is empty
  if (!this.indices) {
    return results;
  }

  for (var i = 0; i < this.indices.length; i++) {
    var index = this.indices[i];
    var result = JSON.parse(localStorage.getItem(index));
    results.push(new LocalDocument(result, this));
  }

  return results;
};

/**
 * Find an entry by ID
 * @public
 * @param {String} id
 * @returns {Object} the object or null
 */
LocalSchema.prototype.findById = function(id) {
  this.indices = this.indices || getIndices(this.name);
  var match = getIndex(this.indices, id);
  if (!match) {
    return null;
  }
  return new LocalDocument(JSON.parse(localStorage.getItem(match)), this);
};

/**
 * Find entries matching a query
 * @public
 * @param {Object} query
 * @returns {Array} an array of matches
 */
LocalSchema.prototype.find = function(query) {
  if (!query || isEmpty(query)) {
    return this.all();
  }

  this.indices = this.indices || getIndices(this.name);
  var results = [];

  // Check if the collection is empty
  if (!this.indices) {
    return results;
  }

  for (var i = 0; i < this.indices.length; i++) {
    var entry = localStorage.getItem(this.indices[i]);
    var parsed = JSON.parse(entry);
    var matches = [];

    for (var key in query) {
      var queryItem = query[key];
      var isRegex = queryItem instanceof RegExp;
      if (!isRegex &&
        (queryItem === '' || isEmpty(queryItem))) {
        continue;
      }

      if (parsed[key]) {
        matches.push(matchQuery(parsed[key], queryItem));
      }
    }

    if (!containsFalse(matches)) {
      results.push(new LocalDocument(parsed, this));
    }
  }
  return results;
};

/**
 * LocalSchema Schema Types
 * For use in validation and return
 * @public
 */
LocalSchema.SchemaTypes = {
  String: 'string',
  Number: 'number',
  Boolean: 'boolean',
  Mixed: 'mixed',
  Date: 'date'
};

/**
 * LocalModel constructor
 * @public
 * @param {Object} options
 */
var LocalModel = function(options) {
  this.options = options || {};
  this.models = {};
};

/**
 * Adds a model schema to the list of models
 * @public
 * @param {String} name - the name of the model
 * @param {Object} schema - the schema for the model
 * @returns {Object} the schema;
 */
LocalModel.prototype.addModel = function(name, schema) {
  var model = new LocalSchema(name, schema);
  this.models[name] = model;
  return model;
};

/**
 * Returns the schema for the model by name
 * @public
 * @param {String} name - the name of the model
 * @returns {Object} the model schema
 */
LocalModel.prototype.model = function(name) {
  if (!this.models[name]) {
    console.error('The model with name "' + name + '" does not exist.');
    return null;
  }
  return this.models[name];
};
<|endoftext|>


<commit_before>(function (root, factory) {
  var name = 'Magnificent';
  if (typeof define === 'function' && define.amd) {
    define(['mag',' jquery', 'jquery-bridget'], function (mag, $) {
        return (root[name] = factory(mag, $));
    });
  } else if (typeof exports === 'object') {
    module.exports = factory(require('./mag'), require('jquery'), require('jquery-bridget'));
  } else {
    root[name] = factory(Mag, $);
  }
}(this, function (Mag, $) {

  var normalizeOffsets = function (e) {
    var offset = $(e.target).offset();
    return {
      x: e.pageX - offset.left,
      y: e.pageY - offset.top
    };
  };

  var ratioOffsets = function (e) {
    var normOff = normalizeOffsets(e);
    var $target = $(e.target);
    return {
      x: normOff.x / $target.width(),
      y: normOff.y / $target.height()
    }
  };

  var ratioOffsetsFor = function ($target, x, y) {
    return {
      x: x / $target.width(),
      y: y / $target.height()
    }
  };

  var cssPerc = function (frac) {
    return (frac * 100) + '%';
  };

  var toCSS = function (pt) {
    var css = {};
    if (pt.x !== undefined) css.left = cssPerc(pt.x);
    if (pt.y !== undefined) css.top = cssPerc(pt.y);
    if (pt.w !== undefined) css.width = cssPerc(pt.w);
    if (pt.h !== undefined) css.height = cssPerc(pt.h);
    return css;
  };

  var Magnificent = function (element, options) {
    this.element = $( element );
    this.options = $.extend( true, {}, this.options, options );
    this._init();
  };

  Magnificent.prototype.options = {
    mode: 'inner',
    position: 'mirror',
    positionEvent: 'move',
    constrainLens: true,
    constrainZoomed: false,
    theme: 'default',
    initialShow: 'thumb',
    zoomMin: 1,
    zoomMax: 10,
    zoomRate: 0.2,
    toggle: true
  };

  /**
   * Default toggle implementation.
   *
   * @param  {[type]} enter [description]
   * @return {[type]}       [description]
   */
  Magnificent.prototype.toggle = function (enter) {
    if (enter) {
      this.$zoomedContainer.fadeIn();
    }
    else {
      this.$zoomedContainer.fadeOut();
    }
  };

  Magnificent.prototype._init = function () {

    var that = this;

    var $el = this.element;

    var options = this.options;

    if ($.isFunction(options.toggle)) {
      this.toggle = options.toggle;
    }

    var $lens;

    var model = this.model = {
      focus: {
        x: 0.5,
        y: 0.5
      },
      zoom: 2,
      lens: {
        w: 0,
        h: 0
      }
    };

    var mag = new Mag({
      zoomMin: options.zoomMin,
      zoomMax: options.zoomMax,
      constrainLens: options.constrainLens,
      constrainZoomed: options.constrainZoomed,
      model: model
    });

    var modelLazy = this.modelLazy = {
      focus: {
        x: 0.5,
        y: 0.5
      },
      zoom: 2,
      lens: {
        w: 0,
        h: 0
      }
    };

    var magLazy = new Mag({
      zoomMin: options.zoomMin,
      zoomMax: options.zoomMax,
      constrainLens: options.constrainLens,
      constrainZoomed: options.constrainZoomed,
      model: modelLazy
    });

    mag.compute();
    magLazy.compute();

    this.compute = function () {
      mag.compute();

      $el.trigger('compute', that);
    };

    var render = function () {
      var lens, zoomed;
      if ($lens) {
        lens = modelLazy.lens;
        var lensCSS = toCSS(lens);
        $lens.css(lensCSS);
      }
      zoomed = modelLazy.zoomed;
      var zoomedCSS = toCSS(zoomed);
      $zoomed.css(zoomedCSS);

      $el.trigger('render', that);
    };

    var $zoomedChildren;
    var $thumbChildren;
    var $zoomed;
    var $zoomedContainer;

    $thumbChildren = $el.children();

    $el.empty();
    $el.addClass('mag-host');

    if (! options.zoomedContainer) {
      var id = $el.attr('mag-thumb');
      options.zoomedContainer = $('[mag-zoom="' + id + '"]');
    }

    if (options.zoomedContainer) {
      $zoomedContainer = $(options.zoomedContainer);
      $zoomedChildren = $zoomedContainer.children();
      $zoomedContainer.empty();

      if (options.mode === 'inner') {
        $zoomedContainer.remove();
      }
    }

    if (options.mode === 'outer' && options.showLens == null) {
      options.showLens = true;
    }

    if (! $zoomedChildren || ! $zoomedChildren.length) {
      $zoomedChildren = $thumbChildren.clone();
    }

    if (options.mode) {
      $el.attr('mag-mode', options.mode);
    }

    if (options.theme) {
      $el.attr('mag-theme', 'default');
    }

    if (options.position) {
      $el.attr('mag-position', options.position);
    }

    if (options.positionEvent) {
      $el.attr('mag-position-event', options.positionEvent);
    }

    if (options.showLens) {
      $lens = $('

');
      $el.append($lens);
    }

    var $noflow = $('

');
    $el.append($noflow);

    var $zone = $('

');
    $noflow.append($zone);

    if (options.mode === 'inner') {
      $zoomedContainer = $noflow;
      $noflow.addClass('mag-zoomed-bg');
    }
    else if (options.mode === 'outer') {
      if (! options.zoomedContainer) {
        throw new Error("Required 'zoomedContainer' option.");
      }
      $zoomedContainer = $(options.zoomedContainer);
      $zoomedContainer.addClass('mag-zoomed-container');
      $zoomedContainer.addClass('mag-zoomed-bg');
      $zoomedContainer.attr('mag-theme', options.theme);
    }
    else {
      throw new Error("Invalid 'mode' option.");
    }

    var $thumb = $('

');
    $thumb.html($thumbChildren);
    $el.append($thumb);

    $zoomed = $('

');
    $zoomed.html($zoomedChildren);
    $zoomedContainer.append($zoomed);

    this.$el = $el;
    this.$zone = $zone;
    this.$noflow = $noflow;
    this.$thumb = $thumb;
    this.$zoomed = $zoomed;
    this.$zoomedContainer = $zoomedContainer;

    if (options.toggle) {
      if (options.initialShow === 'thumb') {
        $zoomedContainer.hide();
      }
      else if (options.initialShow === 'zoomed') {
        //
      }
      else {
        throw new Error("Invalid 'initialShow' option.");
      }

      $el.on('mouseenter', function () {
        that.toggle.call(that, true);
      });

      $el.on('mouseleave', function () {
        that.toggle.call(that, false);
      });
    }

    render();

    var lazyRate = 0.25;
    var renderLoopIntervalTime = 20;
    var dragRate = 0.2;
    var zoomRate = options.zoomRate;

    var approach = function (thresh, rate, dest, src, props, srcProps) {
      srcProps = srcProps ? srcProps : props;
      if (! $.isArray(props)) {
        props = [props];
        srcProps = [srcProps];
      }
      for (var i = 0, m = props.length; i < m; ++i) {
        var prop = props[i];
        var srcProp = srcProps[i];
        var diff = src[srcProp] - dest[prop];
        if (Math.abs(diff) > thresh) {
          dest[prop] += diff * rate;
        }
        else {
          dest[prop] += diff;
        }
      }
    };

    var renderLoop = function () {
      approach(0.01, lazyRate, modelLazy.focus, model.focus, 'x');
      approach(0.01, lazyRate, modelLazy.focus, model.focus, 'y');
      approach(0.05, lazyRate, modelLazy, model, 'zoom');

      magLazy.compute();

      render();
    };

    var adjustForMirror = function (focus) {
      model.focus.x = focus.x;
      model.focus.y = focus.y;
      that.compute();
    };

    if (options.position === 'mirror') {
      if (options.positionEvent === 'move') {
        lazyRate = 0.2;

        $zone.on('mousemove', function(e){
          var ratios = ratioOffsets(e);
          adjustForMirror(ratios);
        });
      }
      else if (options.positionEvent === 'hold') {
        lazyRate = 0.2;

        $zone.drag('start', function () {
          dragging = true;
          $el.addClass('mag--dragging');
        });

        $zone.drag('end', function () {
          dragging = false;
          $el.removeClass('mag--dragging');
        });

        $zone.drag(function( e, dd ){
          var offset = $zone.offset();
          var ratios = ratioOffsetsFor($zone, e.pageX - offset.left, e.pageY - offset.top);
          adjustForMirror(ratios);
        });
      }
      else {
        throw new Error("Invalid 'positionEvent' option.");
      }
    }
    else if (options.position === 'drag') {

      var startFocus = null;

      $zone.drag('start', function () {
        dragging = true;
        $el.addClass('mag--dragging');
        startFocus = {
          x: model.focus.x,
          y: model.focus.y
        };
      });

      $zone.drag('end', function () {
        dragging = false;
        $el.removeClass('mag--dragging');
        startFocus = null;
      });

      $zone.drag(function( e, dd ) {
        var offset = $zone.offset();
        ratios = ratioOffsetsFor($zone, dd.originalX - dd.offsetX, dd.originalY - dd.offsetY);

        ratios = {
          x: ratios.x / model.zoom,
          y: ratios.y / model.zoom
        };

        var focus = model.focus;

        focus.x = startFocus.x + ratios.x;
        focus.y = startFocus.y + ratios.y;

        that.compute();
      });

    }
    else if (options.position === 'joystick') {

      var joystickIntervalTime = 50;

      var dragging = false;

      var ratios = {
        x: model.focus.x,
        y: model.focus.y
      };

      if (options.positionEvent === 'move') {
        dragging = true;
        lazyRate = 0.5;

        $zone.on('mousemove', function(e){
          ratios = ratioOffsets(e);
        });
      }
      else if (options.positionEvent === 'hold') {
        lazyRate = 0.5;

        $zone.drag('start', function () {
          dragging = true;
          $el.addClass('mag--dragging');
        });

        $zone.drag('end', function () {
          dragging = false;
          $el.removeClass('mag--dragging');
        });

        $zone.drag(function( e, dd ){

          var offset = $zone.offset();
          ratios = ratioOffsetsFor($zone, e.pageX - offset.left, e.pageY - offset.top);
        });
      }
      else {
        throw new Error("Invalid 'positionEvent' option.");
      }

      var joystickInterval = setInterval(function () {
        if (! dragging) return;

        var focus = model.focus;

        var adjustedDragRate = dragRate;
        focus.x += (ratios.x - 0.5) * adjustedDragRate;
        focus.y += (ratios.y - 0.5) * adjustedDragRate;
        that.compute();
      }, joystickIntervalTime);

    }
    else {
      throw new Error("Invalid 'position' option.");
    }

    $zone.on('mousewheel', function (e) {
      e.preventDefault();

      var zoom = model.zoom;
      var delta = e.deltaY;
      delta = delta > 0 ? delta + zoomRate : Math.abs(delta) - zoomRate;
      zoom *= delta;
      model.zoom = zoom;
      that.compute();
    });

    var renderLoopInterval = setInterval(renderLoop, renderLoopIntervalTime);

  };

  Magnificent.prototype.zoom = function (factor) {
    // this.model.zoom *= 1 + factor;
    this.model.zoom = 6;
    this.compute();
  };

  $.bridget('mag', Magnificent);

  return Magnificent;
}));
<commit_msg>some classifications<commit_after>(function (root, factory) {
  var name = 'Magnificent';
  if (typeof define === 'function' && define.amd) {
    define(['mag',' jquery', 'jquery-bridget'], function (mag, $) {
        return (root[name] = factory(mag, $));
    });
  } else if (typeof exports === 'object') {
    module.exports = factory(require('./mag'), require('jquery'), require('jquery-bridget'));
  } else {
    root[name] = factory(Mag, $);
  }
}(this, function (Mag, $) {

  var normalizeOffsets = function (e) {
    var offset = $(e.target).offset();
    return {
      x: e.pageX - offset.left,
      y: e.pageY - offset.top
    };
  };

  var ratioOffsets = function (e) {
    var normOff = normalizeOffsets(e);
    var $target = $(e.target);
    return {
      x: normOff.x / $target.width(),
      y: normOff.y / $target.height()
    }
  };

  var ratioOffsetsFor = function ($target, x, y) {
    return {
      x: x / $target.width(),
      y: y / $target.height()
    }
  };

  var cssPerc = function (frac) {
    return (frac * 100) + '%';
  };

  var toCSS = function (pt) {
    var css = {};
    if (pt.x !== undefined) css.left = cssPerc(pt.x);
    if (pt.y !== undefined) css.top = cssPerc(pt.y);
    if (pt.w !== undefined) css.width = cssPerc(pt.w);
    if (pt.h !== undefined) css.height = cssPerc(pt.h);
    return css;
  };

  var Magnificent = function (element, options) {
    this.element = $( element );
    this.options = $.extend( true, {}, this.options, options );
    this._init();
  };

  Magnificent.prototype.options = {
    mode: 'inner',
    position: 'mirror',
    positionEvent: 'move',
    constrainLens: true,
    constrainZoomed: false,
    theme: 'default',
    initialShow: 'thumb',
    zoomMin: 1,
    zoomMax: 10,
    zoomRate: 0.2,
    toggle: true
  };

  /**
   * Default toggle implementation.
   *
   * @param  {[type]} enter [description]
   * @return {[type]}       [description]
   */
  Magnificent.prototype.toggle = function (enter) {
    if (enter) {
      this.$zoomedContainer.fadeIn();
    }
    else {
      this.$zoomedContainer.fadeOut();
    }
  };

  Magnificent.prototype.compute = function () {
    var that = this;
    that.mag.compute();
    that.$el.trigger('compute', that);
  };

  Magnificent.prototype.render = function () {
    var that = this;
    var lens, zoomed;
    var $lens = this.$lens;
    var $zoomed = this.$zoomed;
    if ($lens) {
      lens = this.modelLazy.lens;
      var lensCSS = toCSS(lens);
      $lens.css(lensCSS);
    }
    zoomed = this.modelLazy.zoomed;
    var zoomedCSS = toCSS(zoomed);
    $zoomed.css(zoomedCSS);

    this.$el.trigger('render', that);
  };

  Magnificent.prototype._init = function () {

    var that = this;

    var $el = this.$el = this.element;

    var options = this.options;

    if ($.isFunction(options.toggle)) {
      this.toggle = options.toggle;
    }

    var $lens = this.$lens;

    var model = this.model = {
      focus: {
        x: 0.5,
        y: 0.5
      },
      zoom: 2,
      lens: {
        w: 0,
        h: 0
      }
    };

    var mag = this.mag = new Mag({
      zoomMin: options.zoomMin,
      zoomMax: options.zoomMax,
      constrainLens: options.constrainLens,
      constrainZoomed: options.constrainZoomed,
      model: model
    });

    var modelLazy = this.modelLazy = {
      focus: {
        x: 0.5,
        y: 0.5
      },
      zoom: 2,
      lens: {
        w: 0,
        h: 0
      }
    };

    var magLazy = this.magLazy = new Mag({
      zoomMin: options.zoomMin,
      zoomMax: options.zoomMax,
      constrainLens: options.constrainLens,
      constrainZoomed: options.constrainZoomed,
      model: modelLazy
    });

    mag.compute();
    magLazy.compute();

    var $zoomedChildren;
    var $thumbChildren;
    var $zoomed;
    var $zoomedContainer;

    $thumbChildren = $el.children();

    $el.empty();
    $el.addClass('mag-host');

    if (! options.zoomedContainer) {
      var id = $el.attr('mag-thumb');
      options.zoomedContainer = $('[mag-zoom="' + id + '"]');
    }

    if (options.zoomedContainer) {
      $zoomedContainer = $(options.zoomedContainer);
      $zoomedChildren = $zoomedContainer.children();
      $zoomedContainer.empty();

      if (options.mode === 'inner') {
        $zoomedContainer.remove();
      }
    }

    if (options.mode === 'outer' && options.showLens == null) {
      options.showLens = true;
    }

    if (! $zoomedChildren || ! $zoomedChildren.length) {
      $zoomedChildren = $thumbChildren.clone();
    }

    if (options.mode) {
      $el.attr('mag-mode', options.mode);
    }

    if (options.theme) {
      $el.attr('mag-theme', 'default');
    }

    if (options.position) {
      $el.attr('mag-position', options.position);
    }

    if (options.positionEvent) {
      $el.attr('mag-position-event', options.positionEvent);
    }

    if (options.showLens) {
      $lens = this.$lens = $('

');
      $el.append($lens);
    }

    var $noflow = $('

');
    $el.append($noflow);

    var $zone = $('

');
    $noflow.append($zone);

    if (options.mode === 'inner') {
      $zoomedContainer = $noflow;
      $noflow.addClass('mag-zoomed-bg');
    }
    else if (options.mode === 'outer') {
      if (! options.zoomedContainer) {
        throw new Error("Required 'zoomedContainer' option.");
      }
      $zoomedContainer = $(options.zoomedContainer);
      $zoomedContainer.addClass('mag-zoomed-container');
      $zoomedContainer.addClass('mag-zoomed-bg');
      $zoomedContainer.attr('mag-theme', options.theme);
    }
    else {
      throw new Error("Invalid 'mode' option.");
    }

    var $thumb = $('

');
    $thumb.html($thumbChildren);
    $el.append($thumb);

    $zoomed = this.$zoomed = $('

');
    $zoomed.html($zoomedChildren);
    $zoomedContainer.append($zoomed);

    this.$el = $el;
    this.$zone = $zone;
    this.$noflow = $noflow;
    this.$thumb = $thumb;
    this.$zoomed = $zoomed;
    this.$zoomedContainer = $zoomedContainer;

    if (options.toggle) {
      if (options.initialShow === 'thumb') {
        $zoomedContainer.hide();
      }
      else if (options.initialShow === 'zoomed') {
        //
      }
      else {
        throw new Error("Invalid 'initialShow' option.");
      }

      $el.on('mouseenter', function () {
        that.toggle.call(that, true);
      });

      $el.on('mouseleave', function () {
        that.toggle.call(that, false);
      });
    }

    that.render();

    var lazyRate = 0.25;
    var renderLoopIntervalTime = 20;
    var dragRate = 0.2;
    var zoomRate = options.zoomRate;

    var approach = function (thresh, rate, dest, src, props, srcProps) {
      srcProps = srcProps ? srcProps : props;
      if (! $.isArray(props)) {
        props = [props];
        srcProps = [srcProps];
      }
      for (var i = 0, m = props.length; i < m; ++i) {
        var prop = props[i];
        var srcProp = srcProps[i];
        var diff = src[srcProp] - dest[prop];
        if (Math.abs(diff) > thresh) {
          dest[prop] += diff * rate;
        }
        else {
          dest[prop] += diff;
        }
      }
    };

    var renderLoop = function () {
      approach(0.01, lazyRate, modelLazy.focus, model.focus, 'x');
      approach(0.01, lazyRate, modelLazy.focus, model.focus, 'y');
      approach(0.05, lazyRate, modelLazy, model, 'zoom');

      that.magLazy.compute();

      that.render();
    };

    var adjustForMirror = function (focus) {
      model.focus.x = focus.x;
      model.focus.y = focus.y;
      that.compute();
    };

    if (options.position === 'mirror') {
      if (options.positionEvent === 'move') {
        lazyRate = 0.2;

        $zone.on('mousemove', function(e){
          var ratios = ratioOffsets(e);
          adjustForMirror(ratios);
        });
      }
      else if (options.positionEvent === 'hold') {
        lazyRate = 0.2;

        $zone.drag('start', function () {
          dragging = true;
          $el.addClass('mag--dragging');
        });

        $zone.drag('end', function () {
          dragging = false;
          $el.removeClass('mag--dragging');
        });

        $zone.drag(function( e, dd ){
          var offset = $zone.offset();
          var ratios = ratioOffsetsFor($zone, e.pageX - offset.left, e.pageY - offset.top);
          adjustForMirror(ratios);
        });
      }
      else {
        throw new Error("Invalid 'positionEvent' option.");
      }
    }
    else if (options.position === 'drag') {

      var startFocus = null;

      $zone.drag('start', function () {
        dragging = true;
        $el.addClass('mag--dragging');
        startFocus = {
          x: model.focus.x,
          y: model.focus.y
        };
      });

      $zone.drag('end', function () {
        dragging = false;
        $el.removeClass('mag--dragging');
        startFocus = null;
      });

      $zone.drag(function( e, dd ) {
        var offset = $zone.offset();
        ratios = ratioOffsetsFor($zone, dd.originalX - dd.offsetX, dd.originalY - dd.offsetY);

        ratios = {
          x: ratios.x / model.zoom,
          y: ratios.y / model.zoom
        };

        var focus = model.focus;

        focus.x = startFocus.x + ratios.x;
        focus.y = startFocus.y + ratios.y;

        that.compute();
      });

    }
    else if (options.position === 'joystick') {

      var joystickIntervalTime = 50;

      var dragging = false;

      var ratios = {
        x: model.focus.x,
        y: model.focus.y
      };

      if (options.positionEvent === 'move') {
        dragging = true;
        lazyRate = 0.5;

        $zone.on('mousemove', function(e){
          ratios = ratioOffsets(e);
        });
      }
      else if (options.positionEvent === 'hold') {
        lazyRate = 0.5;

        $zone.drag('start', function () {
          dragging = true;
          $el.addClass('mag--dragging');
        });

        $zone.drag('end', function () {
          dragging = false;
          $el.removeClass('mag--dragging');
        });

        $zone.drag(function( e, dd ){

          var offset = $zone.offset();
          ratios = ratioOffsetsFor($zone, e.pageX - offset.left, e.pageY - offset.top);
        });
      }
      else {
        throw new Error("Invalid 'positionEvent' option.");
      }

      var joystickInterval = setInterval(function () {
        if (! dragging) return;

        var focus = model.focus;

        var adjustedDragRate = dragRate;
        focus.x += (ratios.x - 0.5) * adjustedDragRate;
        focus.y += (ratios.y - 0.5) * adjustedDragRate;
        that.compute();
      }, joystickIntervalTime);

    }
    else {
      throw new Error("Invalid 'position' option.");
    }

    $zone.on('mousewheel', function (e) {
      e.preventDefault();

      var zoom = model.zoom;
      var delta = e.deltaY;
      delta = delta > 0 ? delta + zoomRate : Math.abs(delta) - zoomRate;
      zoom *= delta;
      model.zoom = zoom;
      that.compute();
    });

    var renderLoopInterval = setInterval(renderLoop, renderLoopIntervalTime);

  };

  Magnificent.prototype.zoom = function (factor) {
    // this.model.zoom *= 1 + factor;
    this.model.zoom = 6;
    this.compute();
  };

  $.bridget('mag', Magnificent);

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


<commit_before>
// function Change() {
//   this.memo = {};
// }
//
// Change.prototype.changePossibilitiesTopDown = function(amountLeft, denominations, currentIndex) {
//   currentIndex = (typeof currentIndex !== 'undefined') ? currentIndex : 0;
//
//   var memoKey = [amountLeft, currentIndex].join(', ');
//   // console.log(memoKey);
//
//   if (this.memo.hasOwnProperty(memoKey)) {
//     console.log('grabbing memo [' +memoKey+ ']');
//     return this.memo[memoKey];
//   }
//
//   // BASE CASES:
//   if (amountLeft === 0) return 1;
//
//   // overshoot amountLeft
//   if (amountLeft < 0) return 0;
//
//   // out of denominations
//   if (currentIndex === denominations.length) return 0;
//
//   console.log('Checking ways to make ' +amountLeft+ ' with [' +denominations.slice(currentIndex).join(', ')+ ']');
//
//   var currentCoin = denominations[currentIndex];
//
//   var numPossibilities = 0;
//
//   while (amountLeft >= 0) {
//     numPossibilities += this.changePossibilitiesTopDown(amountLeft, denominations, currentIndex+1)
//     amountLeft -= currentCoin;
//   }
//
//   this.memo[memoKey] = numPossibilities;
//   return numPossibilities;
// };
//
// new Change().changePossibilitiesTopDown(4, [1, 2, 3]);

function changePossibilitiesBottomUp(amount, denominations) {
  var waysOfDoingNCents = [];

  for (var i=0; i<=amount; i++) {
    // initialize arrays of 0s
    waysOfDoingNCents[i] = 0;
  }
  waysOfDoingNCents[0] = 1;

  denominations.forEach( function(coin) {
    for (var higherAmount=coin; higherAmount<=amount; higherAmount++) {
      var higherAmountRemainder = higherAmount - coin;
      waysOfDoingNCents[higherAmount] += waysOfDoingNCents[higherAmountRemainder];
      console.log(waysOfDoingNCents);
      console.log(coin);
    }
    console.log("---");
  });

  console.log(waysOfDoingNCents[amount]);
  return waysOfDoingNCents[amount];
}

// var results = changePossibilitiesBottomUp(5, [1, 3, 5]);
var results = changePossibilitiesBottomUp(4, [1, 2, 3]);
// console.log(results);
<commit_msg>Remove solution from makeChange<commit_after>
function makeChange() {

}<|endoftext|>


<commit_before>/*jshint bitwise:false*/
'use strict';

var lerp = require( './utils' ).lerp;

// RGB values are ints from [0, 255].
function Color( r, g, b ) {
  this.r = r || 0;
  this.g = g || 0;
  this.b = b || 0;
}

Color.prototype.toString = function() {
  return 'rgb(' +
    ( this.r | 0 ) + ', ' +
    ( this.g | 0 ) + ', ' +
    ( this.b | 0 ) +
  ')';
};

Color.prototype.lerp = function( color, alpha ) {
  this.r = lerp( this.r, color.r, alpha );
  this.g = lerp( this.g, color.g, alpha );
  this.b = lerp( this.b, color.b, alpha );
  return this;
};

Color.prototype.copy = function( color ) {
  this.r = color.r;
  this.g = color.g;
  this.b = color.b;
  return this;
};

module.exports = Color;
<commit_msg>Add Color math operations (add, multiply, multiplyScalar).<commit_after>/*jshint bitwise:false*/
'use strict';

var lerp = require( './utils' ).lerp;

// RGB values are ints from [0, 255].
function Color( r, g, b ) {
  this.r = r || 0;
  this.g = g || 0;
  this.b = b || 0;
}

Color.prototype.toString = function() {
  return 'rgb(' +
    ( this.r | 0 ) + ', ' +
    ( this.g | 0 ) + ', ' +
    ( this.b | 0 ) +
  ')';
};

Color.prototype.add = function( color ) {
  this.r += color.r;
  this.g += color.g;
  this.b += color.b;
  return this;
};

Color.prototype.multiply = function( color ) {
  this.r *= color.r;
  this.g *= color.g;
  this.b *= color.b;
  return this;
};

Color.prototype.multiplyScalar = function( s ) {
  this.r *= s;
  this.g *= s;
  this.b *= s;
  return this;
};

Color.prototype.lerp = function( color, alpha ) {
  this.r = lerp( this.r, color.r, alpha );
  this.g = lerp( this.g, color.g, alpha );
  this.b = lerp( this.b, color.b, alpha );
  return this;
};

Color.prototype.copy = function( color ) {
  this.r = color.r;
  this.g = color.g;
  this.b = color.b;
  return this;
};

module.exports = Color;
<|endoftext|>


<commit_before>var jsonData = {"versions":[
                   {"major":0, "minor":0, "patch":0, "day":1, "month":4, "year":2015, "type":2,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows 7 and later",
                        "files":[
                           {"extension":".exe", "bitness":32}, {"extension":".zip", "bitness":32},
                           {"extension":".exe", "bitness":64}, {"extension":".zip", "bitness":64}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 14.04 LTS (Trusty Tahr) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"Mac OS X 10.7 (Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ],
                    "information":"Note: this version is fairly stable and is therefore the version we would recommend using, especially if you want to get access to OpenCOR's latest features."
                   },
                   {"major":0, "minor":3, "patch":0, "day":19, "month":8, "year":2014, "type":1,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows XP and later",
                        "files":[
                           {"extension":".exe"}, {"extension":".zip"}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 14.04 LTS (Trusty Tahr) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"Mac OS X 10.7 (Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ]
                   },
                   {"major":0, "minor":2, "patch":0, "day":19, "month":11, "year":2013, "type":0,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows XP and later",
                        "files":[
                           {"extension":".exe"}, {"extension":".zip"}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 12.04 LTS (Precise Pangolin) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"Mac OS X 10.7 (Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ]
                   },
                   {"major":0, "minor":1, "patch":2, "day":29, "month":5, "year":2013, "type":0,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows XP and later",
                        "files":[
                           {"extension":".exe"}, {"extension":".zip"}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 12.04 LTS (Precise Pangolin) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"OS X 10.8 (Mountain Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ]
                   },
                   {"major":0, "minor":1, "patch":1, "day":17, "month":4, "year":2013, "type":0,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows XP and later",
                        "files":[
                           {"extension":".exe"}, {"extension":".zip"}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 12.04 LTS (Precise Pangolin) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"OS X 10.8 (Mountain Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ]
                   },
                   {"major":0, "minor":1, "patch":0, "day":1, "month":4, "year":2013, "type":0,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows XP and later",
                        "files":[
                           {"extension":".exe"}, {"extension":".zip"}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 12.04 LTS (Precise Pangolin) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"OS X 10.8 (Mountain Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ]
                   }
                ]
               };
<commit_msg>Slight update to our downloads page [ci skip].<commit_after>var jsonData = {"versions":[
                   {"major":0, "minor":0, "patch":0, "day":1, "month":4, "year":2015, "type":2,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows 7 and later",
                        "files":[
                           {"extension":".exe", "bitness":32}, {"extension":".zip", "bitness":32},
                           {"extension":".exe", "bitness":64}, {"extension":".zip", "bitness":64}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 14.04 LTS (Trusty Tahr) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"Mac OS X 10.7 (Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ],
                    "information":"Note: this version is getting close to our next official release, so we would recommend using it, especially if you want to get access to OpenCOR's latest features.
Warning: there is a known issue with a third-party library (LLVM) used by OpenCOR. It affects the 32-bit Windows version of OpenCOR by preventing it from running more than one CellML file in a given session. Other versions of OpenCOR are not affected."
                   },
                   {"major":0, "minor":3, "patch":0, "day":19, "month":8, "year":2014, "type":1,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows XP and later",
                        "files":[
                           {"extension":".exe"}, {"extension":".zip"}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 14.04 LTS (Trusty Tahr) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"Mac OS X 10.7 (Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ]
                   },
                   {"major":0, "minor":2, "patch":0, "day":19, "month":11, "year":2013, "type":0,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows XP and later",
                        "files":[
                           {"extension":".exe"}, {"extension":".zip"}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 12.04 LTS (Precise Pangolin) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"Mac OS X 10.7 (Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ]
                   },
                   {"major":0, "minor":1, "patch":2, "day":29, "month":5, "year":2013, "type":0,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows XP and later",
                        "files":[
                           {"extension":".exe"}, {"extension":".zip"}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 12.04 LTS (Precise Pangolin) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"OS X 10.8 (Mountain Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ]
                   },
                   {"major":0, "minor":1, "patch":1, "day":17, "month":4, "year":2013, "type":0,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows XP and later",
                        "files":[
                           {"extension":".exe"}, {"extension":".zip"}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 12.04 LTS (Precise Pangolin) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"OS X 10.8 (Mountain Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ]
                   },
                   {"major":0, "minor":1, "patch":0, "day":1, "month":4, "year":2013, "type":0,
                    "platforms":[
                       {"name":"Windows", "supported":"Windows XP and later",
                        "files":[
                           {"extension":".exe"}, {"extension":".zip"}
                        ]
                       },
                       {"name":"Linux", "supported":"Ubuntu 12.04 LTS (Precise Pangolin) and later",
                        "files":[
                           {"extension":".tar.gz", "bitness":32},
                           {"extension":".tar.gz", "bitness":64}
                        ]
                       },
                       {"name":"OS X", "supported":"OS X 10.8 (Mountain Lion) and later",
                        "files":[
                           {"extension":".dmg"}, {"extension":".zip"}
                        ]
                       }
                    ]
                   }
                ]
               };
<|endoftext|>


<commit_before>'use strict';

const Lf = require('lovefield');
require('sugar'); // monkeypatches Date

const Track = require('./track');
const Storage = require('./storage');

const Reporting = require('./reporting');

// {userId: <lovefield db>}
// lovefield docs say to avoid multiple calls to connect, even with a close in between.
// so, dbs are treated like singletons.
const _dbs = {};

// Return a lovefield db for this user.
// Will always return the same reference for a user on multiple calls.
exports.openDb = function openDb(userId, callback) {
  if (_dbs[userId]) {
    callback(_dbs[userId]);
    return;
  }

  console.log('opening...');
  const schemaBuilder = Lf.schema.create(`ltracks_${userId}`, 1);

  let table = schemaBuilder.createTable('Track');

  Track.fields.forEach(field => {
    table = table.addColumn(field.name, field.type);

    if (field.name === 'id') {
      table = table.addPrimaryKey(['id']);
    } else {
      table = table.addNullable([field.name]);
      if (!field.hidden) {
        table = table.addIndex(`idx_${field.name}`, [field.name], false, Lf.Order.DESC);
      }
    }
  });

  schemaBuilder.connect({storeType: Lf.schema.DataStoreType.MEMORY})
  .then(db => {
    _dbs[userId] = db;
    callback(db);
  })
  .catch(err => {
    console.error(err);
    Reporting.Raven.captureMessage('schemaBuilder.connect', {
      extra: {err},
    });
  });
};

exports.upsertTracks = function upsertTracks(db, userId, tracks, callback) {
  const track = db.getSchema().table('Track');

  const rows = [];
  for (let i = 0; i < tracks.length; i++) {
    rows.push(track.createRow(tracks[i]));
  }

  return db.insertOrReplace().into(track).values(rows).exec()
  .then(callback)
  .catch(err => {
    console.error(err);
    Reporting.Raven.captureMessage('db.insertOrReplace', {
      extra: {err},
    });
  });
};

exports.deleteTracks = function deleteTracks(db, userId, trackIds, callback) {
  const track = db.getSchema().table('Track');
  db.delete().from(track).where(track.id.in(trackIds)).exec()
  .then(callback)
  .catch(err => {
    console.error(err);
    Reporting.Raven.captureMessage('db.delete', {
      extra: {err},
    });
  });
};

function escapeForRegex(s) {
  // Return a copy of the string s with regex control characters escaped.
  // Source: http://stackoverflow.com/a/3561711.
  return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}

function buildWhereClause(track, playlistsById, splaylistcache, seenIds, rule) {
  // Return a clause for use in a lovefield where() predicate, or null to select all tracks.
  let clause = null;
  let boolOp = null;

  if ('any' in rule) {
    boolOp = 'any';
  } else if ('all' in rule) {
    boolOp = 'all';
  }

  if (boolOp !== null) {
    const subrules = rule[boolOp];
    const lfOp = boolOp === 'all' ? Lf.op.and : Lf.op.or;

    // We need to handle nulls from recursive calls, as well as empty subrule arrays.
    const subClauses = subrules
    .map(buildWhereClause.bind(undefined, track, playlistsById, splaylistcache, seenIds))
    .filter(c => c !== null);

    if (subClauses.length > 0) {
      clause = lfOp.apply(Lf.op, subClauses);
    }
  } else if ('value' in rule && 'operator' in rule) {
    let value = rule.value;
    let operator = rule.operator;

    if (rule.name === 'playlist') {
      if (rule.value[0] === 'P') {
        // splaylist
        let trackIds = new Set();
        try {
          trackIds = splaylistcache.splaylists[rule.value.substring(1)].trackIds;
        } catch (e) {
          // This is likely a desync between the rules and splaylist state.
          console.error(e);
          Reporting.Raven.captureException(e, {
            tags: {playlistId: rule.value},
            extra: {splaylistcache, rule},
          });
        }

        clause = track.id.in(Array.from(trackIds));
        if (rule.operator === 'notEqualTo') {
          clause = Lf.op.not(clause);
        }
      } else {
        // playlist
        const linkedRules = playlistsById[rule.value].rules;
        if (seenIds.has(rule.value)) {
          console.warn('refusing to recurse into a cycle with', rule.value, seenIds);
        } else {
          seenIds.add(rule.value);
          clause = buildWhereClause(track, playlistsById, splaylistcache, seenIds, linkedRules);
          if (rule.operator === 'notEqualTo') {
            clause = Lf.op.not(clause);
          }
        }
      }
    } else {
      if (Track.fieldsByName[rule.name].is_datetime) {
        value = Date.create(rule.value).getTime() * 1000;
      }

      if (rule.operator === 'match-insensitive') {
        operator = 'match';
        value = new RegExp(escapeForRegex(value), 'i');
      } else if (rule.operator === 'no-match') {
        operator = 'match';
        // Negative lookahead on each character.
        // Source: http://stackoverflow.com/a/957581/1231454
        value = new RegExp(`^((?!${escapeForRegex(value)}).)*$`);
      } else if (rule.operator === 'no-match-insensitive') {
        operator = 'match';
        value = new RegExp(`^((?!${escapeForRegex(value)}).)*$`, 'i');
      } else if (rule.operator === 'eq-insensitive') {
        operator = 'match';
        value = new RegExp(`^${escapeForRegex(value)}$`, 'i');
      } else if (rule.operator === 'neq-insensitive') {
        // Negative lookahead once.
        // Source: http://stackoverflow.com/a/2964653.
        operator = 'match';
        value = new RegExp(`^(?!${escapeForRegex(value)}$)`, 'i');
      }

      clause = track[rule.name][operator](value);
    }
  }

  return clause;
}

function execQuery(db, track, whereClause, playlist, callback, onError) {
  let query = db.select().from(track);

  if (whereClause !== null) {
    query = query.where(whereClause);
  }

  for (let i = 0; i < playlist.sorts.length; i++) {
    const sort = playlist.sorts[i];
    const orderBy = track[sort.sortBy];
    const order = Lf.Order[sort.sortByOrder];
    query = query.orderBy(orderBy, order);
  }

  if (playlist.limit) {
    query = query.limit(playlist.limit);
  }

  query.exec().then(callback).catch(onError);
}

exports.queryTracks = function queryTracks(db, splaylistcache, playlist, callback) {
  // Callback a list of tracks that should be in the playlist, or null on problems.

  Storage.getPlaylistsForUser(playlist.userId, playlists => {
    const playlistsById = {};
    for (let i = 0; i < playlists.length; i++) {
      const p = playlists[i];
      playlistsById[p.localId] = p;
    }

    const track = db.getSchema().table('Track');
    let whereClause;
    try {
      whereClause = buildWhereClause(track, playlistsById, splaylistcache, new Set([playlist.localId]), playlist.rules);
    } catch (e) {
      console.error(e);
      Reporting.Raven.captureException(e, {
        tags: {playlistId: playlist.remoteId},
        extra: {playlist},
      });
      return callback(null);
    }

    execQuery(db, track, whereClause, playlist, callback, err => {
      console.error('execQuery', err);
      Reporting.Raven.captureMessage('execQuery', {
        tags: {playlistId: playlist.remoteId},
        extra: {playlist, err},
      });
      return callback(null);
    });
  });
};

exports.orderTracks = function orderTracks(db, playlist, tracks, callback, onError) {
  // Return a copy of tracks, ordered by playlist's sort rules.
  // Tracks that wouldn't normally be in playlist are allowed.

  const track = db.getSchema().table('Track');
  const whereClause = track.id.in(tracks.map(t => t.id));

  execQuery(db, track, whereClause, playlist, callback, onError);
};
<commit_msg>report cache desync as warning<commit_after>'use strict';

const Lf = require('lovefield');
require('sugar'); // monkeypatches Date

const Track = require('./track');
const Storage = require('./storage');

const Reporting = require('./reporting');

// {userId: <lovefield db>}
// lovefield docs say to avoid multiple calls to connect, even with a close in between.
// so, dbs are treated like singletons.
const _dbs = {};

// Return a lovefield db for this user.
// Will always return the same reference for a user on multiple calls.
exports.openDb = function openDb(userId, callback) {
  if (_dbs[userId]) {
    callback(_dbs[userId]);
    return;
  }

  console.log('opening...');
  const schemaBuilder = Lf.schema.create(`ltracks_${userId}`, 1);

  let table = schemaBuilder.createTable('Track');

  Track.fields.forEach(field => {
    table = table.addColumn(field.name, field.type);

    if (field.name === 'id') {
      table = table.addPrimaryKey(['id']);
    } else {
      table = table.addNullable([field.name]);
      if (!field.hidden) {
        table = table.addIndex(`idx_${field.name}`, [field.name], false, Lf.Order.DESC);
      }
    }
  });

  schemaBuilder.connect({storeType: Lf.schema.DataStoreType.MEMORY})
  .then(db => {
    _dbs[userId] = db;
    callback(db);
  })
  .catch(err => {
    console.error(err);
    Reporting.Raven.captureMessage('schemaBuilder.connect', {
      extra: {err},
    });
  });
};

exports.upsertTracks = function upsertTracks(db, userId, tracks, callback) {
  const track = db.getSchema().table('Track');

  const rows = [];
  for (let i = 0; i < tracks.length; i++) {
    rows.push(track.createRow(tracks[i]));
  }

  return db.insertOrReplace().into(track).values(rows).exec()
  .then(callback)
  .catch(err => {
    console.error(err);
    Reporting.Raven.captureMessage('db.insertOrReplace', {
      extra: {err},
    });
  });
};

exports.deleteTracks = function deleteTracks(db, userId, trackIds, callback) {
  const track = db.getSchema().table('Track');
  db.delete().from(track).where(track.id.in(trackIds)).exec()
  .then(callback)
  .catch(err => {
    console.error(err);
    Reporting.Raven.captureMessage('db.delete', {
      extra: {err},
    });
  });
};

function escapeForRegex(s) {
  // Return a copy of the string s with regex control characters escaped.
  // Source: http://stackoverflow.com/a/3561711.
  return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}

function buildWhereClause(track, playlistsById, splaylistcache, seenIds, rule) {
  // Return a clause for use in a lovefield where() predicate, or null to select all tracks.
  let clause = null;
  let boolOp = null;

  if ('any' in rule) {
    boolOp = 'any';
  } else if ('all' in rule) {
    boolOp = 'all';
  }

  if (boolOp !== null) {
    const subrules = rule[boolOp];
    const lfOp = boolOp === 'all' ? Lf.op.and : Lf.op.or;

    // We need to handle nulls from recursive calls, as well as empty subrule arrays.
    const subClauses = subrules
    .map(buildWhereClause.bind(undefined, track, playlistsById, splaylistcache, seenIds))
    .filter(c => c !== null);

    if (subClauses.length > 0) {
      clause = lfOp.apply(Lf.op, subClauses);
    }
  } else if ('value' in rule && 'operator' in rule) {
    let value = rule.value;
    let operator = rule.operator;

    if (rule.name === 'playlist') {
      if (rule.value[0] === 'P') {
        // splaylist
        let trackIds = new Set();
        try {
          trackIds = splaylistcache.splaylists[rule.value.substring(1)].trackIds;
        } catch (e) {
          // This is likely a desync between the rules and splaylist state.
          console.error(e);
          Reporting.Raven.captureException(e, {
            level: 'warning',
            tags: {playlistId: rule.value},
            extra: {splaylistcache, rule},
          });
        }

        clause = track.id.in(Array.from(trackIds));
        if (rule.operator === 'notEqualTo') {
          clause = Lf.op.not(clause);
        }
      } else {
        // playlist
        const linkedRules = playlistsById[rule.value].rules;
        if (seenIds.has(rule.value)) {
          console.warn('refusing to recurse into a cycle with', rule.value, seenIds);
        } else {
          seenIds.add(rule.value);
          clause = buildWhereClause(track, playlistsById, splaylistcache, seenIds, linkedRules);
          if (rule.operator === 'notEqualTo') {
            clause = Lf.op.not(clause);
          }
        }
      }
    } else {
      if (Track.fieldsByName[rule.name].is_datetime) {
        value = Date.create(rule.value).getTime() * 1000;
      }

      if (rule.operator === 'match-insensitive') {
        operator = 'match';
        value = new RegExp(escapeForRegex(value), 'i');
      } else if (rule.operator === 'no-match') {
        operator = 'match';
        // Negative lookahead on each character.
        // Source: http://stackoverflow.com/a/957581/1231454
        value = new RegExp(`^((?!${escapeForRegex(value)}).)*$`);
      } else if (rule.operator === 'no-match-insensitive') {
        operator = 'match';
        value = new RegExp(`^((?!${escapeForRegex(value)}).)*$`, 'i');
      } else if (rule.operator === 'eq-insensitive') {
        operator = 'match';
        value = new RegExp(`^${escapeForRegex(value)}$`, 'i');
      } else if (rule.operator === 'neq-insensitive') {
        // Negative lookahead once.
        // Source: http://stackoverflow.com/a/2964653.
        operator = 'match';
        value = new RegExp(`^(?!${escapeForRegex(value)}$)`, 'i');
      }

      clause = track[rule.name][operator](value);
    }
  }

  return clause;
}

function execQuery(db, track, whereClause, playlist, callback, onError) {
  let query = db.select().from(track);

  if (whereClause !== null) {
    query = query.where(whereClause);
  }

  for (let i = 0; i < playlist.sorts.length; i++) {
    const sort = playlist.sorts[i];
    const orderBy = track[sort.sortBy];
    const order = Lf.Order[sort.sortByOrder];
    query = query.orderBy(orderBy, order);
  }

  if (playlist.limit) {
    query = query.limit(playlist.limit);
  }

  query.exec().then(callback).catch(onError);
}

exports.queryTracks = function queryTracks(db, splaylistcache, playlist, callback) {
  // Callback a list of tracks that should be in the playlist, or null on problems.

  Storage.getPlaylistsForUser(playlist.userId, playlists => {
    const playlistsById = {};
    for (let i = 0; i < playlists.length; i++) {
      const p = playlists[i];
      playlistsById[p.localId] = p;
    }

    const track = db.getSchema().table('Track');
    let whereClause;
    try {
      whereClause = buildWhereClause(track, playlistsById, splaylistcache, new Set([playlist.localId]), playlist.rules);
    } catch (e) {
      console.error(e);
      Reporting.Raven.captureException(e, {
        tags: {playlistId: playlist.remoteId},
        extra: {playlist},
      });
      return callback(null);
    }

    execQuery(db, track, whereClause, playlist, callback, err => {
      console.error('execQuery', err);
      Reporting.Raven.captureMessage('execQuery', {
        tags: {playlistId: playlist.remoteId},
        extra: {playlist, err},
      });
      return callback(null);
    });
  });
};

exports.orderTracks = function orderTracks(db, playlist, tracks, callback, onError) {
  // Return a copy of tracks, ordered by playlist's sort rules.
  // Tracks that wouldn't normally be in playlist are allowed.

  const track = db.getSchema().table('Track');
  const whereClause = track.id.in(tracks.map(t => t.id));

  execQuery(db, track, whereClause, playlist, callback, onError);
};
<|endoftext|>


<commit_before>var ics = require('./ics-builder');
var fs = require('fs')
  , filename = process.argv[2];
  fs.readFile(filename, 'utf8', function(err, data) {
      if (err) throw err;
      console.log(ics.buildICS(parse(data)));
  });

// Relevant regular expressions
var unknownValue = "N/A";
var _locationRegex = /\b[A-Z]{3,5}\s[A-Z0-9]{2,6}\b/;
var _courseRegex = /\b([A-Z]{3})([1-9]{1}[0-9]{3})([A-Z])?\s+(\b([A-Z.]+\s([A-Z.&]+\s?)+)\b)/;
var _courseRegexLookAhead = /(?=\b[A-Z]{3}[1-9]{1}[0-9]{3}[A-Z]?\b)/g;
var _timeRegexLookAhead = /(?=\b(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)\s+[0-9]{1,2}:[0-9]{2}\s+to\s+[0-9]{2}:[0-9]{2}\s+[A-Za-z]*\b)/g;
var _timeRegex = /\b(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)\s+([0-9]{1,2}):([0-9]{2})\s+to\s+([0-9]{2}):([0-9]{2})\s+([A-Za-z]*)\b/;
var _professorRegex = /\b[A-Za-z\-.]+\s(?:[A-Za-z\-.]+\s?)+\b/;
var _semesterRegex = /([0-9]{2}\s[A-Z]{1}[a-z]{2,8})\sto\s([0-9]{2}\s[A-Z]{1}[a-z]{2,8})\s([0-9]{4})/;

/**
 * Returns a timetable array consisting of class section objects which
 * are parsed and generated from the raw uOttawa timetable text.
 * @param {String} text Raw timetable text.
 * @return {Object[]} Returns an array of JSON objects for each parsed class
 *                    section.
 */
function parse(text) {
  var timetable = [];
  // Split lines by course code
  var courses = text.split(_courseRegexLookAhead);
  for (var i = 0; i < courses.length; i++) {
    var courseText = courses[i];
    var semester = _semesterRegex.exec(courseText);
    var course = _courseRegex.exec(courseText);
    if (!course || !semester) continue;
    // Split lines by class sections
    var classes = courseText.split(_timeRegexLookAhead);
    for (var j = 0; j < classes.length; j++) {
      var professor = _professorRegex.exec(classes[j]);
      var time = _timeRegex.exec(classes[j]);
      var where = _locationRegex.exec(classes[j]);
      if (!time) continue;
      var sem_start = new Date(semester[1] + ' ' + semester[3]);
      var sem_end = new Date(semester[2] + ' ' + semester[3]);
      // Build object for this section and push it to the timetable array
      timetable.push({
        'course_code_faculty': course[1],
        'course_code_number': course[2],
        'section': course[3] ? course[3] : '',
        'course_name': course[4],
        'professor': professor ? professor[0] : unknownValue,
        'semester_start':  {
          'year': sem_start.getFullYear(),
          'month': sem_start.getMonth(),
          'day': sem_start.getDate()
        },
        'semester_end': {
          'year': sem_end.getFullYear(),
          'month': sem_end.getMonth(),
          'day': sem_end.getDate()
        },
        'where': where ? where[0] : unknownValue,
        'day': time[1],
        'start_time': {
          'hour': parseInt(time[2], 10),
          'minute': parseInt(time[3], 10)
        },
        'end_time': {
          'hour': parseInt(time[4], 10),
          'minute': parseInt(time[5], 10)
        },
        'class_type': time[6]
      });
    }
  }
  return timetable;
}

module.exports.parse = parse;
<commit_msg>Add support for dashes in course names<commit_after>var ics = require('./ics-builder');
var fs = require('fs')
  , filename = process.argv[2];
  fs.readFile(filename, 'utf8', function(err, data) {
      if (err) throw err;
      console.log(ics.buildICS(parse(data)));
  });

// Relevant regular expressions
var unknownValue = "N/A";
var _locationRegex = /\b[A-Z]{3,5}\s[A-Z0-9]{2,6}\b/;
var _courseRegex = /\b([A-Z]{3})([1-9]{1}[0-9]{3})([A-Z])?\s+(\b([A-Z.-]+\s([A-Z.&-]+\s?)+)\b)/;
var _courseRegexLookAhead = /(?=\b[A-Z]{3}[1-9]{1}[0-9]{3}[A-Z]?\b)/g;
var _timeRegexLookAhead = /(?=\b(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)\s+[0-9]{1,2}:[0-9]{2}\s+to\s+[0-9]{2}:[0-9]{2}\s+[A-Za-z]*\b)/g;
var _timeRegex = /\b(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)\s+([0-9]{1,2}):([0-9]{2})\s+to\s+([0-9]{2}):([0-9]{2})\s+([A-Za-z]*)\b/;
var _professorRegex = /\b[A-Za-z\-.]+\s(?:[A-Za-z\-.]+\s?)+\b/;
var _semesterRegex = /([0-9]{2}\s[A-Z]{1}[a-z]{2,8})\sto\s([0-9]{2}\s[A-Z]{1}[a-z]{2,8})\s([0-9]{4})/;

/**
 * Returns a timetable array consisting of class section objects which
 * are parsed and generated from the raw uOttawa timetable text.
 * @param {String} text Raw timetable text.
 * @return {Object[]} Returns an array of JSON objects for each parsed class
 *                    section.
 */
function parse(text) {
  var timetable = [];
  // Split lines by course code
  var courses = text.split(_courseRegexLookAhead);
  for (var i = 0; i < courses.length; i++) {
    var courseText = courses[i];
    var semester = _semesterRegex.exec(courseText);
    var course = _courseRegex.exec(courseText);
    if (!course || !semester) continue;
    // Split lines by class sections
    var classes = courseText.split(_timeRegexLookAhead);
    for (var j = 0; j < classes.length; j++) {
      var professor = _professorRegex.exec(classes[j]);
      var time = _timeRegex.exec(classes[j]);
      var where = _locationRegex.exec(classes[j]);
      if (!time) continue;
      var sem_start = new Date(semester[1] + ' ' + semester[3]);
      var sem_end = new Date(semester[2] + ' ' + semester[3]);
      // Build object for this section and push it to the timetable array
      timetable.push({
        'course_code_faculty': course[1],
        'course_code_number': course[2],
        'section': course[3] ? course[3] : '',
        'course_name': course[4],
        'professor': professor ? professor[0] : unknownValue,
        'semester_start':  {
          'year': sem_start.getFullYear(),
          'month': sem_start.getMonth(),
          'day': sem_start.getDate()
        },
        'semester_end': {
          'year': sem_end.getFullYear(),
          'month': sem_end.getMonth(),
          'day': sem_end.getDate()
        },
        'where': where ? where[0] : unknownValue,
        'day': time[1],
        'start_time': {
          'hour': parseInt(time[2], 10),
          'minute': parseInt(time[3], 10)
        },
        'end_time': {
          'hour': parseInt(time[4], 10),
          'minute': parseInt(time[5], 10)
        },
        'class_type': time[6]
      });
    }
  }
  return timetable;
}

module.exports.parse = parse;
<|endoftext|>