Source: orderedMap.js

'use strict';

/**
 * Arraylike structure that preserves order while maintaining fast lookup by key. 
 * The order of the array is detirmined by a sorted array of z-indicies, which are *positive* integers including 0. 
 * Used for the Postcard rendering stack.
 * @constructor
 */
function OrderedMap() {

  this.map = {};

  /* the two internal arrays should always represent objects in the same order 
   * i.e. _karray[i] should ALWAYS be the same object as _zarray[i]
   */
  this._karray = [];      /* stores an array of keys */
  this._zarray = [];      /* stores an array of zindices */
}
/**
 * Asserts that key exists in the OrderedMap
 * @param {String} key
 * @returns {Error} if not found
 */
OrderedMap.prototype.assert = function(key) {
  if(!(key in this.map)) throw new Error('key: "' + key + '" does not exist');
};
/**
 * Number of objects in the OrderedMap
 * @returns {Number|Error} Length of the OrderedMap or Error if there's an unresolved issue.
 */
OrderedMap.prototype.length = function() {
  //if(this.map.length != this._karray.length || this.map.length != this._zarray.length) throw new Error('mismatched internals');
  return this._karray.length;
};
/**
 * Traverses the sorted z-index array and find the index where the new z-index value should be inserted.
 * @param {Number} zindex - The new z-index value to search with
 * @returns {Number}
 */
OrderedMap.prototype.findFirst = function(zindex) {

  /* we could do a binary search here, but lets keep it simple for now
   * search a sorted array and return the index at which to place the new value (zindex)
   */
  var i = 0;
  for(i; i < this._zarray.length; i++) {
    if(this._zarray[i] > zindex) return i;
  }
  return i;
};
/**
 * Adds a new object to the OrderedMap. If the key already exists, an Error is thrown.
 * @param {String} key - The unique identifier that can get/set the object 
 * @param {Number} zindex - The new z-index value to search with
 * @param value - Whatever needs to be stored.
 */
OrderedMap.prototype.add = function(key, zindex, value) { 
  if(!this.exists(key)) {
    if(zindex < 0) throw new Error('z-index can\'t be less than 0');
    var idx = this.findFirst(zindex);
    this._karray.splice(idx, 0, key);
    this._zarray.splice(idx, 0, zindex);
    this.map[key] = value;
  } else {
    throw new Error('key already exists');
  }
};
/**
 * Remove a object from the OrderedMap
 * @param {String} key
 */
OrderedMap.prototype.remove = function(key) {
  this.assert(key);
  var idx = this._karray.indexOf(key);
  this._karray.splice(idx, 1);
  this._zarray.splice(idx, 1);
  delete this.map[key];
};
/**
 * Asserts that key exists in the OrderedMap
 * @param {String} key
 * @returns {Boolean}
 */
OrderedMap.prototype.exists = function(key) {
  if(key in this.map) return true;
  return false;
};
/**
 * Get a object from the OrderedMap
 * @param {String} key
 * @returns The value stored or a new Error if the key does not exist
 */
OrderedMap.prototype.get = function(key) {
  this.assert(key);
  return this.map[key];
};
/**
 * Get a object's z-index from the OrderedMap
 * @param {String} key
 * @returns {Number|Error} The z-index or a new Error if the key does not exist
 */
OrderedMap.prototype.getZindex = function(key) {
  this.assert(key);
  var idx = this._karray.indexOf(key);
  return this._zarray[idx];
};
/**
 * Update the value of an object in the OrderedMap
 * @param {String} key
 * @param value - The new value 
 */
OrderedMap.prototype.updateValue = function(key, value) {
  this.assert(key);
  this.map[key] = value;
};
/**
 * Change the order of an object in the OrderedMap
 * @param {String} key
 * @param {Number} zindex 
 * @returns a new Error if the key does not exist or if the new z-index is < 0
 */
OrderedMap.prototype.changeOrder = function(key, zindex) {
  if(zindex < 0) throw new Error('z-index can\'t be less than 0');
  var value = this.get(key);
  this.remove(key);
  this.add(key, zindex, value);
};
/**
 * Bring an object to the start of the OrderMap, i.e. set the z-index to 0.
 * It will still appear after other objects which also have a z-index of 0, however.
 * [TODO] modify this functionality so it is guaranteed to be at the first index? 
 * @param {String} key
 * @returns a new Error if the key does not exist
 */
OrderedMap.prototype.toStart = function(key) {
  var value = this.get(key);
  this.remove(key);
  this._karray.splice(0, 0, key);
  this._zarray.splice(0, 0, 0);
  this.map[key] = value;  
};
/**
 * Bring an object to the end of the OrderMap. 
 * Its new z-index will be the same as the greatest z-index currently in the OrderedMap, appearing after that object.
 * @param {String} key
 * @returns a new Error if the key does not exist
 */
OrderedMap.prototype.toEnd = function(key) {
  var last = this._zarray[this._zarray.length-1];
  this.changeOrder(key,last);
};
/**
 * Traverse through the OrderedMap, starting from lowest z-index to highest z-index. 
 * @param {objectCallback} callback - The callback function for each individual object.
 */
OrderedMap.prototype.forEach = function(callback) {
  var key, value, zindex;
  for(var i = 0; i < this._karray.length; i++) {
      key = this._karray[i];
      zindex = this._zarray[i];
      value = this.map[key];

      /**
       * This is called for each object in the OrderedMap.
       * @callback OrderedMap~objectCallback
       * @param {String} key - The key of the object
       * @param {Number} zindex - The z-index of the object
       * @param value - The value of the object
       */    
      callback(key, zindex, value);
  }
};
/**
 * Creates an array with all elements that pass the test implemented by the provided function.
 * [TODO] Implement thisArg instead of apply-ing with null
 * @param {Function} callback - Function to test each element of the OrderedMap. Invoked with arguments (key, zindex, value). Return true to keep the object, false otherwise.
 */
OrderedMap.prototype.filter = function(callback) {
  var res = [];
  this.forEach(function (key, zindex, value) {
    if(callback.apply(null, [key, zindex, value])) {
      res.push(value);
    }
  });
  return res;
};