<commit_before>var vomnibar = (function() {
  var vomnibarUI = null;  // the dialog instance for this window
  var completers = { };

  function getCompleter(name) {
    if (!(name in completers))
      completers[name] = new BackgroundCompleter(name);
    return completers[name];
  }

  /*
   * Activate the Vomnibox.
   */
  function activate(completerName, refreshInterval, initialQueryValue) {
    var completer = getCompleter(completerName);
    if (!vomnibarUI)
      vomnibarUI = new VomnibarUI(10);
    completer.refresh();
    vomnibarUI.setCompleter(completer);
    vomnibarUI.setRefreshInterval(refreshInterval);
    if (initialQueryValue)
      vomnibarUI.setQuery(initialQueryValue);
    vomnibarUI.show();
    return vomnibarUI;
  }

  /** User interface for fuzzy completion */
  var VomnibarUI = Class.extend({
    init: function(maxResults) {
      this.prompt = '>';
      this.maxResults = maxResults;
      this.refreshInterval = 0;
      this.initDom();
    },

    setQuery: function(query) { this.input.value = query; },

    setCompleter: function(completer) {
      this.completer = completer;
      this.reset();
    },

    setRefreshInterval: function(refreshInterval) { this.refreshInterval = refreshInterval; },

    show: function() {
      this.box.style.display = "block";
      this.input.focus();
      handlerStack.push({ keydown: this.onKeydown.bind(this) });
    },

    hide: function() {
      this.box.style.display = "none";
      this.completionList.style.display = "none";
      this.input.blur();
      handlerStack.pop();
    },

    reset: function() {
      this.input.value = "";
      this.updateTimer = null;
      this.completions = [];
      this.selection = 0;
      this.update(true);
    },

    updateSelection: function() {
      if (this.completions.length > 0)
        this.selection = Math.min(this.selection, this.completions.length - 1);
      for (var i = 0; i < this.completionList.children.length; ++i)
        this.completionList.children[i].className = (i == this.selection) ? "selected" : "";
    },

    /*
     * Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress.
     * We support the arrow keys and other shortcuts for moving, so this method hides that complexity.
     */
    actionFromKeyEvent: function(event) {
      var key = getKeyChar(event);
      if (isEscape(event))
        return "dismiss";
      else if (key == "up" ||
          (event.shiftKey && event.keyCode == keyCodes.tab) ||
          (event.ctrlKey && (key == "k" || key == "p")))
        return "up";
      else if (key == "down" ||
        (event.keyCode == keyCodes.tab && !event.shiftKey) ||
        (event.ctrlKey && (key == "j" || key == "n")))
        return "down";
      else if (event.keyCode == keyCodes.enter)
        return "enter";
    },

    onKeydown: function(event) {
      var action = this.actionFromKeyEvent(event);
      if (!action) return true; // pass through

      if (action == "dismiss") {
        this.hide();
      }
      else if (action == "up") {
        if (this.selection > 0)
          this.selection -= 1;
        this.updateSelection();
      }
      else if (action == "down") {
        if (this.selection < this.completions.length - 1)
          this.selection += 1;
        this.updateSelection();
      }
      else if (action == "enter") {
        this.update(true, function() {
          // Shift+Enter will open the result in a new tab instead of the current tab.
          var openInNewTab = (event.shiftKey || isPrimaryModifierKey(event));
          this.completions[this.selection].performAction(openInNewTab);
          this.hide();
        }.proxy(this));
      }

      // It seems like we have to manually supress the event here and still return true.
      event.stopPropagation();
      event.preventDefault();
      return true;
    },

    updateCompletions: function(callback) {
      query = this.input.value.replace(/^\s*/, "");

      this.completer.filter(query, this.maxResults, function(completions) {
        this.completions = completions;
        this.populateUiWithCompletions(completions);
        if (callback) callback();
      }.proxy(this));
    },

    populateUiWithCompletions: function(completions) {
      // update completion list with the new data
      this.completionList.innerHTML = completions.map(function(completion) {
        return "
" + completion.html + "
";
      }).join('');

      this.completionList.style.display = (completions.length > 0) ? "block" : "none";
      this.updateSelection();
    },

    update: function(updateSynchronously, callback) {
      if (updateSynchronously) {
        // cancel scheduled update
        if (this.updateTimer !== null)
          window.clearTimeout(this.updateTimer);
        this.updateCompletions(callback);
      } else if (this.updateTimer !== null) {
        // an update is already scheduled, don't do anything
        return;
      } else {
        // always update asynchronously for better user experience and to take some load off the CPU
        // (not every keystroke will cause a dedicated update)
        this.updateTimer = setTimeout(function() {
          this.updateCompletions(callback);
          this.updateTimer = null;
        }.proxy(this), this.refreshInterval);
      }
    },

    initDom: function() {
      this.box = utils.createElementFromHtml(
        '
' +
          '
' +
            '<input type="text" />' +
          '
' +
          '

' +
        '
');
      this.box.style.display = 'none';
      document.body.appendChild(this.box);

      this.input = document.querySelector("#vomnibar input");
      this.input.addEventListener("input", function() { this.update(); }.bind(this));
      this.completionList = document.querySelector("#vomnibar ul");
      this.completionList.style.display = "none";
    }
  });

  /*
   * Sends filter and refresh requests to a Vomnibox completer on the background page.
   */
  var BackgroundCompleter = Class.extend({
    /* - name: The background page completer that you want to interface with. Either "omni" or "tabs". */
    init: function(name) {
      this.name = name;
      this.filterPort = chrome.extension.connect({ name: "filterCompleter" });
    },

    refresh: function() { chrome.extension.sendRequest({ handler: "refreshCompleter", name: this.name }); },

    filter: function(query, maxResults, callback) {
      var id = utils.createUniqueId();
      this.filterPort.onMessage.addListener(function(msg) {
        if (msg.id != id) return;
        // The result objects coming from the background page will be of the form:
        //   { html: "", action: { functionName: "", args: [] } }
        // functionName will be either "navigateToUrl" or "switchToTab". args will be a URL or a tab ID.
        var results = msg.results.map(function(result) {
          var functionToCall = completionActions[result.action.functionName];
          result.performAction = functionToCall.curry(result.action.args);
          return result;
        });
        callback(results);
      });
      this.filterPort.postMessage({ id: id, name: this.name, query: query, maxResults: maxResults });
    }
  });

  /*
   * These are the actions we can perform when the user selects a result in the Vomnibox.
   */
  var completionActions = {
    navigateToUrl: function(url, openInNewTab) {
      // If the URL is a bookmarklet prefixed with javascript:, we shouldn't open that in a new tab.
      if (url.indexOf("javascript:") == 0)
        openInNewTab = false;
      chrome.extension.sendRequest({
        handler: openInNewTab ? "openUrlInNewTab" : "openUrlInCurrentTab",
        url: url,
        selected: openInNewTab
      });
    },

    switchToTab: function(tabId) {
      chrome.extension.sendRequest({ handler: "selectSpecificTab", id: tabId });
    }
  };

  // public interface
  return {
    activate: function() { activate("omni", 100); },
    activateWithCurrentUrl: function() { activate("omni", 100, window.location.toString()); },
    activateTabSelection: function() { activate("tabs", 0); },
    /* Used by our vomnibar dev harness. */
    getUI: function() { return vomnibarUI; }
  }
})();
<commit_msg>Remove maxresults option, which isn't currently used.<commit_after>var vomnibar = (function() {
  var vomnibarUI = null;  // the dialog instance for this window
  var completers = { };

  function getCompleter(name) {
    if (!(name in completers))
      completers[name] = new BackgroundCompleter(name);
    return completers[name];
  }

  /*
   * Activate the Vomnibox.
   */
  function activate(completerName, refreshInterval, initialQueryValue) {
    var completer = getCompleter(completerName);
    if (!vomnibarUI)
      vomnibarUI = new VomnibarUI();
    completer.refresh();
    vomnibarUI.setCompleter(completer);
    vomnibarUI.setRefreshInterval(refreshInterval);
    if (initialQueryValue)
      vomnibarUI.setQuery(initialQueryValue);
    vomnibarUI.show();
    return vomnibarUI;
  }

  /** User interface for fuzzy completion */
  var VomnibarUI = Class.extend({
    init: function() {
      this.prompt = '>';
      this.refreshInterval = 0;
      this.initDom();
    },

    setQuery: function(query) { this.input.value = query; },

    setCompleter: function(completer) {
      this.completer = completer;
      this.reset();
    },

    setRefreshInterval: function(refreshInterval) { this.refreshInterval = refreshInterval; },

    show: function() {
      this.box.style.display = "block";
      this.input.focus();
      handlerStack.push({ keydown: this.onKeydown.bind(this) });
    },

    hide: function() {
      this.box.style.display = "none";
      this.completionList.style.display = "none";
      this.input.blur();
      handlerStack.pop();
    },

    reset: function() {
      this.input.value = "";
      this.updateTimer = null;
      this.completions = [];
      this.selection = 0;
      this.update(true);
    },

    updateSelection: function() {
      if (this.completions.length > 0)
        this.selection = Math.min(this.selection, this.completions.length - 1);
      for (var i = 0; i < this.completionList.children.length; ++i)
        this.completionList.children[i].className = (i == this.selection) ? "selected" : "";
    },

    /*
     * Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress.
     * We support the arrow keys and other shortcuts for moving, so this method hides that complexity.
     */
    actionFromKeyEvent: function(event) {
      var key = getKeyChar(event);
      if (isEscape(event))
        return "dismiss";
      else if (key == "up" ||
          (event.shiftKey && event.keyCode == keyCodes.tab) ||
          (event.ctrlKey && (key == "k" || key == "p")))
        return "up";
      else if (key == "down" ||
        (event.keyCode == keyCodes.tab && !event.shiftKey) ||
        (event.ctrlKey && (key == "j" || key == "n")))
        return "down";
      else if (event.keyCode == keyCodes.enter)
        return "enter";
    },

    onKeydown: function(event) {
      var action = this.actionFromKeyEvent(event);
      if (!action) return true; // pass through

      if (action == "dismiss") {
        this.hide();
      }
      else if (action == "up") {
        if (this.selection > 0)
          this.selection -= 1;
        this.updateSelection();
      }
      else if (action == "down") {
        if (this.selection < this.completions.length - 1)
          this.selection += 1;
        this.updateSelection();
      }
      else if (action == "enter") {
        this.update(true, function() {
          // Shift+Enter will open the result in a new tab instead of the current tab.
          var openInNewTab = (event.shiftKey || isPrimaryModifierKey(event));
          this.completions[this.selection].performAction(openInNewTab);
          this.hide();
        }.proxy(this));
      }

      // It seems like we have to manually supress the event here and still return true.
      event.stopPropagation();
      event.preventDefault();
      return true;
    },

    updateCompletions: function(callback) {
      query = this.input.value.replace(/^\s*/, "");

      this.completer.filter(query, function(completions) {
        this.completions = completions;
        this.populateUiWithCompletions(completions);
        if (callback) callback();
      }.proxy(this));
    },

    populateUiWithCompletions: function(completions) {
      // update completion list with the new data
      this.completionList.innerHTML = completions.map(function(completion) {
        return "
" + completion.html + "
";
      }).join('');

      this.completionList.style.display = (completions.length > 0) ? "block" : "none";
      this.updateSelection();
    },

    update: function(updateSynchronously, callback) {
      if (updateSynchronously) {
        // cancel scheduled update
        if (this.updateTimer !== null)
          window.clearTimeout(this.updateTimer);
        this.updateCompletions(callback);
      } else if (this.updateTimer !== null) {
        // an update is already scheduled, don't do anything
        return;
      } else {
        // always update asynchronously for better user experience and to take some load off the CPU
        // (not every keystroke will cause a dedicated update)
        this.updateTimer = setTimeout(function() {
          this.updateCompletions(callback);
          this.updateTimer = null;
        }.proxy(this), this.refreshInterval);
      }
    },

    initDom: function() {
      this.box = utils.createElementFromHtml(
        '
' +
          '
' +
            '<input type="text" />' +
          '
' +
          '

' +
        '
');
      this.box.style.display = 'none';
      document.body.appendChild(this.box);

      this.input = document.querySelector("#vomnibar input");
      this.input.addEventListener("input", function() { this.update(); }.bind(this));
      this.completionList = document.querySelector("#vomnibar ul");
      this.completionList.style.display = "none";
    }
  });

  /*
   * Sends filter and refresh requests to a Vomnibox completer on the background page.
   */
  var BackgroundCompleter = Class.extend({
    /* - name: The background page completer that you want to interface with. Either "omni" or "tabs". */
    init: function(name) {
      this.name = name;
      this.filterPort = chrome.extension.connect({ name: "filterCompleter" });
    },

    refresh: function() { chrome.extension.sendRequest({ handler: "refreshCompleter", name: this.name }); },

    filter: function(query, callback) {
      var id = utils.createUniqueId();
      this.filterPort.onMessage.addListener(function(msg) {
        if (msg.id != id) return;
        // The result objects coming from the background page will be of the form:
        //   { html: "", action: { functionName: "", args: [] } }
        // functionName will be either "navigateToUrl" or "switchToTab". args will be a URL or a tab ID.
        var results = msg.results.map(function(result) {
          var functionToCall = completionActions[result.action.functionName];
          result.performAction = functionToCall.curry(result.action.args);
          return result;
        });
        callback(results);
      });
      this.filterPort.postMessage({ id: id, name: this.name, query: query });
    }
  });

  /*
   * These are the actions we can perform when the user selects a result in the Vomnibox.
   */
  var completionActions = {
    navigateToUrl: function(url, openInNewTab) {
      // If the URL is a bookmarklet prefixed with javascript:, we shouldn't open that in a new tab.
      if (url.indexOf("javascript:") == 0)
        openInNewTab = false;
      chrome.extension.sendRequest({
        handler: openInNewTab ? "openUrlInNewTab" : "openUrlInCurrentTab",
        url: url,
        selected: openInNewTab
      });
    },

    switchToTab: function(tabId) {
      chrome.extension.sendRequest({ handler: "selectSpecificTab", id: tabId });
    }
  };

  // public interface
  return {
    activate: function() { activate("omni", 100); },
    activateWithCurrentUrl: function() { activate("omni", 100, window.location.toString()); },
    activateTabSelection: function() { activate("tabs", 0); },
    /* Used by our vomnibar dev harness. */
    getUI: function() { return vomnibarUI; }
  }
})();
<|endoftext|>


