<commit_before>/*
  Q Light Controller Plus
  devtool.js

  Copyright (c) Heikki Junnila

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0.txt

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

var width;
var height;
var stepCount;
var currentStep;
var testTimer;
var timerRunning;

function initDefinitions()
{
    document.getElementById("apiversion").value = testAlgo.apiVersion;
    document.getElementById("name").value = testAlgo.name;
    document.getElementById("author").value = testAlgo.author;
    if (typeof testAlgo.acceptColors !== "undefined") {
        document.getElementById("acceptColors").value = testAlgo.acceptColors;
    } else {
        document.getElementById("acceptColors").value = "2 (Default)";
    }
}

function initProperties()
{
    var table = document.getElementById("properties");
    var properties = Array();

    // Cleanup the table before updating its contents
    for (var i = table.rows.length - 1; i >= 0; i--) {
        table.deleteRow(i);
    }

    // Algo properties not supported by versions prior to 2.
    if (testAlgo.apiVersion < 2) {
        return;
    }

    // Get the properties
    for (var i = 0; i < testAlgo.properties.length; i++)
    {
        var propDef = testAlgo.properties[i];
        var propKeyValue = propDef.split("|");
        var property = Array();

        for (var entry = 0; entry < propKeyValue.length; entry++) {
            var keyValue = propKeyValue[entry].split(":");
            var key = keyValue[0];
            keyValue.shift();
            property.push(Array(key, keyValue.join(":")));
        }
        properties.push(property);
    }
    // Write the properties
    for (var entry = 0; entry < properties.length; entry++) {
        var row = table.insertRow(-1);
        var property = properties[entry];

        var keys = new Array();
        for (var i = 0; i < property.length; i++) {
            keys.push(property[i][0]);
        }

        var name = "";
        if (keys.indexOf("name") >= 0) {
            name = property[keys.indexOf("name")][1];
        }
        var displayName = name;
        if (keys.indexOf("display") >= 0) {
            displayName = property[keys.indexOf("display")][1];
        }
        var typeProperty = "string";
        if (keys.indexOf("type") >= 0) {
            // list: defines a list of strings that will be displayed by the QLC+ RGB Matrix Editor
            // range: defined a range of integer values that this property can handle
            // integer: an integer value that QLC+ can exchange with the script
            // string: a string that QLC+ can exchange with the script
            typeProperty = property[keys.indexOf("type")][1];
        }
        var values = Array(0, 0);
        if (keys.indexOf("values") >= 0) {
            values = property[keys.indexOf("values")][1].split(",");
        }
        var writeFunction = "";
        if (keys.indexOf("write") >= 0) {
            writeFunction = property[keys.indexOf("write")][1];
            if (name !== "") {
                var storedValue = localStorage.getItem(name);
                if (storedValue !== null) {
                    eval("testAlgo." + writeFunction + "(\"" + storedValue + "\")");
                }
            }
        }
        var readFunction = "";
        if (keys.indexOf("read") >= 0) {
            readFunction = property[keys.indexOf("read")][1];
        }

        var nameCell = row.insertCell(-1);
        var t = document.createTextNode(displayName);
        nameCell.appendChild(t);

        var formCell = row.insertCell(-1);
        if (typeProperty === "list") {
            var input = document.createElement("select");
            input.name = name;
            input.id = name;
            input.onChange = "writeFunction(\"" + writeFunction + "', '" + name + "\", this.value); setStep(0); writeCurrentStep()";
            for (var i = 0; i < values.length; i++) {
                var opt = document.createElement("option");
                var t = document.createTextNode(values[i]);
                if (eval("testAlgo." + readFunction)() === values[i]) {
                    opt.selected = "selected";
                }
                opt.value = values[i];
                opt.appendChild(t);
                input.appendChild(opt);
            }
            formCell.appendChild(input);
        } else if (typeProperty === "range") {
            var input = document.createElement("input");
            input.type = "number";
            input.required = "required";
            input.name = name;
            input.value = eval("testAlgo." + name);
            input.id = name;
            input.min = values[0];
            input.max = values[1];
            input.onChange = "writeFunction(\"" + writeFunction + "', '" + name + "\", this.value); setStep(0); writeCurrentStep()";
            formCell.appendChild(input);
        } else if (typeProperty === "integer") {
            var input = document.createElement("input");
            input.type = "number";
            input.required = "required";
            input.name = name;
            input.value = eval("testAlgo." + name);
            input.id = name;
            input.onChange = "writeFunction(\"" + writeFunction + "', '" + name + "\", this.value); setStep(0); writeCurrentStep()";
            formCell.appendChild(input);
        } else { // string
            var input = document.createElement("input");
            input.type = "text";
            input.name = name;
            input.value = eval("testAlgo." + name);
            input.id = name;
            input.onChange = "writeFunction(\"" + writeFunction + "', '" + name + "\", this.value); setStep(0); writeCurrentStep()";
            formCell.appendChild(input);
        }
    }
}

function initPixelColors()
{
    var pixelColorChooser = document.getElementById("pixelColorChooser");
    pixelColorChooser.hidden = testAlgo.acceptColors === 0;

    var secondaryColorChooser = document.getElementById("secondaryColorChooser");
    secondaryColorChooser.hidden = testAlgo.acceptColors === 1;
}

function initSpeedValue()
{
    var speed = localStorage.getItem("speed");
    if (speed === null) {
        speed = 500;
    }
    document.getElementById("speed").value = speed;
}

function getRgbFromColorInt(color)
{
    var red = color >> 16;
    var green = (color >> 8) - red * 256;
    var blue = color - red * 256 * 256 - green * 256;
    return [red, green, blue];
}

function getCurrentColorInt()
{
    var primaryColorInput = document.getElementById("primaryColor");
    var primaryColor = parseInt(primaryColorInput.value, 16);
    var secondaryColorInput = document.getElementById("secondaryColor");
    var secondaryColor = parseInt(secondaryColorInput.value, 16);

    if (testAlgo.acceptColors === 0 || Number.isNaN(primaryColor)) {
        return null;
    }

    if (testAlgo.acceptColors === 1 || Number.isNaN(secondaryColor) || stepCount <= 1) {
        return primaryColor;
    }

    var primaryColorRgb = getRgbFromColorInt(primaryColor);
    var secondaryColorRgb = getRgbFromColorInt(secondaryColor);

    var primaryFactor = (stepCount - currentStep - 1) / (stepCount - 1);
    var secondaryFactor = currentStep / (stepCount - 1);

    var red = Math.round(primaryColorRgb[0] * primaryFactor + secondaryColorRgb[0] * secondaryFactor);
    var green = Math.round(primaryColorRgb[1] * primaryFactor + secondaryColorRgb[1] * secondaryFactor);
    var blue = Math.round(primaryColorRgb[2] * primaryFactor + secondaryColorRgb[2] * secondaryFactor);

    return red * 256 * 256 + green * 256 + blue;
}

function writeCurrentStep()
{
    currentStep = parseInt(document.getElementById("currentStep").value); // currentStep may have been changed manually

    var map = document.getElementById("map");
    for (var i = map.rows.length - 1; i >= 0; i--) {
        map.deleteRow(i);
    }
    var rgb = testAlgo.rgbMap(width, height, getCurrentColorInt(), currentStep);

    for (var y = 0; y < height; y++)
    {
        var row = map.insertRow(y);

        for (var x = 0; x < width; x++)
        {
            var cell = row.insertCell(x);
            var rgbStr = rgb[y][x].toString(16);
            while (rgbStr.length !== 6) {
                rgbStr = "0" + rgbStr;
            }
            rgbStr = "#" + rgbStr;
            cell.style.backgroundColor = rgbStr;
            cell.style.height = "20px";
            cell.style.width = "20px";
            cell.title = "(" + x + ", " + y + "): " + rgbStr + " – " + cell.style.backgroundColor; // rgbStr will be #rrggbb whereas the cell style will be rgb(255, 255, 255)
        }
    }
}

function setStep(step) {
    currentStep = step;
    document.getElementById("currentStep").value = currentStep;
    writeCurrentStep();
}

function onGridSizeUpdated()
{
    width = parseInt(document.getElementById("width").value);
    height = parseInt(document.getElementById("height").value);

    stepCount = testAlgo.rgbMapStepCount(width, height);
    document.getElementById("stepCount").value = stepCount;
    document.getElementById("currentStep").max = stepCount - 1;

    setStep(0);
    writeCurrentStep();
}

function startTest()
{
    var speed = document.getElementById("speed").value;
    window.clearInterval(testTimer); // avoid multiple timers running simultaneously
    testTimer = window.setInterval("nextStep()", speed);
    localStorage.setItem("timerRunning", 1);
}

function stopTest()
{
    window.clearInterval(testTimer);
    localStorage.setItem("timerRunning", 0);
}

function initTestStatus()
{
    var timerStatus = localStorage.getItem("timerRunning");
    if (timerStatus === null || parseInt(timerStatus) === 1) {
        startTest();
    }
}

function init()
{
    if (typeof testAlgo === "undefined") {
        return;
    }
    initDefinitions();
    initSpeedValue();
    initProperties();
    initPixelColors();
    onGridSizeUpdated();
    writeCurrentStep();
    initTestStatus();
}

function handleLoadError()
{
    return;
}

function onFilenameUpdated(filename)
{
    if (filename === "") {
        return;
    }

    localStorage.setItem("filename", filename);

    var script = document.getElementById("algoScript");
    if (script === null) {
        script = document.createElement("script");
        script.id = "algoScript";
        script.addEventListener("load", () => init(), false);
        script.addEventListener("error", () => handleLoadError(), false);
        script.type = "text/javascript";
        script.src = filename;
        document.head.appendChild(script);
    } else {
        script.src = filename;
    }
    // init(); // is called onload.
}

function loadAlgoFile()
{
    var storedValue = localStorage.getItem("filename");
    if (storedValue === null) {
        storedValue = "evenodd.js";
    }
    document.getElementById("filename").value = storedValue;
    onFilenameUpdated(storedValue);
}

function writeFunction(functionName, propertyName, value)
{
    eval("testAlgo." + functionName + "(\"" + value + "\")");
    localStorage.setItem(propertyName, value);
}

function onSpeedChanged()
{
    var speed = document.getElementById("speed").value;
    localStorage.setItem("speed", speed);
    initTestStatus();
}

function nextStep()
{
    if (currentStep + 1 < stepCount) {
        setStep(currentStep + 1);
    }
    else {
        setStep(0);
    }
}

function previousStep()
{
    if (currentStep > 0) {
        setStep(currentStep - 1);
    } else {
        setStep(stepCount - 1); // last step
    }
}
<commit_msg>Concistently initialize vars in function blocks once, use Array.forEach to loop through array (of array objects)<commit_after>/*
  Q Light Controller Plus
  devtool.js

  Copyright (c) Heikki Junnila

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0.txt

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

var width;
var height;
var stepCount;
var currentStep;
var testTimer;
var timerRunning;

function initDefinitions()
{
    document.getElementById("apiversion").value = testAlgo.apiVersion;
    document.getElementById("name").value = testAlgo.name;
    document.getElementById("author").value = testAlgo.author;
    if (typeof testAlgo.acceptColors !== "undefined") {
        document.getElementById("acceptColors").value = testAlgo.acceptColors;
    } else {
        document.getElementById("acceptColors").value = "2 (Default)";
    }
}

function addPropertyTableEntry(property)
{
    var table = document.getElementById("properties");
    var row = table.insertRow(-1);
	var i = 0;

	var keys = new Array();
	for (i = 0; i < property.length; i++) {
		keys.push(property[i][0]);
	}

	var name = "";
	if (keys.indexOf("name") >= 0) {
		name = property[keys.indexOf("name")][1];
	}
	var displayName = name;
	if (keys.indexOf("display") >= 0) {
		displayName = property[keys.indexOf("display")][1];
	}
	var typeProperty = "string";
	if (keys.indexOf("type") >= 0) {
		// list: defines a list of strings that will be displayed by the QLC+ RGB Matrix Editor
		// range: defined a range of integer values that this property can handle
		// integer: an integer value that QLC+ can exchange with the script
		// string: a string that QLC+ can exchange with the script
		typeProperty = property[keys.indexOf("type")][1];
	}
	var values = Array(0, 0);
	if (keys.indexOf("values") >= 0) {
		values = property[keys.indexOf("values")][1].split(",");
	}
	var writeFunction = "";
	if (keys.indexOf("write") >= 0) {
		writeFunction = property[keys.indexOf("write")][1];
		if (name !== "") {
			var storedValue = localStorage.getItem(name);
			if (storedValue !== null) {
				eval("testAlgo." + writeFunction + "(\"" + storedValue + "\")");
			}
		}
	}
	var readFunction = "";
	if (keys.indexOf("read") >= 0) {
		readFunction = property[keys.indexOf("read")][1];
	}

	var nameCell = row.insertCell(-1);
	var t = document.createTextNode(displayName);
	nameCell.appendChild(t);

	var formCell = row.insertCell(-1);
	if (typeProperty === "list") {
		var input = document.createElement("select");
		input.name = name;
		input.id = name;
		input.onChange = "writeFunction(\"" + writeFunction + "', '" + name + "\", this.value); setStep(0); writeCurrentStep()";
		for (i = 0; i < values.length; i++) {
			var opt = document.createElement("option");
			var t = document.createTextNode(values[i]);
			if (eval("testAlgo." + readFunction)() === values[i]) {
				opt.selected = "selected";
			}
			opt.value = values[i];
			opt.appendChild(t);
			input.appendChild(opt);
		}
		formCell.appendChild(input);
	} else if (typeProperty === "range") {
		var input = document.createElement("input");
		input.type = "number";
		input.required = "required";
		input.name = name;
		input.value = eval("testAlgo." + name);
		input.id = name;
		input.min = values[0];
		input.max = values[1];
		input.onChange = "writeFunction(\"" + writeFunction + "', '" + name + "\", this.value); setStep(0); writeCurrentStep()";
		formCell.appendChild(input);
	} else if (typeProperty === "integer") {
		var input = document.createElement("input");
		input.type = "number";
		input.required = "required";
		input.name = name;
		input.value = eval("testAlgo." + name);
		input.id = name;
		input.onChange = "writeFunction(\"" + writeFunction + "', '" + name + "\", this.value); setStep(0); writeCurrentStep()";
		formCell.appendChild(input);
	} else { // string
		var input = document.createElement("input");
		input.type = "text";
		input.name = name;
		input.value = eval("testAlgo." + name);
		input.id = name;
		input.onChange = "writeFunction(\"" + writeFunction + "', '" + name + "\", this.value); setStep(0); writeCurrentStep()";
		formCell.appendChild(input);
    }
}

function initProperties()
{
    var table = document.getElementById("properties");
    var properties = Array();
    var property = Array();
	var i = 0;
    var entry = 0;

    // Cleanup the table before updating its contents
    for (i = table.rows.length - 1; i >= 0; i--) {
        table.deleteRow(i);
    }

    // Algo properties not supported by versions prior to 2.
    if (testAlgo.apiVersion < 2) {
        return;
    }

    // Get the properties
    for (i = 0; i < testAlgo.properties.length; i++)
    {
        var propDef = testAlgo.properties[i];
        var propKeyValue = propDef.split("|");
        property = Array();

        for (entry = 0; entry < propKeyValue.length; entry++) {
            var keyValue = propKeyValue[entry].split(":");
            var key = keyValue[0];
            keyValue.shift();
            property.push(Array(key, keyValue.join(":")));
        }
        properties.push(property);
    }
    // Write the properties
    properties.forEach(addPropertyTableEntry);
}

function initPixelColors()
{
    var pixelColorChooser = document.getElementById("pixelColorChooser");
    pixelColorChooser.hidden = testAlgo.acceptColors === 0;

    var secondaryColorChooser = document.getElementById("secondaryColorChooser");
    secondaryColorChooser.hidden = testAlgo.acceptColors === 1;
}

function initSpeedValue()
{
    var speed = localStorage.getItem("speed");
    if (speed === null) {
        speed = 500;
    }
    document.getElementById("speed").value = speed;
}

function getRgbFromColorInt(color)
{
    var red = color >> 16;
    var green = (color >> 8) - red * 256;
    var blue = color - red * 256 * 256 - green * 256;
    return [red, green, blue];
}

function getCurrentColorInt()
{
    var primaryColorInput = document.getElementById("primaryColor");
    var primaryColor = parseInt(primaryColorInput.value, 16);
    var secondaryColorInput = document.getElementById("secondaryColor");
    var secondaryColor = parseInt(secondaryColorInput.value, 16);

    if (testAlgo.acceptColors === 0 || Number.isNaN(primaryColor)) {
        return null;
    }

    if (testAlgo.acceptColors === 1 || Number.isNaN(secondaryColor) || stepCount <= 1) {
        return primaryColor;
    }

    var primaryColorRgb = getRgbFromColorInt(primaryColor);
    var secondaryColorRgb = getRgbFromColorInt(secondaryColor);

    var primaryFactor = (stepCount - currentStep - 1) / (stepCount - 1);
    var secondaryFactor = currentStep / (stepCount - 1);

    var red = Math.round(primaryColorRgb[0] * primaryFactor + secondaryColorRgb[0] * secondaryFactor);
    var green = Math.round(primaryColorRgb[1] * primaryFactor + secondaryColorRgb[1] * secondaryFactor);
    var blue = Math.round(primaryColorRgb[2] * primaryFactor + secondaryColorRgb[2] * secondaryFactor);

    return red * 256 * 256 + green * 256 + blue;
}

function writeCurrentStep()
{
    currentStep = parseInt(document.getElementById("currentStep").value); // currentStep may have been changed manually

    var map = document.getElementById("map");
    for (var i = map.rows.length - 1; i >= 0; i--) {
        map.deleteRow(i);
    }
    var rgb = testAlgo.rgbMap(width, height, getCurrentColorInt(), currentStep);

    for (var y = 0; y < height; y++)
    {
        var row = map.insertRow(y);

        for (var x = 0; x < width; x++)
        {
            var cell = row.insertCell(x);
            var rgbStr = rgb[y][x].toString(16);
            while (rgbStr.length !== 6) {
                rgbStr = "0" + rgbStr;
            }
            rgbStr = "#" + rgbStr;
            cell.style.backgroundColor = rgbStr;
            cell.style.height = "20px";
            cell.style.width = "20px";
            cell.title = "(" + x + ", " + y + "): " + rgbStr + " – " + cell.style.backgroundColor; // rgbStr will be #rrggbb whereas the cell style will be rgb(255, 255, 255)
        }
    }
}

function setStep(step) {
    currentStep = step;
    document.getElementById("currentStep").value = currentStep;
    writeCurrentStep();
}

function onGridSizeUpdated()
{
    width = parseInt(document.getElementById("width").value);
    height = parseInt(document.getElementById("height").value);

    stepCount = testAlgo.rgbMapStepCount(width, height);
    document.getElementById("stepCount").value = stepCount;
    document.getElementById("currentStep").max = stepCount - 1;

    setStep(0);
    writeCurrentStep();
}

function startTest()
{
    var speed = document.getElementById("speed").value;
    window.clearInterval(testTimer); // avoid multiple timers running simultaneously
    testTimer = window.setInterval("nextStep()", speed);
    localStorage.setItem("timerRunning", 1);
}

function stopTest()
{
    window.clearInterval(testTimer);
    localStorage.setItem("timerRunning", 0);
}

function initTestStatus()
{
    var timerStatus = localStorage.getItem("timerRunning");
    if (timerStatus === null || parseInt(timerStatus) === 1) {
        startTest();
    }
}

function init()
{
    if (typeof testAlgo === "undefined") {
        return;
    }
    initDefinitions();
    initSpeedValue();
    initProperties();
    initPixelColors();
    onGridSizeUpdated();
    writeCurrentStep();
    initTestStatus();
}

function handleLoadError()
{
    return;
}

function onFilenameUpdated(filename)
{
    if (filename === "") {
        return;
    }

    localStorage.setItem("filename", filename);

    var script = document.getElementById("algoScript");
    if (script === null) {
        script = document.createElement("script");
        script.id = "algoScript";
        script.addEventListener("load", () => init(), false);
        script.addEventListener("error", () => handleLoadError(), false);
        script.type = "text/javascript";
        script.src = filename;
        document.head.appendChild(script);
    } else {
        script.src = filename;
    }
    // init(); // is called onload.
}

function loadAlgoFile()
{
    var storedValue = localStorage.getItem("filename");
    if (storedValue === null) {
        storedValue = "evenodd.js";
    }
    document.getElementById("filename").value = storedValue;
    onFilenameUpdated(storedValue);
}

function writeFunction(functionName, propertyName, value)
{
    eval("testAlgo." + functionName + "(\"" + value + "\")");
    localStorage.setItem(propertyName, value);
}

function onSpeedChanged()
{
    var speed = document.getElementById("speed").value;
    localStorage.setItem("speed", speed);
    initTestStatus();
}

function nextStep()
{
    if (currentStep + 1 < stepCount) {
        setStep(currentStep + 1);
    }
    else {
        setStep(0);
    }
}

function previousStep()
{
    if (currentStep > 0) {
        setStep(currentStep - 1);
    } else {
        setStep(stepCount - 1); // last step
    }
}
<|endoftext|>


<commit_before>'use strict';
var underscore = require('underscore');
var RenderUpdate = require('./render_update');
var Outliers = require('./outliers');

// TODO: Figure out how to differntiate - and if it matters - between the
// following subtly different timestamps to determine which chronology of
// events an API is actually *trying* to expose:
// 1) execution *window* requestAnimationFrame timestamp {DOMHighResTimeStamp}
// 2) user's *touch* time - (Touch)Event.timeStamp
// 3) sequence we ran for #1
//    (ie: multiple {RenderUpdate}s happen per *window* in #1)

/** @constructor */
var ContextHistory = module.exports = function ContextHistory() {
  /**
   * Chronologically stored RenderUpdate records for previous
   * {@link TouchRendition} occurences, sorted by {@link RenderUpdate#timeStamp}
   * of respective original {@link TouchEvent} source.
   *
   * @private {!ContextHistory.Timeline}
   */
   this.history_ = [];

   /** @private {?number} -1 indicates Context is at clean slate */
   this.redoBranchIndex_ = null;

   /**
    * Existing alternative branches to the current timeline in
    * {@link #history_}, keyed by the index of the point in a timeline for
    * which they represent an alterntative.
    *
    * @private {!Object.<number, !Array.<!ContextHistory.Branch>}
    */
   this.branches_ = {};

   /**
    * Stores map of {@link TimestampDiff}s describing {@link RenderUpdate}s -
    * keyed by the updates' respective {@code timeStamp}s - which occurred just
    * before a significant pause in overall historical activity.
    *
    * @see {@link Outliers} for more
    *
    * @private {!Object.<number, !Object.<number, !ContextHistory.TimstampDiff>>}
    */
   this.cachedPrePauses_ = {};
};

