<commit_before>'use strict';

/*!
 * Module dependencies.
 */
var assert = require('assert');
var restify = require('restify');
var url = require('url');

module.exports = function(options){
  var apiUrls = {};
  apiUrls.ps = '/ps';
  apiUrls.config = '/config';
  apiUrls.status = '/status';
  apiUrls.log = '/log';
  apiUrls.tasks = '/tasks';
  apiUrls.createTask = apiUrls.tasks + '/new';
  apiUrls.collections = '/collections';

  var api = false;
  var client = {};

  /**
   * Setup the restify client
   *
   * @param  {object} options Restify client options like url, timeout and retries
   */
  client.configure = function(options){
    if (!options) { options = {}; }
    var config = {};

    // Cheap and dirty default values.
    config.version = '*';
    config.url = options.url || 'http://localhost:8888';
    if (options.retry) { config.retry = { "retries":options.retry }; }
    config.timeout = options.timeout || 1;

    api = restify.createJsonClient(config);

    if (options.username && options.password) {
      api.basicAuth(options.username, options.password);
    }
  };
  // Configure the restify client
  client.configure(options);

  /**
   * Authenticate your client
   *
   * If you pass username and password keys to the constructor
   * it will authenticate for you
   *
   * @see client.configure()
   * @param  {String} user
   * @param  {String} pass
   */
  client.basicAuth = function(user, pass){
    api.basicAuth(user, pass);
  };

  /**
   * Retreive the status of your ummon server
   *
   * Returns:
   *
   *      // TODO
   *
   * @param  {Function} callback(err, result)
   */
  client.getStatus = function(callback) {
    api.get(apiUrls.status, function(err, req, res, result) {
      callback(err, result);
    })
  };

  /**
   * Return ummon-servers configuration
   *
   * Returns:
   *
   *      // TODO
   *
   * @param  {Function} callback(err, result)
   */
  client.getConfig = function(callback) {
    api.get(apiUrls.config, function(err, req, res, result) {
      callback(err, result);
    })
  };

  /**
   * Set configuration options
   *
   * Example:
   *
   *      client.setConfig( {pause: true}, function(err, result){
   *        console.log('Ummon has been paused');
   *      });
   *
   *
   * @param {Object} options configuration options to set
   * @param  {Function} callback(err, result)
   */
  client.setConfig = function(options, callback) {
    var setConfigUrl = url.parse(apiUrls.config);
    setConfigUrl.query = options;
    setConfigUrl = url.format(setConfigUrl);
    api.put(setConfigUrl, function(err, req, res, result) {
      callback(err, result);
    })
  };

  /**
   * Get the tasks currently in the queue
   *
   * @return {Function}            callback(err, result)
   */
  client.getQueue = function(callback) {
    api.get('/queue', function(err, req, res, result) {
      callback(err, result);
    })
  };

  /**
   * Clear the entire queue or a specific task from it
   *
   * @param  {String}   task     The id of the task, eg: collection.name
   * @param  {Function} callback callback(err, result)
   */
  client.clearQueue = function(task, callback) {
    var url = '/queue/clear';
    if (task) { url += '?task=' + task; }

    api.del(url, function(err, req, res, result) {
      callback(err, result);
    })
  };

  /**
   * Return a portion of the server's log
   *
   * ## Example:
   *
   *      client.showLog({ lines: 500, filter: {collection: 'ummon'}}, function(err, result){
   *        console.log(result)
   *      })
   *
   * ## Options:
   *
   * * lines: The number of lines to retreive
   * * runsOnly: Return only a history of completed tasks, without task output
   * * filter: Object with key collection, task or run
   *
   * @param  {Object}   options  Options to control the filtering of the logs
   * @param  {Function} callback callback(err, result)
   */
  client.showLog = function(options, callback){
    if (!callback && 'function' === typeof options){
      callback = options;
    }

    var query = {};

    ['filter', 'runsOnly', 'start', 'from', 'to', 'follow'].forEach(function(key){
      if (options[key]) { query[key] = options[key]; }
    });

    // Grossly build out the url
    var logUrl = apiUrls.log;

    if (Object.keys(query).length) {
      var count = 0;
      logUrl += '?';
      for (var key in query) {
        if (count > 0) logUrl += '&';
        logUrl += key + '=' + query[key];
        count++;
      }
    }

    api.get(logUrl, function(err, req, res, result) {
      if (err) return callback(err);
      callback(null, res.body); // This is weird that result is empty and res.body isn't
    });
  };

  /**
   * Return the defautls for a particular collection
   *
   * @param  {String}   collection A collection name
   * @param  {Function} callback   callback(err, result)
   */
  client.getCollectionDefaults = function(collection, callback) {
    api.get(apiUrls.collections+'/'+collection+'/defaults', function(err, req, res, result) {
      callback(err, result);
    });
  };

  /**
   * Set the defaults for a particular collection
   *
   * @param  {String}   collection A collection name
   * @param  {Object} config The values to set as defaults, eg: cwd
   * @param  {Function} callback   callback(err, result)
   */
  client.setCollectionDefaults = function(collection, config, callback) {
    api.put(apiUrls.collections+'/'+collection+'/defaults', config, function(err, req, res, result) {
      callback(err, result);
    });
  };

  /**
   * Create a new collection. Also useful for renaming collections
   *
   * @param {String}   collection The new collection's name
   * @param {Object}   config     An object containing defaults, settings and tasks
   * @param {Function} callback   callback(err, result)
   */
  client.setTasks = function(collection, config, callback) {
    api.put(apiUrls.collections+'/'+collection, config, function(err, req, res, result) {
      callback(err, result);
    });
  };

  /**
   * Enable an individual task or a collection of tasks
   *
   * ## Examples:
   *
   *     // Enable a single task
   *     client.enableTasks({ task:'ummon.doAwesome' }, callback)
   *
   *     // Enable a collection
   *     client.enableTasks({ collection:'ummon' }, callback)
   *
   *
   * @param  {Object}   options  Object describing what to enable
   * @param  {Function} callback callback(err, result)
   */
  client.enableTasks = function(options, callback) {
    var enableTasksUrl;
    if (options.collection) {
      enableTasksUrl = apiUrls.collections+'/'+options.collection + '/enable'
    } else if (options.task) {
      enableTasksUrl = apiUrls.tasks+'/' + options.task + '/enable'
    } else {
      return callback(new Error('You did not specify a collection or task to enable'));
    }

    api.put(enableTasksUrl, function(err, req, res, result) {
      if (res.statusCode === 304) {
        err = new Error('Collection already disabled')
      }
      callback(err, result);
    });
  };

  /**
   * Disable an individual task or a collection of tasks
   *
   * ## Examples:
   *
   *     // Enable a single task
   *     client.disableTasks({ task:'ummon.doAwesome' }, callback)
   *
   *     // Enable a collection
   *     client.disableTasks({ collection:'ummon' }, callback)
   *
   *
   * @param  {Object}   options  Object describing what to disable
   * @param  {Function} callback callback(err, result)
   */
  client.disableTasks = function(options, callback) {
    var disableTasksUrl;
    if (options.collection) {
      disableTasksUrl = apiUrls.collections+'/'+options.collection + '/disable'
    } else if (options.task) {
      disableTasksUrl = apiUrls.tasks+'/' + options.task + '/disable'
    } else {
      return callback(new Error('You did not specify a collection or task to disable'));
    }

    api.put(disableTasksUrl, function(err, req, res, result) {
      if (res.statusCode === 304) {
        err = new Error('Collection already disabled')
      }
      callback(err, result);
    });
  };

  /**
   * Return an individual task or a collection of tasks
   *
   * @param  {Object}   options  Object describing what to get
   * @param  {Function} callback callback(err, result)
   */
  client.getTasks = function(options, callback){
    if (!callback && "function" === typeof options) {
      callback = options;
      options = false;
    }
    // URL BUILDER!
    var taskurl;
    if (options.collection) {
      taskurl = apiUrls.collections + '/' + options.collection;
    } else if (options.task) {
      if (options.task.indexOf('.*') !== -1) { options.task = options.task.substr(0, options.task.length-2)}
      taskurl = apiUrls.tasks + '/' + options.task;
    } else {
      taskurl = apiUrls.tasks;
    }

    api.get(taskurl, function(err, req, res, result) {
      // console.log(err, result);
      if (err) {
        return callback(err);
      }

      callback(null, result);
    });
  };

  /**
   * Create a task
   *
   * ## Example
   *
   *      client.createTask({
   *        collection:"ummon",
   *        name: "doAwesome",
   *        command: "sh fixTheWorld.sh",
   *        trigger: {time: '1 * * * *'}},
   *      callback)
   *
   * @param  {Object}   options  Object describing what to create
   * @param  {Function} callback callback(err, result)
   */
  client.createTask = function(config, callback){
    api.post(apiUrls.createTask, config, function(err, req, res, result) {
      if (err) {
        return callback(err);
      }

      callback(null, result);
    });
  };

  /**
   * Update an existing task
   *
   * @param  {Object}   config   A tasks updated config object
   * @param  {Function} callback callback(err, result)
   */
  client.updateTask = function(taskid, config, callback){
    api.put(apiUrls.tasks + '/' + taskid, config, function(err, req, res, result) {
      if (err) {
        return callback(err);
      }

      callback(null, result);
    });
  };

  /**
   * Delete a task
   *
   * @param  {String}   taskid
   * @param  {Function} callback callback(err)
   */
  client.deleteTask = function(taskid, callback){
    api.del(apiUrls.tasks + '/' + taskid, function(err, req, res) {
      callback(err);
    });
  };

  /**
   * Delete a collection, it's defaults and all tasks in it
   *
   * @param  {String}   collection
   * @param  {Function} callback   callback(err)
   */
  client.deleteCollection = function(collection, callback){
    api.del(apiUrls.collections + '/' + collection, function(err, req, res) {
      callback(err);
    });
  };

  return client;
};
<commit_msg>Fewer lines<commit_after>'use strict';

/*!
 * Module dependencies.
 */
var assert = require('assert');
var restify = require('restify');
var url = require('url');