<commit_before>var tv = require('intervalometer/time-value.js');
var interpolate = require('intervalometer/interpolate.js');

var exp = {};
var local = {};
exp.status = {};
exp.config = {};

exp.init = function(minEv, maxEv, nightCompensation, highlightProtection) {
    if (nightCompensation === null) nightCompensation = 'auto';

    if(nightCompensation != 'auto') {
        nightCompensation = parseFloat(nightCompensation);
    }

    local = {
        lumArray: [],
        evArray: [],
        historyArray: [],
        highlightArray: [],
        rateHistoryArray: [],
        targetHighlights: null,
        targetLum: null,
        first: true
    };

    exp.status = {
        rampEv: null,
        highlights: null,
        rate: null,
        direction: null,
        highlightProtection: 0
    };

    exp.config = {
        sunrise: {
            p: 0.9,
            i: 0.4,
            d: 0.6,
            targetTimeSeconds: 300,
            evIntegrationSeconds: 300,
            historyIntegrationSeconds: 480,
            highlightIntegrationFrames: 3,
        },
        sunset: {
            p: 1.1,
            i: 0.6,
            d: 0.4,
            targetTimeSeconds: 480,
            evIntegrationSeconds: 480,
            historyIntegrationSeconds: 480,
            highlightIntegrationFrames: 5,
        },
        maxEv: maxEv,
        minEv: minEv,
        maxRate: 30,
        hysteresis: 0.4,
        nightCompensationDayEv: 10,
        nightCompensationNightEv: -2,
        nightCompensation: nightCompensation,
        highlightProtection: highlightProtection,
        highlightProtectionLimit: 1
    };

    return exp.config;
}

exp.calculate = function(algorithm, direction, currentEv, lastPhotoLum, lastPhotoHistogram, minEv, maxEv) {
    if(minEv != null) exp.config.minEv = minEv;
    if(maxEv != null) exp.config.maxEv = maxEv;
    lastPhotoHistogram = normalizeHistogram(lastPhotoHistogram);

    if(['auto', 'sunset', 'sunrise'].indexOf(direction) === -1) direction = 'auto';

    if(algorithm == "lrt") {
        return exp.calculate_LRTtimelapse(currentEv, direction, lastPhotoLum, lastPhotoHistogram, minEv, maxEv);
    } else {
        return exp.calculate_TLPAuto(currentEv, lastPhotoLum, lastPhotoHistogram, minEv, maxEv);
    }
}

exp.calculate_LRTtimelapse = function(currentEv, direction, lastPhotoLum, lastPhotoHistogram, minEv, maxEv) {
    var lum = 0;
    for(var i = 0; i < 256; i++) {
        lum += Math.pow(i, i / 256) / 256 * lastPhotoHistogram[i];
    }

    var lum1 = lum;

    lum -= (lum * getEvOffsetScale(currentEv, lastPhotoLum)) / 2; // apply night compensation

    var lum2 = lum;

    if(local.targetLum === null) {  // first time
        exp.status.rampEv = currentEv;
        local.lrtLumArray = [];
        local.targetLum = lum;

        if(direction == 'sunrise') {
            local.direction = 1;
        } else if(direction == 'sunset') {
            local.direction = -1;
        } else {
            local.direction = 0;
        }
    }

    local.lrtLumArray.push(lum);
    if(local.lrtLumArray.length > 5) {
        local.lrtLumArray.shift();
    }
    lum = (local.lrtLumArray.reduce(function(sum, l) { return sum + l; }, 0)) / local.lrtLumArray.length;

    var lum3 = lum;

    var directionFactor;

    directionFactor = local.direction >= 0 ? 1 : 4;
    if((direction == 'auto' || direction == 'sunrise') && lum > local.targetLum * (1 + 0.2 * directionFactor)) {
        exp.status.rampEv = currentEv + 1/3;
        local.direction = 1;
    }
    directionFactor = local.direction <= 0 ? 1 : 4;
    if((direction == 'auto' || direction == 'sunset') && lum < local.targetLum / (1 + 0.2 * directionFactor)) {
        exp.status.rampEv = currentEv -  1/3;
        local.direction = -1;
    }

    //console.log("LRT LumX:", lum1, lum2, lum3);

    console.log("LRT Lum:", lum, local.direction, local.targetLum, " currentEv:", currentEv, ", newEv:", exp.status.rampEv, ", Dir:", direction);

    return exp.status.rampEv;
}