/** @typdef {Array.<!ContextHistory.Update>} */
ContextHistory.Timeline;

/**
 * - history: the branched version of history
 * - branchedAt: timeStamp when the branch occurred.
 *
 * @typedef {{branchedAt: number, history: !ContextHistory.Timeline}}
 */
ContextHistory.Branch;

/**
 * Sorted {@link RenderUpdate}s, or sets of related updates from drag events
 * (say of a brush stroke).
 *
 * @typedef {RenderUpdate|Array.<!RenderUpdate>}
 */
ContextHistory.Update;

/** @typedef {{previous: number, next: number, delta: number}} */
ContextHistory.TimestampDiff;

/**
 * Whether major chapters in history should be found using
 * {@link Outliers#getMajorOutliers} as opposed to
 * {@link Outliers#getMinorOutliers}.
 *
 * @const {boolean}
 */
ContextHistory.HISTORY_OUTLIERS_MAJOR = true;

/**
 * @param {!ContextHistory.Update} update
 * @return {boolean}
 * @private
 */
ContextHistory.isSingleRender_ = function(update) {
  return !ContextHistory.isStrokedRender_(update);
};

/**
 * @param {!ContextHistory.Update} update
 * @return {!RenderUpdate}
 * @private
 */
ContextHistory.isStrokedRender_ = Array.isArray;