module.exports = function(options){
  var apiUrls = {};
  apiUrls.ps = '/ps';
  apiUrls.config = '/config';
  apiUrls.status = '/status';
  apiUrls.log = '/log';
  apiUrls.tasks = '/tasks';
  apiUrls.createTask = apiUrls.tasks + '/new';
  apiUrls.collections = '/collections';

  var api = false;
  var client = {};

  /**
   * Setup the restify client
   *
   * @param  {object} options Restify client options like url, timeout and retries
   */
  client.configure = function(options){
    if (!options) { options = {}; }
    var config = {};

    // Cheap and dirty default values.
    config.version = '*';
    config.url = options.url || 'http://localhost:8888';
    if (options.retry) { config.retry = { "retries":options.retry }; }
    config.timeout = options.timeout || 1;

    api = restify.createJsonClient(config);

    if (options.username && options.password) {
      api.basicAuth(options.username, options.password);
    }
  };
  // Configure the restify client
  client.configure(options);

  /**
   * Authenticate your client
   *
   * If you pass username and password keys to the constructor
   * it will authenticate for you
   *
   * @see client.configure()
   * @param  {String} user
   * @param  {String} pass
   */
  client.basicAuth = function(user, pass){
    api.basicAuth(user, pass);
  };

  /**
   * Retreive the status of your ummon server
   *
   * Returns:
   *
   *      // TODO
   *
   * @param  {Function} callback(err, result)
   */
  client.getStatus = function(callback) {
    api.get(apiUrls.status, function(err, req, res, result) {
      callback(err, result);
    })
  };

  /**
   * Return ummon-servers configuration
   *
   * Returns:
   *
   *      // TODO
   *
   * @param  {Function} callback(err, result)
   */
  client.getConfig = function(callback) {
    api.get(apiUrls.config, function(err, req, res, result) {
      callback(err, result);
    })
  };

  /**
   * Set configuration options
   *
   * Example:
   *
   *      client.setConfig( {pause: true}, function(err, result){
   *        console.log('Ummon has been paused');
   *      });
   *
   *
   * @param {Object} options configuration options to set
   * @param  {Function} callback(err, result)
   */
  client.setConfig = function(options, callback) {
    var setConfigUrl = url.parse(apiUrls.config);
    setConfigUrl.query = options;
    setConfigUrl = url.format(setConfigUrl);
    api.put(setConfigUrl, function(err, req, res, result) {
      callback(err, result);
    })
  };

  /**
   * Get the tasks currently in the queue
   *
   * @return {Function}            callback(err, result)
   */
  client.getQueue = function(callback) {
    api.get('/queue', function(err, req, res, result) {
      callback(err, result);
    })
  };

  /**
   * Clear the entire queue or a specific task from it
   *
   * @param  {String}   task     The id of the task, eg: collection.name
   * @param  {Function} callback callback(err, result)
   */
  client.clearQueue = function(task, callback) {
    var url = '/queue/clear';
    if (task) { url += '?task=' + task; }

    api.del(url, function(err, req, res, result) {
      callback(err, result);
    })
  };

  /**
   * Return a portion of the server's log
   *
   * ## Example:
   *
   *      client.showLog({ lines: 500, filter: {collection: 'ummon'}}, function(err, result){
   *        console.log(result)
   *      })
   *
   * ## Options:
   *
   * * lines: The number of lines to retreive
   * * runsOnly: Return only a history of completed tasks, without task output
   * * filter: Object with key collection, task or run
   *
   * @param  {Object}   options  Options to control the filtering of the logs
   * @param  {Function} callback callback(err, result)
   */
  client.showLog = function(options, callback){
    if (!callback && 'function' === typeof options){
      callback = options;
    }

    var query = {};

    ['filter', 'runsOnly', 'start', 'from', 'to', 'follow'].forEach(function(key){
      if (options[key]) { query[key] = options[key]; }
    });

    // Grossly build out the url
    var logUrl = apiUrls.log;

    if (Object.keys(query).length) {
      var count = 0;
      logUrl += '?';
      for (var key in query) {
        if (count > 0) logUrl += '&';
        logUrl += key + '=' + query[key];
        count++;
      }
    }

    api.get(logUrl, function(err, req, res, result) {
      if (err) return callback(err);
      callback(null, res.body); // This is weird that result is empty and res.body isn't
    });
  };

  /**
   * Return the defautls for a particular collection
   *
   * @param  {String}   collection A collection name
   * @param  {Function} callback   callback(err, result)
   */
  client.getCollectionDefaults = function(collection, callback) {
    api.get(apiUrls.collections+'/'+collection+'/defaults', function(err, req, res, result) {
      callback(err, result);
    });
  };

  /**
   * Set the defaults for a particular collection
   *
   * @param  {String}   collection A collection name
   * @param  {Object} config The values to set as defaults, eg: cwd
   * @param  {Function} callback   callback(err, result)
   */
  client.setCollectionDefaults = function(collection, config, callback) {
    api.put(apiUrls.collections+'/'+collection+'/defaults', config, function(err, req, res, result) {
      callback(err, result);
    });
  };

  /**
   * Create a new collection. Also useful for renaming collections
   *
   * @param {String}   collection The new collection's name
   * @param {Object}   config     An object containing defaults, settings and tasks
   * @param {Function} callback   callback(err, result)
   */
  client.setTasks = function(collection, config, callback) {
    api.put(apiUrls.collections+'/'+collection, config, function(err, req, res, result) {
      callback(err, result);
    });
  };

  /**
   * Enable an individual task or a collection of tasks
   *
   * ## Examples:
   *
   *     // Enable a single task
   *     client.enableTasks({ task:'ummon.doAwesome' }, callback)
   *
   *     // Enable a collection
   *     client.enableTasks({ collection:'ummon' }, callback)
   *
   *
   * @param  {Object}   options  Object describing what to enable
   * @param  {Function} callback callback(err, result)
   */
  client.enableTasks = function(options, callback) {
    var enableTasksUrl;
    if (options.collection) {
      enableTasksUrl = apiUrls.collections+'/'+options.collection + '/enable'
    } else if (options.task) {
      enableTasksUrl = apiUrls.tasks+'/' + options.task + '/enable'
    } else {
      return callback(new Error('You did not specify a collection or task to enable'));
    }

    api.put(enableTasksUrl, function(err, req, res, result) {
      if (res.statusCode === 304) {
        err = new Error('Collection already disabled')
      }
      callback(err, result);
    });
  };

  /**
   * Disable an individual task or a collection of tasks
   *
   * ## Examples:
   *
   *     // Enable a single task
   *     client.disableTasks({ task:'ummon.doAwesome' }, callback)
   *
   *     // Enable a collection
   *     client.disableTasks({ collection:'ummon' }, callback)
   *
   *
   * @param  {Object}   options  Object describing what to disable
   * @param  {Function} callback callback(err, result)
   */
  client.disableTasks = function(options, callback) {
    var disableTasksUrl;
    if (options.collection) {
      disableTasksUrl = apiUrls.collections+'/'+options.collection + '/disable'
    } else if (options.task) {
      disableTasksUrl = apiUrls.tasks+'/' + options.task + '/disable'
    } else {
      return callback(new Error('You did not specify a collection or task to disable'));
    }

    api.put(disableTasksUrl, function(err, req, res, result) {
      if (res.statusCode === 304) {
        err = new Error('Collection already disabled')
      }
      callback(err, result);
    });
  };

  /**
   * Return an individual task or a collection of tasks
   *
   * @param  {Object}   options  Object describing what to get
   * @param  {Function} callback callback(err, result)
   */
  client.getTasks = function(options, callback){
    if (!callback && "function" === typeof options) {
      callback = options;
      options = false;
    }
    // URL BUILDER!
    var taskurl;
    if (options.collection) {
      taskurl = apiUrls.collections + '/' + options.collection;
    } else if (options.task) {
      if (options.task.indexOf('.*') !== -1) { options.task = options.task.substr(0, options.task.length-2)}
      taskurl = apiUrls.tasks + '/' + options.task;
    } else {
      taskurl = apiUrls.tasks;
    }

    api.get(taskurl, function(err, req, res, result) {
      // console.log(err, result);
      if (err) {
        return callback(err);
      }

      callback(null, result);
    });
  };

  /**
   * Create a task
   *
   * ## Example
   *
   *      client.createTask({
   *        collection:"ummon",
   *        name: "doAwesome",
   *        command: "sh fixTheWorld.sh",
   *        trigger: {time: '1 * * * *'}},
   *      callback)
   *
   * @param  {Object}   options  Object describing what to create
   * @param  {Function} callback callback(err, result)
   */
  client.createTask = function(config, callback){
    api.post(apiUrls.createTask, config, function(err, req, res, result) {
      if (err) return callback(err);
      callback(null, result);
    });
  };

  /**
   * Update an existing task
   *
   * @param  {Object}   config   A tasks updated config object
   * @param  {Function} callback callback(err, result)
   */
  client.updateTask = function(taskid, config, callback){
    api.put(apiUrls.tasks + '/' + taskid, config, function(err, req, res, result) {
      if (err) {
        return callback(err);
      }

      callback(null, result);
    });
  };

  /**
   * Delete a task
   *
   * @param  {String}   taskid
   * @param  {Function} callback callback(err)
   */
  client.deleteTask = function(taskid, callback){
    api.del(apiUrls.tasks + '/' + taskid, function(err, req, res) {
      callback(err);
    });
  };

  /**
   * Delete a collection, it's defaults and all tasks in it
   *
   * @param  {String}   collection
   * @param  {Function} callback   callback(err)
   */
  client.deleteCollection = function(collection, callback){
    api.del(apiUrls.collections + '/' + collection, function(err, req, res) {
      callback(err);
    });
  };

  return client;
};
<|endoftext|>


<commit_before>var processInputMapping = {};
var processOutputMapping = {};
var workflowURI = localStorage.getItem("workflow-uri");
getGraphJSON(workflowURI, function(res) {
    var results = res['results']['bindings'];
    var processNodeIndices = {};
    var putNodeIndices = {};

    /*
        @params: string processName, string inputName
        - inserts the inputName to the list of inputs that the processName maps
        to within the object processInputMapping
    */
    var addInputProcess = function(processName, inputName) {
        processInputMapping[processName] = processInputMapping[processName] || [];
        processInputMapping[processName].push(inputName);
    }

    /*
        @params: string processName, string outputName
        - inserts the outputName to the list of outputs that the processName maps
        to within the object processOutputMapping
    */
    var addOutputProcess = function(processName, outputName) {
        processOutputMapping[processName] = processOutputMapping[processName] || [];
        processOutputMapping[processName].push(outputName);
    }

    /*
        @params: dagreD3 graph
        - pulls from JSON result and sets nodes according to whether or not they are labeled as inputs, outputs, or processes
    */
    var mapNodesEdges = function(graph) {
        var j = 0;
        for (var i = 0; i < results.length; i++) {
            //get URI of step from JSON
            var step = results[i].hasOwnProperty('step') ? results[i]['step']['value'] : null;

            //add step to processNodeIndices if the step hasn't been added yet
            if (processNodeIndices[step] == null) {
                processNodeIndices[step] = j;
                //get readable display for step name
                var stepToDisplay = stripNameFromURI(step);
                stepToDisplay = stepToDisplay.substring(0, stepToDisplay.lastIndexOf('node')) == '' ? stepToDisplay : stepToDisplay.substring(0, stepToDisplay.lastIndexOf('node'));
                vis.setNode(j, {
                    shape: 'process',
                    label: stepToDisplay,
                    labelStyle: "fill: #000",
                    style: "fill: #FFCC99;",
                    uri: step,
                    type: 'process',
                    dimensions: 3
                });
                j++;
            }
            if (results[i].hasOwnProperty('input')) {
                //get URI of input from JSON
                var input = results[i]['input']['value'];
                if (putNodeIndices[input] == null) {
                    putNodeIndices[input] = j;
                    //get readable display for input name
                    var inputToDisplay = stripNameFromURI(input);
                    vis.setNode(j, {
                        label: inputToDisplay,
                        labelStyle: 'fill: #FFF',
                        shape: 'customEllipse',
                        style: 'fill: #003366;',
                        uri: input,
                        type: 'input',
                        dimensions: 2
                    });
                    j++;
                }
                addInputProcess(step, input);
            } else if (results[i].hasOwnProperty('output')) {
                //get URI of output from JSON
                var output = results[i]['output']['value'];
                if (putNodeIndices[output] == null) {
                    putNodeIndices[output] = j;
                    //get readable display for output name
                    var outputToDisplay = stripNameFromURI(output);
                    vis.setNode(j, {
                        label: outputToDisplay,
                        labelStyle: "fill: #FFF",
                        shape: 'customEllipse',
                        style: "fill: #003366;",
                        uri: output,
                        type: 'output',
                        dimensions: 6
                    });
                    j++;
                }
                addOutputProcess(step, output);
            }
        }
        setGraphEdges(vis);
    }

    /*
        @params: dagreD3 vis
        - connects nodes to each other via node numbers from processInputMapping
        and processOutputMapping
    */
    var setGraphEdges = function (vis) {
        for (var process in processInputMapping) {
            for (var i = 0; i < processInputMapping[process].length; i++) {
                vis.setEdge(putNodeIndices[processInputMapping[process][i]], processNodeIndices[process], {
                    style: "stroke: #000; fill:none"
                });
            }
        }
        for (var process in processOutputMapping) {
            for (var i = 0; i < processOutputMapping[process].length; i++) {
                vis.setEdge(processNodeIndices[process], putNodeIndices[processOutputMapping[process][i]], {
                    style: "stroke: #000; fill:none"
                });
            }
        }
        formatInputs(vis, renderGraph);
    }

    var renderGraph = function(vis) {
        // Create the renderer
        var render = new dagreD3.render();

        // Add dimensions to nodes if there are any
        addDimensions(render);
        // Set up an SVG group so that we can translate the final graph.
        var svg = d3.select("svg").attr('width','50%').attr('height','100%'), svgGroup = svg.append("g");

        // Set up zoom support
        var zoom = d3.behavior.zoom().on("zoom", function() {
            svgGroup.attr("transform", "translate(" + d3.event.translate + ")" +
                                        "scale(" + d3.event.scale + ")");
        });
        svg.call(zoom);

        // Run the renderer. This is what draws the final graph.
        render(svgGroup, vis);

        //centers graph and calculates top margin of graph based on screen size
        var xCenterOffset = (document.getElementsByClassName('visualization-container')[0].clientWidth / 2 - vis.graph().width) / 2;
        var yTopMargin = screen.height * .05;
        var scale = .75;
        zoom.translate([xCenterOffset, 20])
            .scale(scale)
            .event(svg);
        svg.attr('height', vis.graph().height * scale + yTopMargin);

        setupNodeOnClick(svg, vis);
    }

    /*

    */
    var formatInputs = function(vis, callback) {
        getInputs(workflowURI, function (inputs) {
            for (var i = 0; i < inputs.length; i++) {
                var nodeIndex = putNodeIndices[inputs[i].input.value];
                var newLabel = vis.node(nodeIndex).label;
                var newURI = vis.node(nodeIndex).uri;
                vis.setNode(nodeIndex, {
                    label: newLabel,
                    labelStyle: "fill: #FFF",
                    shape: 'customInputEllipse',
                    style: "fill: #336633;",
                    uri: newLabel,
                    type: 'input'
                });
            }
            callback(vis);
        })
    }

    /*
        renders graph
    */
    var vis = new dagreD3.graphlib.Graph()
        .setGraph({
            nodesep: 10,
            ranksep: 20,
        })
        .setDefaultEdgeLabel(function() {
            return {}
        });

    mapNodesEdges(vis);
});