exp.calculate_TLPAuto = function(currentEv, lastPhotoLum, lastPhotoHistogram, minEv, maxEv) {
    // measure the interval
    exp.status.intervalSeconds = 0;
    var thisPhotoTime = new Date();
    if (local.lastPhotoTime) {
        exp.status.intervalSeconds = (thisPhotoTime - local.lastPhotoTime) / 1000;
    }
    local.lastPhotoTime = thisPhotoTime;

    // get config based on rate up/down
    var config = exp.config.sunset;
    if (exp.status.direction && exp.status.direction > 0) {
        config = exp.config.sunrise;
    }

    // get the current rate of change
    exp.status.rate = calculateRate(currentEv, lastPhotoLum, config);
    console.log("rate: ", exp.status.rate);

    // don't change if within hysteresis setting
    if (Math.abs(exp.status.rate) < exp.config.hysteresis) exp.status.rate = 0;

    // limit to max rate
    if(exp.status.rate > exp.config.maxRate) exp.status.rate = exp.config.maxRate;
    if(exp.status.rate < -exp.config.maxRate) exp.status.rate = -exp.config.maxRate;

    // don't swing quickly past zero (stops oscillation)
    if (exp.status.rate > 3 && exp.status.direction < -0.5) exp.status.rate = 0;
    if (exp.status.rate < -3 && exp.status.direction > 0.5) exp.status.rate = 0;

    // adjust exposure according to rate in stops/hour
    exp.status.rampEv += (exp.status.rate / 3600) * exp.status.intervalSeconds;

    if(exp.config.highlightProtection) {
        var highlights = lastPhotoHistogram[255] + lastPhotoHistogram[254] / 2;

        // highlight protection
        local.highlightArray.unshift(highlights);
        local.highlightArray = local.highlightArray.slice(0, config.highlightIntegrationFrames);
        var tempArray = local.highlightArray.slice(0).sort(function (a, b) {  return a - b;  }).slice(local.highlightArray.length > 2 ? 1 : 0, local.highlightArray.length > 2 ? local.highlightArray.length - 1 : local.highlightArray.length);
        exp.status.highlights = tempArray.reduce(function(sum, val) { return sum + val}) / tempArray.length;
        if(local.targetHighlights === null) local.targetHighlights = Math.max(exp.status.highlights, 2);

        if(exp.status.highlights > local.targetHighlights * 2 && lastPhotoHistogram[255] > local.targetHighlights && exp.status.highlightProtection < exp.config.highlightProtectionLimit) {
            exp.status.highlightProtection += 0.333;
            exp.status.manualOffsetEv -= 0.333;
            exp.status.rampEv += 0.333;
        } else if(exp.status.highlights < local.targetHighlights / 2 && exp.status.highlightProtection > 0.3) {
            exp.status.highlightProtection -= 0.333;
            exp.status.manualOffsetEv += 0.333;
            exp.status.rampEv -= 0.333;
        }
        exp.status.highlightProtection = Math.round(exp.status.highlightProtection * 1000) / 1000;
    }

    console.log("status: ", exp.status);
    //console.log("local: ", local);

    return exp.status.rampEv;
}

// ******* Private Functions ******* //

function filteredMean(tvArr, trim) {
    if (!trim) trim = 1;
    return tv.mean(tv.insideArray(tvArr, trim));
}

function filteredSlope(tvArr, trim) {
    if (!trim) trim = 1;
    return tv.mean(tv.insideArray(tv.slopes(tvArr), trim));
}

function calculateRate(currentEv, lastPhotoLum, config) {
    var diff = calculateDelta(currentEv, lastPhotoLum, config);
    exp.status.targetEv = currentEv + diff;
    if (exp.status.targetEv < exp.config.minEv) exp.status.targetEv = exp.config.minEv;
    if (exp.status.targetEv > exp.config.maxEv) exp.status.targetEv = exp.config.maxEv;
    if (exp.status.rampEv === null) exp.status.rampEv = exp.status.targetEv;
    var delta = exp.status.targetEv - exp.status.rampEv;
    local.historyArray.push({
        val: delta,
        time: new Date()
    });
    local.historyArray = tv.purgeArray(local.historyArray, config.historyIntegrationSeconds);

    exp.status.pastError = tv.mean(local.historyArray);

    exp.status.pComponent = delta * config.p;
    exp.status.iComponent = exp.status.pastError * config.i;
    exp.status.dComponent = exp.status.evSlope * config.d;

    delta *= config.p;
    delta += exp.status.pastError * config.i;
    delta += exp.status.evSlope * config.d;

    var rate = delta * (3600 / config.targetTimeSeconds);

    local.rateHistoryArray.push({
        val: rate,
        time: new Date()
    });
    local.rateHistoryArray = tv.purgeArray(local.rateHistoryArray, config.historyIntegrationSeconds);
    exp.status.direction = tv.mean(local.rateHistoryArray);

    return rate;
}

function calculateDelta(currentEv, lastPhotoLum, config) {
    local.lumArray.push({
        val: lastPhotoLum,
        time: new Date()
    });

    local.evArray.push({
        val: currentEv + lastPhotoLum,
        time: new Date()
    });

    if (local.first) {
        var evScale = [{
            x: exp.config.nightCompensationNightEv,
            y: 1
        }, {
            x: exp.config.nightCompensationDayEv,
            y: 0
        }]
        var nightRatio = interpolate.linear(evScale, currentEv);

        exp.status.nightRefEv = lastPhotoLum * nightRatio + -1.5 * (1 - nightRatio);
        exp.status.dayRefEv = lastPhotoLum * (1 - nightRatio);
        exp.status.fixedRefEv = lastPhotoLum;
        exp.status.manualOffsetEv = lastPhotoLum - getEvOffsetScale(currentEv, lastPhotoLum);
        console.log("EXPOSURE: lastPhotoLum =", lastPhotoLum);
        console.log("EXPOSURE: exp.status.manualOffsetEv =", exp.status.manualOffsetEv);
        console.log("EXPOSURE: getEvOffsetScale(currentEv, lastPhotoLum) =", getEvOffsetScale(currentEv, lastPhotoLum));
        exp.status.offsetEv = getEvOffsetScale(currentEv, lastPhotoLum) + exp.status.manualOffsetEv;
        console.log("EXPOSURE: exp.status.offsetEv =", exp.status.offsetEv);
        console.log("EXPOSURE: lastPhotoLum + exp.status.offsetEv =", lastPhotoLum - exp.status.offsetEv);
        local.first = false;
    }

    local.lumArray = tv.purgeArray(local.lumArray, config.evIntegrationSeconds);
    local.evArray = tv.purgeArray(local.evArray, config.evIntegrationSeconds);

    var trim = 1;
    if (exp.status.intervalSeconds) {
        trim = Math.ceil((config.evIntegrationSeconds / exp.status.intervalSeconds) * 0.2); // trim outer 20% from sorted array
    }

    exp.status.evMean = filteredMean(local.lumArray, trim);
    exp.status.evSlope = filteredSlope(local.evArray, trim) * config.targetTimeSeconds;

    exp.status.offsetEv = getEvOffsetScale(currentEv, lastPhotoLum) + exp.status.manualOffsetEv;
    return lastPhotoLum - exp.status.offsetEv;
}

function getEvOffsetScale(ev, lastPhotoLum, noAuto) {
    var evScale
    if(exp.config.nightCompensation == 'auto') {
        if(noAuto) { // for LRT algorithm
            evScale = [{
                ev: exp.config.nightCompensationNightEv,
                offset: -1.333333
            }, {
                ev: exp.config.nightCompensationDayEv,
                offset: 0
            }]
        } else { // auto calculate night exposure
            evScale = [{
                ev: exp.config.nightCompensationNightEv,
                offset: -((lastPhotoLum - exp.status.nightRefEv) - -1.5) // -1.5
            }, {
                ev: exp.config.nightCompensationDayEv,
                offset: -((lastPhotoLum - exp.status.dayRefEv) - 0.0)
            }]
        }
    } else {
        evScale = [{
            ev: exp.config.nightCompensationNightEv,
            offset: exp.status.fixedRefEv - (parseFloat(exp.config.nightCompensation) || 0.0)
        }, {
            ev: exp.config.nightCompensationDayEv,
            offset: exp.status.fixedRefEv - 0
        }]
    }

    var values = evScale.map(function(item) {
        return {
            x: item.ev,
            y: item.offset
        }
    });
    return interpolate.linear(values, ev);
}

function normalizeHistogram(histogramArray) {
    var lum = 0, sum = 0;
    for(var i = 0; i < 256; i++) {
        sum += histogramArray[i];
    }
    sum /= 256;
    for(var i = 0; i < 256; i++) {
        histogramArray[i] = histogramArray[i] / sum;
    }
    return histogramArray;
}

module.exports = exp;<commit_msg>bugfixes<commit_after>var tv = require('intervalometer/time-value.js');
var interpolate = require('intervalometer/interpolate.js');

var exp = {};
var local = {};
exp.status = {};
exp.config = {};