/**
 * @param {!ContextHistory.Update} update
 * @return {!RenderUpdate}
 * @private
 */
ContextHistory.getSingleRender_ = function(update) {
  return ContextHistory.isStrokedRender_(update) ? update[0] : update;
};

/**
 * @param {!ContextHistory.Update} updateA
 * @param {!ContextHistory.Update} updateB
 * @return {number}
 *     -1, 0, or 1; See Array.prototype.sort API.
 * @private
 */
ContextHistory.compareRenderUpdates_ = function(updateA, updateB) {
  var timeStampA = ContextHistory.getSingleRender_(updateA).timeStamp;
  var timeStampB = ContextHistory.getSingleRender_(updateB).timeStamp;

  if (timeStampA === timeStampB) {
    return 0;
  } else {
    return timeStampA < timeStampB ? -1 : 1;
  }
};

/**
 * @param {ContextHistory.TimestampDiff} diff
 * @return {number}
 * @private
 */
ContextHistory.getDeltaFromDiff_ = function(diff) {
  return diff.delta;
};

/** @return {!Object.<number, !ContextHistory.TimstampDiff>} stored results */
ContextHistory.prototype.getCachedPrePauses_ = function() {
  if (this.history_.length < 10) {
    return {};  // skip processing without some real data
  }

  // Try to fetch results from cache
  if (!underscore.isEmpty(this.cachedPrePauses_)) {
    var largestHistorySize = Object.
        keys(this.cachedPrePauses_).
        map(function(historySize, idx) {
          return parseInt(historySize, 10  /*radix*/);
        }).
        sort().
        pop();

    if (largestHistorySize < this.history_.length * 2) {
      return this.cachedPrePauses_[largestHistorySize];  // use cache
    }
  }

  // Populate and return from cache
  return this.cachedPrePauses_[this.history_.length] = (function() {
    var map = {};
    this.findSignificantDelats_(ContextHistory.HISTORY_OUTLIERS_MAJOR).
        forEach(function(diff) {
          map[diff.previous] = diff;
        }.bind(this));
    return map;
  }.bind(this))();
};

/**
 * @param {boolean} forMajor
 * @return {!Array.<!ContextHistory.TimestampDiff>}
 * @private
 */
ContextHistory.prototype.findSignificantDelats_ = function(forMajor) {
  var outliers = new Outliers(this.buildTimestampOutlierDeltas_());
  return forMajor ? outliers.getMajorOutliers() : outliers.getMinorOutliers();
};

/**
 * @return {!Array.<!Outliers.Value>}
 *     Sorted deltas, one less than number of {@link RenderUpdate}s in history.
 * @private
 */
ContextHistory.prototype.buildTimestampOutlierDeltas_ = function() {
  return this.history_.
      map(function(update, index, history) {
        if (index === 0) {
          return null;  // should be sliced off our results
        }

        var current = ContextHistory.getSingleRender_(update).timeStamp;
        var previous = ContextHistory.
            getSingleRender_(history[index - 1]).timeStamp;
        return new Outliers.Value({
          previous: previous,
          next: current,
          delta: current - previous
        }, ContextHistory.getDeltaFromDiff_);
      }).

      slice(1).  // remove index 0, where we returned null

      sort(function(diffA, diffB) {
        if (diffA.getValue() === diffB.getValue()) {
          return 0;
        }

        return diffA.getValue() < diffB.getValue() ? -1 : 1;
      });
};

/** @return {!ContextHistory.Update} */
ContextHistory.prototype.getPrePauseHistory = function() {
  return this.history_.filter(this.isUpdatePrePause.bind(this));
};

/** @return {boolean} */
ContextHistory.prototype.isEmpty = function() {
  return !Boolean(this.history_.length);
};

/**
 * @param {!RenderUpdate|!Array.<!RenderUpdate>} update
 * @return {boolean}
 */
ContextHistory.prototype.isUpdatePrePause = function(update) {
  return this.isUpdateStampPrePause(
      ContextHistory.getSingleRender_(update).timeStamp);
};

/**
 * @param {number} timeStamp
 * @return {boolean}
 */
ContextHistory.prototype.isUpdateStampPrePause = function(timeStamp) {
  return Boolean(this.getCachedPrePauses_()[timeStamp]);
};

/**
 * @return {number} Array index of the final possible {@link RenderUpdate}.
 * @private
 */
ContextHistory.prototype.getEndingIndex_ = function() {
  return this.history_.length - 1;
};

/**
 * Erases {@code context} without updating {@link ContextHistory}.
 *
 * @param {!CanvasRenderingContext2D} context
 */
ContextHistory.erase = function(context) {
  context.clearRect(
      0,  // pointX
      0,  // pointY
      context.canvas.width,
      context.canvas.height);
};

/**
 * Re-renders all previously rendered touch surface animations through the
 * present to the future, unless {@code opt_endIndex} is present.
 *
 * @param {!CanvasRenderingContext2D} context
 * @param {number=} opt_endIndex
 */
ContextHistory.prototype.playBack = function(context, opt_endIndex) {
  var isPartialPlayback = underscore.
      isNumber(opt_endIndex) && opt_endIndex !== this.getEndingIndex_();

  var isRewind = isPartialPlayback && opt_endIndex < this.getPresentIndex();
  var isRedo = isPartialPlayback && opt_endIndex > this.getPresentIndex();
  if (isRewind) {
    ContextHistory.erase(context);  // we're rewinding, clear the canvas first
  }

  var isPartialPlaybackInProgress = isPartialPlayback;
  this.history_.forEach(function(update, index, history) {
    if (isRedo && index <= this.getPresentIndex()) {
      return;  // ignore: this is old history, already rendered
    }

    if (isPartialPlayback) {
      if (!isPartialPlaybackInProgress) {
        return;  // we've already finished partially rerendering
      } else if (index > opt_endIndex) {
        this.setRedoBranch_(index - 1);
        isPartialPlaybackInProgress = false;
        return;  // last update was the end of partial rendering
      }
    }

    ContextHistory.playBackUpdate_(context, update);
  }.bind(this));

  if (!isPartialPlayback) {
    this.redoBranchIndex_ = null;
  }
};

/**
 * @param {!CanvasRenderingContext2D} context
 * @param {!ContextHistory.Update} update
 * @private
 */
ContextHistory.playBackUpdate_ = function(context, update) {
  (ContextHistory.isStrokedRender_(update) ? update : [update]).
      forEach(function(renderUpdate) {
        renderUpdate.playBack(context);
      });
};

/**
 * @return {boolean}
 *     Whether we're rewound in history, and have more {@link RenderUpdate}s to
 *     potentially playback.
 */
ContextHistory.prototype.isRedoPossible = function() {
  return underscore.isNumber(this.redoBranchIndex_);
};

/** @return {number} */
ContextHistory.prototype.countPossibleRedos = function() {
  return this.isRedoPossible() ?
      this.getEndingIndex_() - this.getPresentIndex() :
      0;
};

/** @return {boolean} Whether we've any history older than our current state. */
ContextHistory.prototype.isUndoPossible = function() {
  return Boolean(this.countPossibleUndos());
};

/**
 * @param {number=} opt_historyIndex
 *     Optionally specify a historical index for which to get a timeStamp.
 *     Defaults to {@link #getPresentIndex}.
 * @return {number} timeStamp
 */
ContextHistory.prototype.getTimeStamp = function(opt_historyIndex) {
  var historyIndex = underscore.isUndefined(opt_historyIndex) ?
                     this.getPresentIndex() :
                     opt_historyIndex;
  return ContextHistory.getSingleRender_(this.history_[historyIndex]).timeStamp;
};