/*
    @params: d3 svg
    - setup on click process for each node
*/
var setupNodeOnClick = function (svg, vis) {
    //setup on click listeners for every node
    svg.selectAll("g.node").on('click', function(id) {
        var node = vis.node(id);
        if(d3.select(this).style('opacity') == 0.7) {
            d3.select(this).style('opacity','1.0');
            // TODO: remove chart of info for node, since it is being 'unselected'
        }
        else {
            d3.select(this).style('opacity', '0.7');
            // TODO: add chart of info for input/output nodes, since they are being 'selected'
            if (node.type == 'process') {
                addProcessInfo(node.uri, processInputMapping[node.uri], processOutputMapping[node.uri]);
            }
        }
    });
}

/*
    @params: string URI
    @return: a parsed, human-readable substring of the URI
*/
var stripNameFromURI = function(uri) {
    return uri.substring(uri.lastIndexOf('CE_')+3, uri.length).toLowerCase();
}

var addDimensions = function(render) {
    render.shapes().customEllipse = function(parent, bbox, node) {
        var rx = bbox.width/2,
            ry = bbox.height/2,
            shapeSvg = parent.insert("ellipse", ":first-child")
            .attr('x', -bbox.width/2)
            .attr('y', -bbox.height/2)
            .attr('rx', rx)
            .attr('ry', ry)
            .attr('style', 'fill: #003366');
        if (node.dimensions > 1) {
            for (var i = 1; i < node.dimensions; i++) {
                shapeSvg = parent.insert("ellipse", ":first-child")
                .attr('x', -bbox.width/2)
                .attr('y', -bbox.height/2)
                .attr('rx', rx)
                .attr('ry', ry)
                .attr('style', "fill: #FFF; stroke: #003366")
                .attr('transform', 'translate(' + 0 + ',' + (bbox.height/10)*i + ')' + 'scale(' + '1.0' + ')');
            }

            shapeSvg = parent.insert("ellipse", ":first-child")
                .attr('x', -bbox.width/2)
                .attr('y', -bbox.height/2)
                .attr('rx', rx)
                .attr('ry', ry)
                .attr('transform', 'translate(' + 0 + ',' + (bbox.height/10)*(node.dimensions) + ')' + 'scale(' + '1.0' + ')');
                node.style = "fill: #FFF; stroke: #003366";
        }

        node.intersect = function(point) {
            return dagreD3.intersect.ellipse(node, rx, ry, point);
        };
        return shapeSvg;
    };

    render.shapes().customInputEllipse = function(parent, bbox, node) {
        var rx = bbox.width/2,
            ry = bbox.height/2,
            shapeSvg = parent.insert("ellipse", ":first-child")
            .attr('x', -bbox.width/2)
            .attr('y', -bbox.height/2)
            .attr('rx', rx)
            .attr('ry', ry)
            .attr('style', 'fill: #336633');

        if (node.dimensions > 1) {
            for (var i = 1; i < node.dimensions; i++) {
                shapeSvg = parent.insert("ellipse", ":first-child")
                .attr('x', -bbox.width/2)
                .attr('y', -bbox.height/2)
                .attr('rx', rx)
                .attr('ry', ry)
                .attr('style', "fill: #FFF; stroke: #336633")
                .attr('transform', 'translate(' + 3*i + ',' + (bbox.height/10)*i + ')' + 'scale(' + '1.0' + ')');
            }
            shapeSvg = parent.insert("ellipse", ":first-child")
                .attr('x', -bbox.width/2)
                .attr('y', -bbox.height/2)
                .attr('rx', rx)
                .attr('ry', ry)
                .attr('transform', 'translate(' + 3*(node.dimensions) + ',' + (bbox.height/10)*(node.dimensions) + ')' + 'scale(' + '1.0' + ')');
                node.style = "fill: #FFF; stroke: #336633";
        }

        node.intersect = function(point) {
            return dagreD3.intersect.ellipse(node, rx, ry, point);
        };
        return shapeSvg;
    };

    render.shapes().process = function(parent, bbox, node) {
        var width = bbox.width,
            height = bbox.height,
            shapeSvg = parent.insert('rect', ':first-child')
                .attr('rx', node.rx)
                .attr('ry', node.ry)
                .attr('x', -width/2)
                .attr('y', -height/2)
                .attr('width', width)
                .attr('height', height)
                .attr('style', 'fill: #FFCC99;');

        for (var i = 1; i < node.dimensions; i++) {
            shapeSvg = parent.insert('rect', ':first-child')
                .attr('rx', node.rx)
                .attr('ry', node.ry)
                .attr('x', -width/2 + (5*i))
                .attr('y', -height/2 + (5*i))
                .attr('width', width)
                .attr('height', height)
                .attr('style', 'fill: #FFCC99;');
        }
        node.intersect = function(point) {
            return dagreD3.intersect.rect(node, point);
        }
        return shapeSvg;
    }
}<commit_msg>starting highlighting inputs/outputs feature<commit_after>var processInputMapping = {};
var processOutputMapping = {};
var workflowURI = localStorage.getItem("workflow-uri");
var vis = {};
var svg = {};
getGraphJSON(workflowURI, function(res) {
    var results = res['results']['bindings'];
    var processNodeIndices = {};
    var putNodeIndices = {};

    /*
        @params: string processName, string inputName
        - inserts the inputName to the list of inputs that the processName maps
        to within the object processInputMapping
    */
    var addInputProcess = function(processName, inputName) {
        processInputMapping[processName] = processInputMapping[processName] || [];
        processInputMapping[processName].push(inputName);
    }

    /*
        @params: string processName, string outputName
        - inserts the outputName to the list of outputs that the processName maps
        to within the object processOutputMapping
    */
    var addOutputProcess = function(processName, outputName) {
        processOutputMapping[processName] = processOutputMapping[processName] || [];
        processOutputMapping[processName].push(outputName);
    }

    /*
        @params: dagreD3 graph
        - pulls from JSON result and sets nodes according to whether or not they are labeled as inputs, outputs, or processes
    */
    var mapNodesEdges = function(graph) {
        var j = 0;
        for (var i = 0; i < results.length; i++) {
            //get URI of step from JSON
            var step = results[i].hasOwnProperty('step') ? results[i]['step']['value'] : null;

            //add step to processNodeIndices if the step hasn't been added yet
            if (processNodeIndices[step] == null) {
                processNodeIndices[step] = j;
                //get readable display for step name
                var stepToDisplay = stripNameFromURI(step);
                stepToDisplay = stepToDisplay.substring(0, stepToDisplay.lastIndexOf('node')) == '' ? stepToDisplay : stepToDisplay.substring(0, stepToDisplay.lastIndexOf('node'));
                vis.setNode(j, {
                    shape: 'process',
                    label: stepToDisplay,
                    labelStyle: "fill: #000",
                    style: "fill: #FFCC99;",
                    uri: step,
                    type: 'process',
                    dimensions: 3
                });
                j++;
            }
            if (results[i].hasOwnProperty('input')) {
                //get URI of input from JSON
                var input = results[i]['input']['value'];
                if (putNodeIndices[input] == null) {
                    putNodeIndices[input] = j;
                    //get readable display for input name
                    var inputToDisplay = stripNameFromURI(input);
                    vis.setNode(j, {
                        label: inputToDisplay,
                        labelStyle: 'fill: #FFF',
                        shape: 'customEllipse',
                        style: 'fill: #003366;',
                        uri: input,
                        type: 'input',
                        dimensions: 2
                    });
                    j++;
                }
                addInputProcess(step, input);
            } else if (results[i].hasOwnProperty('output')) {
                //get URI of output from JSON
                var output = results[i]['output']['value'];
                if (putNodeIndices[output] == null) {
                    putNodeIndices[output] = j;
                    //get readable display for output name
                    var outputToDisplay = stripNameFromURI(output);
                    vis.setNode(j, {
                        label: outputToDisplay,
                        labelStyle: "fill: #FFF",
                        shape: 'customEllipse',
                        style: "fill: #003366;",
                        uri: output,
                        type: 'output',
                        dimensions: 6
                    });
                    j++;
                }
                addOutputProcess(step, output);
            }
        }
        setGraphEdges(vis);
    }

    /*
        @params: dagreD3 vis
        - connects nodes to each other via node numbers from processInputMapping
        and processOutputMapping
    */
    var setGraphEdges = function (vis) {
        for (var process in processInputMapping) {
            for (var i = 0; i < processInputMapping[process].length; i++) {
                vis.setEdge(putNodeIndices[processInputMapping[process][i]], processNodeIndices[process], {
                    style: "stroke: #000; fill:none"
                });
            }
        }
        for (var process in processOutputMapping) {
            for (var i = 0; i < processOutputMapping[process].length; i++) {
                vis.setEdge(processNodeIndices[process], putNodeIndices[processOutputMapping[process][i]], {
                    style: "stroke: #000; fill:none"
                });
            }
        }
        formatInputs(vis, renderGraph);
    }

    var renderGraph = function(vis) {
        // Create the renderer
        var render = new dagreD3.render();

        // Add dimensions to nodes if there are any
        addDimensions(render);
        // Set up an SVG group so that we can translate the final graph.
        svg = d3.select("svg").attr('width','50%').attr('height','100%'), svgGroup = svg.append("g");

        // Set up zoom support
        var zoom = d3.behavior.zoom().on("zoom", function() {
            svgGroup.attr("transform", "translate(" + d3.event.translate + ")" +
                                        "scale(" + d3.event.scale + ")");
        });
        svg.call(zoom);

        // Run the renderer. This is what draws the final graph.
        render(svgGroup, vis);

        //centers graph and calculates top margin of graph based on screen size
        var xCenterOffset = (document.getElementsByClassName('visualization-container')[0].clientWidth / 2 - vis.graph().width) / 2;
        var yTopMargin = screen.height * .05;
        var scale = .75;
        zoom.translate([xCenterOffset, 20])
            .scale(scale)
            .event(svg);
        svg.attr('height', vis.graph().height * scale + yTopMargin);

        setupNodeOnClick(svg, vis);
    }

    /*

    */
    var formatInputs = function(vis, callback) {
        getInputs(workflowURI, function (inputs) {
            for (var i = 0; i < inputs.length; i++) {
                var nodeIndex = putNodeIndices[inputs[i].input.value];
                var newLabel = vis.node(nodeIndex).label;
                var newURI = vis.node(nodeIndex).uri;
                vis.setNode(nodeIndex, {
                    label: newLabel,
                    labelStyle: "fill: #FFF",
                    shape: 'customInputEllipse',
                    style: "fill: #336633;",
                    uri: newLabel,
                    type: 'input'
                });
            }
            callback(vis);
        })
    }

    /*
        renders graph
    */
    vis = new dagreD3.graphlib.Graph()
        .setGraph({
            nodesep: 10,
            ranksep: 20,
        })
        .setDefaultEdgeLabel(function() {
            return {}
        });

    mapNodesEdges(vis);
});