exp.init = function(minEv, maxEv, nightCompensation, highlightProtection) {
    if (nightCompensation === null) nightCompensation = 'auto';

    if(nightCompensation != 'auto') {
        nightCompensation = parseFloat(nightCompensation);
    }

    local = {
        lumArray: [],
        evArray: [],
        historyArray: [],
        highlightArray: [],
        rateHistoryArray: [],
        targetHighlights: null,
        targetLum: null,
        first: true
    };

    exp.status = {
        rampEv: null,
        highlights: null,
        rate: null,
        direction: null,
        highlightProtection: 0
    };

    exp.config = {
        sunrise: {
            p: 0.9,
            i: 0.4,
            d: 0.6,
            targetTimeSeconds: 300,
            evIntegrationSeconds: 300,
            historyIntegrationSeconds: 480,
            highlightIntegrationFrames: 3,
        },
        sunset: {
            p: 1.1,
            i: 0.6,
            d: 0.4,
            targetTimeSeconds: 480,
            evIntegrationSeconds: 480,
            historyIntegrationSeconds: 480,
            highlightIntegrationFrames: 5,
        },
        maxEv: maxEv,
        minEv: minEv,
        maxRate: 30,
        hysteresis: 0.4,
        nightCompensationDayEv: 10,
        nightCompensationNightEv: -2,
        nightCompensation: nightCompensation,
        highlightProtection: highlightProtection,
        highlightProtectionLimit: 1
    };

    return exp.config;
}

exp.calculate = function(algorithm, direction, currentEv, lastPhotoLum, lastPhotoHistogram, minEv, maxEv) {
    if(minEv != null) exp.config.minEv = minEv;
    if(maxEv != null) exp.config.maxEv = maxEv;
    lastPhotoHistogram = normalizeHistogram(lastPhotoHistogram);

    if(['auto', 'sunset', 'sunrise'].indexOf(direction) === -1) direction = 'auto';

    if(algorithm == "lrt") {
        return exp.calculate_LRTtimelapse(currentEv, direction, lastPhotoLum, lastPhotoHistogram, minEv, maxEv);
    } else {
        return exp.calculate_TLPAuto(currentEv, lastPhotoLum, lastPhotoHistogram, minEv, maxEv);
    }
}

exp.calculate_LRTtimelapse = function(currentEv, direction, lastPhotoLum, lastPhotoHistogram, minEv, maxEv) {
    var lum = 0;
    for(var i = 0; i < 256; i++) {
        lum += Math.pow(i, i / 256) / 256 * lastPhotoHistogram[i];
    }

    var lum1 = lum;

    lum -= (lum * getEvOffsetScale(currentEv, lastPhotoLum)) / 2; // apply night compensation

    var lum2 = lum;

    if(local.targetLum === null) {  // first time
        exp.status.rampEv = currentEv;
        local.lrtLumArray = [];
        local.targetLum = lum;

        if(direction == 'sunrise') {
            local.direction = 1;
        } else if(direction == 'sunset') {
            local.direction = -1;
        } else {
            local.direction = 0;
        }
    }

    local.lrtLumArray.push(lum);
    if(local.lrtLumArray.length > 5) {
        local.lrtLumArray.shift();
    }
    lum = (local.lrtLumArray.reduce(function(sum, l) { return sum + l; }, 0)) / local.lrtLumArray.length;

    var lum3 = lum;

    var directionFactor;

    directionFactor = local.direction >= 0 ? 1 : 4;
    if((direction == 'auto' || direction == 'sunrise') && lum > local.targetLum * (1 + 0.2 * directionFactor)) {
        exp.status.rampEv = currentEv + 1/3;
        local.direction = 1;
    }
    directionFactor = local.direction <= 0 ? 1 : 4;
    if((direction == 'auto' || direction == 'sunset') && lum < local.targetLum / (1 + 0.2 * directionFactor)) {
        exp.status.rampEv = currentEv -  1/3;
        local.direction = -1;
    }

    //console.log("LRT LumX:", lum1, lum2, lum3);

    console.log("LRT Lum:", lum, local.direction, local.targetLum, " currentEv:", currentEv, ", newEv:", exp.status.rampEv, ", Dir:", direction);

    return exp.status.rampEv;
}

exp.calculate_TLPAuto = function(currentEv, lastPhotoLum, lastPhotoHistogram, minEv, maxEv) {
    // measure the interval
    exp.status.intervalSeconds = 0;
    var thisPhotoTime = new Date();
    if (local.lastPhotoTime) {
        exp.status.intervalSeconds = (thisPhotoTime - local.lastPhotoTime) / 1000;
    }
    local.lastPhotoTime = thisPhotoTime;

    // get config based on rate up/down
    var config = exp.config.sunset;
    if (exp.status.direction && exp.status.direction > 0) {
        config = exp.config.sunrise;
    }

    // get the current rate of change
    exp.status.rate = calculateRate(currentEv, lastPhotoLum, config);
    console.log("rate: ", exp.status.rate);

    // don't change if within hysteresis setting
    if (Math.abs(exp.status.rate) < exp.config.hysteresis) exp.status.rate = 0;

    // limit to max rate
    if(exp.status.rate > exp.config.maxRate) exp.status.rate = exp.config.maxRate;
    if(exp.status.rate < -exp.config.maxRate) exp.status.rate = -exp.config.maxRate;

    // don't swing quickly past zero (stops oscillation)
    if (exp.status.rate > 3 && exp.status.direction < -0.5) exp.status.rate = 0;
    if (exp.status.rate < -3 && exp.status.direction > 0.5) exp.status.rate = 0;

    // adjust exposure according to rate in stops/hour
    exp.status.rampEv += (exp.status.rate / 3600) * exp.status.intervalSeconds;

    if(exp.config.highlightProtection) {
        var highlights = lastPhotoHistogram[255] + lastPhotoHistogram[254] / 2;

        // highlight protection
        local.highlightArray.unshift(highlights);
        local.highlightArray = local.highlightArray.slice(0, config.highlightIntegrationFrames);
        var tempArray = local.highlightArray.slice(0).sort(function (a, b) {  return a - b;  }).slice(local.highlightArray.length > 2 ? 1 : 0, local.highlightArray.length > 2 ? local.highlightArray.length - 1 : local.highlightArray.length);
        exp.status.highlights = tempArray.reduce(function(sum, val) { return sum + val}) / tempArray.length;
        if(local.targetHighlights === null) local.targetHighlights = Math.max(exp.status.highlights, 2);

        if(exp.status.highlights > local.targetHighlights * 2 && lastPhotoHistogram[255] > local.targetHighlights && exp.status.highlightProtection < exp.config.highlightProtectionLimit) {
            exp.status.highlightProtection += 0.333;
            exp.status.manualOffsetEv -= 0.333;
            exp.status.rampEv += 0.333;
        } else if(exp.status.highlights < local.targetHighlights / 2 && exp.status.highlightProtection > 0.3) {
            exp.status.highlightProtection -= 0.333;
            exp.status.manualOffsetEv += 0.333;
            exp.status.rampEv -= 0.333;
        }
        exp.status.highlightProtection = Math.round(exp.status.highlightProtection * 1000) / 1000;
    }

    console.log("status: ", exp.status);
    //console.log("local: ", local);

    return exp.status.rampEv;
}

// ******* Private Functions ******* //

function filteredMean(tvArr, trim) {
    if (!trim) trim = 1;
    return tv.mean(tv.insideArray(tvArr, trim));
}

function filteredSlope(tvArr, trim) {
    if (!trim) trim = 1;
    return tv.mean(tv.insideArray(tv.slopes(tvArr), trim));
}

function calculateRate(currentEv, lastPhotoLum, config) {
    var diff = calculateDelta(currentEv, lastPhotoLum, config);
    exp.status.targetEv = currentEv + diff;
    if (exp.status.targetEv < exp.config.minEv) exp.status.targetEv = exp.config.minEv;
    if (exp.status.targetEv > exp.config.maxEv) exp.status.targetEv = exp.config.maxEv;
    if (exp.status.rampEv === null) exp.status.rampEv = exp.status.targetEv;
    var delta = exp.status.targetEv - exp.status.rampEv;
    local.historyArray.push({
        val: delta,
        time: new Date()
    });
    local.historyArray = tv.purgeArray(local.historyArray, config.historyIntegrationSeconds);

    exp.status.pastError = tv.mean(local.historyArray);

    exp.status.pComponent = delta * config.p;
    exp.status.iComponent = exp.status.pastError * config.i;
    exp.status.dComponent = exp.status.evSlope * config.d;

    delta *= config.p;
    delta += exp.status.pastError * config.i;
    delta += exp.status.evSlope * config.d;

    var rate = delta * (3600 / config.targetTimeSeconds);

    local.rateHistoryArray.push({
        val: rate,
        time: new Date()
    });
    local.rateHistoryArray = tv.purgeArray(local.rateHistoryArray, config.historyIntegrationSeconds);
    exp.status.direction = tv.mean(local.rateHistoryArray);

    return rate;
}