/** @return {number} */
ContextHistory.prototype.countPossibleUndos = function() {
  return this.getPresentIndex() + 1  /* we can always undo [0] */;
};

/** @return {?RenderUpdate} created after the current  */
ContextHistory.prototype.getNextUpdateIndex = function() {
  return this.isRedoPossible() ? this.getPresentIndex() + 1 : null;
};

/** @return {?number} index occurring just before the current */
ContextHistory.prototype.getPreviousUpdateIndex = function() {
  return this.isUndoPossible() ? this.getPresentIndex() - 1 : null;
};

/** @return {number} index of the most recently rendered update in history.*/
ContextHistory.prototype.getPresentIndex = function() {
  return this.isRedoPossible() ? this.redoBranchIndex_ : this.getEndingIndex_();
};

/**
 * Throws if {@code index} is invalid or the current {@link #history_}.
 *
 * @param {number} index
 * @private
 */
ContextHistory.prototype.assertValidHistoryIndex_ = function(index) {
  if (index >= 0 && index <= this.getEndingIndex_()) {
    return;
  }

  throw new Error(
      'Invalid history(length: ' +  this.history_.length + ') index, ' +
      '"' + index + '"; expected value ' +
      '[0-' + this.getEndingIndex_() + ']');
};

/**
 * Mark history as totally rewound to before its first {@link RenderUpdate}
 *
 * @param {!CanvasRenderingContext2D} context
 */
ContextHistory.prototype.undoAll = function(context) {
  ContextHistory.erase(context);
  this.redoBranchIndex_ = -1;
};

/**
 * @param {number} historyIndex
 * @private
 */
ContextHistory.prototype.setRedoBranch_ = function(historyIndex) {
  if (historyIndex !== -1 ) {
    this.assertValidHistoryIndex_(historyIndex);
  }

  this.redoBranchIndex_ = historyIndex === this.getEndingIndex_() ?
      null :  // We're at the forefront of history
      historyIndex;
};

/**
 * Records {@code relatedUpdates} for reference among wider history of updates.
 *
 * NOTE: Multiple {@link RenderUpdate} events may have the same trigger time,
 * but have been rendered in a particular order. This means, for example, that
 * if {@code renderUpdates} first element draws the first half of a line and the
 * second element draws the second half of a line, that significance is lost
 * here (the same is true for two simultaneously rendered items: eg, with
 * multi-touch events).
 *
 * @param {number} recordingTimeStamp
 *     {@code relatedUpdates}'s last newest timeStamp causing this recording.
 * @param {!Array.<!RenderUpdate>} relatedUpdates
 */
ContextHistory.prototype.recordRelatedUpdates = function(
    recordingTimeStamp, relatedUpdates) {
  this.maybeBranchForUpdatesAt_(recordingTimeStamp);

  var isStrokedRender = relatedUpdates.length > 1;
  var updates = isStrokedRender ? relatedUpdates : relatedUpdates[0];

  this.history_.push(updates);

  if (!isStrokedRender) {
    this.history_ = this.history_.sort(ContextHistory.compareRenderUpdates_);
  }
};

/**
 * @param {number} branchTimeStamp
 * @private
 */
ContextHistory.prototype.maybeBranchForUpdatesAt_ = function(branchTimeStamp) {
  if (!this.isRedoPossible()) {
    return;  // nothing to branch
  }

  var presentIndex = this.getPresentIndex();
  this.branches_[presentIndex] = this.branches_[presentIndex] || [];
  this.branches_[presentIndex].push({
    branchedAt: branchTimeStamp,
    history: this.history_.splice(presentIndex + 1, this.history_.length)
  });
  this.redoBranchIndex_ = null;
};

/** @return {Array.<!ContextHistory.Branch>} for the current point in history */
ContextHistory.prototype.getHistoricalBranches = function() {
  return this.branches_[this.getPresentIndex()] || null;
};

/** @return {boolean} */
ContextHistory.prototype.hasHistoricalPauses = function() {
  return underscore.some(this.cachedPrePauses_, function(update) {
    return Boolean(update && update.length);
  });
};

/** @return {boolean} */
ContextHistory.prototype.hasHistoricalBranches = function() {
  return !underscore.isEmpty(this.branches_);
};

/** @return {boolean} whether current point in history has alternatives. */
ContextHistory.prototype.isBranchedHistoryAvailable = function() {
  return Boolean(this.getHistoricalBranches());
};
<commit_msg>noop: rm unused require(RenderUpdate)<commit_after>'use strict';
var underscore = require('underscore');
var Outliers = require('./outliers');

// TODO: Figure out how to differntiate - and if it matters - between the
// following subtly different timestamps to determine which chronology of
// events an API is actually *trying* to expose:
// 1) execution *window* requestAnimationFrame timestamp {DOMHighResTimeStamp}
// 2) user's *touch* time - (Touch)Event.timeStamp
// 3) sequence we ran for #1
//    (ie: multiple {RenderUpdate}s happen per *window* in #1)

/** @constructor */
var ContextHistory = module.exports = function ContextHistory() {
  /**
   * Chronologically stored RenderUpdate records for previous
   * {@link TouchRendition} occurences, sorted by {@link RenderUpdate#timeStamp}
   * of respective original {@link TouchEvent} source.
   *
   * @private {!ContextHistory.Timeline}
   */
   this.history_ = [];

   /** @private {?number} -1 indicates Context is at clean slate */
   this.redoBranchIndex_ = null;

   /**
    * Existing alternative branches to the current timeline in
    * {@link #history_}, keyed by the index of the point in a timeline for
    * which they represent an alterntative.
    *
    * @private {!Object.<number, !Array.<!ContextHistory.Branch>}
    */
   this.branches_ = {};

   /**
    * Stores map of {@link TimestampDiff}s describing {@link RenderUpdate}s -
    * keyed by the updates' respective {@code timeStamp}s - which occurred just
    * before a significant pause in overall historical activity.
    *
    * @see {@link Outliers} for more
    *
    * @private {!Object.<number, !Object.<number, !ContextHistory.TimstampDiff>>}
    */
   this.cachedPrePauses_ = {};
};

/** @typdef {Array.<!ContextHistory.Update>} */
ContextHistory.Timeline;

/**
 * - history: the branched version of history
 * - branchedAt: timeStamp when the branch occurred.
 *
 * @typedef {{branchedAt: number, history: !ContextHistory.Timeline}}
 */
ContextHistory.Branch;

/**
 * Sorted {@link RenderUpdate}s, or sets of related updates from drag events
 * (say of a brush stroke).
 *
 * @typedef {RenderUpdate|Array.<!RenderUpdate>}
 */
ContextHistory.Update;

/** @typedef {{previous: number, next: number, delta: number}} */
ContextHistory.TimestampDiff;

/**
 * Whether major chapters in history should be found using
 * {@link Outliers#getMajorOutliers} as opposed to
 * {@link Outliers#getMinorOutliers}.
 *
 * @const {boolean}
 */
ContextHistory.HISTORY_OUTLIERS_MAJOR = true;

/**
 * @param {!ContextHistory.Update} update
 * @return {boolean}
 * @private
 */
ContextHistory.isSingleRender_ = function(update) {
  return !ContextHistory.isStrokedRender_(update);
};

/**
 * @param {!ContextHistory.Update} update
 * @return {!RenderUpdate}
 * @private
 */
ContextHistory.isStrokedRender_ = Array.isArray;

/**
 * @param {!ContextHistory.Update} update
 * @return {!RenderUpdate}
 * @private
 */
ContextHistory.getSingleRender_ = function(update) {
  return ContextHistory.isStrokedRender_(update) ? update[0] : update;
};

/**
 * @param {!ContextHistory.Update} updateA
 * @param {!ContextHistory.Update} updateB
 * @return {number}
 *     -1, 0, or 1; See Array.prototype.sort API.
 * @private
 */
ContextHistory.compareRenderUpdates_ = function(updateA, updateB) {
  var timeStampA = ContextHistory.getSingleRender_(updateA).timeStamp;
  var timeStampB = ContextHistory.getSingleRender_(updateB).timeStamp;

  if (timeStampA === timeStampB) {
    return 0;
  } else {
    return timeStampA < timeStampB ? -1 : 1;
  }
};

/**
 * @param {ContextHistory.TimestampDiff} diff
 * @return {number}
 * @private
 */
ContextHistory.getDeltaFromDiff_ = function(diff) {
  return diff.delta;
};

/** @return {!Object.<number, !ContextHistory.TimstampDiff>} stored results */
ContextHistory.prototype.getCachedPrePauses_ = function() {
  if (this.history_.length < 10) {
    return {};  // skip processing without some real data
  }

  // Try to fetch results from cache
  if (!underscore.isEmpty(this.cachedPrePauses_)) {
    var largestHistorySize = Object.
        keys(this.cachedPrePauses_).
        map(function(historySize, idx) {
          return parseInt(historySize, 10  /*radix*/);
        }).
        sort().
        pop();

    if (largestHistorySize < this.history_.length * 2) {
      return this.cachedPrePauses_[largestHistorySize];  // use cache
    }
  }

  // Populate and return from cache
  return this.cachedPrePauses_[this.history_.length] = (function() {
    var map = {};
    this.findSignificantDelats_(ContextHistory.HISTORY_OUTLIERS_MAJOR).
        forEach(function(diff) {
          map[diff.previous] = diff;
        }.bind(this));
    return map;
  }.bind(this))();
};

/**
 * @param {boolean} forMajor
 * @return {!Array.<!ContextHistory.TimestampDiff>}
 * @private
 */
ContextHistory.prototype.findSignificantDelats_ = function(forMajor) {
  var outliers = new Outliers(this.buildTimestampOutlierDeltas_());
  return forMajor ? outliers.getMajorOutliers() : outliers.getMinorOutliers();
};

/**
 * @return {!Array.<!Outliers.Value>}
 *     Sorted deltas, one less than number of {@link RenderUpdate}s in history.
 * @private
 */
ContextHistory.prototype.buildTimestampOutlierDeltas_ = function() {
  return this.history_.
      map(function(update, index, history) {
        if (index === 0) {
          return null;  // should be sliced off our results
        }

        var current = ContextHistory.getSingleRender_(update).timeStamp;
        var previous = ContextHistory.
            getSingleRender_(history[index - 1]).timeStamp;
        return new Outliers.Value({
          previous: previous,
          next: current,
          delta: current - previous
        }, ContextHistory.getDeltaFromDiff_);
      }).

      slice(1).  // remove index 0, where we returned null

      sort(function(diffA, diffB) {
        if (diffA.getValue() === diffB.getValue()) {
          return 0;
        }

        return diffA.getValue() < diffB.getValue() ? -1 : 1;
      });
};