var highlightHandler = function (checkbox) {
    console.log(checkbox.checked);
    highlightInputs(checkbox.checked);
}

var highlightInputs = function(checked) {
    if (checked) {
    } else {
        // make an array of objects- the objects contain a process URI

        // how can I know which checkbox is checked to highlight inputs?
    }
}

/*
    @params: d3 svg
    - setup on click process for each node
*/
var setupNodeOnClick = function (svg, vis) {
    //setup on click listeners for every node
    svg.selectAll("g.node").on('click', function(id) {
        var node = vis.node(id);
        if(d3.select(this).style('opacity') == 0.7) {
            d3.select(this).style('opacity','1.0');
            // TODO: remove chart of info for node, since it is being 'unselected'
        }
        else {
            d3.select(this).style('opacity', '0.7');
            // TODO: add chart of info for input/output nodes, since they are being 'selected'
            if (node.type == 'process') {
                addProcessInfo(node.uri, processInputMapping[node.uri], processOutputMapping[node.uri]);
            }
        }
    });
}

/*
    @params: string URI
    @return: a parsed, human-readable substring of the URI
*/
var stripNameFromURI = function(uri) {
    return uri.substring(uri.lastIndexOf('CE_')+3, uri.length).toLowerCase();
}

var addDimensions = function(render) {
    render.shapes().customEllipse = function(parent, bbox, node) {
        var rx = bbox.width/2,
            ry = bbox.height/2,
            shapeSvg = parent.insert("ellipse", ":first-child")
            .attr('x', -bbox.width/2)
            .attr('y', -bbox.height/2)
            .attr('rx', rx)
            .attr('ry', ry)
            .attr('style', 'fill: #003366');
        if (node.dimensions > 1) {
            for (var i = 1; i < node.dimensions; i++) {
                shapeSvg = parent.insert("ellipse", ":first-child")
                .attr('x', -bbox.width/2)
                .attr('y', -bbox.height/2)
                .attr('rx', rx)
                .attr('ry', ry)
                .attr('style', "fill: #FFF; stroke: #003366")
                .attr('transform', 'translate(' + 0 + ',' + (bbox.height/10)*i + ')' + 'scale(' + '1.0' + ')');
            }

            shapeSvg = parent.insert("ellipse", ":first-child")
                .attr('x', -bbox.width/2)
                .attr('y', -bbox.height/2)
                .attr('rx', rx)
                .attr('ry', ry)
                .attr('transform', 'translate(' + 0 + ',' + (bbox.height/10)*(node.dimensions) + ')' + 'scale(' + '1.0' + ')');
                node.style = "fill: #FFF; stroke: #003366";
        }

        node.intersect = function(point) {
            return dagreD3.intersect.ellipse(node, rx, ry, point);
        };
        return shapeSvg;
    };

    render.shapes().customInputEllipse = function(parent, bbox, node) {
        var rx = bbox.width/2,
            ry = bbox.height/2,
            shapeSvg = parent.insert("ellipse", ":first-child")
            .attr('x', -bbox.width/2)
            .attr('y', -bbox.height/2)
            .attr('rx', rx)
            .attr('ry', ry)
            .attr('style', 'fill: #336633');

        if (node.dimensions > 1) {
            for (var i = 1; i < node.dimensions; i++) {
                shapeSvg = parent.insert("ellipse", ":first-child")
                .attr('x', -bbox.width/2)
                .attr('y', -bbox.height/2)
                .attr('rx', rx)
                .attr('ry', ry)
                .attr('style', "fill: #FFF; stroke: #336633")
                .attr('transform', 'translate(' + 3*i + ',' + (bbox.height/10)*i + ')' + 'scale(' + '1.0' + ')');
            }
            shapeSvg = parent.insert("ellipse", ":first-child")
                .attr('x', -bbox.width/2)
                .attr('y', -bbox.height/2)
                .attr('rx', rx)
                .attr('ry', ry)
                .attr('transform', 'translate(' + 3*(node.dimensions) + ',' + (bbox.height/10)*(node.dimensions) + ')' + 'scale(' + '1.0' + ')');
                node.style = "fill: #FFF; stroke: #336633";
        }

        node.intersect = function(point) {
            return dagreD3.intersect.ellipse(node, rx, ry, point);
        };
        return shapeSvg;
    };

    render.shapes().process = function(parent, bbox, node) {
        var width = bbox.width,
            height = bbox.height,
            shapeSvg = parent.insert('rect', ':first-child')
                .attr('rx', node.rx)
                .attr('ry', node.ry)
                .attr('x', -width/2)
                .attr('y', -height/2)
                .attr('width', width)
                .attr('height', height)
                .attr('style', 'fill: #FFCC99;');

        for (var i = 1; i < node.dimensions; i++) {
            shapeSvg = parent.insert('rect', ':first-child')
                .attr('rx', node.rx)
                .attr('ry', node.ry)
                .attr('x', -width/2 + (5*i))
                .attr('y', -height/2 + (5*i))
                .attr('width', width)
                .attr('height', height)
                .attr('style', 'fill: #FFCC99;');
        }
        node.intersect = function(point) {
            return dagreD3.intersect.rect(node, point);
        }
        return shapeSvg;
    }
}<|endoftext|>


<commit_before>// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as DataGrid from '../data_grid/data_grid.js';
import * as UI from '../ui/ui.js';

import {PlayerEvent} from './MediaModel.js';  // eslint-disable-line no-unused-vars

/**
 * @typedef {{
 *     id: string,
 *     title: string,
 *     sortable: boolean,
 *     weight: (number|undefined),
 *     sortingFunction: (!function(!EventNode, !EventNode):number|undefined),
 * }}
 */
export let EventDisplayColumnConfig;

/** @enum {string} */
export const MediaEventColumnKeys = {
  Timestamp: 'displayTimestamp',
  Event: 'event',
  Value: 'value'
};

/**
 * @unrestricted
 */
export class EventNode extends DataGrid.DataGrid.DataGridNode {
  /**
   * @param {!Event} event
   */
  constructor(event) {
    super(event, false);
    this._expandableElement = null;
  }

  /**
   * @override
   * @param {string} columnId
   * @return {!HTMLElement}
   */
  createCell(columnId) {
    const cell = this.createTD(columnId);
    const cellData = /** @type string */ (this.data[columnId]);
    if (columnId === MediaEventColumnKeys.Value) {
      const enclosed = cell.createChild('div', 'event-display-table-contents-json-wrapper');
      this._expandableElement = new SourceFrame.JSONView(new SourceFrame.ParsedJSON(cellData, '', ''), true);
      this._expandableElement.markAsRoot();
      this._expandableElement.show(enclosed);
    } else {
      cell.classList.add('event-display-table-basic-text-table-entry');
      cell.createTextChild(cellData);
    }
    return cell;
  }
}

/**
 * @unrestricted
 */
export class PlayerEventsView extends UI.Widget.VBox {
  constructor() {
    super();

    // Set up element styles.
    this.registerRequiredCSS('media/eventDisplayTable.css');
    this.contentElement.classList.add('event-display-table-contents-table-container');

    this._dataGrid = this._createDataGrid([
      {
        id: MediaEventColumnKeys.Timestamp,
        title: ls`Timestamp`,
        weight: 1,
        sortingFunction: DataGrid.SortableDataGrid.SortableDataGrid.NumericComparator.bind(
            null, MediaEventColumnKeys.Timestamp)
      },
      {id: MediaEventColumnKeys.Event, title: ls`Event Name`, weight: 2},
      {id: MediaEventColumnKeys.Value, title: ls`Value`, weight: 7}
    ]);

    this._firstEventTime = 0;
    this._dataGrid.setStriped(true);
    this._dataGrid.asWidget().show(this.contentElement);
  }

  /**
   * @param {!Array.<!EventDisplayColumnConfig>} headers
   * @return !DataGrid.SortableDataGrid
   */
  _createDataGrid(headers) {
    const gridColumnDescs = [];
    for (const headerDesc of headers) {
      gridColumnDescs.push(PlayerEventsView._convertToGridDescriptor(headerDesc));
    }

    // TODO(tmathmeyer) SortableDataGrid doesn't play nice with nested JSON
    // renderers, since they can change size, and this breaks the visible
    // element computation in ViewportDataGrid.
    const datagrid = new DataGrid.DataGrid.DataGridImpl({displayName: ls`Event Display`, columns: gridColumnDescs});
    datagrid.asWidget().contentElement.classList.add('no-border-top-datagrid');
    return datagrid;
  }

  /**
   * @param {!PlayerEvent} event
   */
  onEvent(event) {
    if (this._firstEventTime === 0) {
      this._firstEventTime = event.timestamp;
    }

    event = this._subtractFirstEventTime(event);
    const stringified = /** @type {string} */ (event.value);
    try {
      const json = JSON.parse(stringified);
      event.event = json.event;
      delete json['event'];
      event.value = json;
      const node = new EventNode(event);
      this._dataGrid.rootNode().appendChild(node);
    } catch (e) {
      // If this is a legacy message event, ignore it for now until they
      // are handled.
    }
  }

  /**
   * @param {!PlayerEvent} event
   */
  _subtractFirstEventTime(event) {
    event.displayTimestamp = (event.timestamp - this._firstEventTime).toFixed(3);
    return event;
  }

  /**
   * @param {!EventDisplayColumnConfig} columnConfig
   * @return {!DataGrid.DataGrid.ColumnDescriptor}
   */
  static _convertToGridDescriptor(columnConfig) {
    return /** @type {!DataGrid.DataGrid.ColumnDescriptor} */ ({
      id: columnConfig.id,
      title: columnConfig.title,
      sortable: columnConfig.sortable,
      weight: columnConfig.weight || 0,
      sort: DataGrid.DataGrid.Order.Ascending
    });
  }
}
<commit_msg>Keep event pane scrolled to bottom if it is already there<commit_after>// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as DataGrid from '../data_grid/data_grid.js';
import * as UI from '../ui/ui.js';

import {PlayerEvent} from './MediaModel.js';  // eslint-disable-line no-unused-vars

/**
 * @typedef {{
 *     id: string,
 *     title: string,
 *     sortable: boolean,
 *     weight: (number|undefined),
 *     sortingFunction: (!function(!EventNode, !EventNode):number|undefined),
 * }}
 */