function calculateDelta(currentEv, lastPhotoLum, config) {
    local.lumArray.push({
        val: lastPhotoLum,
        time: new Date()
    });

    local.evArray.push({
        val: currentEv + lastPhotoLum,
        time: new Date()
    });

    if (local.first) {
        var evScale = [{
            x: exp.config.nightCompensationNightEv,
            y: 1
        }, {
            x: exp.config.nightCompensationDayEv,
            y: 0
        }]
        var nightRatio = interpolate.linear(evScale, currentEv);

        exp.status.nightRefEv = lastPhotoLum * nightRatio + -1.5 * (1 - nightRatio);
        exp.status.dayRefEv = lastPhotoLum * (1 - nightRatio);
        exp.status.fixedRefEv = lastPhotoLum;
        exp.status.manualOffsetEv = getEvOffsetScale(currentEv, lastPhotoLum);
        console.log("EXPOSURE: lastPhotoLum =", lastPhotoLum);
        console.log("EXPOSURE: exp.status.manualOffsetEv =", exp.status.manualOffsetEv);
        console.log("EXPOSURE: getEvOffsetScale(currentEv, lastPhotoLum) =", getEvOffsetScale(currentEv, lastPhotoLum));
        exp.status.offsetEv = getEvOffsetScale(currentEv, lastPhotoLum) + exp.status.manualOffsetEv;
        console.log("EXPOSURE: exp.status.offsetEv =", exp.status.offsetEv);
        console.log("EXPOSURE: lastPhotoLum - exp.status.offsetEv =", lastPhotoLum - exp.status.offsetEv);
        local.first = false;
    }

    local.lumArray = tv.purgeArray(local.lumArray, config.evIntegrationSeconds);
    local.evArray = tv.purgeArray(local.evArray, config.evIntegrationSeconds);

    var trim = 1;
    if (exp.status.intervalSeconds) {
        trim = Math.ceil((config.evIntegrationSeconds / exp.status.intervalSeconds) * 0.2); // trim outer 20% from sorted array
    }

    exp.status.evMean = filteredMean(local.lumArray, trim);
    exp.status.evSlope = filteredSlope(local.evArray, trim) * config.targetTimeSeconds;

    exp.status.offsetEv = getEvOffsetScale(currentEv, lastPhotoLum) + exp.status.manualOffsetEv;
    return lastPhotoLum - exp.status.offsetEv;
}

function getEvOffsetScale(ev, lastPhotoLum, noAuto) {
    var evScale
    if(exp.config.nightCompensation == 'auto') {
        if(noAuto) { // for LRT algorithm
            evScale = [{
                ev: exp.config.nightCompensationNightEv,
                offset: -1.333333
            }, {
                ev: exp.config.nightCompensationDayEv,
                offset: 0
            }]
        } else { // auto calculate night exposure
            evScale = [{
                ev: exp.config.nightCompensationNightEv,
                offset: -(exp.status.nightRefEv - -1.5) // -1.5
            }, {
                ev: exp.config.nightCompensationDayEv,
                offset: -(exp.status.dayRefEv - 0.0)
            }]
        }
    } else {
        evScale = [{
            ev: exp.config.nightCompensationNightEv,
            offset: exp.status.fixedRefEv - (parseFloat(exp.config.nightCompensation) || 0.0)
        }, {
            ev: exp.config.nightCompensationDayEv,
            offset: exp.status.fixedRefEv - 0
        }]
    }

    var values = evScale.map(function(item) {
        return {
            x: item.ev,
            y: item.offset
        }
    });
    return interpolate.linear(values, ev);
}

function normalizeHistogram(histogramArray) {
    var lum = 0, sum = 0;
    for(var i = 0; i < 256; i++) {
        sum += histogramArray[i];
    }
    sum /= 256;
    for(var i = 0; i < 256; i++) {
        histogramArray[i] = histogramArray[i] / sum;
    }
    return histogramArray;
}

module.exports = exp;<|endoftext|>


<commit_before>/* eslint-disable no-var */
var entryModel = require('./entry')
var dropOne = require('./lib/dropOne')
var forward = require('./lib/forward')
var forwardOne = require('./lib/forwardOne')

var forwardEntry = forwardOne(entryModel, function (entry, ev) {
  return entry._id === ev.data.entryId
})

exports.forward = forward({
  location_entry_changed: forwardEntry,
  location_entry_comment_created: forwardEntry,
  location_entry_comment_changed: forwardEntry,
  location_entry_comment_removed: forwardEntry,

  location_entry_created: function (entries, ev) {
    entries.unshift(ev.data.entry)
  },

  location_entry_removed: function (entries, ev) {
    dropOne(entries, function (entry) {
      return entry._id === ev.data.entryId
    })
  }
})

exports.getImageEntries = function (entries) {
  // Return entries that have one or more image attachments.
  //
  var i, j, att
  var imgEnts = []
  var HEAD = 6

  for (i = 0; i < entries.length; i += 1) {
    for (j = 0; j < entries[i].attachments.length; j += 1) {
      att = entries[i].attachments[j]
      if (att.mimetype.substr(0, HEAD) === 'image/') {
        imgEnts.push(entries[i])
        break // break inner loop
      }
    }
  }

  return imgEnts
}
<commit_msg>models.entries.getAttachments as array<commit_after>/* eslint-disable no-var */
var entryModel = require('./entry')
var dropOne = require('./lib/dropOne')
var forward = require('./lib/forward')
var forwardOne = require('./lib/forwardOne')

var forwardEntry = forwardOne(entryModel, function (entry, ev) {
  return entry._id === ev.data.entryId
})

exports.forward = forward({
  location_entry_changed: forwardEntry,
  location_entry_comment_created: forwardEntry,
  location_entry_comment_changed: forwardEntry,
  location_entry_comment_removed: forwardEntry,

  location_entry_created: function (entries, ev) {
    entries.unshift(ev.data.entry)
  },

  location_entry_removed: function (entries, ev) {
    dropOne(entries, function (entry) {
      return entry._id === ev.data.entryId
    })
  }
})

exports.getAttachments = function (entries) {
  // Return all attachments as array
  //
  var i
  var atts = []

  for (i = 0; i < entries.length; i += 1) {
    atts = atts.concat(entries[i].attachments)
  }

  return atts
}

exports.getImageEntries = function (entries) {
  // Return entries that have one or more image attachments.
  //
  var i, j, att
  var imgEnts = []
  var HEAD = 6

  for (i = 0; i < entries.length; i += 1) {
    for (j = 0; j < entries[i].attachments.length; j += 1) {
      att = entries[i].attachments[j]
      if (att.mimetype.substr(0, HEAD) === 'image/') {
        imgEnts.push(entries[i])
        break // break inner loop
      }
    }
  }

  return imgEnts
}
<|endoftext|>


<commit_before>/*globals chrome,fdom:true,console,Promise*/
/*jslint indent:2,white:true,sloppy:true */
/**
 * A freedom.js interface to Chrome sockets
 * @constructor
 * @private
 * @param {fdom.Port} channel the module creating this provider.
 * @param {Function} dispatchEvent Method for emitting events.
 * @param {number?} id A pre-existing socket Id for the socket.
 */
var Socket_chrome = function(channel, dispatchEvent, id) {
  this.dispatchEvent = dispatchEvent;
  this.id = id || undefined;
  this.namespace = chrome.sockets.tcp;
  this.prepareSecureCalled = false;
  if (this.id) {
    Socket_chrome.addActive(this.id, this);
    chrome.sockets.tcp.setPaused(this.id, false, function() {
      console.log('Unpaused receiver');
    });
  }
};

/**
 * A static list of active sockets, so that global on-receive messages
 * from chrome can be routed properly.
 * @static
 * @private
 * @type {Object.<number,Socket_chrome>}
 */
Socket_chrome.active = {};

/**
 * Get Information about the socket.
 * @method getInfo
 * @param {Function} continuation Function to call with information.
 * @return {Object} connection and address information about the socket.
 */
Socket_chrome.prototype.getInfo = function(continuation) {
  if (this.id) {
    // Note: this.namespace used, since this method is common to tcp and
    // tcpServer sockets.
    this.namespace.getInfo(this.id, continuation);
  } else {
    continuation({
      connected: false
    });
  }
};

/**
 * Connect to a designated location and begin reading.
 * @method connect
 * @param {String} hostname The host or ip to connect to.
 * @param {number} port The port to connect on.
 * @param {Function} cb Function to call with completion or error.
 */
Socket_chrome.prototype.connect = function(hostname, port, cb) {
  if (this.id) {
    cb(undefined, {
      'errcode': 'ALREADY_CONNECTED',
      'message': 'Cannot Connect Existing Socket'
    });
    return;
  }
  chrome.sockets.tcp.create({}, function(createInfo) {
    this.id = createInfo.socketId;
    chrome.sockets.tcp.connect(this.id, hostname, port, function (result) {
      if (result < 0) {
        cb(undefined, {
          'errcode': 'CONNECTION_FAILED',
          'message': 'Chrome Connection Failed: ' +
              Socket_chrome.ERROR_MAP[result]
        });
      } else {
        Socket_chrome.addActive(this.id, this);
        cb();
      }
    }.bind(this));
  }.bind(this));
};

/**
 * Secure an already connected socket.
 * @method secure
 * @param {Function} cb Function to call with completion or error.
 */
Socket_chrome.prototype.secure = function(cb) {
  if (!this.id) {
    cb(undefined, {
      'errcode': 'NOT_CONNECTED',
      'message': 'Cannot secure a disconnected socket'
    });
    return;
  } else if (!this.prepareSecureCalled) {
    cb(undefined, {
      'errcode': 'CONNECTION_FAILED',
      'message': 'prepareSecure not called before secure'
    });
    return;
  } else if (!chrome.sockets.tcp.secure) {
    cb(undefined, {
      'errcode': 'CONNECTION_FAILED',
      'message': 'Secure method not available'
    });
    return;
  }
  chrome.sockets.tcp.secure(this.id, {}, function(secureResult) {
    // Always unpause the socket, regardless of whether .secure succeeds.
    this.unpause().then(function() {
      if (secureResult !== 0) {
        cb(undefined, {
          'errcode': 'CONNECTION_FAILED',
          'message': 'Secure failed: ' + secureResult + ': ' +
              Socket_chrome.ERROR_MAP[secureResult]
        });
        return;
      }
      cb();
    }.bind(this), function(e) {
      cb(undefined, {
        'errcode': 'CONNECTION_FAILED',
        'message': 'Secure failed: error unpausing socket'
      });
    });
  }.bind(this));
};