/** @return {!ContextHistory.Update} */
ContextHistory.prototype.getPrePauseHistory = function() {
  return this.history_.filter(this.isUpdatePrePause.bind(this));
};

/** @return {boolean} */
ContextHistory.prototype.isEmpty = function() {
  return !Boolean(this.history_.length);
};

/**
 * @param {!RenderUpdate|!Array.<!RenderUpdate>} update
 * @return {boolean}
 */
ContextHistory.prototype.isUpdatePrePause = function(update) {
  return this.isUpdateStampPrePause(
      ContextHistory.getSingleRender_(update).timeStamp);
};

/**
 * @param {number} timeStamp
 * @return {boolean}
 */
ContextHistory.prototype.isUpdateStampPrePause = function(timeStamp) {
  return Boolean(this.getCachedPrePauses_()[timeStamp]);
};

/**
 * @return {number} Array index of the final possible {@link RenderUpdate}.
 * @private
 */
ContextHistory.prototype.getEndingIndex_ = function() {
  return this.history_.length - 1;
};

/**
 * Erases {@code context} without updating {@link ContextHistory}.
 *
 * @param {!CanvasRenderingContext2D} context
 */
ContextHistory.erase = function(context) {
  context.clearRect(
      0,  // pointX
      0,  // pointY
      context.canvas.width,
      context.canvas.height);
};

/**
 * Re-renders all previously rendered touch surface animations through the
 * present to the future, unless {@code opt_endIndex} is present.
 *
 * @param {!CanvasRenderingContext2D} context
 * @param {number=} opt_endIndex
 */
ContextHistory.prototype.playBack = function(context, opt_endIndex) {
  var isPartialPlayback = underscore.
      isNumber(opt_endIndex) && opt_endIndex !== this.getEndingIndex_();

  var isRewind = isPartialPlayback && opt_endIndex < this.getPresentIndex();
  var isRedo = isPartialPlayback && opt_endIndex > this.getPresentIndex();
  if (isRewind) {
    ContextHistory.erase(context);  // we're rewinding, clear the canvas first
  }

  var isPartialPlaybackInProgress = isPartialPlayback;
  this.history_.forEach(function(update, index, history) {
    if (isRedo && index <= this.getPresentIndex()) {
      return;  // ignore: this is old history, already rendered
    }

    if (isPartialPlayback) {
      if (!isPartialPlaybackInProgress) {
        return;  // we've already finished partially rerendering
      } else if (index > opt_endIndex) {
        this.setRedoBranch_(index - 1);
        isPartialPlaybackInProgress = false;
        return;  // last update was the end of partial rendering
      }
    }

    ContextHistory.playBackUpdate_(context, update);
  }.bind(this));

  if (!isPartialPlayback) {
    this.redoBranchIndex_ = null;
  }
};

/**
 * @param {!CanvasRenderingContext2D} context
 * @param {!ContextHistory.Update} update
 * @private
 */
ContextHistory.playBackUpdate_ = function(context, update) {
  (ContextHistory.isStrokedRender_(update) ? update : [update]).
      forEach(function(renderUpdate) {
        renderUpdate.playBack(context);
      });
};

/**
 * @return {boolean}
 *     Whether we're rewound in history, and have more {@link RenderUpdate}s to
 *     potentially playback.
 */
ContextHistory.prototype.isRedoPossible = function() {
  return underscore.isNumber(this.redoBranchIndex_);
};

/** @return {number} */
ContextHistory.prototype.countPossibleRedos = function() {
  return this.isRedoPossible() ?
      this.getEndingIndex_() - this.getPresentIndex() :
      0;
};

/** @return {boolean} Whether we've any history older than our current state. */
ContextHistory.prototype.isUndoPossible = function() {
  return Boolean(this.countPossibleUndos());
};

/**
 * @param {number=} opt_historyIndex
 *     Optionally specify a historical index for which to get a timeStamp.
 *     Defaults to {@link #getPresentIndex}.
 * @return {number} timeStamp
 */
ContextHistory.prototype.getTimeStamp = function(opt_historyIndex) {
  var historyIndex = underscore.isUndefined(opt_historyIndex) ?
                     this.getPresentIndex() :
                     opt_historyIndex;
  return ContextHistory.getSingleRender_(this.history_[historyIndex]).timeStamp;
};

/** @return {number} */
ContextHistory.prototype.countPossibleUndos = function() {
  return this.getPresentIndex() + 1  /* we can always undo [0] */;
};

/** @return {?RenderUpdate} created after the current  */
ContextHistory.prototype.getNextUpdateIndex = function() {
  return this.isRedoPossible() ? this.getPresentIndex() + 1 : null;
};

/** @return {?number} index occurring just before the current */
ContextHistory.prototype.getPreviousUpdateIndex = function() {
  return this.isUndoPossible() ? this.getPresentIndex() - 1 : null;
};

/** @return {number} index of the most recently rendered update in history.*/
ContextHistory.prototype.getPresentIndex = function() {
  return this.isRedoPossible() ? this.redoBranchIndex_ : this.getEndingIndex_();
};

/**
 * Throws if {@code index} is invalid or the current {@link #history_}.
 *
 * @param {number} index
 * @private
 */
ContextHistory.prototype.assertValidHistoryIndex_ = function(index) {
  if (index >= 0 && index <= this.getEndingIndex_()) {
    return;
  }

  throw new Error(
      'Invalid history(length: ' +  this.history_.length + ') index, ' +
      '"' + index + '"; expected value ' +
      '[0-' + this.getEndingIndex_() + ']');
};

/**
 * Mark history as totally rewound to before its first {@link RenderUpdate}
 *
 * @param {!CanvasRenderingContext2D} context
 */
ContextHistory.prototype.undoAll = function(context) {
  ContextHistory.erase(context);
  this.redoBranchIndex_ = -1;
};

/**
 * @param {number} historyIndex
 * @private
 */
ContextHistory.prototype.setRedoBranch_ = function(historyIndex) {
  if (historyIndex !== -1 ) {
    this.assertValidHistoryIndex_(historyIndex);
  }

  this.redoBranchIndex_ = historyIndex === this.getEndingIndex_() ?
      null :  // We're at the forefront of history
      historyIndex;
};

/**
 * Records {@code relatedUpdates} for reference among wider history of updates.
 *
 * NOTE: Multiple {@link RenderUpdate} events may have the same trigger time,
 * but have been rendered in a particular order. This means, for example, that
 * if {@code renderUpdates} first element draws the first half of a line and the
 * second element draws the second half of a line, that significance is lost
 * here (the same is true for two simultaneously rendered items: eg, with
 * multi-touch events).
 *
 * @param {number} recordingTimeStamp
 *     {@code relatedUpdates}'s last newest timeStamp causing this recording.
 * @param {!Array.<!RenderUpdate>} relatedUpdates
 */
ContextHistory.prototype.recordRelatedUpdates = function(
    recordingTimeStamp, relatedUpdates) {
  this.maybeBranchForUpdatesAt_(recordingTimeStamp);

  var isStrokedRender = relatedUpdates.length > 1;
  var updates = isStrokedRender ? relatedUpdates : relatedUpdates[0];

  this.history_.push(updates);

  if (!isStrokedRender) {
    this.history_ = this.history_.sort(ContextHistory.compareRenderUpdates_);
  }
};

/**
 * @param {number} branchTimeStamp
 * @private
 */
ContextHistory.prototype.maybeBranchForUpdatesAt_ = function(branchTimeStamp) {
  if (!this.isRedoPossible()) {
    return;  // nothing to branch
  }

  var presentIndex = this.getPresentIndex();
  this.branches_[presentIndex] = this.branches_[presentIndex] || [];
  this.branches_[presentIndex].push({
    branchedAt: branchTimeStamp,
    history: this.history_.splice(presentIndex + 1, this.history_.length)
  });
  this.redoBranchIndex_ = null;
};

/** @return {Array.<!ContextHistory.Branch>} for the current point in history */
ContextHistory.prototype.getHistoricalBranches = function() {
  return this.branches_[this.getPresentIndex()] || null;
};

/** @return {boolean} */
ContextHistory.prototype.hasHistoricalPauses = function() {
  return underscore.some(this.cachedPrePauses_, function(update) {
    return Boolean(update && update.length);
  });
};

/** @return {boolean} */
ContextHistory.prototype.hasHistoricalBranches = function() {
  return !underscore.isEmpty(this.branches_);
};

/** @return {boolean} whether current point in history has alternatives. */
ContextHistory.prototype.isBranchedHistoryAvailable = function() {
  return Boolean(this.getHistoricalBranches());
};
<|endoftext|>