export let EventDisplayColumnConfig;

/** @enum {string} */
export const MediaEventColumnKeys = {
  Timestamp: 'displayTimestamp',
  Event: 'event',
  Value: 'value'
};

/**
 * @unrestricted
 */
export class EventNode extends DataGrid.DataGrid.DataGridNode {
  /**
   * @param {!Event} event
   */
  constructor(event) {
    super(event, false);
    this._expandableElement = null;
  }

  /**
   * @override
   * @param {string} columnId
   * @return {!HTMLElement}
   */
  createCell(columnId) {
    const cell = this.createTD(columnId);
    const cellData = /** @type string */ (this.data[columnId]);
    if (columnId === MediaEventColumnKeys.Value) {
      const enclosed = cell.createChild('div', 'event-display-table-contents-json-wrapper');
      this._expandableElement = new SourceFrame.JSONView(new SourceFrame.ParsedJSON(cellData, '', ''), true);
      this._expandableElement.markAsRoot();
      this._expandableElement.show(enclosed);
    } else {
      cell.classList.add('event-display-table-basic-text-table-entry');
      cell.createTextChild(cellData);
    }
    return cell;
  }
}

/**
 * @unrestricted
 */
export class PlayerEventsView extends UI.Widget.VBox {
  constructor() {
    super();

    // Set up element styles.
    this.registerRequiredCSS('media/eventDisplayTable.css');
    this.contentElement.classList.add('event-display-table-contents-table-container');

    this._dataGrid = this._createDataGrid([
      {
        id: MediaEventColumnKeys.Timestamp,
        title: ls`Timestamp`,
        weight: 1,
        sortingFunction: DataGrid.SortableDataGrid.SortableDataGrid.NumericComparator.bind(
            null, MediaEventColumnKeys.Timestamp)
      },
      {id: MediaEventColumnKeys.Event, title: ls`Event Name`, weight: 2},
      {id: MediaEventColumnKeys.Value, title: ls`Value`, weight: 7}
    ]);

    this._firstEventTime = 0;
    this._dataGrid.setStriped(true);
    this._dataGrid.asWidget().show(this.contentElement);
  }

  /**
   * @param {!Array.<!EventDisplayColumnConfig>} headers
   * @return !DataGrid.SortableDataGrid
   */
  _createDataGrid(headers) {
    const gridColumnDescs = [];
    for (const headerDesc of headers) {
      gridColumnDescs.push(PlayerEventsView._convertToGridDescriptor(headerDesc));
    }

    // TODO(tmathmeyer) SortableDataGrid doesn't play nice with nested JSON
    // renderers, since they can change size, and this breaks the visible
    // element computation in ViewportDataGrid.
    const datagrid = new DataGrid.DataGrid.DataGridImpl({displayName: ls`Event Display`, columns: gridColumnDescs});
    datagrid.asWidget().contentElement.classList.add('no-border-top-datagrid');
    return datagrid;
  }

  /**
   * @param {!PlayerEvent} event
   */
  onEvent(event) {
    if (this._firstEventTime === 0) {
      this._firstEventTime = event.timestamp;
    }

    event = this._subtractFirstEventTime(event);
    const stringified = /** @type {string} */ (event.value);
    try {
      const json = JSON.parse(stringified);
      event.event = json.event;
      delete json['event'];
      event.value = json;
      const node = new EventNode(event);
      const scroll = this._dataGrid.scrollContainer;
      const isAtBottom = scroll.scrollTop === (scroll.scrollHeight - scroll.offsetHeight);
      this._dataGrid.rootNode().appendChild(node);
      if (isAtBottom) {
        scroll.scrollTop = scroll.scrollHeight;
      }
    } catch (e) {
      // If this is a legacy message event, ignore it for now until they
      // are handled.
    }
  }

  /**
   * @param {!PlayerEvent} event
   */
  _subtractFirstEventTime(event) {
    event.displayTimestamp = (event.timestamp - this._firstEventTime).toFixed(3);
    return event;
  }

  /**
   * @param {!EventDisplayColumnConfig} columnConfig
   * @return {!DataGrid.DataGrid.ColumnDescriptor}
   */
  static _convertToGridDescriptor(columnConfig) {
    return /** @type {!DataGrid.DataGrid.ColumnDescriptor} */ ({
      id: columnConfig.id,
      title: columnConfig.title,
      sortable: columnConfig.sortable,
      weight: columnConfig.weight || 0,
      sort: DataGrid.DataGrid.Order.Ascending
    });
  }
}
<|endoftext|>


<commit_before>const canvas = document.body.appendChild(document.createElement('canvas'))
const fit = require('canvas-fit')
const regl = require('../regl')(canvas)
const mat4 = require('gl-mat4')
const camera = require('canvas-orbit-camera')(canvas)
window.addEventListener('resize', fit(canvas), false)
const bunny = require('bunny')
const normals = require('angle-normals')

// configure intial camera view.
camera.rotate([0.0, 0.0], [0.0, -0.4])
camera.zoom(300.0)

// geometry arrays.
const elements = []
var xzPosition = []

const N = 64 // num quads of the plane

var size = 0.5
var xmin = -size
var xmax = +size
var ymin = -size
var ymax = +size

/*
  For the terrain geometry, we create a plane with min position as (x=-0.5,z=-0.5) and max position as (x=+0.5, z=+0.5).

  In the vertex shader, we enlarge this plane. And the y-values are sampled from the heightmap texture,

  The uv-coordinates are computed from the positions.
  The normals can be approximated from the heightmap texture.

  So we only have to upload the x- and z-values and the heightmap texture to the GPU, and nothing else.
  */
var row
var col
for (row = 0; row <= N; ++row) {
  var z = (row / N) * (ymax - ymin) + ymin
  for (col = 0; col <= N; ++col) {
    var x = (col / N) * (xmax - xmin) + xmin
    xzPosition.push([x, z])
  }
}

// create plane faces.
for (row = 0; row <= (N - 1); ++row) {
  for (col = 0; col <= (N - 1); ++col) {
    var i = row * (N + 1) + col

    var i0 = i + 0
    var i1 = i + 1
    var i2 = i + (N + 1) + 0
    var i3 = i + (N + 1) + 1

    elements.push([i3, i1, i0])
    elements.push([i0, i2, i3])
  }
}

/*
  This function encapsulates all the state that is common between drawTerrain and drawBunny()
  */
const setupDefault = regl({
  cull: {
    enable: true
  },
  uniforms: {
    // View Projection matrices.
    view: () => camera.view(),
    projection: ({viewportWidth, viewportHeight}) =>
      mat4.perspective([],
                       Math.PI / 4,
                       viewportWidth / viewportHeight,
                       0.01,
                       3000),

    // light settings. These can of course by tweaked to your likings.
    lightDir: [0.39, 0.87, 0.29],
    ambientLightAmount: 0.3,
    diffuseLightAmount: 0.7
  }
})

const drawTerrain = regl({
  frag: `
  precision mediump float;

  varying vec2 vUv;
  varying vec3 vNormal;

  uniform sampler2D rockTexture;
  uniform vec3 lightDir;
  uniform float ambientLightAmount;
  uniform float diffuseLightAmount;

  void main () {
    vec3 tex = texture2D(rockTexture, vUv*2.0).rgb;

    vec3 ambient = ambientLightAmount * tex;
    vec3 diffuse = diffuseLightAmount * tex * clamp( dot(vNormal, lightDir ), 0.0, 1.0 );

    gl_FragColor = vec4(ambient + diffuse, 1.0);
  }`,
  vert: `
  // the size of the world on the x and z-axes.
#define WORLD_SIZE 300.0
  // the height of the world.
#define WORLD_HEIGHT 100.0

  uniform sampler2D heightTexture;

  float getHeight(vec2 xz) {
    vec2 uv = vec2(0.5, 0.5) + xz.xy;
    return WORLD_HEIGHT*(-1.0 + 2.0 * texture2D(heightTexture, uv).r);
  }

  vec3 getPosition(vec2 xz) {
    return vec3(WORLD_SIZE*xz.x, getHeight(xz), WORLD_SIZE*xz.y);
  }

  precision mediump float;

  attribute vec2 xzPosition;

  varying vec3 vPosition;
  varying vec2 vUv;
  varying vec3 vNormal;

  uniform mat4 projection, view;

  void main() {
    vec3 xyzPosition = getPosition(xzPosition);

    vec2 uv = vec2(0.5, 0.5) + xzPosition.xy;
    vUv = uv;

    float eps = 1.0/16.0;

    // approximate the normal with central differences.
    vec3 va = vec3(2.0*eps,
                   getHeight(xzPosition + vec2(eps,0.0)) - getHeight(xzPosition - vec2(eps,0.0)) , 0.0 );
    vec3 vb = vec3(0.0,
                   getHeight(xzPosition + vec2(0.0, eps)) - getHeight(xzPosition - vec2(0.0, eps)), 2.0*eps );
    vNormal = normalize(cross(normalize(vb), normalize(va) ));

    vPosition = xyzPosition;
    gl_Position = projection * view * vec4(xyzPosition, 1);
  }`,

  uniforms: {
    heightTexture: regl.prop('heightTexture'),
    rockTexture: regl.prop('rockTexture')
  },
  attributes: {
    xzPosition: regl.prop('xzPosition')
  },
  elements: regl.prop('elements')
})

const drawBunny = regl({
  frag: `
  precision mediump float;

  varying vec3 vNormal;

  uniform vec3 lightDir;
  uniform vec3 color;
  uniform float ambientLightAmount;
  uniform float diffuseLightAmount;

  void main () {
    vec3 ambient = ambientLightAmount * color;
    vec3 diffuse = diffuseLightAmount * color * clamp( dot(vNormal, lightDir ), 0.0, 1.0 );

    gl_FragColor = vec4(ambient + diffuse, 1.0);
  }`,
  vert: `
  precision mediump float;

  attribute vec3 position;
  attribute vec3 normal;

  varying vec3 vNormal;

  uniform mat4 projection, model, view;

  void main () {
    vNormal = normal;
    gl_Position = projection * view * model * vec4(position, 1.0);
  }`,
  attributes: {
    position: bunny.positions,
    normal: normals(bunny.cells, bunny.positions)
  },
  elements: bunny.cells,
  uniforms: {
    model: (_, props, batchId) => {
      var m = mat4.identity([])

      mat4.translate(m, m, props.position)

      var s = props.scale
      mat4.scale(m, m, [s, s, s])

      var r = props.rotation
      mat4.rotateX(m, m, r[0])
      mat4.rotateY(m, m, r[1])
      mat4.rotateZ(m, m, r[2])

      return m
    },
    color: regl.prop('color')
  }
})