/**
 * Prepares a socket for becoming secure after the next read event.
 * See details at
 * https://github.com/freedomjs/freedom/wiki/prepareSecure-API-Usage
 * @method prepareSecure
 * @param {Function} cb Function to call with completion or error.
 */
Socket_chrome.prototype.prepareSecure = function(cb) {
  if (!this.id) {
    cb(undefined, {
      'errcode': 'NOT_CONNECTED',
      'message': 'Cannot prepareSecure a disconnected socket'
    });
    return;
  }
  this.pause().then(
    function() {
      this.prepareSecureCalled = true;
      cb();
    }.bind(this), function(e) {
    cb(undefined, {
      'errcode': 'CONNECTION_FAILED',
      'message': 'prepareSecure failed: error pausing socket'
    });
  });
};

/**
 * Pauses the socket
 * @method pause
 * @private
 * @returns {Promise} Promise to be fulfilled on pause.
 */
Socket_chrome.prototype.pause = function() {
  if (!this.id) {
    return Promise.reject('Cannot pause disconnected socket');
  }
  return new Promise(function(fulfill, reject) {
    chrome.sockets.tcp.setPaused(this.id, true, fulfill);
  }.bind(this));
};

/**
 * Unpauses the socket
 * @method unpause
 * @private
 * @returns {Promise} Promise to be fulfilled on unpause.
 */
Socket_chrome.prototype.unpause = function() {
  if (!this.id) {
    return Promise.reject('Cannot unpause disconnected socket');
  }
  return new Promise(function(fulfill, reject) {
    chrome.sockets.tcp.setPaused(this.id, false, fulfill);
  }.bind(this));
};

/**
 * Write data to the socket.
 * @method write
 * @param {ArrayBuffer} data The data to write
 * @param {Function} cb Function to call when data is written
 */
Socket_chrome.prototype.write = function(data, cb) {
  if (!this.id) {
    cb(undefined, {
      'errcode': 'NOT_CONNECTED',
      'message': 'Cannot Write on Closed Socket'
    });
    return;
  }

  chrome.sockets.tcp.send(this.id, data, function(sendInfo) {
    if (sendInfo.resultCode < 0) {
      this.dispatchDisconnect(sendInfo.resultCode);
      return cb(undefined, {
        'errcode': 'UNKNOWN',
        'message': 'Send Error: ' + sendInfo.resultCode + ': ' + Socket_chrome.errorStringOfCode(sendInfo.resultCode),
      });
    } else if (sendInfo.bytesSent !== data.byteLength) {
      this.dispatchDisconnect('UNKNOWN');
      return cb(undefined, {
        'errcode': 'UNKNOWN',
        'message': 'Write Partially completed.'
      });
    }
    cb();
  }.bind(this));
};

Socket_chrome.ERROR_MAP = {
  // Error codes are at:
  // https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
  '-1': 'IO_PENDING',
  '-2': 'FAILED',
  '-3': 'ABORTED',
  '-4': 'INVALID_ARGUMENT',
  '-5': 'INVALID_HANDLE',
  '-7': 'TIMED_OUT',
  '-13': 'OUT_OF_MEMORY',
  '-15': 'SOCKET_NOT_CONNECTED',
  '-21': 'NETWORK_CHANGED',
  '-23': 'SOCKET_IS_CONNECTED',
  '-100': 'CONNECTION_CLOSED',
  '-101': 'CONNECTION_RESET',
  '-102': 'CONNECTION_REFUSED',
  '-103': 'CONNECTION_ABORTED',
  '-104': 'CONNECTION_FAILED',
  '-105': 'NAME_NOT_RESOLVED',
  '-106': 'INTERNET_DISCONNECTED',
  '-107': 'SSL_PROTOCOL_ERROR',
  '-200': 'CERT_COMMON_NAME_INVALID',
   // See Cordova Plugin socket.js
  '-1000': 'GENERIC_CORDOVA_FAILURE'
};

/**
 * Get the error code associated with a chrome.socket error code.
 * @method errorStringOfCode
 * @static
 * @private
 * @param {Number} code The error number as described by chrome
 * @returns {String} The error code as defined in the freedom.js interface.
 */
Socket_chrome.errorStringOfCode = function(code) {
  return Socket_chrome.ERROR_MAP[String(code)] ||
      'UNKNOWN';
};

/*
 * Alert a consumer that a socket is disconnected, with appropriate
 * error message.
 * @method dispatchDisconnect
 * @private
 * @param {Number} code the code returned by chrome when the socket closed.
 */
Socket_chrome.prototype.dispatchDisconnect = function (code) {
  if (!this.id) {
    // Don't send more than one dispatchDisconnect event.
    return;
  }

  Socket_chrome.removeActive(this.id);
  delete this.id;

  if (code === 0) {
    this.dispatchEvent('onDisconnect', {
      errcode: 'SUCCESS',
      message: 'Socket closed by call to close'
    });
  } else if(code === -100) {
    this.dispatchEvent('onDisconnect', {
      errcode: 'CONNECTION_CLOSED',
      message: 'Connection closed gracefully'
    });
  } else {
    this.dispatchEvent('onDisconnect', {
      errcode: 'UNKOWN',
      message: 'Socket Error: ' + code + ': ' + Socket_chrome.errorStringOfCode(code)
    });
  }
};

/**
 * Mark a socket as active, so that dispatched events can be routed to it.
 * This method is needed because chrome.sockets exposes a single event handler
 * for incoming data across all sockets.
 * @method addActive
 * @static
 * @param {number} id The socketId as provided by chrome.sockets
 * @param {Socket_chrome} socket The socket class associated with the id.
 */
Socket_chrome.addActive = function(id, socket) {
  if (Object.keys(Socket_chrome.active).length === 0) {
    if (chrome.sockets.tcp) {
      chrome.sockets.tcp.onReceive.addListener(Socket_chrome.handleReadData);
      chrome.sockets.tcp.onReceiveError.addListener(
          Socket_chrome.handleReadError);
    }
    if (chrome.sockets.tcpServer) {
      chrome.sockets.tcpServer.onAccept.addListener(
          Socket_chrome.handleAccept);
      chrome.sockets.tcpServer.onAcceptError.addListener(
          Socket_chrome.handleAcceptError);
    }
  }
  Socket_chrome.active[id] = socket;
};

/**
 * Unmark a socket as active, and clean up event handlers if needed.
 * @method removeActive
 * @static
 * @param {number} id The socketId to no longer watch.
 */
Socket_chrome.removeActive = function(id) {
  delete Socket_chrome.active[id];
  if (Object.keys(Socket_chrome.active).length === 0) {
    if (chrome.sockets.tcp) {
      chrome.sockets.tcp.onReceive.removeListener(
          Socket_chrome.handleReadData);
      chrome.sockets.tcp.onReceiveError.removeListener(
          Socket_chrome.handleReadError);
    }
    if (chrome.sockets.tcpServer) {
      chrome.sockets.tcpServer.onAccept.removeListener(
          Socket_chrome.handleAccept);
      chrome.sockets.tcpServer.onAcceptError.removeListener(
          Socket_chrome.handleAcceptError);
    }
  }
};

/**
 * React to read data.
 * @method handleReadData
 * @private
 * @param {ReadInfo} readInfo The value returned by onReceive.
 * @static
 */
Socket_chrome.handleReadData = function (readInfo) {
  if (!Socket_chrome.active[readInfo.socketId]) {
    console.warn('Dropped Read: ', readInfo);
    return;
  }
  Socket_chrome.active[readInfo.socketId].dispatchEvent('onData', {data: readInfo.data});
};

/**
 * React to read errors.
 * @method handleReadError
 * @private
 * @param {readInfo} readInfo The value returned by onReceiveError.
 * @static
 */
Socket_chrome.handleReadError = function (readInfo) {
  if (!Socket_chrome.active[readInfo.socketId]) {
    console.warn('Dropped Read Error: ', readInfo);
    return;
  }
  Socket_chrome.active[readInfo.socketId].dispatchDisconnect(readInfo.resultCode);
};

/**
 * React to client accepts.
 * @method handleAccept
 * @private
 * @param {acceptInfo} acceptInfo The value returned by onAccept.
 * @static
 */
Socket_chrome.handleAccept = function (acceptInfo) {
  if (!Socket_chrome.active[acceptInfo.socketId]) {
    console.warn('Dropped Accept: ', acceptInfo);
    return;
  }

  chrome.sockets.tcp.getInfo(acceptInfo.clientSocketId, function(info) {
    Socket_chrome.active[acceptInfo.socketId].dispatchEvent('onConnection', {
      socket: acceptInfo.clientSocketId,
      host: info.peerAddress,
      port: info.peerPort
    });
  });
};

/**
 * React to client accept errors.
 * @method handleAcceptError
 * @private
 * @param {info} info The value returned by onAcceptError.
 * @static
 */