<commit_before>(function (undefined) {
    "use strict";

    /**
     * Esta es la clase RemoteDAO verdadera que genera el RemoteDAOFactory
     * @param {String} entityName Nombre de la entidad
     * @param {String} ix3Configuration Configuracion de ix3 para obtener la URL base de API REST
     * @param {Http} $http Servicio de Http de AngularJS
     * @param {Q} $q Servicio de promesas de AngularJS
     */
    RemoteDAO.$inject = ['entityName', 'ix3Configuration', '$http', '$q'];
    function RemoteDAO(entityName, ix3Configuration, $http, $q) {
        this.entityName = entityName;
        this.baseUrl = ix3Configuration.server.api;
        this.$http = $http;
        this.$q = $q;

        this.create = function (expand, parent) {
            var deferred = this.$q.defer();

            var params = {};
            if (parent) {
                angular.extend(params, parent);
            }
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'GET',
                url: this.baseUrl + '/' + this.entityName + "/$create",
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else {
                    throw new Error("Fallo al crear la entidad los datos:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };
        this.get = function (id, expand) {
            var deferred = this.$q.defer();

            var params = {};
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'GET',
                url: this.baseUrl + '/' + this.entityName + "/" + id,
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else {
                    throw new Error("Fallo al obtener la entidad:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };
        this.insert = function (entity, expand) {
            var deferred = this.$q.defer();

            var params = {};
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'POST',
                url: this.baseUrl + '/' + this.entityName + "/",
                params: params,
                data: entity
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else {
                    throw new Error("Fallo al insertar la entidad:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };
        this.update = function (id, entity, expand) {
            var deferred = this.$q.defer();

            var params = {};
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'PUT',
                url: this.baseUrl + '/' + this.entityName + "/" + id,
                params: params,
                data: entity
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else {
                    throw new Error("Fallo al insertar la entidad:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };
        this.delete = function (id) {
            var deferred = this.$q.defer();

            var params = {};

            var config = {
                method: 'DELETE',
                url: this.baseUrl + '/' + this.entityName + "/" + id,
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else {
                    throw new Error("Fallo al borrar la entidad:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };
        this.search = function (filters, order, expand, pageNumber, pageSize) {
            var deferred = this.$q.defer();

            var params = {};
            if (filters) {

                for (var key in filters) {
                    if (!filters.hasOwnProperty(key)) {
                        continue;
                    }

                    if (key.charAt(0) === "$") {
                        var operator=key.substr(1).toUpperCase();
                        var filtersWithOperator=filters[key];  //Objeto con filtros cuya operador es "operator"

                        for (var propertyName in filtersWithOperator) {
                            if (!filtersWithOperator.hasOwnProperty(propertyName)) {
                                continue;
                            }

                            params[propertyName + "__" + operator + "__" ] = filtersWithOperator[propertyName];
                        }
                    } else {
                        //Los filtros que cuelgan directamente son del tipo "EQ" y no hace falta poner nada.
                        params[key] = filters[key];
                    }

                }

            }
            if (order) {
                params.$orderby = "";
                for (var i = 0; i < order.length; i++) {
                    var simpleOrder = order[i];
                    if (params.$orderby !== "") {
                        params.$orderby = params.$orderby + ",";
                    }
                    params.$orderby = params.$orderby + simpleOrder.fieldName + " " + simpleOrder.orderDirection;
                }
            }

            if (expand) {
                params.$expand = expand;
            }
            if ((pageNumber >= 0) && (pageSize > 0)) {
                params.$pagenumber = pageNumber;
                params.$pagesize = pageSize;
            }

            var config = {
                method: 'GET',
                url: this.baseUrl + '/' + this.entityName,
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else {
                    throw new Error("Fallo al buscar los datos:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };

        this.getChild = function (id, child, expand) {
            var deferred = this.$q.defer();

            var params = {};
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'GET',
                url: this.baseUrl + '/' + this.entityName + "/" + id + "/" + child,
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un array vacio
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else {
                    throw new Error("Fallo al obtener la entidad hija:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };

        this.metadata = function (expand) {
            var deferred = this.$q.defer();

            var params = {};
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'GET',
                url: this.baseUrl + '/' + this.entityName + "/$metadata",
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else {
                    throw new Error("Fallo al obtener los metadatos:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };

    }

    RemoteDAOFactory.$inject = ['$injector', 'extendRemoteDAO'];
    function RemoteDAOFactory($injector, extendRemoteDAO) {

        var remoteDAOs = {
        };

        this.getRemoteDAO = function (entityName) {
            if (!remoteDAOs[entityName]) {
                var locals = {
                    entityName: entityName
                };
                remoteDAOs[entityName] = $injector.instantiate(RemoteDAO, locals);

                if (extendRemoteDAO[entityName]) {
                    var locals = {
                        remoteDAO: remoteDAOs[entityName]
                    };
                    for (var i = 0; i < extendRemoteDAO[entityName].length; i++) {
                        $injector.invoke(extendRemoteDAO[entityName][i], undefined, locals);
                    }
                }

            }

            return remoteDAOs[entityName];
        };
    }

    RemoteDAOFactoryProvider.$inject = [];
    function RemoteDAOFactoryProvider() {
        var extendRemoteDAO = {
        };

        this.addExtendRemoteDAO = function (entityName, fn) {
            if (!extendRemoteDAO[entityName]) {
                extendRemoteDAO[entityName] = [];
            }
            extendRemoteDAO[entityName].push(fn);
        };

        this.$get = ['$injector', function ($injector) {
                var locals = {
                    extendRemoteDAO: extendRemoteDAO
                };
                return $injector.instantiate(RemoteDAOFactory, locals);
            }];
    }

    angular.module("es.logongas.ix3").provider("remoteDAOFactory", RemoteDAOFactoryProvider);

})();<commit_msg>Que no de error si el statu de una llamada http es 0 ya que suele ser cuando estamos cambiando de página.<commit_after>(function (undefined) {
    "use strict";

    /**
     * Esta es la clase RemoteDAO verdadera que genera el RemoteDAOFactory
     * @param {String} entityName Nombre de la entidad
     * @param {String} ix3Configuration Configuracion de ix3 para obtener la URL base de API REST
     * @param {Http} $http Servicio de Http de AngularJS
     * @param {Q} $q Servicio de promesas de AngularJS
     */
    RemoteDAO.$inject = ['entityName', 'ix3Configuration', '$http', '$q'];
    function RemoteDAO(entityName, ix3Configuration, $http, $q) {
        this.entityName = entityName;
        this.baseUrl = ix3Configuration.server.api;
        this.$http = $http;
        this.$q = $q;

        this.create = function (expand, parent) {
            var deferred = this.$q.defer();

            var params = {};
            if (parent) {
                angular.extend(params, parent);
            }
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'GET',
                url: this.baseUrl + '/' + this.entityName + "/$create",
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else if (status === 0) {
                    //No hacemos nada. Suele ser cuando el navegador ha cancelado la petición y estamos cambiando de página
                } else {
                    throw new Error("Fallo al crear la entidad los datos:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };
        this.get = function (id, expand) {
            var deferred = this.$q.defer();

            var params = {};
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'GET',
                url: this.baseUrl + '/' + this.entityName + "/" + id,
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else if (status === 0) {
                    //No hacemos nada. Suele ser cuando el navegador ha cancelado la petición y estamos cambiando de página
                } else {
                    throw new Error("Fallo al obtener la entidad:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };
        this.insert = function (entity, expand) {
            var deferred = this.$q.defer();

            var params = {};
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'POST',
                url: this.baseUrl + '/' + this.entityName + "/",
                params: params,
                data: entity
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else if (status === 0) {
                    //No hacemos nada. Suele ser cuando el navegador ha cancelado la petición y estamos cambiando de página
                } else {
                    throw new Error("Fallo al insertar la entidad:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };
        this.update = function (id, entity, expand) {
            var deferred = this.$q.defer();

            var params = {};
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'PUT',
                url: this.baseUrl + '/' + this.entityName + "/" + id,
                params: params,
                data: entity
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else if (status === 0) {
                    //No hacemos nada. Suele ser cuando el navegador ha cancelado la petición y estamos cambiando de página
                } else {
                    throw new Error("Fallo al insertar la entidad:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };
        this.delete = function (id) {
            var deferred = this.$q.defer();

            var params = {};

            var config = {
                method: 'DELETE',
                url: this.baseUrl + '/' + this.entityName + "/" + id,
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else if (status === 0) {
                    //No hacemos nada. Suele ser cuando el navegador ha cancelado la petición y estamos cambiando de página
                } else {
                    throw new Error("Fallo al borrar la entidad:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };
        this.search = function (filters, order, expand, pageNumber, pageSize) {
            var deferred = this.$q.defer();

            var params = {};
            if (filters) {

                for (var key in filters) {
                    if (!filters.hasOwnProperty(key)) {
                        continue;
                    }

                    if (key.charAt(0) === "$") {
                        var operator=key.substr(1).toUpperCase();
                        var filtersWithOperator=filters[key];  //Objeto con filtros cuya operador es "operator"

                        for (var propertyName in filtersWithOperator) {
                            if (!filtersWithOperator.hasOwnProperty(propertyName)) {
                                continue;
                            }

                            params[propertyName + "__" + operator + "__" ] = filtersWithOperator[propertyName];
                        }
                    } else {
                        //Los filtros que cuelgan directamente son del tipo "EQ" y no hace falta poner nada.
                        params[key] = filters[key];
                    }

                }

            }
            if (order) {
                params.$orderby = "";
                for (var i = 0; i < order.length; i++) {
                    var simpleOrder = order[i];
                    if (params.$orderby !== "") {
                        params.$orderby = params.$orderby + ",";
                    }
                    params.$orderby = params.$orderby + simpleOrder.fieldName + " " + simpleOrder.orderDirection;
                }
            }

            if (expand) {
                params.$expand = expand;
            }
            if ((pageNumber >= 0) && (pageSize > 0)) {
                params.$pagenumber = pageNumber;
                params.$pagesize = pageSize;
            }

            var config = {
                method: 'GET',
                url: this.baseUrl + '/' + this.entityName,
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else if (status === 0) {
                    //No hacemos nada. Suele ser cuando el navegador ha cancelado la petición y estamos cambiando de página
                } else {
                    throw new Error("Fallo al buscar los datos:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };

        this.getChild = function (id, child, expand) {
            var deferred = this.$q.defer();

            var params = {};
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'GET',
                url: this.baseUrl + '/' + this.entityName + "/" + id + "/" + child,
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un array vacio
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else if (status === 0) {
                    //No hacemos nada. Suele ser cuando el navegador ha cancelado la petición y estamos cambiando de página
                } else {
                    throw new Error("Fallo al obtener la entidad hija:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };

        this.metadata = function (expand) {
            var deferred = this.$q.defer();

            var params = {};
            if (expand) {
                params.$expand = expand;
            }

            var config = {
                method: 'GET',
                url: this.baseUrl + '/' + this.entityName + "/$metadata",
                params: params
            };

            this.$http(config).success(function (data, status, headers, config) {
                if (status === 204) {
                    //El 204 (no content) realmente es un null
                    deferred.resolve(null);
                } else {
                    deferred.resolve(data);
                }
            }).error(function (data, status, headers, config) {
                if (status === 400) {
                    deferred.reject(data);
                } else if (status === 0) {
                    //No hacemos nada. Suele ser cuando el navegador ha cancelado la petición y estamos cambiando de página
                } else {
                    throw new Error("Fallo al obtener los metadatos:" + status + "\n" + data);
                }
            });

            return deferred.promise;
        };

    }

    RemoteDAOFactory.$inject = ['$injector', 'extendRemoteDAO'];
    function RemoteDAOFactory($injector, extendRemoteDAO) {

        var remoteDAOs = {
        };

        this.getRemoteDAO = function (entityName) {
            if (!remoteDAOs[entityName]) {
                var locals = {
                    entityName: entityName
                };
                remoteDAOs[entityName] = $injector.instantiate(RemoteDAO, locals);

                if (extendRemoteDAO[entityName]) {
                    var locals = {
                        remoteDAO: remoteDAOs[entityName]
                    };
                    for (var i = 0; i < extendRemoteDAO[entityName].length; i++) {
                        $injector.invoke(extendRemoteDAO[entityName][i], undefined, locals);
                    }
                }

            }

            return remoteDAOs[entityName];
        };
    }

    RemoteDAOFactoryProvider.$inject = [];
    function RemoteDAOFactoryProvider() {
        var extendRemoteDAO = {
        };

        this.addExtendRemoteDAO = function (entityName, fn) {
            if (!extendRemoteDAO[entityName]) {
                extendRemoteDAO[entityName] = [];
            }
            extendRemoteDAO[entityName].push(fn);
        };

        this.$get = ['$injector', function ($injector) {
                var locals = {
                    extendRemoteDAO: extendRemoteDAO
                };
                return $injector.instantiate(RemoteDAOFactory, locals);
            }];
    }

    angular.module("es.logongas.ix3").provider("remoteDAOFactory", RemoteDAOFactoryProvider);

})();<|endoftext|>


<commit_before>import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import Divider from '@material-ui/core/Divider'

import Logo from 'js/components/Logo/Logo'
import {
  adblockerWhitelistingForSearchURL,
  externalContactUsURL,
  financialsURL,
  jobsURL,
  privacyPolicyURL,
  searchHomeURL,
  searchExternalHelpURL,
  teamURL,
  termsOfServiceURL,
} from 'js/navigation/navigation'
import Link from 'js/components/General/Link'
import { SEARCH_APP } from 'js/constants'

// Styles match tab-homepage style.

//  Footer link component
const footerLinkStyles = theme => ({
  footerLink: {
    fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
    textDecoration: 'none',
    color: '#cecece',
    fontSize: 12,
    margin: 20,
    '&:hover': {
      color: '#838383',
    },
  },
})

const FooterLinkComponent = props => {
  const { children, classes, ...otherProps } = props
  return (
    <Link {...otherProps} className={classes.footerLink}>
      {children}
    </Link>
  )
}

FooterLinkComponent.propTypes = {
  children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
  classes: PropTypes.object.isRequired,
}
FooterLinkComponent.defaultProps = {}
const FooterLink = withStyles(footerLinkStyles)(FooterLinkComponent)
export { FooterLink }

// Footer component
const styles = theme => ({
  divider: {
    width: '100%',
    marginBottom: 20,
  },
  contentContainer: {
    display: 'flex',
    justifyContent: 'flex-start',
    flexWrap: 'nowrap',
    alignItems: 'center',
  },
  textLinkContainer: {
    marginLeft: 30,
    display: 'flex',
    flexWrap: 'wrap',
    justifyContent: 'flex-start',
  },
})

class Footer extends React.Component {
  render() {
    const { classes, style } = this.props

    // Currently only for Search for a Cause. Customize if used
    // for other apps.
    return (

        <Divider className={classes.divider} />

          <Link to={searchHomeURL}>
            <Logo
              brand={SEARCH_APP}
              color={'grey'}
              style={{
                width: 43,
                height: 43,
              }}
            />
          </Link>

            <FooterLink to={searchExternalHelpURL}>Help</FooterLink>
            <FooterLink to={adblockerWhitelistingForSearchURL}>
              Adblockers
            </FooterLink>
            <FooterLink to={financialsURL}>Financials</FooterLink>
            <FooterLink to={termsOfServiceURL}>Terms</FooterLink>
            <FooterLink to={privacyPolicyURL}>Privacy</FooterLink>
            <FooterLink to={teamURL}>Team</FooterLink>
            <FooterLink to={externalContactUsURL}>Contact</FooterLink>
            <FooterLink to={jobsURL}>Jobs</FooterLink>

    )
  }
}

Footer.propTypes = {
  style: PropTypes.object,
  classes: PropTypes.object.isRequired,
}

Footer.defaultProps = {
  style: {},
}

export default withStyles(styles)(Footer)
<commit_msg>Revert "Try moving problematic JSS styling inline"<commit_after>import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import Divider from '@material-ui/core/Divider'

import Logo from 'js/components/Logo/Logo'
import {
  adblockerWhitelistingForSearchURL,
  externalContactUsURL,
  financialsURL,
  jobsURL,
  privacyPolicyURL,
  searchHomeURL,
  searchExternalHelpURL,
  teamURL,
  termsOfServiceURL,
} from 'js/navigation/navigation'
import Link from 'js/components/General/Link'
import { SEARCH_APP } from 'js/constants'

// Styles match tab-homepage style.

//  Footer link component
const footerLinkStyles = theme => ({
  footerLink: {
    fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
    textDecoration: 'none',
    color: '#cecece',
    fontSize: 12,
    margin: 20,
    '&:hover': {
      color: '#838383',
    },
  },
})

const FooterLinkComponent = props => {
  const { children, classes, ...otherProps } = props
  return (
    <Link {...otherProps} className={classes.footerLink}>
      {children}
    </Link>
  )
}

FooterLinkComponent.propTypes = {
  children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
  classes: PropTypes.object.isRequired,
}
FooterLinkComponent.defaultProps = {}
const FooterLink = withStyles(footerLinkStyles)(FooterLinkComponent)
export { FooterLink }

// Footer component
const styles = theme => ({
  container: {
    background: 'rgba(128, 128, 128, 0.04)',
    paddingTop: 1,
    paddingBottom: 20,
    paddingLeft: 40,
    paddingRight: 40,
  },
  divider: {
    width: '100%',
    marginBottom: 20,
  },
  contentContainer: {
    display: 'flex',
    justifyContent: 'flex-start',
    flexWrap: 'nowrap',
    alignItems: 'center',
  },
  textLinkContainer: {
    marginLeft: 30,
    display: 'flex',
    flexWrap: 'wrap',
    justifyContent: 'flex-start',
  },
})

class Footer extends React.Component {
  render() {
    const { classes, style } = this.props

    // Currently only for Search for a Cause. Customize if used
    // for other apps.
    return (

        <Divider className={classes.divider} />

          <Link to={searchHomeURL}>
            <Logo
              brand={SEARCH_APP}
              color={'grey'}
              style={{
                width: 43,
                height: 43,
              }}
            />
          </Link>

            <FooterLink to={searchExternalHelpURL}>Help</FooterLink>
            <FooterLink to={adblockerWhitelistingForSearchURL}>
              Adblockers
            </FooterLink>
            <FooterLink to={financialsURL}>Financials</FooterLink>
            <FooterLink to={termsOfServiceURL}>Terms</FooterLink>
            <FooterLink to={privacyPolicyURL}>Privacy</FooterLink>
            <FooterLink to={teamURL}>Team</FooterLink>
            <FooterLink to={externalContactUsURL}>Contact</FooterLink>
            <FooterLink to={jobsURL}>Jobs</FooterLink>

    )
  }
}

Footer.propTypes = {
  style: PropTypes.object,
  classes: PropTypes.object.isRequired,
}

Footer.defaultProps = {
  style: {},
}

export default withStyles(styles)(Footer)
<|endoftext|>


<commit_before>/* globals performance */
var CASES = require('./list')
var extend = require('../lib/util/extend')
var createREGL = require('../../regl')
var gitCommits = require('git-commits');
var path = require('path');
var present = require('present');

const WIDTH = 384
const HEIGHT = 240
var regl
var isHeadless = false
var canvas
var gl

if (isHeadless) {
  var gl = require('gl')(WIDTH, HEIGHT)

} else {
  canvas = document.createElement('canvas')
  gl = canvas.getContext('webgl', {
    antialias: false,
    stencil: true,
    preserveDrawingBuffer: true
  })
  canvas.style.position = 'fixed'
  canvas.style.top = '0'
  canvas.style.right = '0'

  canvas.style.width = WIDTH + 'px'
  canvas.style.height = HEIGHT + 'px'
  canvas.width = WIDTH
  canvas.height = HEIGHT

  document.body.appendChild(canvas)

}

regl = createREGL(gl)

function analyze (samples, fmt) {
  // Moments
  var m0 = samples.length
  var m1 = 0.0
  var m2 = 0.0
  for (var i = 0; i < m0; ++i) {
    var x = samples[i]
    m1 += x
    m2 += Math.pow(x, 2)
  }

  // Descriptive stats
  var mean = m1 / m0
  var stddev = Math.sqrt(m2 / m0 - Math.pow(mean, 2))

  // Order stats
  var sorted = samples.slice().sort(function (a, b) {
    return a - b
  })

  return [
    'μ=', fmt(mean), '∓', fmt(stddev),
    ', q=[',
    fmt(sorted[(0.5 * m0) | 0]), ', ',
    fmt(sorted[(0.95 * m0) | 0]), ', ',
    fmt(sorted[m0 - 1]), ']'
  ].join('')
}

function sigfigs (x) {
  var xr = Math.round(x * 100)
  return (xr / 100)
}

function formatTime (x) {
  if (x > 1000) {
    return sigfigs(x / 1000.0) + 's'
  }
  if (x > 1) {
    return sigfigs(x) + 'ms'
  }
  return sigfigs(x * 1e3) + 'μs'
}

function formatMemory (x) {
  if (x > (1 << 20)) {
    return sigfigs(x / (1 << 20)) + 'Mb'
  }
  if (x > (1 << 10)) {
    return sigfigs(x / (1 << 10)) + 'kb'
  }
  return x + 'b'
}

function benchmark (procedure, samples, warmupSamples) {
  var timeSamples = []
  var heapSamples = []

  function sample (tick) {
    regl.clear({
      color: [ 0, 0, 0, 0 ],
      depth: 1,
      stencil: 0
    })
    var start = present()//performance.now()
    procedure({tick: tick})
    timeSamples.push(present() - start)

    // dont have this in headless.
//    heapSamples.push(performance.memory.usedJSHeapSize)
  }

  return function run () {

    var i
    for (i = 0; i < warmupSamples; ++i) {
      regl.clear({
        color: [ 0, 0, 0, 0 ],
        depth: 1,
        stencil: 0
      })
      regl.updateTimer()

      procedure({tick: i})
    }

    timeSamples.length = 0
    heapSamples.length = 0

    for (i = 0; i < samples; i++) {
      regl.updateTimer()

      sample(i)
    }

    //    console.log("samples: ", timeSamples)
    return {
      n: timeSamples.length,
      time: analyze(timeSamples, formatTime)
//      space: analyze(heapSamples, formatMemory)
    }
  }
}

function button (text, onClick) {
  var result = document.createElement('a')
  result.text = text
  result.href = '#' + text
  result.addEventListener('click', onClick)

  var statNode = document.createElement('h5')
  statNode.innerText = 'n:0, t:(---), m:(---)'
  extend(statNode.style, {
    'margin': '4px',
    'display': 'inline'
  })

  var buttonContainer = document.createElement('div')
  buttonContainer.appendChild(result)
  buttonContainer.appendChild(statNode)
  document.body.appendChild(buttonContainer)

  return {
    link: result,
    text: statNode,
    container: buttonContainer
  }
}

Object.keys(CASES).map(function (caseName) {

  var obj = CASES[caseName]

  var proc
  if(caseName === 'cube-webgl') {
    proc = obj.proc(gl, WIDTH, HEIGHT)
  } else {
    proc = obj.proc(regl)
  }

  var sample = benchmark(proc, obj.samples, obj.warmupSamples)

  if (isHeadless) {
    var bench = sample()
    console.log(caseName + " : " +  'n:' + bench.n + ', t:(' + bench.time + '),')
  } else {
    var result
    result = button(caseName, function () {
      var bench = sample()
      result.text.innerText = 'n:' + bench.n + ', t:(' + bench.time + '),' //+ 'm:(' + bench.space + ')'
      return result

    })
  }

})

//document.removeChild(document.documentElement);

/*
  var c =  document.getElementsByTagName("canvas")[0]
  //http://130.241.188.19:9966/
  document.body.removeChild(c);

  var p = document.createElement('p')
  p.innerHTML = json
  document.body.appendChild(p)
*/

/*
  clearn:100, t:(μ=10.3μs∓37.43μs, q=[5μs, 30μs, 310μs]), m:(μ=12.11Mb∓0b, q=[12.11Mb, 12.11Mb, 12.11Mb])
cuben:30000, t:(μ=12.24μs∓78.41μs, q=[10μs, 15μs, 9.18ms]), m:(μ=12.11Mb∓0b, q=[12.11Mb, 12.11Mb, 12.11Mb])
cube_webgln:30000, t:(μ=14.46μs∓88.63μs, q=[15μs, 15μs, 7.32ms]), m:(μ=12.11Mb∓0b, q=[12.11Mb, 12.11Mb, 12.11Mb])
  */

//     draw('for(var gggg = 0; gggg < 1000; gggg++) ', env.shared.gl, '.useProgram(', program, '.program);')
<commit_msg>Now headless is automatically detected in the benchmarks.<commit_after>/* globals performance */
var CASES = require('./list')
var extend = require('../lib/util/extend')
var createREGL = require('../../regl')
var gitCommits = require('git-commits');
var path = require('path');
var present = require('present');

const WIDTH = 384
const HEIGHT = 240
var regl
var isHeadless = typeof document === 'undefined'
var canvas
var gl

if (isHeadless) {
  var gl = require('gl')(WIDTH, HEIGHT)

} else {
  canvas = document.createElement('canvas')
  gl = canvas.getContext('webgl', {
    antialias: false,
    stencil: true,
    preserveDrawingBuffer: true
  })
  canvas.style.position = 'fixed'
  canvas.style.top = '0'
  canvas.style.right = '0'

  canvas.style.width = WIDTH + 'px'
  canvas.style.height = HEIGHT + 'px'
  canvas.width = WIDTH
  canvas.height = HEIGHT

  document.body.appendChild(canvas)

}

regl = createREGL(gl)

function analyze (samples, fmt) {
  // Moments
  var m0 = samples.length
  var m1 = 0.0
  var m2 = 0.0
  for (var i = 0; i < m0; ++i) {
    var x = samples[i]
    m1 += x
    m2 += Math.pow(x, 2)
  }

  // Descriptive stats
  var mean = m1 / m0
  var stddev = Math.sqrt(m2 / m0 - Math.pow(mean, 2))

  // Order stats
  var sorted = samples.slice().sort(function (a, b) {
    return a - b
  })

  return [
    'μ=', fmt(mean), '∓', fmt(stddev),
    ', q=[',
    fmt(sorted[(0.5 * m0) | 0]), ', ',
    fmt(sorted[(0.95 * m0) | 0]), ', ',
    fmt(sorted[m0 - 1]), ']'
  ].join('')
}

function sigfigs (x) {
  var xr = Math.round(x * 100)
  return (xr / 100)
}

function formatTime (x) {
  if (x > 1000) {
    return sigfigs(x / 1000.0) + 's'
  }
  if (x > 1) {
    return sigfigs(x) + 'ms'
  }
  return sigfigs(x * 1e3) + 'μs'
}

function formatMemory (x) {
  if (x > (1 << 20)) {
    return sigfigs(x / (1 << 20)) + 'Mb'
  }
  if (x > (1 << 10)) {
    return sigfigs(x / (1 << 10)) + 'kb'
  }
  return x + 'b'
}

function benchmark (procedure, samples, warmupSamples) {
  var timeSamples = []
  var heapSamples = []

  function sample (tick) {
    regl.clear({
      color: [ 0, 0, 0, 0 ],
      depth: 1,
      stencil: 0
    })
    var start = present()//performance.now()
    procedure({tick: tick})
    timeSamples.push(present() - start)

    // dont have this in headless.
//    heapSamples.push(performance.memory.usedJSHeapSize)
  }

  return function run () {

    var i
    for (i = 0; i < warmupSamples; ++i) {
      regl.clear({
        color: [ 0, 0, 0, 0 ],
        depth: 1,
        stencil: 0
      })
      regl.updateTimer()

      procedure({tick: i})
    }

    timeSamples.length = 0
    heapSamples.length = 0

    for (i = 0; i < samples; i++) {
      regl.updateTimer()

      sample(i)
    }

    //    console.log("samples: ", timeSamples)
    return {
      n: timeSamples.length,
      time: analyze(timeSamples, formatTime)
//      space: analyze(heapSamples, formatMemory)
    }
  }
}

function button (text, onClick) {
  var result = document.createElement('a')
  result.text = text
  result.href = '#' + text
  result.addEventListener('click', onClick)

  var statNode = document.createElement('h5')
  statNode.innerText = 'n:0, t:(---), m:(---)'
  extend(statNode.style, {
    'margin': '4px',
    'display': 'inline'
  })

  var buttonContainer = document.createElement('div')
  buttonContainer.appendChild(result)
  buttonContainer.appendChild(statNode)
  document.body.appendChild(buttonContainer)

  return {
    link: result,
    text: statNode,
    container: buttonContainer
  }
}

Object.keys(CASES).map(function (caseName) {

  var obj = CASES[caseName]

  var proc
  if(caseName === 'cube-webgl') {
    proc = obj.proc(gl, WIDTH, HEIGHT)
  } else {
    proc = obj.proc(regl)
  }

  var sample = benchmark(proc, obj.samples, obj.warmupSamples)

  if (isHeadless) {
    var bench = sample()
    console.log(caseName + " : " +  'n:' + bench.n + ', t:(' + bench.time + '),')
  } else {
    var result
    result = button(caseName, function () {
      var bench = sample()
      result.text.innerText = 'n:' + bench.n + ', t:(' + bench.time + '),' //+ 'm:(' + bench.space + ')'
      return result

    })
  }

})

//document.removeChild(document.documentElement);

/*
  var c =  document.getElementsByTagName("canvas")[0]
  //http://130.241.188.19:9966/
  document.body.removeChild(c);

  var p = document.createElement('p')
  p.innerHTML = json
  document.body.appendChild(p)
*/

/*
  clearn:100, t:(μ=10.3μs∓37.43μs, q=[5μs, 30μs, 310μs]), m:(μ=12.11Mb∓0b, q=[12.11Mb, 12.11Mb, 12.11Mb])
cuben:30000, t:(μ=12.24μs∓78.41μs, q=[10μs, 15μs, 9.18ms]), m:(μ=12.11Mb∓0b, q=[12.11Mb, 12.11Mb, 12.11Mb])
cube_webgln:30000, t:(μ=14.46μs∓88.63μs, q=[15μs, 15μs, 7.32ms]), m:(μ=12.11Mb∓0b, q=[12.11Mb, 12.11Mb, 12.11Mb])
  */

//     draw('for(var gggg = 0; gggg < 1000; gggg++) ', env.shared.gl, '.useProgram(', program, '.program);')
<|endoftext|>


<commit_before>angular.module('ui.bootstrap.demo').controller('CarouselDemoCtrl', function ($scope) {
  $scope.myInterval = 5000;
  $scope.noWrapSlides = false;
  var slides = $scope.slides = [];
  var currIndex = 0;

  $scope.addSlide = function() {
    var newWidth = 600 + slides.length + 1;
    slides.push({
      image: '//placekitten.com/' + newWidth + '/300',
      text: ['More','Extra','Lots of','Surplus'][slides.length % 4] + ' ' +
      ['Cats', 'Kittys', 'Felines', 'Cutes'][slides.length % 4],
      id: currIndex++
    });
  };

  $scope.randomize = function() {
    var indexes = generateIndexesArray();
    assignNewIndexesToSlides(indexes);
  };

  for (var i = 0; i < 4; i++) {
    $scope.addSlide();
  }

  // Randomize logic below

  function assignNewIndexesToSlides(indexes) {
    for (var i = 0, l = slides.length; i < l; i++) {
      slides[i].id = indexes.pop();
    }
  }

  function generateIndexesArray() {
    var indexes = [];
    for (var i = 0; i < currIndex; ++i) {
      indexes[i] = i;
    }
    return shuffle(indexes);
  }

  // http://stackoverflow.com/questions/962802#962890
  function shuffle(array) {
    var tmp, current, top = array.length;

    if (top) {
      while (--top) {
        current = Math.floor(Math.random() * (top + 1));
        tmp = array[current];
        array[current] = array[top];
        array[top] = tmp;
      }
    }

    return array;
  }
});
<commit_msg>chore(carousel): change images on demo<commit_after>angular.module('ui.bootstrap.demo').controller('CarouselDemoCtrl', function ($scope) {
  $scope.myInterval = 5000;
  $scope.noWrapSlides = false;
  var slides = $scope.slides = [];
  var currIndex = 0;

  $scope.addSlide = function() {
    var newWidth = 600 + slides.length + 1;
    slides.push({
      image: '//lorempixel.com/' + newWidth + '/300',
      text: ['Nice image','Awesome photograph','That is so cool','I love that'][slides.length % 4],
      id: currIndex++
    });
  };

  $scope.randomize = function() {
    var indexes = generateIndexesArray();
    assignNewIndexesToSlides(indexes);
  };

  for (var i = 0; i < 4; i++) {
    $scope.addSlide();
  }

  // Randomize logic below

  function assignNewIndexesToSlides(indexes) {
    for (var i = 0, l = slides.length; i < l; i++) {
      slides[i].id = indexes.pop();
    }
  }

  function generateIndexesArray() {
    var indexes = [];
    for (var i = 0; i < currIndex; ++i) {
      indexes[i] = i;
    }
    return shuffle(indexes);
  }

  // http://stackoverflow.com/questions/962802#962890
  function shuffle(array) {
    var tmp, current, top = array.length;

    if (top) {
      while (--top) {
        current = Math.floor(Math.random() * (top + 1));
        tmp = array[current];
        array[current] = array[top];
        array[top] = tmp;
      }
    }

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