require('resl')({
  manifest: {
    heightTexture: {
      type: 'image',
      src: 'assets/textureplane.png',
      parser: (data) => regl.texture({
        data: data
      })
    },
    rockTexture: {
      type: 'image',
      src: 'assets/rock_texture.png',
      parser: (data) => regl.texture({
        data: data,
        wrap: 'repeat'
      })
    }
  },

  onDone: ({ heightTexture, rockTexture }) => {
    regl.frame(({deltaTime}) => {
      regl.clear({
        color: [0, 0, 0, 255],
        depth: 1
      })

      setupDefault({}, () => {
        drawTerrain({elements, xzPosition, heightTexture, rockTexture})

        drawBunny([
          {color: [0.4, 0.2, 0.1], scale: 1.7, position: [0.0, -65.0, 0.0], rotation: [0.0, 0.8, 0.0]},
          {color: [0.6, 0.2, 0.5], scale: 5.2, position: [30.0, -65.0, -80.0], rotation: [-0.5, 0.0, 0.0]},
          {color: [0.4, 0.2, 0.6], scale: 1.5, position: [120.0, -55.0, -100.0], rotation: [-0.5, 1.9, 0.0]},
          {color: [0.7, 0.7, 0.7], scale: 2.2, position: [50.0, -60.0, 0.0], rotation: [-0.2, 0.0, 0.0]},
          {color: [0.0, 0.2, 0.5], scale: 1.0, position: [-50.0, -60.0, 0.0], rotation: [0.0, 1.2, 0.0]},
          {color: [0.4, 0.4, 0.0], scale: 1.0, position: [-50.0, -45.0, 40.0], rotation: [0.0, 0, -0.6]},
          {color: [0.2, 0.2, 0.2], scale: 3.3, position: [100.0, -65.0, 50.0], rotation: [0.0, -0.4, -0.0]},
          {color: [0.4, 0.1, 0.1], scale: 2.1, position: [70.0, -65.0, 80.0], rotation: [0.1, 0.6, 0.2]},
          {color: [0.2, 0.5, 0.2], scale: 6.1, position: [-50.0, -70.0, 80.0], rotation: [0.0, -0.9, 0.0]},
          {color: [0.3, 0.5, 0.5], scale: 4.1, position: [-50.0, -70.0, -60.0], rotation: [0.7, -0.0, 0.0]},
          {color: [0.4, 0.4, 0.1], scale: 1.8, position: [-80.0, -50.0, -110.0], rotation: [0.0, -0.0, 0.0]},
          {color: [0.7, 0.4, 0.1], scale: 1.3, position: [-120.0, -85.0, -40.0], rotation: [0.0, +2.1, -0.3]}
        ])
        camera.tick()
      })
    })
  }
})
<commit_msg>Implement box filter blur in the `blur` example.<commit_after>/*

  This examples demonstrates how we can render a height map, how to place out several models(using the batching feature), and how to
  implement a simple fullscreen post-process effect(using the framebuffer feature) in REGL. The post-process effect is a simple box filter blur.

  The FBO feature, and the batching feature is used to implement the above.
 */

const canvas = document.body.appendChild(document.createElement('canvas'))
const fit = require('canvas-fit')
const regl = require('../regl')(canvas)
const mat4 = require('gl-mat4')
const camera = require('canvas-orbit-camera')(canvas)
window.addEventListener('resize', fit(canvas), false)
const bunny = require('bunny')
const normals = require('angle-normals')

// increase and decrease the blur amount by modifying this value.
const FILTER_RADIUS = 1

// configure intial camera view.
camera.rotate([0.0, 0.0], [0.0, -0.4])
camera.zoom(300.0)

// create fbo. We set the size in `regl.frame`
const fbo = regl.framebuffer({
  color: regl.texture({
    width: 1,
    height: 1,
    wrap: 'clamp'
  }),
  depth: true
})

// geometry arrays.
const elements = []
var xzPosition = []

const N = 64 // num quads of the plane

var size = 0.5
var xmin = -size
var xmax = +size
var ymin = -size
var ymax = +size

/*
  For the terrain geometry, we create a plane with min position as (x=-0.5,z=-0.5) and max position as (x=+0.5, z=+0.5).

  In the vertex shader, we enlarge this plane on the x- and z-axis. And the y-values are sampled from the heightmap texture,

  The uv-coordinates are computed from the positions.
  The normals can be approximated from the heightmap texture and the positions.

  So we only have to upload the x- and z-values and the heightmap texture to the GPU, and nothing else.
*/
var row
var col
for (row = 0; row <= N; ++row) {
  var z = (row / N) * (ymax - ymin) + ymin
  for (col = 0; col <= N; ++col) {
    var x = (col / N) * (xmax - xmin) + xmin
    xzPosition.push([x, z])
  }
}

// create plane faces.
for (row = 0; row <= (N - 1); ++row) {
  for (col = 0; col <= (N - 1); ++col) {
    var i = row * (N + 1) + col

    var i0 = i + 0
    var i1 = i + 1
    var i2 = i + (N + 1) + 0
    var i3 = i + (N + 1) + 1

    elements.push([i3, i1, i0])
    elements.push([i0, i2, i3])
  }
}

/*
  This function encapsulates all the state that is common between drawTerrain() and drawBunny()

  Also, configure so that we render to an FBO.
*/
const setupDefault = regl({
  cull: {
    enable: true
  },
  uniforms: {
    // View Projection matrices.
    view: () => camera.view(),
    projection: ({viewportWidth, viewportHeight}) =>
      mat4.perspective([],
                       Math.PI / 4,
                       viewportWidth / viewportHeight,
                       0.01,
                       3000),

    // light settings. These can of course by tweaked to your likings.
    lightDir: [0.39, 0.87, 0.29],
    ambientLightAmount: 0.3,
    diffuseLightAmount: 0.7
  },
  framebuffer: fbo
})

const drawTerrain = regl({
  frag: `
  precision mediump float;

  varying vec2 vUv;
  varying vec3 vNormal;

  uniform sampler2D rockTexture;
  uniform vec3 lightDir;
  uniform float ambientLightAmount;
  uniform float diffuseLightAmount;

  void main () {
    vec3 tex = texture2D(rockTexture, vUv*2.0).rgb;

    vec3 ambient = ambientLightAmount * tex;
    vec3 diffuse = diffuseLightAmount * tex * clamp( dot(vNormal, lightDir ), 0.0, 1.0 );

    gl_FragColor = vec4(ambient + diffuse, 1.0);
  }`,
  vert: `
  // the size of the world on the x and z-axes.
#define WORLD_SIZE 300.0
  // the height of the world.
#define WORLD_HEIGHT 100.0

  uniform sampler2D heightTexture;

  float getHeight(vec2 xz) {
    vec2 uv = vec2(0.5, 0.5) + xz.xy;
    return WORLD_HEIGHT*(-1.0 + 2.0 * texture2D(heightTexture, uv).r);
  }

  vec3 getPosition(vec2 xz) {
    return vec3(WORLD_SIZE*xz.x, getHeight(xz), WORLD_SIZE*xz.y);
  }

  precision mediump float;

  attribute vec2 xzPosition;

  varying vec3 vPosition;
  varying vec2 vUv;
  varying vec3 vNormal;

  uniform mat4 projection, view;

  void main() {
    vec3 xyzPosition = getPosition(xzPosition);

    vec2 uv = vec2(0.5, 0.5) + xzPosition.xy;
    vUv = uv;

    float eps = 1.0/16.0;

    // approximate the normal with central differences.
    vec3 va = vec3(2.0*eps,
                   getHeight(xzPosition + vec2(eps,0.0)) - getHeight(xzPosition - vec2(eps,0.0)) , 0.0 );
    vec3 vb = vec3(0.0,
                   getHeight(xzPosition + vec2(0.0, eps)) - getHeight(xzPosition - vec2(0.0, eps)), 2.0*eps );
    vNormal = normalize(cross(normalize(vb), normalize(va) ));

    vPosition = xyzPosition;
    gl_Position = projection * view * vec4(xyzPosition, 1);
  }`,

  uniforms: {
    heightTexture: regl.prop('heightTexture'),
    rockTexture: regl.prop('rockTexture')
  },
  attributes: {
    xzPosition: regl.prop('xzPosition')
  },
  elements: regl.prop('elements')
})

const drawBunny = regl({
  frag: `
  precision mediump float;

  varying vec3 vNormal;

  uniform vec3 lightDir;
  uniform vec3 color;
  uniform float ambientLightAmount;
  uniform float diffuseLightAmount;

  void main () {
    vec3 ambient = ambientLightAmount * color;
    vec3 diffuse = diffuseLightAmount * color * clamp( dot(vNormal, lightDir ), 0.0, 1.0 );

    gl_FragColor = vec4(ambient + diffuse, 1.0);
  }`,
  vert: `
  precision mediump float;

  attribute vec3 position;
  attribute vec3 normal;

  varying vec3 vNormal;

  uniform mat4 projection, model, view;

  void main () {
    vNormal = normal;
    gl_Position = projection * view * model * vec4(position, 1.0);
  }`,
  attributes: {
    position: bunny.positions,
    normal: normals(bunny.cells, bunny.positions)
  },
  elements: bunny.cells,
  uniforms: {
    model: (_, props, batchId) => {
      /*
        By using props, we translate the bunny, scale it, and rotate it.
        */
      var m = mat4.identity([])

      mat4.translate(m, m, props.position)

      var s = props.scale
      mat4.scale(m, m, [s, s, s])

      var r = props.rotation
      mat4.rotateX(m, m, r[0])
      mat4.rotateY(m, m, r[1])
      mat4.rotateZ(m, m, r[2])

      return m
    },
    color: regl.prop('color')
  }
})

const drawFboBlurred = regl({
  frag: `
  precision mediump float;
  varying vec2 uv;
  uniform sampler2D tex;
  uniform float wRcp, hRcp;
#define R int(${FILTER_RADIUS})

  void main() {

    float W =  float((1 + 2 * R) * (1 + 2 * R));

    vec3 avg = vec3(0.0);
    for (int x = -R; x <= +R; x++) {
      for (int y = -R; y <= +R; y++) {
        avg += (1.0 / W) * texture2D(tex, uv + vec2(float(x) * wRcp, float(y) * hRcp)).xyz;
      }
    }

    gl_FragColor = vec4(avg, 1.0);
  }`,

  vert: `
  precision mediump float;
  attribute vec2 position;
  varying vec2 uv;
  void main() {
    uv = 0.5 * (position + 1.0);
    gl_Position = vec4(position, 0, 1);
  }`,
  attributes: {
    position: [ -4, -4, 4, -4, 0, 4 ]
  },
  uniforms: {
    tex: ({count}) => fbo.color[0],
    wRcp: ({viewportWidth}) => 1.0 / viewportWidth,
    hRcp: ({viewportHeight}) => 1.0 / viewportHeight
  },
  depth: { enable: false },
  count: 3
})

require('resl')({
  manifest: {
    heightTexture: {
      type: 'image',
      src: 'assets/textureplane.png',
      parser: (data) => regl.texture({
        data: data
      })
    },
    rockTexture: {
      type: 'image',
      src: 'assets/rock_texture.png',
      parser: (data) => regl.texture({
        data: data,
        wrap: 'repeat'
      })
    }
  },

  onDone: ({ heightTexture, rockTexture }) => {
    regl.frame(({deltaTime, viewportWidth, viewportHeight}) => {
      /*
        We need to set the FBO size in `regl.frame`, because the viewport size will change if
        the user resizes the browser window.

        However, note that regl is clever, and will only actually resize the fbo when the
        viewport size actually changes!
       */
      fbo.resize(viewportWidth, viewportHeight)

      // begin render to FBO
      setupDefault({}, () => {
        regl.clear({
          color: [0, 0, 0, 255],
          depth: 1
        })

        drawTerrain({elements, xzPosition, heightTexture, rockTexture})

        drawBunny([
          {color: [0.4, 0.2, 0.1], scale: 1.7, position: [0.0, -65.0, 0.0], rotation: [0.0, 0.8, 0.0]},
          {color: [0.6, 0.2, 0.5], scale: 5.2, position: [30.0, -65.0, -80.0], rotation: [-0.5, 0.0, 0.0]},
          {color: [0.4, 0.2, 0.6], scale: 1.5, position: [120.0, -55.0, -100.0], rotation: [-0.5, 1.9, 0.0]},
          {color: [0.7, 0.7, 0.7], scale: 2.2, position: [50.0, -60.0, 0.0], rotation: [-0.2, 0.0, 0.0]},
          {color: [0.0, 0.2, 0.5], scale: 1.0, position: [-50.0, -60.0, 0.0], rotation: [0.0, 1.2, 0.0]},
          {color: [0.4, 0.4, 0.0], scale: 1.0, position: [-50.0, -45.0, 40.0], rotation: [0.0, 0, -0.6]},
          {color: [0.2, 0.2, 0.2], scale: 3.3, position: [100.0, -65.0, 50.0], rotation: [0.0, -0.4, -0.0]},
          {color: [0.4, 0.1, 0.1], scale: 2.1, position: [70.0, -65.0, 80.0], rotation: [0.1, 0.6, 0.2]},
          {color: [0.2, 0.5, 0.2], scale: 6.1, position: [-50.0, -70.0, 80.0], rotation: [0.0, -0.9, 0.0]},
          {color: [0.3, 0.5, 0.5], scale: 4.1, position: [-50.0, -70.0, -60.0], rotation: [0.7, -0.0, 0.0]},
          {color: [0.4, 0.4, 0.1], scale: 1.8, position: [-80.0, -50.0, -110.0], rotation: [0.0, -0.0, 0.0]},
          {color: [0.7, 0.4, 0.1], scale: 1.3, position: [-120.0, -85.0, -40.0], rotation: [0.0, +2.1, -0.3]}
        ])
      })
      // end render to FBO

      regl.clear({
        color: [0, 0, 0, 255],
        depth: 1
      })

      // Now render fbo to quad, but also blur it.
      drawFboBlurred()

      camera.tick()
    })
  }
})
<|endoftext|>