Socket_chrome.handleAcceptError = function (info) {
  if (!Socket_chrome.active[info.socketId]) {
    console.warn('Dropped Accept Error: ', info);
    return;
  }

  Socket_chrome.active[info.socketId].dispatchDisconnect(info.resultCode);
};

/**
 * Listen on a socket to accept new clients.
 * @method listen
 * @param {String} address the address to listen on
 * @param {number} port the port to listen on
 * @param {Function} callback Callback to call when listening has occured.
 */
Socket_chrome.prototype.listen = function(address, port, callback) {
  if (this.id) {
    callback(undefined, {
      errcode: 'ALREADY_CONNECTED',
      message: 'Cannot Listen on existing socket.'
    });
    return;
  }
  this.namespace = chrome.sockets.tcpServer;
  chrome.sockets.tcpServer.create({}, function(createInfo) {
    this.id = createInfo.socketId;
    // See https://developer.chrome.com/apps/socket#method-listen
    chrome.sockets.tcpServer.listen(this.id, address, port,
        // TODO: find out what the default is, and what this really means, the
        // webpage is pretty sparse on it:
        //   https://developer.chrome.com/apps/socket#method-listen
        //
        // Length of the socket's listen queue (number of pending connections
        // to open)
        100,
        this.startAcceptLoop.bind(this, callback));
  }.bind(this));
};

/**
 * @method startAcceptLoop
 * @param {Function} callbackFromListen Resolves Freedom's promise for
 * |this.listen|
 * @param {number} result The argument |result| comes from the callback in
 * |chrome.socket.listen|. Its value is a number that represents a chrome
 * socket error, as specified in:
 * https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
 * @private
 */
Socket_chrome.prototype.startAcceptLoop =
    function(callbackFromListen, result) {
  var errorMsg;

  if (result !== 0) {
    if (Socket_chrome.ERROR_MAP.hasOwnProperty(result)) {
      errorMsg = 'Chrome Listen failed: ' + Socket_chrome.ERROR_MAP[result];
    } else {
      errorMsg = 'Chrome Listen failed: Unknown code ' + result;
    }
    callbackFromListen(undefined, {
      errcode: 'CONNECTION_FAILURE',
      message: errorMsg
    });
    return;
  }

  callbackFromListen();
  Socket_chrome.addActive(this.id, this);
};

/**
 * Close and Destroy a socket
 * @method close
 * @param {Function} continuation Function to call once socket is destroyed.
 */
Socket_chrome.prototype.close = function(continuation) {
  if (this.id) {
    // Note: this.namespace used, since this method is common to tcp and
    // tcpServer sockets.
    this.namespace.close(this.id, function() {
      this.dispatchDisconnect(0);
      continuation();
    }.bind(this));
  } else {
    continuation(undefined, {
      'errcode': 'SOCKET_CLOSED',
      'message': 'Socket Already Closed, or was never opened'
    });
  }
};

/** REGISTER PROVIDER **/
exports.provider = Socket_chrome;
exports.name = 'core.tcpsocket';
<commit_msg>Handle dns failure error. Test in freedom https://github.com/freedomjs/freedom/pull/123 fix #31<commit_after>/*globals chrome,fdom:true,console,Promise*/
/*jslint indent:2,white:true,sloppy:true */
/**
 * A freedom.js interface to Chrome sockets
 * @constructor
 * @private
 * @param {fdom.Port} channel the module creating this provider.
 * @param {Function} dispatchEvent Method for emitting events.
 * @param {number?} id A pre-existing socket Id for the socket.
 */
var Socket_chrome = function(channel, dispatchEvent, id) {
  this.dispatchEvent = dispatchEvent;
  this.id = id || undefined;
  this.namespace = chrome.sockets.tcp;
  this.prepareSecureCalled = false;
  if (this.id) {
    Socket_chrome.addActive(this.id, this);
    chrome.sockets.tcp.setPaused(this.id, false, function() {
      console.log('Unpaused receiver');
    });
  }
};

/**
 * A static list of active sockets, so that global on-receive messages
 * from chrome can be routed properly.
 * @static
 * @private
 * @type {Object.<number,Socket_chrome>}
 */
Socket_chrome.active = {};

/**
 * Get Information about the socket.
 * @method getInfo
 * @param {Function} continuation Function to call with information.
 * @return {Object} connection and address information about the socket.
 */
Socket_chrome.prototype.getInfo = function(continuation) {
  if (this.id) {
    // Note: this.namespace used, since this method is common to tcp and
    // tcpServer sockets.
    this.namespace.getInfo(this.id, continuation);
  } else {
    continuation({
      connected: false
    });
  }
};

/**
 * Connect to a designated location and begin reading.
 * @method connect
 * @param {String} hostname The host or ip to connect to.
 * @param {number} port The port to connect on.
 * @param {Function} cb Function to call with completion or error.
 */
Socket_chrome.prototype.connect = function(hostname, port, cb) {
  if (this.id) {
    cb(undefined, {
      'errcode': 'ALREADY_CONNECTED',
      'message': 'Cannot Connect Existing Socket'
    });
    return;
  }
  chrome.sockets.tcp.create({}, function(createInfo) {
    this.id = createInfo.socketId;
    try {
      chrome.sockets.tcp.connect(this.id, hostname, port, function (result) {
        if (result < 0) {
          cb(undefined, {
            'errcode': 'CONNECTION_FAILED',
            'message': 'Chrome Connection Failed: ' +
                Socket_chrome.ERROR_MAP[result]
          });
        } else {
          Socket_chrome.addActive(this.id, this);
          cb();
        }
      }.bind(this));
    } catch (e) {
      cb(undefined, {
        'errcode': 'CONNECTION_FAILED',
        'message': 'Chrome Connection Failed: ' + e.message
      });
    }
  }.bind(this));
};

/**
 * Secure an already connected socket.
 * @method secure
 * @param {Function} cb Function to call with completion or error.
 */
Socket_chrome.prototype.secure = function(cb) {
  if (!this.id) {
    cb(undefined, {
      'errcode': 'NOT_CONNECTED',
      'message': 'Cannot secure a disconnected socket'
    });
    return;
  } else if (!this.prepareSecureCalled) {
    cb(undefined, {
      'errcode': 'CONNECTION_FAILED',
      'message': 'prepareSecure not called before secure'
    });
    return;
  } else if (!chrome.sockets.tcp.secure) {
    cb(undefined, {
      'errcode': 'CONNECTION_FAILED',
      'message': 'Secure method not available'
    });
    return;
  }
  chrome.sockets.tcp.secure(this.id, {}, function(secureResult) {
    // Always unpause the socket, regardless of whether .secure succeeds.
    this.unpause().then(function() {
      if (secureResult !== 0) {
        cb(undefined, {
          'errcode': 'CONNECTION_FAILED',
          'message': 'Secure failed: ' + secureResult + ': ' +
              Socket_chrome.ERROR_MAP[secureResult]
        });
        return;
      }
      cb();
    }.bind(this), function(e) {
      cb(undefined, {
        'errcode': 'CONNECTION_FAILED',
        'message': 'Secure failed: error unpausing socket'
      });
    });
  }.bind(this));
};

/**
 * Prepares a socket for becoming secure after the next read event.
 * See details at
 * https://github.com/freedomjs/freedom/wiki/prepareSecure-API-Usage
 * @method prepareSecure
 * @param {Function} cb Function to call with completion or error.
 */
Socket_chrome.prototype.prepareSecure = function(cb) {
  if (!this.id) {
    cb(undefined, {
      'errcode': 'NOT_CONNECTED',
      'message': 'Cannot prepareSecure a disconnected socket'
    });
    return;
  }
  this.pause().then(
    function() {
      this.prepareSecureCalled = true;
      cb();
    }.bind(this), function(e) {
    cb(undefined, {
      'errcode': 'CONNECTION_FAILED',
      'message': 'prepareSecure failed: error pausing socket'
    });
  });
};

/**
 * Pauses the socket
 * @method pause
 * @private
 * @returns {Promise} Promise to be fulfilled on pause.
 */
Socket_chrome.prototype.pause = function() {
  if (!this.id) {
    return Promise.reject('Cannot pause disconnected socket');
  }
  return new Promise(function(fulfill, reject) {
    chrome.sockets.tcp.setPaused(this.id, true, fulfill);
  }.bind(this));
};

/**
 * Unpauses the socket
 * @method unpause
 * @private
 * @returns {Promise} Promise to be fulfilled on unpause.
 */
Socket_chrome.prototype.unpause = function() {
  if (!this.id) {
    return Promise.reject('Cannot unpause disconnected socket');
  }
  return new Promise(function(fulfill, reject) {
    chrome.sockets.tcp.setPaused(this.id, false, fulfill);
  }.bind(this));
};

/**
 * Write data to the socket.
 * @method write
 * @param {ArrayBuffer} data The data to write
 * @param {Function} cb Function to call when data is written
 */
Socket_chrome.prototype.write = function(data, cb) {
  if (!this.id) {
    cb(undefined, {
      'errcode': 'NOT_CONNECTED',
      'message': 'Cannot Write on Closed Socket'
    });
    return;
  }

  chrome.sockets.tcp.send(this.id, data, function(sendInfo) {
    if (sendInfo.resultCode < 0) {
      this.dispatchDisconnect(sendInfo.resultCode);
      return cb(undefined, {
        'errcode': 'UNKNOWN',
        'message': 'Send Error: ' + sendInfo.resultCode + ': ' + Socket_chrome.errorStringOfCode(sendInfo.resultCode),
      });
    } else if (sendInfo.bytesSent !== data.byteLength) {
      this.dispatchDisconnect('UNKNOWN');
      return cb(undefined, {
        'errcode': 'UNKNOWN',
        'message': 'Write Partially completed.'
      });
    }
    cb();
  }.bind(this));
};