<commit_before>/*
 * posts a json object to the server
 * No checks are in place to see if the data is valid
 * The reason for not just calling $.post directly is that this
 * 	first adds an anti forgery token to the request
 */
var post = function(url, json, callback) {
	// TODO can I handle the anti forgery token like this?
	$.get("/note/token/raw", function(token) {
		json["__anti-forgery-token"] = token;
		$.post(url, json, callback);
	});
}

/*
 * This enables the functionalty of selecting and deselecting
 * the list bullet points.
 *
 * TODO reenable post
 */
var checkboxHandler = function(event) {
	var el = $(this);

	var newValue;
	if(el.hasClass("checked")) {
		newValue = 0;
		el.removeClass("checked");
	} else {
		newValue = 1;
		el.addClass("checked");
	}

	// this is also called when pressing the new-item 'item'
	// it probably shouldn't
	/*
	post("/note/list/set-checkbox",
			{
				"list-id": el.data("listId"),
				"id": el.data("id"),
				"new-value": newValue,
			});
			*/
	event.stopPropagation();
}

/*
 * This takes a json list of list bullets, and returns the HTML equivalent
 */
/*
var createHTMLList = function(json) {
	var bullets = "";
	var bulletTemplate = Handlebars.compile($("#single-bullet-template").html());
	json.map(function(item) {
		if(item.done)
			item.checked = "checked";
		bullets += bulletTemplate(item);
	});

	return "
"+bullets+"
";
}
*/

/*
 * This handles the post request when adding a new bullet to a list
 *
 * TODO this only needs minor cleanup for the new version
 */
var newBulletSubmit = function(event) {
	var form = $(event.target);

	var textArea = form.find("input[name=text]");
	var text = textArea.val();
	var listId = form.data("listId");

	post("/note/list/add-item",
			{
				"list-id": listId,
				"text": text
			},
			function(resp) {
				// resp [{"generated_key": 6}]
				var json =
				{
					"text": text,
					"checked": "",
					"list_id": listId,
					"id": JSON.parse(resp)[0].generated_key
				}

				var template = Handlebars.compile($("#single-bullet-template").html());

				// THIS ELEMENT SOMEHOW HAS THE CLICK LISTENER ALREADY ENABLED‽
				var element = $(template(json));

				// possibly better parent finding
				// should be nearest
 element
				form.parent().before(element);
				textArea.val("");
			});
	event.preventDefault();
}

/*
 * This takes a note in json form and adds it it in html form to the dom
 * If the note is a list it also fetches the list items, and adds
 * them to the dom
 */
/*
var createNote = function(json) {
	var template = Handlebars.compile($("#note-template").html());

	var note = $(template(json));

	// trigers for the header
	note.find("h1").click(function (event) {
		var element = $(event.currentTarget);

		if(!element.find("input").is(":focus"))
			makeNoteEditable(element);
		event.stopPropagation();
	});

	// trigers for the rest of the note
	// but not the lists, since they are checked for event propagation before
	note.click(function(event) {
		var element = $(event.currentTarget);

		if(!element.find("textarea").is(":focus"))
			makeNoteEditable(element);
		event.stopPropagation();
	});

	//$("#note-container").prepend(template(json));
	$("#note-container").find(".new-note").after(note);

	if(json.type == "list") {
		$.getJSON("/note/list?id="+json.id, function(list) {
			var note = $("#note-" + json.id);

			var items = $(createHTMLList(list));
			// this enables the "clickability" of the list items
			items.click(checkboxHandler);

			note.children("footer").before(items);

			// adds the new-item form for the bullet list
			var source = $("#new-bullet-template").html();
			var template = Handlebars.compile(source);
			var form = template(json);
			note.children("footer").siblings("ul").append(form);

			var form = note.find("form[name=new-bullet]");
			form.submit(newBulletSubmit);
		});
	}
}
*/

/*
 * Get notes by url, and adds them to the dom
 * /note/all
 * /note?id=<id>
 * 		<id> is a comma separated list of note id's
 */
/*
var getNotes = function(url) {
	$.getJSON(url, function(data) {
		data.map(createNote);
	});
}
*/

/*
 * Get notes by id's, and adds them to the dom
 */
/*
var getNotesById = function(id) {
	var url = "/note?id=" + id;
	getNotes(url);
}
*/

/*
 * Handles the post request when creating a new note
 * Adds the newly posted note to the dom
 */
/*
var newNoteForm = function(event) {
	event.preventDefault();
	var form = $(event.target);

	var json =
	{
		"header": form.find("input[name=header]").val(),
		"body": form.find("textarea[name=body]").val(),
		"type": "list"
	}

	post("/note/submit", json,
			function (response) {
				var key = JSON.parse(response)[0].generated_key;
				getNotesById(key);
			});

	form[0].reset();
}
*/

/*
 * Makes the node editable.
 * Currently only works with articles which have one
 child,
 * or

 *
 * TODO make <input> submit also work with enter
 */
/*
var makeNoteEditable = function(inObject) {
	var object;
	var id;
	var input;
	var type;

	if(inObject.is("h1")) {
		object = inObject;
		id = object.closest("article").data("id");
		var text = object.text();
		input = $("<input type='text' class='seamless' value='"+text+"'/>");
		type = "header";
	} else {
		object = inObject.find("p");
		id = inObject.data("id");
		var text = object.text();
		input = $("<textarea class='seamless'>"+text+"</textarea>");
		type = "body";
	}

	object.html(input);

	// update dom with new data on
	// also send new data to server
	input.focusout(function (event) {
		var newData = event.currentTarget.value;
		object.html(newData);

		var json = { "id": id, };
		json[type] = newData;

		post("/note/update", json);
	});

	input.focus();
}
*/

// ----------------------------------------------------------------------------

/*
 * Enters the note into edit mode.
 * This should allow the user to modify any text or data about the modules,
 * as well as move and remove modules in and from the note.
 */
$.fn.editModule = function() {

	this.each(function() {
		var module = $(this);

		var type = module.data("type");
		var contents = module.children();

		//console.log(contents);

		switch(type) {
			case "header":
			case "text":
				contents.attr("contenteditable", "true");
				break;
			case "image":
				// replace image with text fields for all the atributes
				break;
			case "list":
				var listItems = contents.find("li:not(.new-item)");
				listItems.attr("contenteditable", "true");
				listItems.wrap("");
				// Remove the button when unfocused,
				// but first after a short delay so it still is clickable
				// TODO if this delay is to short the button
				// click isn't registered, keep this in mind
				listItems.focusout(function(event) {
					var li = $(this);
					setTimeout(function() {
						li.siblings("button").remove();
					}, 100);
				});
				break;
		}
	});

	return this;

}

/*
 * Call this function when a note is done editing
 * This should extract the new data from the note,
 * and send it off to the server.
 */
$.fn.endEdit = function(note) {
	this.each(function() {
		var module = $(this);
		var type = module.data("type");
		var contents = module.children();

		switch(type) {
			case "header":
			case "text":
				contents.attr("contenteditable", "false");
				break;
			case "list":
				module.find("span").replaceWith(function() {
					var items = $(this).find("li");
					items.attr("contenteditable", "false");
					return items;
				});
				break;
		}
	});

	return this;
}

// Dialog is from where you came, module is where you are going
// Both should be jQuery objects
$.fn.showNextDialog = function(module, callback) {

	var dialog = this;

	// slideUp hides, sildeDown unhides
	dialog.slideUp(200);
	module.slideDown(200);

	// TODO possibly find a more elegant way to
	// stop multiple listeners from being added
	module.find("button").unbind("click");
	module.find("button").click(function(event) {
		callback(event, this);

		// TODO these animations lag in chrome for linux
		// But not in chrome for android, or firefox for linux
		// look into this
		dialog.slideDown(200);
		module.slideUp(200);

		//dialog.show(0);
		//module.hide(0);
	});

	return this;
}

/*
 * addes a new module of the type 'moduleType',
 * the body 'moduleBody' to the note 'note'
 *
 * Should be applied to a 'note'
 */
$.fn.addNewModule = function(moduleType, moduleBody) {

	var note = this;

	var module = "
" + moduleBody + "
";
	note.find(".module-divide").before(module);

	return this;
}

$(document).ready(function() {
	/*
	 * TODO send data back to the server!
	 */

	// add the submit handler to the new note submit form
	//$(".new-note").submit(newNoteForm);

	// add all notes from the server to start with
	//getNotes("/note/all");

	// enable checkbox lists
	$(document).on(
			"click",
			".checkbox-list > li[contenteditable!='true']:not(.new-item)",
			checkboxHandler);

	// this listener is here so it isn't added every time edit mode is entered
	// It adds the bullet remove button when a bullet is pressed, the selector
	// targets the li element so that the button doesn't spawn another of itself.
	//
	// Note that deleting all the text in a bullet should also remove it
	$(document).on("focus", ".li-spacer > li", function(event) {
		var btn = $("<button class='remove-bullet-button'>❌</button>");
		btn.click(function(event) {
			// TODO send to server that the bullet is removed
			// or at least add it to the stack of pending changes,
			// if I decide to both have a "save changes" and
			// "cancel changes" button
			console.log(this);
			$(this).parents(".li-spacer").remove();
		});
		$(this).after(btn);
	});

	// add meta buttons
	$(".module-divide").after($("#meta-control-template").html());

	// edit button
	$(document).on("click", "button.edit-module-btn", function(event) {
		var note = $(this).parents(".note");
		note.find("div[role='module']:not(.meta-module)").editModule();
		note.find(".meta-control-module").showNextDialog(
				note.find(".edit-control-module"),
				function(event, source) {
					note.find("div[role='module']:not(.meta-module)").endEdit();
				});
	});

	// new module button
	$(document).on("click", "button.new-module-btn", function(event) {
		var note = $(this).parents(".note");
		note.find(".meta-control-module").showNextDialog(
				note.find(".module-adder-module"),
				function(event, source) {
					console.log(source);
					var moduleBody;
					var moduleType = source.name;
					switch(moduleType) {
						case "header":
							moduleBody = "
New Header!
";
							break;
						case "text":
							moduleBody = "
New text module created!
";
							break;
						case "image":
							moduleBody = "";
							break;
						case "list":
							// TODO this should also add in the item for adding more bullets,
							// ...yet another reason for it to be in a template
							moduleBody = "

sample

";
							break;
					}
					if(moduleType !== "cancel") {
						note.addNewModule(moduleType, moduleBody);
					}
				});

		event.stopPropagation();
	});

	// remove button
	$(document).on("click", "button.remove-module-btn", function(event) {
		var note = $(this).parents(".note");

		note.find(".meta-control-module").showNextDialog(
				note.find(".remove-confirm-module"),
				function(event, source) {
					var note = $(source).closest(".note");
					switch (source.name) {
						case "confirm":
							note.hide("slow", function() {
								note.remove();
							});
							break;
						case "cancel":
							break;
						default:
					}
				});
		event.stopPropagation();
	});
});
<commit_msg>added animation to module adding<commit_after>/*
 * posts a json object to the server
 * No checks are in place to see if the data is valid
 * The reason for not just calling $.post directly is that this
 * 	first adds an anti forgery token to the request
 */
var post = function(url, json, callback) {
	// TODO can I handle the anti forgery token like this?
	$.get("/note/token/raw", function(token) {
		json["__anti-forgery-token"] = token;
		$.post(url, json, callback);
	});
}

/*
 * This enables the functionalty of selecting and deselecting
 * the list bullet points.
 *
 * TODO reenable post
 */
var checkboxHandler = function(event) {
	var el = $(this);

	var newValue;
	if(el.hasClass("checked")) {
		newValue = 0;
		el.removeClass("checked");
	} else {
		newValue = 1;
		el.addClass("checked");
	}

	// this is also called when pressing the new-item 'item'
	// it probably shouldn't
	/*
	post("/note/list/set-checkbox",
			{
				"list-id": el.data("listId"),
				"id": el.data("id"),
				"new-value": newValue,
			});
			*/
	event.stopPropagation();
}

/*
 * This takes a json list of list bullets, and returns the HTML equivalent
 */
/*
var createHTMLList = function(json) {
	var bullets = "";
	var bulletTemplate = Handlebars.compile($("#single-bullet-template").html());
	json.map(function(item) {
		if(item.done)
			item.checked = "checked";
		bullets += bulletTemplate(item);
	});

	return "
"+bullets+"
";
}
*/

/*
 * This handles the post request when adding a new bullet to a list
 *
 * TODO this only needs minor cleanup for the new version
 */
var newBulletSubmit = function(event) {
	var form = $(event.target);

	var textArea = form.find("input[name=text]");
	var text = textArea.val();
	var listId = form.data("listId");

	post("/note/list/add-item",
			{
				"list-id": listId,
				"text": text
			},
			function(resp) {
				// resp [{"generated_key": 6}]
				var json =
				{
					"text": text,
					"checked": "",
					"list_id": listId,
					"id": JSON.parse(resp)[0].generated_key
				}

				var template = Handlebars.compile($("#single-bullet-template").html());

				// THIS ELEMENT SOMEHOW HAS THE CLICK LISTENER ALREADY ENABLED‽
				var element = $(template(json));

				// possibly better parent finding
				// should be nearest
 element
				form.parent().before(element);
				textArea.val("");
			});
	event.preventDefault();
}

/*
 * This takes a note in json form and adds it it in html form to the dom
 * If the note is a list it also fetches the list items, and adds
 * them to the dom
 */
/*
var createNote = function(json) {
	var template = Handlebars.compile($("#note-template").html());

	var note = $(template(json));

	// trigers for the header
	note.find("h1").click(function (event) {
		var element = $(event.currentTarget);

		if(!element.find("input").is(":focus"))
			makeNoteEditable(element);
		event.stopPropagation();
	});

	// trigers for the rest of the note
	// but not the lists, since they are checked for event propagation before
	note.click(function(event) {
		var element = $(event.currentTarget);

		if(!element.find("textarea").is(":focus"))
			makeNoteEditable(element);
		event.stopPropagation();
	});

	//$("#note-container").prepend(template(json));
	$("#note-container").find(".new-note").after(note);

	if(json.type == "list") {
		$.getJSON("/note/list?id="+json.id, function(list) {
			var note = $("#note-" + json.id);

			var items = $(createHTMLList(list));
			// this enables the "clickability" of the list items
			items.click(checkboxHandler);

			note.children("footer").before(items);

			// adds the new-item form for the bullet list
			var source = $("#new-bullet-template").html();
			var template = Handlebars.compile(source);
			var form = template(json);
			note.children("footer").siblings("ul").append(form);

			var form = note.find("form[name=new-bullet]");
			form.submit(newBulletSubmit);
		});
	}
}
*/

/*
 * Get notes by url, and adds them to the dom
 * /note/all
 * /note?id=<id>
 * 		<id> is a comma separated list of note id's
 */
/*
var getNotes = function(url) {
	$.getJSON(url, function(data) {
		data.map(createNote);
	});
}
*/

/*
 * Get notes by id's, and adds them to the dom
 */
/*
var getNotesById = function(id) {
	var url = "/note?id=" + id;
	getNotes(url);
}
*/

/*
 * Handles the post request when creating a new note
 * Adds the newly posted note to the dom
 */
/*
var newNoteForm = function(event) {
	event.preventDefault();
	var form = $(event.target);

	var json =
	{
		"header": form.find("input[name=header]").val(),
		"body": form.find("textarea[name=body]").val(),
		"type": "list"
	}

	post("/note/submit", json,
			function (response) {
				var key = JSON.parse(response)[0].generated_key;
				getNotesById(key);
			});

	form[0].reset();
}
*/

/*
 * Makes the node editable.
 * Currently only works with articles which have one
 child,
 * or

 *
 * TODO make <input> submit also work with enter
 */
/*
var makeNoteEditable = function(inObject) {
	var object;
	var id;
	var input;
	var type;

	if(inObject.is("h1")) {
		object = inObject;
		id = object.closest("article").data("id");
		var text = object.text();
		input = $("<input type='text' class='seamless' value='"+text+"'/>");
		type = "header";
	} else {
		object = inObject.find("p");
		id = inObject.data("id");
		var text = object.text();
		input = $("<textarea class='seamless'>"+text+"</textarea>");
		type = "body";
	}

	object.html(input);

	// update dom with new data on
	// also send new data to server
	input.focusout(function (event) {
		var newData = event.currentTarget.value;
		object.html(newData);

		var json = { "id": id, };
		json[type] = newData;

		post("/note/update", json);
	});

	input.focus();
}
*/

// ----------------------------------------------------------------------------

/*
 * Enters the note into edit mode.
 * This should allow the user to modify any text or data about the modules,
 * as well as move and remove modules in and from the note.
 */
$.fn.editModule = function() {

	this.each(function() {
		var module = $(this);

		var type = module.data("type");
		var contents = module.children();

		//console.log(contents);

		switch(type) {
			case "header":
			case "text":
				contents.attr("contenteditable", "true");
				break;
			case "image":
				// replace image with text fields for all the atributes
				break;
			case "list":
				var listItems = contents.find("li:not(.new-item)");
				listItems.attr("contenteditable", "true");
				listItems.wrap("");
				// Remove the button when unfocused,
				// but first after a short delay so it still is clickable
				// TODO if this delay is to short the button
				// click isn't registered, keep this in mind
				listItems.focusout(function(event) {
					var li = $(this);
					setTimeout(function() {
						li.siblings("button").remove();
					}, 100);
				});
				break;
		}
	});

	return this;

}

/*
 * Call this function when a note is done editing
 * This should extract the new data from the note,
 * and send it off to the server.
 */
$.fn.endEdit = function(note) {
	this.each(function() {
		var module = $(this);
		var type = module.data("type");
		var contents = module.children();

		switch(type) {
			case "header":
			case "text":
				contents.attr("contenteditable", "false");
				break;
			case "list":
				module.find("span").replaceWith(function() {
					var items = $(this).find("li");
					items.attr("contenteditable", "false");
					return items;
				});
				break;
		}
	});

	return this;
}

// Dialog is from where you came, module is where you are going
// Both should be jQuery objects
$.fn.showNextDialog = function(module, callback) {

	var dialog = this;

	// slideUp hides, sildeDown unhides
	dialog.slideUp(200);
	module.slideDown(200);

	// TODO possibly find a more elegant way to
	// stop multiple listeners from being added
	module.find("button").unbind("click");
	module.find("button").click(function(event) {
		callback(event, this);

		// TODO these animations lag in chrome for linux
		// But not in chrome for android, or firefox for linux
		// look into this
		dialog.slideDown(200);
		module.slideUp(200);

		//dialog.show(0);
		//module.hide(0);
	});

	return this;
}

/*
 * addes a new module of the type 'moduleType',
 * the body 'moduleBody' to the note 'note'
 *
 * Should be applied to a 'note'
 */
$.fn.addNewModule = function(moduleType, moduleBody) {

	var note = this;
	var moduleDivide = note.find(".module-divide");

	$("
" + moduleBody + "
")
		.hide()
		.insertBefore(moduleDivide)
		.slideDown();

	return this;
}

$(document).ready(function() {
	/*
	 * TODO send data back to the server!
	 */

	// add the submit handler to the new note submit form
	//$(".new-note").submit(newNoteForm);

	// add all notes from the server to start with
	//getNotes("/note/all");

	// enable checkbox lists
	$(document).on(
			"click",
			".checkbox-list > li[contenteditable!='true']:not(.new-item)",
			checkboxHandler);

	// this listener is here so it isn't added every time edit mode is entered
	// It adds the bullet remove button when a bullet is pressed, the selector
	// targets the li element so that the button doesn't spawn another of itself.
	//
	// Note that deleting all the text in a bullet should also remove it
	$(document).on("focus", ".li-spacer > li", function(event) {
		var btn = $("<button class='remove-bullet-button'>❌</button>");
		btn.click(function(event) {
			// TODO send to server that the bullet is removed
			// or at least add it to the stack of pending changes,
			// if I decide to both have a "save changes" and
			// "cancel changes" button
			console.log(this);
			$(this).parents(".li-spacer").remove();
		});
		$(this).after(btn);
	});

	// add meta buttons
	$(".module-divide").after($("#meta-control-template").html());

	// edit button
	$(document).on("click", "button.edit-module-btn", function(event) {
		var note = $(this).parents(".note");
		note.find("div[role='module']:not(.meta-module)").editModule();
		note.find(".meta-control-module").showNextDialog(
				note.find(".edit-control-module"),
				function(event, source) {
					note.find("div[role='module']:not(.meta-module)").endEdit();
				});
	});

	// new module button
	$(document).on("click", "button.new-module-btn", function(event) {
		var note = $(this).parents(".note");
		note.find(".meta-control-module").showNextDialog(
				note.find(".module-adder-module"),
				function(event, source) {
					console.log(source);
					var moduleBody;
					var moduleType = source.name;
					switch(moduleType) {
						case "header":
							moduleBody = "
New Header!
";
							break;
						case "text":
							moduleBody = "
New text module created!
";
							break;
						case "image":
							moduleBody = "";
							break;
						case "list":
							// TODO this should also add in the item for adding more bullets,
							// ...yet another reason for it to be in a template
							moduleBody = "

sample

";
							break;
					}
					if(moduleType !== "cancel") {
						note.addNewModule(moduleType, moduleBody);
					}
				});

		event.stopPropagation();
	});

	// remove button
	$(document).on("click", "button.remove-module-btn", function(event) {
		var note = $(this).parents(".note");

		note.find(".meta-control-module").showNextDialog(
				note.find(".remove-confirm-module"),
				function(event, source) {
					var note = $(source).closest(".note");
					switch (source.name) {
						case "confirm":
							note.hide("slow", function() {
								note.remove();
							});
							break;
						case "cancel":
							break;
						default:
					}
				});
		event.stopPropagation();
	});
});
<|endoftext|>