Socket_chrome.ERROR_MAP = {
  // Error codes are at:
  // https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
  '-1': 'IO_PENDING',
  '-2': 'FAILED',
  '-3': 'ABORTED',
  '-4': 'INVALID_ARGUMENT',
  '-5': 'INVALID_HANDLE',
  '-7': 'TIMED_OUT',
  '-13': 'OUT_OF_MEMORY',
  '-15': 'SOCKET_NOT_CONNECTED',
  '-21': 'NETWORK_CHANGED',
  '-23': 'SOCKET_IS_CONNECTED',
  '-100': 'CONNECTION_CLOSED',
  '-101': 'CONNECTION_RESET',
  '-102': 'CONNECTION_REFUSED',
  '-103': 'CONNECTION_ABORTED',
  '-104': 'CONNECTION_FAILED',
  '-105': 'NAME_NOT_RESOLVED',
  '-106': 'INTERNET_DISCONNECTED',
  '-107': 'SSL_PROTOCOL_ERROR',
  '-200': 'CERT_COMMON_NAME_INVALID',
   // See Cordova Plugin socket.js
  '-1000': 'GENERIC_CORDOVA_FAILURE'
};

/**
 * Get the error code associated with a chrome.socket error code.
 * @method errorStringOfCode
 * @static
 * @private
 * @param {Number} code The error number as described by chrome
 * @returns {String} The error code as defined in the freedom.js interface.
 */
Socket_chrome.errorStringOfCode = function(code) {
  return Socket_chrome.ERROR_MAP[String(code)] ||
      'UNKNOWN';
};

/*
 * Alert a consumer that a socket is disconnected, with appropriate
 * error message.
 * @method dispatchDisconnect
 * @private
 * @param {Number} code the code returned by chrome when the socket closed.
 */
Socket_chrome.prototype.dispatchDisconnect = function (code) {
  if (!this.id) {
    // Don't send more than one dispatchDisconnect event.
    return;
  }

  Socket_chrome.removeActive(this.id);
  delete this.id;

  if (code === 0) {
    this.dispatchEvent('onDisconnect', {
      errcode: 'SUCCESS',
      message: 'Socket closed by call to close'
    });
  } else if(code === -100) {
    this.dispatchEvent('onDisconnect', {
      errcode: 'CONNECTION_CLOSED',
      message: 'Connection closed gracefully'
    });
  } else {
    this.dispatchEvent('onDisconnect', {
      errcode: 'UNKOWN',
      message: 'Socket Error: ' + code + ': ' + Socket_chrome.errorStringOfCode(code)
    });
  }
};

/**
 * Mark a socket as active, so that dispatched events can be routed to it.
 * This method is needed because chrome.sockets exposes a single event handler
 * for incoming data across all sockets.
 * @method addActive
 * @static
 * @param {number} id The socketId as provided by chrome.sockets
 * @param {Socket_chrome} socket The socket class associated with the id.
 */
Socket_chrome.addActive = function(id, socket) {
  if (Object.keys(Socket_chrome.active).length === 0) {
    if (chrome.sockets.tcp) {
      chrome.sockets.tcp.onReceive.addListener(Socket_chrome.handleReadData);
      chrome.sockets.tcp.onReceiveError.addListener(
          Socket_chrome.handleReadError);
    }
    if (chrome.sockets.tcpServer) {
      chrome.sockets.tcpServer.onAccept.addListener(
          Socket_chrome.handleAccept);
      chrome.sockets.tcpServer.onAcceptError.addListener(
          Socket_chrome.handleAcceptError);
    }
  }
  Socket_chrome.active[id] = socket;
};

/**
 * Unmark a socket as active, and clean up event handlers if needed.
 * @method removeActive
 * @static
 * @param {number} id The socketId to no longer watch.
 */
Socket_chrome.removeActive = function(id) {
  delete Socket_chrome.active[id];
  if (Object.keys(Socket_chrome.active).length === 0) {
    if (chrome.sockets.tcp) {
      chrome.sockets.tcp.onReceive.removeListener(
          Socket_chrome.handleReadData);
      chrome.sockets.tcp.onReceiveError.removeListener(
          Socket_chrome.handleReadError);
    }
    if (chrome.sockets.tcpServer) {
      chrome.sockets.tcpServer.onAccept.removeListener(
          Socket_chrome.handleAccept);
      chrome.sockets.tcpServer.onAcceptError.removeListener(
          Socket_chrome.handleAcceptError);
    }
  }
};

/**
 * React to read data.
 * @method handleReadData
 * @private
 * @param {ReadInfo} readInfo The value returned by onReceive.
 * @static
 */
Socket_chrome.handleReadData = function (readInfo) {
  if (!Socket_chrome.active[readInfo.socketId]) {
    console.warn('Dropped Read: ', readInfo);
    return;
  }
  Socket_chrome.active[readInfo.socketId].dispatchEvent('onData', {data: readInfo.data});
};

/**
 * React to read errors.
 * @method handleReadError
 * @private
 * @param {readInfo} readInfo The value returned by onReceiveError.
 * @static
 */
Socket_chrome.handleReadError = function (readInfo) {
  if (!Socket_chrome.active[readInfo.socketId]) {
    console.warn('Dropped Read Error: ', readInfo);
    return;
  }
  Socket_chrome.active[readInfo.socketId].dispatchDisconnect(readInfo.resultCode);
};

/**
 * React to client accepts.
 * @method handleAccept
 * @private
 * @param {acceptInfo} acceptInfo The value returned by onAccept.
 * @static
 */
Socket_chrome.handleAccept = function (acceptInfo) {
  if (!Socket_chrome.active[acceptInfo.socketId]) {
    console.warn('Dropped Accept: ', acceptInfo);
    return;
  }

  chrome.sockets.tcp.getInfo(acceptInfo.clientSocketId, function(info) {
    Socket_chrome.active[acceptInfo.socketId].dispatchEvent('onConnection', {
      socket: acceptInfo.clientSocketId,
      host: info.peerAddress,
      port: info.peerPort
    });
  });
};

/**
 * React to client accept errors.
 * @method handleAcceptError
 * @private
 * @param {info} info The value returned by onAcceptError.
 * @static
 */
Socket_chrome.handleAcceptError = function (info) {
  if (!Socket_chrome.active[info.socketId]) {
    console.warn('Dropped Accept Error: ', info);
    return;
  }

  Socket_chrome.active[info.socketId].dispatchDisconnect(info.resultCode);
};

/**
 * Listen on a socket to accept new clients.
 * @method listen
 * @param {String} address the address to listen on
 * @param {number} port the port to listen on
 * @param {Function} callback Callback to call when listening has occured.
 */
Socket_chrome.prototype.listen = function(address, port, callback) {
  if (this.id) {
    callback(undefined, {
      errcode: 'ALREADY_CONNECTED',
      message: 'Cannot Listen on existing socket.'
    });
    return;
  }
  this.namespace = chrome.sockets.tcpServer;
  chrome.sockets.tcpServer.create({}, function(createInfo) {
    this.id = createInfo.socketId;
    // See https://developer.chrome.com/apps/socket#method-listen
    chrome.sockets.tcpServer.listen(this.id, address, port,
        // TODO: find out what the default is, and what this really means, the
        // webpage is pretty sparse on it:
        //   https://developer.chrome.com/apps/socket#method-listen
        //
        // Length of the socket's listen queue (number of pending connections
        // to open)
        100,
        this.startAcceptLoop.bind(this, callback));
  }.bind(this));
};

/**
 * @method startAcceptLoop
 * @param {Function} callbackFromListen Resolves Freedom's promise for
 * |this.listen|
 * @param {number} result The argument |result| comes from the callback in
 * |chrome.socket.listen|. Its value is a number that represents a chrome
 * socket error, as specified in:
 * https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
 * @private
 */
Socket_chrome.prototype.startAcceptLoop =
    function(callbackFromListen, result) {
  var errorMsg;

  if (result !== 0) {
    if (Socket_chrome.ERROR_MAP.hasOwnProperty(result)) {
      errorMsg = 'Chrome Listen failed: ' + Socket_chrome.ERROR_MAP[result];
    } else {
      errorMsg = 'Chrome Listen failed: Unknown code ' + result;
    }
    callbackFromListen(undefined, {
      errcode: 'CONNECTION_FAILURE',
      message: errorMsg
    });
    return;
  }

  callbackFromListen();
  Socket_chrome.addActive(this.id, this);
};

/**
 * Close and Destroy a socket
 * @method close
 * @param {Function} continuation Function to call once socket is destroyed.
 */
Socket_chrome.prototype.close = function(continuation) {
  if (this.id) {
    // Note: this.namespace used, since this method is common to tcp and
    // tcpServer sockets.
    this.namespace.close(this.id, function() {
      this.dispatchDisconnect(0);
      continuation();
    }.bind(this));
  } else {
    continuation(undefined, {
      'errcode': 'SOCKET_CLOSED',
      'message': 'Socket Already Closed, or was never opened'
    });
  }
};

/** REGISTER PROVIDER **/
exports.provider = Socket_chrome;
exports.name = 'core.tcpsocket';
<|endoftext|>