summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorAlon Levy <alon@pobox.com>2014-12-16 17:37:42 +0200
committerAlon Levy <alon@pobox.com>2014-12-16 17:37:42 +0200
commitade19de410e0a789a921ec1666b278b2a64176d6 (patch)
treec4c54b106de93609ca7bb70a20fd4be696715dbe /src/client
parentc7d026b1d504323a8c61d51b726e5e8d2337fc4b (diff)
moving files around after repository merger
Diffstat (limited to 'src/client')
-rw-r--r--src/client/ScrollTo.js17
-rw-r--r--src/client/app.js31
-rw-r--r--src/client/buttons.js82
-rw-r--r--src/client/consts.js25
-rw-r--r--src/client/drag_n_drop.js31
-rw-r--r--src/client/history.js149
-rw-r--r--src/client/main.js94
-rw-r--r--src/client/model/core.js187
-rw-r--r--src/client/model/diff.js187
-rw-r--r--src/client/model/graph.js873
-rw-r--r--src/client/model/util.js134
-rw-r--r--src/client/robot.js70
-rw-r--r--src/client/rz_api_backend.js222
-rw-r--r--src/client/rz_api_mesh.js22
-rw-r--r--src/client/rz_bus.js15
-rw-r--r--src/client/rz_config.js9
-rw-r--r--src/client/rz_core.js756
-rw-r--r--src/client/rz_observer.js114
-rw-r--r--src/client/textanalysis.js501
-rw-r--r--src/client/textanalysis.ui.js195
-rw-r--r--src/client/util.js72
-rw-r--r--src/client/view/completer.js229
-rw-r--r--src/client/view/edge_info.js36
-rw-r--r--src/client/view/helpers.js87
-rw-r--r--src/client/view/internal.js14
-rw-r--r--src/client/view/node_info.js117
-rw-r--r--src/client/view/selection.js102
-rw-r--r--src/client/view/tab.js57
-rw-r--r--src/client/view/timeline.js180
-rw-r--r--src/client/view/view.js12
30 files changed, 4620 insertions, 0 deletions
diff --git a/src/client/ScrollTo.js b/src/client/ScrollTo.js
new file mode 100644
index 00000000..a015f08c
--- /dev/null
+++ b/src/client/ScrollTo.js
@@ -0,0 +1,17 @@
+$.fn.scrollTo = function( target, options, callback ){
+ if(typeof options == 'function' && arguments.length == 2){ callback = options; options = target; }
+ var settings = $.extend({
+ scrollTarget : target,
+ offsetTop : 50,
+ duration : 500,
+ easing : 'swing'
+ }, options);
+ return this.each(function(){
+ var scrollPane = $(this);
+ var scrollTarget = (typeof settings.scrollTarget == "number") ? settings.scrollTarget : $(settings.scrollTarget);
+ var scrollY = (typeof scrollTarget == "number") ? scrollTarget : scrollTarget.offset().top + scrollPane.scrollTop() - parseInt(settings.offsetTop);
+ scrollPane.animate({scrollTop : scrollY }, parseInt(settings.duration), settings.easing, function(){
+ if (typeof callback == 'function') { callback.call(this); }
+ });
+ });
+} \ No newline at end of file
diff --git a/src/client/app.js b/src/client/app.js
new file mode 100644
index 00000000..39acdde7
--- /dev/null
+++ b/src/client/app.js
@@ -0,0 +1,31 @@
+(function() {
+ var lib_path = '../lib/';
+ var config = {
+ paths: {
+ jquery: lib_path + 'jquery',
+ 'jquery-ui': lib_path + 'jquery-ui',
+ 'd3': lib_path + 'd3/d3',
+ FileSaver: lib_path + 'FileSaver',
+ caret: lib_path + 'caret',
+ autocomplete: lib_path + 'autocomplete',
+ Bacon: lib_path + 'Bacon',
+ }
+ }
+
+ config.urlArgs = (typeof local_config != 'undefined') && local_config.urlArgs;
+
+if (window.is_node) {
+ // Testing path only
+ console.log('app: running under node');
+ config.baseUrl = '../src/';
+ window.rhizi_require_config = config;
+} else {
+ // Main app path
+ require.config(config);
+
+ requirejs(['main'], function(main) {
+ console.log('starting rhizi logic');
+ main.main();
+ });
+}
+}());
diff --git a/src/client/buttons.js b/src/client/buttons.js
new file mode 100644
index 00000000..860c8573
--- /dev/null
+++ b/src/client/buttons.js
@@ -0,0 +1,82 @@
+"use strict"
+
+define(['jquery', 'FileSaver', 'rz_core'], function ($, saveAs, rz_core) {
+$('.tutorial').click(function(){});
+
+var key="#47989379";
+
+
+$('.save a').click(function(){
+ var json = rz_core.graph.save_to_json();
+ console.log('saving to local storage ' + json.length + ' bytes');
+ localStorage.setItem(key, json);
+});
+
+$('.export').click(function() {
+ var json = rz_core.graph.save_to_json();
+ var filename = 'graph.json';
+ var blob = new Blob([json], {type: 'application/json'});
+ console.log('saving ' + json.length + ' bytes to ' + filename);
+ saveAs(blob, filename);
+});
+
+$('.url-copy a').click(function() {
+ var json = rz_core.graph.save_to_json();
+ // TODO use jquery BBQ $.param({json: json});
+ var encoded = document.location.origin + '/?json=' + encodeURIComponent(json);
+ window.prompt('Copy to clipboard: Ctrl-C, Enter (or Cmd-C for Mac)', encoded);
+});
+
+var really_load = function() {
+ if (!rz_core.graph.empty()) {
+ return confirm('All unsaved changes will be deleted, are you sure?');
+ }
+ return true;
+}
+
+$('.file-import').on('change', function(event) {
+ var file = event.target.files[0];
+ var reader;
+
+ if (!really_load()) {
+ return;
+ }
+ if (file === undefined) {
+ return;
+ }
+ console.log(file);
+ reader = new FileReader();
+ reader.onload = (function(theFile) {
+ return function(e) {
+ var result = e.target.result;
+ if (e.target.readyState === FileReader.DONE) {
+ console.log('done reading ' + theFile.name);
+ console.log('got #' + result.length + ' bytes in ' + typeof(result));
+ rz_core.load_from_json(result);
+ }
+ }
+ })(file);
+ reader.readAsText(file, "text/javascript");
+});
+
+$('.local-storage-load a').click(function(){
+ if (!really_load()) {
+ return;
+ }
+ var json_blob = localStorage.getItem(key)
+ rz_core.load_from_json(json_blob);
+});
+
+var logout_button = $('#logout-button');
+logout_button.click(function() {
+ $.ajax({ type: "POST", url: '/logout'}); // server should redirect back to /login
+});
+
+$('a.save-history').click(function() {
+ if (rz_core.graph.history === undefined) {
+ throw "History is undefined";
+ }
+ rz_core.graph.history.save_to_file();
+});
+return {'buttons': 'nothing here'};
+}); // define
diff --git a/src/client/consts.js b/src/client/consts.js
new file mode 100644
index 00000000..32167811
--- /dev/null
+++ b/src/client/consts.js
@@ -0,0 +1,25 @@
+"use strict"
+
+define(function() {
+
+ var nodetypes = ["person", "club", "skill", "interest", "third-internship-proposal", "internship"];
+
+ var description = {
+ person: 'A person in CRI - student or teacher',
+ club: 'A shared club or project within the CRI',
+ skill: 'Ability or expertise you possess',
+ interest: 'Scientific skill or domain expertise you wish you had',
+ 'third-internship-proposal': 'Create this to submit your third internship proposal',
+ internship: "Title of first or second internship you've done"
+ };
+
+ // TODO: enums, sometime
+ return {
+ KEYSTROKE_WHERE_EDIT_NODE: 'keystroke_where_edit_node',
+ KEYSTROKE_WHERE_DOCUMENT: 'keystroke_where_document',
+ KEYSTROKE_WHERE_TEXTANALYSIS: 'keystroke_where_textanalysis',
+ INPUT_WHERE_TEXTANALYSIS: 'input_where_textanalysis',
+ nodetypes: nodetypes,
+ description: description,
+ };
+});
diff --git a/src/client/drag_n_drop.js b/src/client/drag_n_drop.js
new file mode 100644
index 00000000..da166ad4
--- /dev/null
+++ b/src/client/drag_n_drop.js
@@ -0,0 +1,31 @@
+define(['jquery', 'rz_core'], function($, rz_core) {
+
+function init() {
+console.log('rhizi: init drag-n-drop');
+$(document).on('drop', function(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ var files = e.originalEvent.dataTransfer.files;
+ var file = files[files.length - 1];
+ var fr = new FileReader();
+ fr.onload = function() {
+ if (fr.readyState != 2) {
+ console.log('drop: error: reading from file failed');
+ } else {
+ console.log('loading dropped file');
+ rz_core.load_from_json(fr.result);
+ }
+ }
+ fr.readAsText(file);
+ return false;
+});
+$(document).on('dragover', function (e)
+{
+ e.stopPropagation();
+ e.preventDefault();
+ return false;
+});
+};
+return {'init': init };
+
+}); // define
diff --git a/src/client/history.js b/src/client/history.js
new file mode 100644
index 00000000..ab864d1c
--- /dev/null
+++ b/src/client/history.js
@@ -0,0 +1,149 @@
+"use strict"
+// Once upon a time we shall have a versioned property graph from which history
+// will be one extractable aspect, much like a git for graphs. Now we just have
+// a plain list of events for a specific user.
+
+// Enums and chrome don't play along well. Object.freeze I guess? actually rhizi code cuases
+// exceptions but that shouldn't break the console, as evidenced by the '__commandLineAPI is not defined'
+// error below.
+//
+// Uncaught TypeError: Can't add property addednodes, object is not extensible rz_core.js:4
+// Uncaught TypeError: Can't add property text, object is not extensible textanalysis.js:3
+// Uncaught TypeError: Can't add property key, object is not extensible buttons.js:5
+// Uncaught ReferenceError: sentence is not defined robot.js:11
+// Resource interpreted as Font but transferred with MIME type application/font-sfnt: "http://localhost:8000/external/Lato300.ttf". jquery.js:2
+// > document
+// ReferenceError: __commandLineAPI is not defined
+//var ActionEnum = Enum();
+
+define(['jquery', 'FileSaver', 'consts', 'rz_bus'],
+ function($, saveAs, consts, rz_bus) {
+
+/* user - username (string)
+ * svg - svg element for catching zoom events (jquery DOMNode wrapper)
+ */
+function History(user, graph, transform_element) {
+ var that = this;
+ this.records = [];
+ this.user = user;
+ this.transform_element = transform_element;
+ graph.diffBus.onValue(function (obj) {
+ return that.record_graph_diff(obj)
+ });
+ rz_bus.ui_key.onValue(that.record_keystrokes.bind(that));
+ rz_bus.ui_input.onValue(that.record_input.bind(that));
+ // XXX create zoom behavior - then proof to event name change
+ $(window).on('wheel.history', function(obj) {
+ that.record_zoom(obj);
+ return true;
+ });
+}
+
+var ACTION_KEYSTROKES = 'ACTION_KEYSTROKES';
+var ACTION_INPUT = 'ACTION_INPUT';
+var ACTION_GRAPH_DIFF = 'ACTION_GRAPH_DIFF';
+var ACTION_ZOOM = 'ACTION_ZOOM';
+
+var KEYSTROKE_WHERE_TEXTANALYSIS = 'KEYSTROKE_WHERE_TEXTANALYSIS';
+var KEYSTROKE_WHERE_DOCUMENT = 'KEYSTROKE_WHERE_DOCUMENT';
+var KEYSTROKE_WHERE_EDIT_NODE = 'KEYSTROKE_WHERE_EDIT_NODE';
+
+History.prototype.record = function(action, d)
+{
+ if (d === undefined || action === undefined) {
+ throw "Invalid arguments";
+ }
+ d['action'] = action;
+ d['user'] = this.user;
+ d['timestamp'] = new Date();
+ this.records.push(d);
+ $('.history-timeline').html('<pre>' + JSON.stringify(d) + '</pre>');
+};
+
+function svg_extract_translate_and_scale(e)
+{
+ // See: http://stackoverflow.com/questions/10349811/how-to-manipulate-translate-transforms-on-a-svg-element-with-javascript-in-chrom
+ // Using the regexp option right now, did only firefox testing 36
+ var str = e.attributes['transform'].value;
+ var parts = /translate\(\s*([^\s,)]+)[ ,]([^\s,)]+)/.exec(str);
+ var scale = /scale\(\s*([^\s)]+)\)/.exec(str);
+ if (scale) {
+ var x = parts[1], y = parts[2];
+ return {scale:+scale[1], translate: [+x, +y]};
+ } else {
+ return {scale:1.0, translate: [0.0, 0.0]};
+ }
+}
+
+History.prototype.record_zoom = function(d)
+{
+ var transform = svg_extract_translate_and_scale(this.transform_element);
+
+ if (transform === undefined) {
+ console.log('record_zoom: bug: transform_element has no transform attribute');
+ return;
+ }
+ this.record(ACTION_ZOOM, {transform: transform});
+}
+
+History.prototype.save_to_file = function()
+{
+ var json = JSON.stringify(this.records, function (k, v) { return v; }, 2);
+
+ saveAs(new Blob([json], {type: 'application/json'}), 'history.json');
+};
+
+History.prototype.clear = function()
+{
+ this.records = [];
+}
+
+History.prototype.record_graph_diff = function(obj)
+{
+ this.record(ACTION_GRAPH_DIFF, {
+ nodes: {add: obj.nodes && obj.nodes.add, remove: obj.nodes && obj.nodes.remove,
+ change: obj.nodes && obj.nodes.change},
+ links: {add: obj.links && obj.links.add, remove: obj.links && obj.links.remove,
+ change: obj.links && obj.links.change},
+ });
+}
+
+History.prototype.record_keystrokes = function(obj)
+{
+ var where = obj.where,
+ keys = obj.keys;
+
+ if (where === undefined || keys === undefined || keys.length === undefined ||
+ keys.length <= 0) {
+ console.log("invalid arguments");
+ return;
+ }
+ keys = keys.filter(function(k) { return k !== undefined; });
+ if (keys.length == 0) {
+ return;
+ }
+ this.record(ACTION_KEYSTROKES, {
+ keys: keys,
+ where: where
+ });
+}
+
+History.prototype.record_input = function(obj)
+{
+ var where = obj.where,
+ input = obj.input;
+
+ if (where === undefined || input === undefined || input.length === undefined || typeof input !== 'string') {
+ console.log('invalid arguments');
+ return;
+ }
+ this.record(ACTION_INPUT, {where: where, input: input});
+}
+
+return {
+ History:History,
+ KEYSTROKE_WHERE_TEXTANALYSIS:KEYSTROKE_WHERE_TEXTANALYSIS,
+ KEYSTROKE_WHERE_DOCUMENT:KEYSTROKE_WHERE_DOCUMENT,
+ KEYSTROKE_WHERE_EDIT_NODE:KEYSTROKE_WHERE_EDIT_NODE
+};
+}); // define
diff --git a/src/client/main.js b/src/client/main.js
new file mode 100644
index 00000000..863d0440
--- /dev/null
+++ b/src/client/main.js
@@ -0,0 +1,94 @@
+define(['textanalysis.ui', 'textanalysis', 'buttons', 'history', 'drag_n_drop', 'robot', 'model/core', 'rz_config', 'rz_core', 'view/selection', 'util', 'view/completer'],
+function(textanalysis_ui, textanalysis, buttons, history, drag_n_drop, robot, model_core, rz_config, rz_core, selection, util, completer) {
+
+ function expand(obj){
+ if (!obj.savesize) {
+ obj.savesize = obj.size;
+ }
+ obj.size = Math.max(obj.savesize, obj.value.length);
+ }
+
+ this.main = function() {
+ var json,
+ search = $('#search'),
+ search_completer = completer(search, $('#search-suggestion'),
+ {triggerStart:' ', triggerEnd:' '});
+
+ console.log('Rhizi main started');
+ search_completer.options.plug(textanalysis.suggestions_options);
+ drag_n_drop.init();
+ $('#editname').onkeyup = function() { expand(this); };
+ $('#editlinkname').onkeyup = function() { expand(this); };
+ $('#textanalyser').onkeyup = function() { expand(this); };
+
+ textanalysis_ui.main();
+
+ json = util.getParameterByName('json');
+ if (json) {
+ rz_core.load_from_json(json);
+ }
+ if (util.getParameterByName('debug')) {
+ $(document.body).addClass('debug');
+ rz_core.graph.set_user('fakeuser');
+ }
+
+ document.body.onkeyup = function(e) {
+ var key = (e.key || (e.charCode && String.fromCharCode(e.charCode))
+ || (e.which && String.fromCharCode(e.which))).toLowerCase();
+
+ if (e.altKey && e.ctrlKey && key == 'i') {
+ $('#textanalyser').focus();
+ }
+ if (e.altKey && e.ctrlKey && key == 'o') {
+ search.focus();
+ }
+ if (e.ctrlKey && key == 'z' && e.target.nodeName !== 'INPUT') {
+ // TODO: rz_core.graph.undo();
+ }
+ };
+ // TODO: move me somewhere
+ function search_on_submit() {
+ var text = search[0].value.trim(),
+ r;
+
+ try {
+ r = new RegExp(text.replace(/ /, '|'), 'i');
+ } catch (e) {
+ return; // don't clear selection either
+ }
+ if (text.length > 0) {
+ selection.byVisitors(function (n) { return n.name.match(r); });
+ } else {
+ selection.clear();
+ }
+ rz_core.update_view__graph(false);
+ };
+ search.on('input', search_on_submit);
+ search.on('keydown', function(e) {
+ if (e.which == 13 && !search_completer.handleEnter()) {
+ e.preventDefault();
+ search_on_submit(e);
+ return false;
+ }
+ return undefined;
+ });
+
+ var intro_task_elem = $('#intro-task');
+ // TODO: messages (why tasks?) - this one is special but we want them to be handled in their own file.
+ if (!localStorage.intro_task_hide) {
+ intro_task_elem.show();
+ }
+ $('#intro-task .task-close-button').click(function(e) {
+ localStorage.intro_task_hide = true;
+ intro_task_elem.hide();
+ });
+
+ // TODO: interaction between the hack above and this
+ model_core.init(rz_config);
+ textanalysis.init(rz_core.graph);
+ }
+
+ return {
+ main: main };
+ }
+);
diff --git a/src/client/model/core.js b/src/client/model/core.js
new file mode 100644
index 00000000..5c643ea1
--- /dev/null
+++ b/src/client/model/core.js
@@ -0,0 +1,187 @@
+"use strict"
+
+/**
+ * core model module - currently unused
+ */
+define(['util'], function(util) {
+
+ /**
+ * return a random id
+ */
+ var random_id;
+
+ var random_id__hash = function() {
+ return Math.random().toString(36).substring(2, 10);
+ }
+
+ var random_id__seq = function () {
+ var id = 0;
+ function get_next() {
+ var next = id;
+ id += 1;
+ return next;
+ }
+ return get_next;
+ }
+
+ function random_node_name() {
+ return random_id__hash();
+ }
+
+ function init(config){
+ if (config['rand_id_generator'] == 'hash') {
+ random_id = random_id__hash;
+ }
+ if (config['rand_id_generator'] == 'seq') {
+ random_id = random_id__seq();
+ }
+ }
+
+ function Node() {
+ }
+ Node.prototype.equals = function(other_node){
+ return this.id == other_node.id;
+ }
+
+ function Link() {
+ }
+ // adapte Link to force_layoutL create __src,__dst aliases
+ Link.prototype.__defineGetter__('source', function(){
+ return this.__src;
+ });
+ Link.prototype.__defineGetter__('target', function(){
+ return this.__dst;
+ });
+
+ /**
+ * the most flexible way to create a node: - perform spec field validation -
+ * fill-in missing spec fields
+ */
+ function create_node_from_spec(node_spec) {
+ var ret = new Node();
+
+ if (undefined != node_spec.id) {
+ // reuse id if present
+ __set_obj_id(ret, node_spec.id);
+ }
+
+ util.assert(undefined != node_spec.name, 'create_node_from_spec: name missing');
+
+ ret.name = node_spec.name;
+
+ // type
+ if (undefined == node_spec.type) {
+ console.debug('create_node_from_spec: undefined type, falling back to \'empty\'');
+ node_spec.type = 'empty';
+ }
+ ret.type = node_spec.type;
+
+ // status
+ ret.status = node_spec.status || 'unknown';
+
+ // visual
+ ret.x = node_spec.x;
+ ret.y = node_spec.y;
+
+ // other
+ ret.state = node_spec.state;
+ ret.url = node_spec.url;
+ ret.start = node_spec.start;
+ ret.end = node_spec.end;
+
+ return ret;
+ }
+
+ function __set_obj_id(obj, id) {
+ Object.defineProperty(obj, "id", {
+ value: id,
+ enumerable: true,
+ writable: false
+ });
+ }
+
+ /**
+ * @param node_spec: id must not be defined
+ */
+ function create_node__set_random_id(node_spec) {
+ if (undefined == node_spec) {
+ node_spec = {};
+ }
+
+ var ret = create_node_from_spec(node_spec);
+
+ util.assert(undefined == ret.id); // id must not be defined in spec
+ __set_obj_id(ret, random_id());
+
+ return ret;
+ }
+
+ function create_link__set_random_id(src, dst, link_spec) {
+ var ret = create_link_from_spec(src, dst, link_spec);
+ __set_obj_id(ret, random_id());
+ return ret;
+ }
+
+ /**
+ * determine if nodes are equal by name
+ *
+ * @param other_node
+ * @returns {Boolean}
+ */
+ Node.prototype.equal_by_name = function(other) {
+ ret = this.name.toLowerCase() == other.name.toLowerCase();
+ if (false == ret) {
+ console.debug(this.id + ' != ' + other.id);
+ }
+ return ret;
+ }
+
+ function create_link_from_spec(src, dst, link_spec) {
+ var ret = new Link();
+
+ if (undefined != link_spec.id) {
+ // reuse id if present
+ __set_obj_id(ret, link_spec.id);
+ }
+
+ util.assert(undefined != src, 'create_link_from_spec: src missing');
+ util.assert(undefined != dst, 'create_link_from_spec: dst missing');
+ util.assert(undefined != src.id, 'create_link_from_spec: src missing id');
+ util.assert(undefined != dst.id, 'create_link_from_spec: dst missing id');
+ util.assert(undefined != link_spec.name, 'create_link_from_spec: name missing, unable to deduce type');
+
+ ret.__src = src;
+ ret.__dst = dst;
+ ret.__type = link_spec.name;
+
+ if (undefined == link_spec.name){
+ console.warn('create_link_from_spec: name: ' + link_spec.name);
+ link_spec.name = "";
+ }
+ ret.name = link_spec.name.trim();
+
+ ret.state = link_spec.state;
+ return ret;
+ }
+
+ /**
+ * determine if links are equal by ID
+ *
+ * @param other_node
+ * @returns {Boolean}
+ */
+ Link.prototype.equal_by_id = function(other) {
+ return this.id.toLowerCase() == other.id.toLowerCase();
+ }
+
+ return {
+ init : init,
+ Node: Node, // allow model adaptation
+ Link: Link, // allow model adaptation
+ random_node_name : random_node_name,
+ create_node_from_spec : create_node_from_spec,
+ create_node__set_random_id : create_node__set_random_id,
+ create_link_from_spec : create_link_from_spec,
+ create_link__set_random_id : create_link__set_random_id,
+ };
+});
diff --git a/src/client/model/diff.js b/src/client/model/diff.js
new file mode 100644
index 00000000..8903b161
--- /dev/null
+++ b/src/client/model/diff.js
@@ -0,0 +1,187 @@
+"use strict"
+
+/**
+ * Diff module
+ */
+define([],
+ function() {
+
+ /**
+ * A set of diff objects
+ */
+ function Diff_Set(obj_spec) {
+ this.__diff_set_topo = [];
+ this.__diff_set_attr = [];
+ this.__diff_set_vis = [];
+ }
+ Diff_Set.prototype.add_diff_obj = function(diff_obj) {
+ if (diff_obj instanceof Topo_Diff) {
+ this.__diff_set_topo.push(diff_obj);
+ }
+ if (diff_obj instanceof Attr_Diff) {
+ this.__diff_set_attr.push(diff_obj);
+ }
+ if (diff_obj instanceof Vis_Diff) {
+ this.__diff_set_vis.push(diff_obj);
+ }
+ }
+
+ /**
+ * Topological diff object
+ */
+ function Topo_Diff(obj_spec) {
+
+ this.link_set_rm = obj_spec.link_set_rm;
+ this.node_set_rm = obj_spec.node_set_rm;
+ this.node_set_add = obj_spec.node_set_add;
+ this.link_set_add = obj_spec.link_set_add;
+
+ }
+ Topo_Diff.prototype.for_each_node_add = function(callback, this_arg) {
+ this.node_set_add.forEach(callback, this_arg);
+ }
+
+ Topo_Diff.prototype.for_each_node_rm = function(callback, this_arg) {
+ this.node_set_rm.forEach(callback, this_arg);
+ }
+
+ Topo_Diff.prototype.for_each_link_add = function(callback, this_arg) {
+ this.link_set_add.forEach(callback, this_arg);
+ }
+
+ Topo_Diff.prototype.for_each_link_rm = function(callback, this_arg) {
+ this.link_set_rm.forEach(callback, this_arg);
+ }
+
+ /**
+ * Attribute diff object, organized by type, where currently
+ * node,link types are supported
+ */
+ function Attr_Diff(obj_spec) {
+ this.__type_node = {};
+ this.__type_link = {};
+ }
+
+ Attr_Diff.prototype.init_attr_diff = function(type_name, id) {
+
+ if ('node' != type_name && 'link' != type_name) {
+ console.error('attempt to init attribute diff for unsupported type: ' + type_name);
+ return;
+ }
+
+ var type_field = '__type_' + type_name;
+ this[type_field][id] = {
+ '__attr_write' : {},
+ '__attr_remove' : []
+ };
+
+ return this;
+ }
+
+ Attr_Diff.prototype.init_attr_diff_node = function(id) {
+ return this.init_attr_diff('node', id);
+ }
+
+ Attr_Diff.prototype.init_attr_diff_link = function(id) {
+ return this.init_attr_diff('link', id);
+ }
+
+ Attr_Diff.prototype.add_node_attr_write = function(n_id, attr_name,
+ attr_val) {
+
+ if (undefined == this.__type_node[n_id]) {
+ this.init_attr_diff_node(n_id);
+ }
+ this.__type_node[n_id].__attr_write[attr_name] = attr_val;
+ return this;
+ }
+
+ Attr_Diff.prototype.add_node_attr_rm = function(n_id, attr_name) {
+ if (undefined == this[n_id]) {
+ this.init_attr_diff(n_id);
+ }
+ this.__type_node[n_id].__attr_remove.push(attr_name);
+ return this;
+ }
+
+ Attr_Diff.prototype.add_link_attr_write = function(l_id, attr_name,
+ attr_val) {
+
+ if (undefined == this.__type_link[l_id]) {
+ this.init_attr_diff_link(l_id);
+ }
+ this.__type_link[l_id].__attr_write[attr_name] = attr_val;
+ return this;
+ }
+
+ Attr_Diff.prototype.add_link_attr_rm = function(l_id, attr_name) {
+ if (undefined == this[l_id]) {
+ this.init_attr_diff(l_id);
+ }
+ this.__type_link[l_id].__attr_remove.push(attr_name);
+ return this;
+ }
+
+ /**
+ * Visual diff object expressing any visual change to the state of a
+ * particular visualization type.
+ *
+ * @obj_spec if none is passed a default topo_diff is constructed
+ * with node,link add sets
+ */
+ function Vis_Diff(obj_spec) {
+ }
+
+ function new_topo_diff(obj_spec) {
+ /*
+ * validate obj_spec
+ */
+ var ret;
+ if (undefined == obj_spec) {
+ obj_spec = {
+ node_set_add : [],
+ link_set_add : [],
+ }
+ ret = new Topo_Diff(obj_spec);
+ } else {
+ ret = new Topo_Diff(obj_spec);
+ }
+ return ret;
+ }
+
+ function new_attr_diff(obj_spec) {
+ /*
+ * validate obj_spec
+ */
+ // TODO
+ var ret = new Attr_Diff(obj_spec);
+ ret.__type_node = {}; // id-to-obj map
+ ret.__type_link = {}; // id-to-obj map
+ return ret;
+ }
+
+ function new_vis_diff(obj_spec) {
+ /*
+ * validate obj_spec
+ */
+ // TODO
+ var ret = new Vis_Diff(obj_spec);
+ return ret;
+ }
+
+ function new_diff_set(obj_spec) {
+ /*
+ * validate obj_spec
+ */
+ // TODO
+ var ret = new Diff_Set(obj_spec);
+ return ret;
+ }
+
+ return {
+ new_topo_diff : new_topo_diff,
+ new_attr_diff : new_attr_diff,
+ new_vis_diff : new_vis_diff,
+ new_diff_set : new_diff_set,
+ }
+ }); \ No newline at end of file
diff --git a/src/client/model/graph.js b/src/client/model/graph.js
new file mode 100644
index 00000000..b3c8e33f
--- /dev/null
+++ b/src/client/model/graph.js
@@ -0,0 +1,873 @@
+"use strict"
+
+define(['Bacon', 'consts', 'util', 'model/core', 'model/util', 'model/diff', 'rz_api_backend', 'rz_api_mesh', 'history', 'rz_bus', 'rz_config'],
+function (Bacon, consts, util, model_core, model_util, model_diff, rz_api_backend, rz_api_mesh, history, rz_bus, rz_config) {
+
+var debug = false;
+
+function Graph() {
+
+ var nodes = [],
+ id_to_node_map = {},
+ links = [],
+ diffBus = new Bacon.Bus();
+
+ this.diffBus = diffBus;
+
+ /**
+ * add node if no previous node is present whose id equals that of the node being added
+ *
+ * @return node if node was actually added
+ */
+ this.addNode = function(spec) {
+ var node = this.__addNode(spec);
+ if (node) {
+ return node;
+ }
+ }
+
+ /**
+ * Inner implementation
+ *
+ * @param notify whether or not a presenter notification will be sent, default = true
+ */
+ function __addNode(spec, notify, peer_notify) {
+ var existing_node,
+ node;
+
+ notify = undefined === notify ? true : notify;
+ peer_notify = undefined === peer_notify ? true : peer_notify;
+
+ if (undefined == spec.id) {
+ existing_node = findNodeByName(spec.name)
+ if (existing_node){
+ return existing_node;
+ } else {
+ node = model_core.create_node__set_random_id(spec);
+ if (debug) {
+ if ('bubble' != node.type){
+ console.log('__addNode: stamping node id: ' + node.id + ', name: \'' + node.name + '\' (bubble)');
+ }else {
+ console.log('__addNode: stamping node id: ' + node.id + ', name: \'' + node.name + '\'');
+ }
+ }
+ }
+ } else {
+ node = model_core.create_node_from_spec(spec);
+ }
+
+ existing_node = find_node__by_id(node.id);
+ if (existing_node) {
+ console.log('__addNode: id collision: existing-node.id: \'' + existing_node.id + '\', ' + 'new-node.id: \'' + node.id + '\'');
+ return existing_node;
+ }
+
+ util.assert(undefined != node.id, '__addNode: node id missing');
+ nodes.push(node);
+ id_to_node_map[node.id] = node;
+ console.log('__addNode: node added: id: ' + node.id);
+
+ if (rz_config.backend_enabled && peer_notify){
+ var topo_diff = model_diff.new_topo_diff({
+ node_set_add : [node].map(model_util.adapt_format_write_node),
+ });
+ var on_success = function(){
+ // FIXME: handle possible outcomes:
+ // - id merge: node already exists -> update id
+ // - link-merge: node already exists -> merge links, recurse?
+ };
+ var on_error = function(){
+ // TODO: add problem emblem to node
+ };
+ rz_api_backend.commit_diff__topo(topo_diff, on_success, on_error);
+ }
+
+ if (notify) {
+ diffBus.push({nodes: {add: [node]}});
+ }
+
+ return node;
+ }
+ this.__addNode = __addNode;
+
+ this._remove_node_set = function(ns, peer_notify) {
+
+ peer_notify = undefined === peer_notify ? true : peer_notify;
+
+ var cascade_link_rm_set = []; // track cascading link removals
+ for (var j = 0; j < ns.length; j++) {
+ var n = ns[j];
+ var i = 0;
+ while (i < links.length) {
+ var link = links[i];
+ if ((link['__src'].equals(n)) || (link['__dst'].equals(n))) { // compare by id
+ links.splice(i, 1);
+ cascade_link_rm_set.push(link);
+ }
+ else {
+ i++;
+ }
+ }
+ var index = findNodeIndex(n.id, n.state);
+ if (index !== undefined) {
+ nodes.splice(index, 1);
+
+ util.assert(undefined != n.id, '_remove_node_set: node id missing');
+ delete id_to_node_map[n.id];
+ }
+ }
+
+ cascade_link_rm_set.forEach(function(n){
+ console.log('_remove_node_set: removed node: id: ' + n.id);
+ });
+
+ if (rz_config.backend_enabled && peer_notify){
+ var topo_diff = model_diff.new_topo_diff({
+ node_set_rm : ns.map(function(n){ return n.id; }),
+ link_set_rm : cascade_link_rm_set.map(function(l){ return l.id; }),
+ });
+ var on_success = function(){
+ // FIXME: handle possible outcomes:
+ // - rm cascade of connected links
+ };
+ var on_error = function(){
+ // TODO: add problem emblem to node
+ };
+ rz_api_backend.commit_diff__topo(topo_diff, on_success, on_error);
+ }
+
+ if (ns.length > 0) {
+ diffBus.push({nodes: {removed: ns.map(function(n) { return n.id; })}});
+ }
+ }
+
+ this.removeNode = function(id) {
+ var n = find_node__by_id(id);
+ this._remove_node_set([n]);
+ }
+
+ this.removeNodes = function(n_filer) {
+ var ns = find_node_set_by_filer(n_filer);
+ this._remove_node_set(ns);
+ }
+
+ /**
+ *
+ * getConnectedNodesAndLinks
+ *
+ * @id
+ * @state - defines the starting node (must have id and state)
+ * @d - depth defining connected component. If -1 returns the entire connected component. (can be the whole graph)
+ *
+ * NOTE: chainlinks are treated specially, they don't count for distance. So all their decendants will be added.
+ *
+ * NOTE: temp state nodes (n.state === 'temp') are ignored.
+ *
+ * @return - {
+ * 'node': [node]
+ * 'link': [link]
+ * }
+ *
+ * TODO: rewrite using efficient data structure. Right now iterates over everything
+ * TODO: implement for d !== 1
+ *
+ */
+ this.getConnectedNodesAndLinks = function(chosen_nodes, d) {
+ var ret = {'nodes':[], 'links':[]};
+
+ function addNode(node) {
+ if (chosen_nodes.filter(function (n) { return n.id == node.id; }).length == 1) {
+ return;
+ }
+ ret.nodes.push(node);
+ }
+ function same(n1, n2) {
+ // XXX: using name comparison because one of the nodes might be stale
+ return compareNames(n1.name, n2.name);
+ }
+
+ if (chosen_nodes === undefined) {
+ console.log('getConnectedNodesAndLinks: bug: called with undefined node');
+ return;
+ }
+ if (d !== 1) {
+ console.log('getConnectedNodesAndLinks: bug: not implemented for d == ' + d);
+ }
+ d = d || 1;
+
+ if (chosen_nodes.length === undefined) {
+ console.log('getConnectedNodesAndLinks: expected array');
+ }
+
+ links.forEach(function(link) {
+ chosen_nodes.forEach(function (n) {
+ var adjacentnode;
+ if (same(link.__src, n)) {
+ adjacentnode = find_node__by_id(link.__dst.id);
+ if (adjacentnode.state !== "temp") {
+ addNode({type: 'exit', node: adjacentnode});
+ }
+ ret.links.push({type: 'exit', link: link});
+ if (link.__dst.type === "chainlink") {
+ links.forEach(function(link2) {
+ if (link.__dst.id === link2.__dst.id &&
+ link2.__dst.type === "chainlink" &&
+ link2.__dst.state !== "temp") {
+ adjacentnode = find_node__by_id(link2.__src.id);
+ if (adjacentnode.state !== "temp") {
+ addNode({type: 'enter', node: adjacentnode});
+ }
+ ret.links.push({type: 'enter', link: link2});
+ }
+ });
+ }
+ }
+ if (same(link.__dst, n)) {
+ adjacentnode = find_node__by_id(link.__src.id);
+ if (adjacentnode.state !== "temp") {
+ addNode({type: 'enter', node: adjacentnode});
+ }
+ ret.links.push({type: 'enter', link: link});
+ }
+ });
+ });
+ return ret;
+ }
+
+ /* compareSubset:
+ * state: one of the optional states that defines a subgraph
+ * new_nodes: array of objects with name
+ * new_links: array of length two arrays [source_name, target_name]
+ * returns: true if current and new graph are homomorphic up to
+ * a single node id change. false otherwise
+ */
+ this.compareSubset = function(state, new_nodes, new_links) {
+ // Note: the nodes include a state=='temp', type=='bubble' node
+ // but it's ok since it exists both in new_nodes and in state_nodes
+ var state_nodes = findNodes(null, state).filter(function (nd) {
+ return nd.type !== 'bubble';
+ });
+ var state_links = findLinks(state).map(function(link) {
+ return [link.__src.name, link.__dst.name];
+ }).sort();
+ var k;
+ var state_source, state_target, new_source, new_target;
+ var changed_nodes;
+ var verbose = false; // XXX should be global.
+ var set_old_name, set_new_name;
+
+ new_nodes.map(function (f) {
+ if (!f.name) {
+ console.log('missing name on node. node follows');
+ console.log(f);
+ }
+ });
+ new_nodes.sort();
+ new_links.sort();
+ if (new_nodes.length != state_nodes.length || new_links.length != state_links.length) {
+ if (verbose) {
+ console.log('not same size: new/old ' + new_nodes.length + ' / ' + state_nodes.length + '; ' +
+ new_links.length + ' / ' + state_links.length);
+ }
+ return {graph_same: false};
+ }
+ changed_nodes = util.set_diff(util.set_from_array(state_nodes.map(function(d) { return d.name; })),
+ util.set_from_array(new_nodes.map(function (f) { return f.name; })));
+ // we allow any number of changed nodes as long as we it is 1 or 2 :)
+ if (changed_nodes.a_b.length > 2) {
+ if (verbose) {
+ console.log('changed too many nodes');
+ console.log(changed_nodes);
+ }
+ return {graph_same: false};
+ }
+ set_old_name = util.set_from_array(changed_nodes.a_b);
+ set_new_name = util.set_from_array(changed_nodes.b_a);
+ for (k = 0 ; k < state_links.length ; ++k) {
+ state_source = state_links[k][0];
+ state_target = state_links[k][1];
+ new_source = new_links[k][0];
+ new_target = new_links[k][1];
+ if ((state_source !== new_source &&
+ !(state_source in set_old_name && new_source in set_new_name))
+ ||
+ (state_target !== new_target &&
+ !(state_target in set_old_name && new_target in set_new_name))) {
+ if (verbose) {
+ console.log('not same link: ' +
+ state_source + '->' + state_target + ' != ' +
+ new_source + '->' + new_target);
+ console.log('state_source === new_source: ' + String(state_source === new_source));
+ console.log('state_target === new_target: ' + String(state_target === new_target));
+ console.log(set_old_name);
+ console.log(set_new_name);
+ }
+ return {graph_same: false};
+ }
+ }
+ return {graph_same: true, old_name: changed_nodes.a_b, new_name: changed_nodes.b_a};
+ }
+
+ this.addLinkByName = function(src_name, dst_name, name, state, drop_conjugator_links) {
+
+ var src = findNodeByName(src_name),
+ dst = findNodeByName(dst_name),
+ src_id = src ? src.id : null,
+ dst_id = dst ? dst.id : null;
+
+ if (src_id === null || dst_id === null) {
+ console.log('error: link of missing nodes: ' + src_name + ' (' + src_id + ') -> '
+ + dst_name + ' (' + dst_id + ')');
+ return;
+ }
+
+ var link = model_core.create_link__set_random_id(src, dst, { name: name,
+ state: state });
+ this.addLink(link);
+ }
+
+ function addLink(link, peer_notify) {
+
+ util.assert(link instanceof model_core.Link);
+
+ peer_notify = undefined === peer_notify ? true : peer_notify;
+
+ var existing_link = findLink(link.__src.id, link.__dst.id, link.name);
+
+ if (undefined == existing_link) {
+
+ links.push(link);
+
+ if (rz_config.backend_enabled && peer_notify){
+ var topo_diff = model_diff.new_topo_diff({
+ link_set_add : [link].map(model_util.adapt_format_write_link),
+ });
+ var on_success = function(){
+ // FIXME: handle possible outcomes:
+ // - id merge: link already exists -> update id
+ // - attr-merge: link already exists -> merge attrs
+ };
+ var on_error = function(){
+ // TODO: add problem emblem to node
+ };
+ rz_api_backend.commit_diff__topo(topo_diff, on_success, on_error);
+ }
+
+ diffBus.push({links: {add: [link]}});
+ } else {
+ existing_link.name = link.name;
+ existing_link.state = link.state;
+ }
+ }
+ this.addLink = addLink;
+
+ this.editLink = function(src_id, dst_id, newname, newstate) {
+ var link = findLink(src_id, dst_id, newname);
+
+ if (link === undefined) {
+ return;
+ }
+ link.name = newname;
+ if (newstate !== undefined) {
+ link.state = newstate;
+ }
+ rz_bus.names.push([newname]);
+ }
+
+ this.editLinkTarget = function(src_id, dst_id, new_dst_id) {
+ var link = findLink(src_id, dst_id, null);
+ if (link !== undefined) {
+ link.__dst = find_node__by_id(new_dst_id);
+
+ } else {
+
+ }
+ }
+
+ this.update_node = function(node, new_node_spec, on_success, on_error) {
+ util.assert(node instanceof model_core.Node);
+
+ if (rz_config.backend_enabled){
+
+ if (node.name != new_node_spec.name){
+ /*
+ * handle name update collision: suggest removal first
+ */
+ var n_eq_name = findNodeByName(new_node_spec.name);
+ if (undefined != n_eq_name) {
+ // delete colliding node on rename
+ console.warn('update_node: name collision blocked due to node rename');
+ undefined != on_error && on_error();
+ return;
+ }
+
+ node['name'] = new_node_spec['name']; // [!] may still fail due to server NAK
+ }
+
+ var attr_diff = model_diff.new_attr_diff();
+ for (var key in new_node_spec){
+ attr_diff.add_node_attr_write(node.id, key, new_node_spec[key]);
+ }
+
+ var on_ajax_success = function(id_to_node_map){
+ var node_id = node.id; // original node id
+ if (id_to_node_map[node_id].id != node_id){
+ // TODO: handle incoming ID update
+ util.assert(false, 'update_node: id attr change');
+ }
+
+ var ret_node = id_to_node_map[node_id];
+ for (var key in ret_node){
+ if ('name' == key || 'id' == key){
+ continue;
+ }
+ node[key] = ret_node[key];
+ }
+
+ // TODO: handle NAK: add problem emblem to node
+ on_success();
+ };
+
+ var on_ajax_error = function(){
+ };
+
+ rz_api_backend.commit_diff__attr(attr_diff, on_ajax_success, on_ajax_error);
+ }
+ }
+
+ this.editNameByName = function(old_name, new_name) {
+ var node = findNodeByName(old_name);
+
+ if (node === undefined) {
+ console.log('editNameByName: error: cannot find node with name ' + old_name);
+ return;
+ }
+ return this.editName(node.id, new_name); // TODO: introduce Node class (yes Amir, I'm now down with that).
+ }
+
+ this.editName = function(id, new_name) {
+ var n_eq_name = findNodeByName(new_name);
+ var n_eq_id = find_node__by_id(id);
+ var acceptReplace=true;
+
+ if (n_eq_id === undefined) {
+ return;
+ }
+ if (n_eq_id.name == new_name) {
+ return;
+ }
+ if (n_eq_name !== undefined && n_eq_id.state !== 'temp' && !compareNames(n_eq_id.name, new_name)) {
+ acceptReplace = confirm('"' + n_eq_name.name + '" will replace "' + n_eq_id.name + '", are you sure?');
+ if (acceptReplace){
+ for (var i = 0; i < links.length; i++) {
+ if (links[i].__src === n_eq_id) {
+ links[i].__src = n_eq_name;
+ }
+ if (links[i].__dst === n_eq_id) {
+ links[i].__dst = n_eq_name;
+ }
+ }
+ this.removeNode(n_eq_id.id);
+ }
+ } else {
+ n_eq_id.name = new_name;
+ }
+ }
+
+ this.editDates = function(id, state, start, end) {
+ var n = find_node__by_id(id);
+ if (state != n.state){
+ return;
+ }
+ if ((n !== undefined)) {
+ n.start = start;
+ n.end = end;
+ }
+ }
+
+ /**
+ * editType:
+ *
+ * @return true if type changed
+ */
+ this.editType = function(id, state, newtype) {
+ return this._editProperty(id, state, 'type', newtype);
+ }
+
+ this.editURL = function(id, state, url) {
+ return this._editProperty(id, state, 'url', url);
+ }
+
+ this._editProperty = function(id, state, prop, value) {
+ var n = find_node__by_id(id);
+ if (state != n.state){
+ return false;
+ }
+
+ if ((n === undefined)) {
+ return false;
+ }
+ n[prop] = value;
+ return true;
+ }
+
+ this.editStatus = function(id, state, status) {
+ return this._editProperty(id, state, 'status', status);
+ }
+
+ this.editState = function(id, state, newstate) {
+ return this._editProperty(id, state, 'state', newstate);
+ }
+
+ this.findCoordinates = function(id) {
+ var n = find_node__by_id(id);
+ if ((index !== undefined)) {
+ $('.typeselection').css('top', n.y - 90);
+ $('.typeselection').css('left', n.x - 230);
+ }
+ }
+
+ this.removeLink = function(link) {
+ var i;
+
+ for (i = 0 ; i < links.length; ++i) {
+ if (link.id !== undefined) {
+ if (link.id === links[i].id) {
+ links.splice(i, 1);
+ return;
+ }
+ } else {
+ if (link.__src.id === links[i].__src.id && link.__dst.id === links[i].__dst.id) {
+ links.splice(i, 1);
+ return;
+ }
+ }
+ }
+ console.log('bug: attempt to remove non existant link');
+ }
+
+ this.removeLinks = function(state) {
+ var id = null;
+ var ls = findLinks(state);
+ for (var j = 0; j < ls.length; j++) {
+ var l = ls[j];
+ var i = 0;
+ while (i < links.length) {
+ if (links[i] === l) links.splice(i, 1);
+ else i++;
+ }
+ }
+ }
+
+ var findLink = function(src_id, dst_id, name) {
+ for (var i = 0; i < links.length; i++) {
+ if (links[i].__src.id === src_id && links[i].__dst.id === dst_id) {
+ return links[i];
+ }
+ }
+ }
+
+ var findLinks = function(state) {
+ var foundLinks = [];
+ for (var i = 0; i < links.length; i++) {
+ if (links[i].state == state) {
+ foundLinks.push(links[i]);
+ }
+ }
+ return foundLinks;
+ }
+
+ var compareNames = function(name1, name2) {
+ return name1.toLowerCase() === name2.toLowerCase();
+ };
+
+ var hasNodeByName = function(name, state) {
+ return nodes.filter(function (n) {
+ return compareNames(n.name, name) && n.state === state;
+ }).length > 0;
+ }
+ this.hasNodeByName = hasNodeByName;
+
+ var hasNodeByNameAndNotState = function(name, state) {
+ return nodes.filter(function(n) {
+ return compareNames(n.name, name) && n.state !== state;
+ }).length > 0;
+ }
+ this.hasNodeByNameAndNotState = hasNodeByNameAndNotState;
+
+ var hasNode = function(id, state) {
+ var i;
+
+ for (i = 0 ; i < nodes.length; ++i) {
+ if (nodes[i].id === id && nodes[i].state === state) {
+ return true;
+ }
+ }
+ return false;
+ }
+ this.hasNode = hasNode;
+
+ /**
+ * return node whose id matches the given id or undefined if no node was found
+ */
+ var find_node__by_id = function(id) {
+ return id_to_node_map[id];
+ }
+
+ /**
+ * @param filer: must return true in order for node to be included in the returned set
+ */
+ var find_node_set_by_filer = function(filter) {
+ var ret = [];
+ nodes.map(function(n){
+ if (true == filter(n)){
+ ret.push(n);
+ }
+ });
+ return ret;
+ }
+
+ var findNodeByName = function(name) {
+ for (var i = 0 ; i < nodes.length ; ++i) {
+ if (compareNames(nodes[i].name, name)) {
+ return nodes[i];
+ }
+ }
+ }
+
+ var findNodes = function(id, state) {
+ // id=id.toLowerCase();
+ var foundNodes = [];
+ for (var i = 0; i < nodes.length; i++) {
+ if ((id && nodes[i].id === id) || (state && nodes[i].state === state))
+ foundNodes.push(nodes[i]);
+ }
+ return foundNodes;
+ }
+
+ var findNodeIndex = function(id, state) {
+ for (var i = 0; i < nodes.length; i++) {
+ if ((id && nodes[i].id === id) || (state && nodes[i].state === state))
+ return i;
+ };
+ }
+
+ function clear() {
+ nodes.length = 0;
+ links.length = 0;
+ }
+ this.clear = clear;
+
+ function empty() {
+ return nodes.length == 0 && links.length == 0;
+ }
+ this.empty = empty;
+
+ // @ajax-trans
+ this.commit_diff_set = function (diff_set) {
+
+ function on_success(data){
+ console.log('commit_diff_set:on_success: TODO impl');
+ }
+
+ rz_api_mesh.broadcast_possible_next_diff_block(diff_set);
+ }
+
+ /**
+ * perform initial DB load from backend
+ *
+ * @param on_success: should be used by MVP presentors to trigger UI update
+ */
+ // @ajax-trans
+ function load_from_backend(on_success) {
+
+ function on_success__ajax(data){
+ var n_set = []; // added node set
+ var l_set = []; // added link set
+ var len;
+
+ data['node_set'].map(function(n_spec) {
+ n_spec = model_util.adapt_format_read_node(n_spec);
+
+ util.assert(undefined != n_spec.id, 'load_from_backend: n_spec missing id');
+
+ var n = __addNode(n_spec, false, false);
+ n_set.push(n);
+ });
+
+ data['link_set'].map(function(l_spec){
+ var l_ptr = model_util.adapt_format_read_link_ptr(l_spec);
+
+ util.assert(undefined != l_ptr.id, 'load_from_backend: l_ptr missing id');
+
+ // resolve link ptr
+ var src = find_node__by_id(l_ptr.__src_id),
+ dst = find_node__by_id(l_ptr.__dst_id);
+
+ // cleanup & reuse as link_spec
+ delete l_ptr.__src_id;
+ delete l_ptr.__dst_id;
+ var link_spec = l_ptr;
+ var link = model_core.create_link_from_spec(src, dst, link_spec);
+ var l = addLink(link, false);
+ l_set.push(l);
+ });
+
+ undefined != on_success && on_success()
+ }
+
+ rz_api_backend.clone(0, on_success__ajax);
+ }
+ this.load_from_backend = load_from_backend;
+
+ this.load_from_json = function(json) {
+ var data = JSON.parse(json),
+ added_names,
+ that = this;
+
+ clear();
+ if (data == null) {
+ console.log('load callback: no data to load');
+ return;
+ }
+ added_names = data.nodes.map(function(node) {
+ return that.__addNode({id:node.id, name:node.name ? node.name : node.id,
+ type:node.type,state:"perm",
+ start:new Date(node.start),
+ end:new Date(node.end),
+ status:node.status,
+ url:node.url,
+ x: node.x,
+ y: node.y,
+ }, false, false).name;
+ });
+ data.links.forEach(function(link) {
+ that.addLink(link.__src, link.__dst, link.name, "perm");
+ });
+ this.clear_history();
+ rz_bus.names.push(added_names);
+ }
+
+ this.save_to_json = function() {
+ var d = {"nodes":[], "links":[]};
+ for(var i = 0 ; i < nodes.length ; i++){
+ var node = nodes[i];
+ d['nodes'].push({
+ "id": node.id,
+ "name": node.name,
+ "type":node.type,
+ "state":"perm",
+ "start":node.start,
+ "end":node.end,
+ "status": node.status,
+ "url": node.url,
+ "x": node.x,
+ "y": node.y,
+ });
+ }
+ for(var j=0 ; j < links.length ; j++){
+ var link = links[j];
+ d['links'].push({
+ "__src":link.__src.id,
+ "__dst":link.__dst.id,
+ "name":link.name
+ });
+ }
+ return JSON.stringify(d);
+ }
+
+ this.set_user = function(user) {
+ var elem = $('svg g.zoom')[0];
+ this.user = user;
+ this.history = new history.History(this.user, this, elem);
+ }
+
+ function clear_history() {
+ if (this.history !== undefined) {
+ this.history.clear();
+ }
+ }
+
+ this.clear_history = clear_history;
+
+ var get_nodes = function() {
+ return nodes;
+ };
+ this.nodes = get_nodes;
+
+ var get_links = function() { return links; };
+ this.links = get_links;
+
+ function setRegularState() {
+ var x, node, link, s;
+
+ for (x in nodes) {
+ node = nodes[x];
+ s = node.state;
+ if (s === 'chosen' || s === 'enter' || s === 'exit') {
+ node.state = 'perm';
+ }
+ }
+ for (x in links) {
+ link = links[x];
+ s = link.state;
+ if (s === 'chosen' || s === 'enter' || s === 'exit') {
+ link.state = 'perm';
+ }
+ }
+ }
+ this.setRegularState = setRegularState;
+
+ this.findByVisitors = function(node_visitor, link_visitor) {
+ var n_length = nodes.length,
+ l_length = links.length,
+ selected = [],
+ i,
+ node,
+ link,
+ state;
+
+ if (!node_visitor) {
+ return;
+ }
+
+ for (i = 0 ; i < n_length; ++i) {
+ node = nodes[i];
+ if (node.state == 'temp') {
+ continue;
+ }
+ if (node_visitor(node)) {
+ selected.push(node);
+ }
+ }
+ return selected;
+ }
+
+ function markRelated(names) {
+ removeRelated();
+ nodes.forEach(function (node) {
+ names.forEach(function (name) {
+ if (compareNames(node.name, name) && node.state != 'temp') {
+ node.state = 'related';
+ }
+ });
+ });
+ }
+ this.markRelated = markRelated;
+
+ function removeRelated() {
+ nodes.forEach(function (node) {
+ if (node.state == 'related') {
+ node.state = 'perm';
+ }
+ });
+ }
+ this.removeRelated = removeRelated;
+
+}
+
+return {
+ Graph: Graph,
+};
+
+});
diff --git a/src/client/model/util.js b/src/client/model/util.js
new file mode 100644
index 00000000..d938fbc9
--- /dev/null
+++ b/src/client/model/util.js
@@ -0,0 +1,134 @@
+"use strict"
+
+/**
+ * model utility functions: - convert from/to client/backend data
+ * representations
+ */
+define([ 'jquery', 'model/diff' ], function($, model_diff) {
+
+ function __sanitize_label__write(label_str){
+ var ret = label_str[0].toUpperCase() +
+ label_str.substring(1).toLowerCase();
+ return ret;
+ }
+
+ function __sanitize_label__read(label_str){
+ return label_str.toLowerCase();
+ }
+
+ /**
+ * read by adapting from backend format
+ */
+ function adapt_format_read_node(n_raw) {
+ var ret;
+
+ ret = $.extend({
+ // type:
+ // - discard all but first label
+ // - adjust to lowercase
+ 'type' : __sanitize_label__read(n_raw['__label_set'][0]),
+ 'state' : 'perm',
+ }, n_raw);
+
+ delete ret.__label_set;
+
+ return ret;
+ }
+
+ /**
+ * write by adapting to backend format
+ */
+ function adapt_format_write_node(n_raw) {
+ var ret = $.extend({
+ }, n_raw);
+
+ ret['__label_set'] = [__sanitize_label__write(n_raw.type)];
+
+ delete ret.state;
+ delete ret.status
+ delete ret.type;
+
+ return ret
+ }
+
+ /**
+ * read by adapting from backend format
+ */
+ function adapt_format_read_link_ptr(l_raw) {
+ var ret;
+
+ ret = $.extend({
+ '__src_id' : l_raw['__src_id'],
+ '__dst_id' : l_raw['__dst_id'],
+ // type:
+ // - discard all but first label
+ // - adjust to lowercase
+ '__type' : __sanitize_label__read(l_raw['__label_set'][0]),
+ 'state' : 'perm',
+ }, l_raw);
+
+ ret['name'] = ret['__type'];
+
+ delete ret.__label_set;
+
+ return ret;
+ }
+
+ /**
+ * write by adapting to backend format
+ */
+ function adapt_format_write_link(l_raw) {
+ var ret = $.extend({
+ '__src_id' : l_raw.source.id,
+ '__dst_id' : l_raw.target.id,
+ }, l_raw);
+
+ ret['__label_set'] = [__sanitize_label__write(l_raw.__type)];
+
+ delete ret.__dst;
+ delete ret.__src;
+ delete ret.source; // introduced by d3 accessor methods
+ delete ret.state;
+ delete ret.status;
+ delete ret.target;
+
+ return ret;
+ }
+
+ /**
+ * write adapt diff from node set, link set. sets may be passed by reference
+ * as they are cloned
+ */
+ function adapt_format_write_topo_diff(n_set, l_set) {
+
+ var new_n_set = $.extend([], n_set);
+ var new_l_set = $.extend([], l_set);
+
+ // filter out 'bubble' nodes
+ new_n_set = new_n_set.filter(function(n) {
+ return 'bubble' != n.type;
+ });
+
+ new_n_set = $.map(new_n_set, function(n, _) {
+ return adapt_format_write_node(n);
+ })
+
+ new_l_set = $.map(new_l_set, function(l, _) {
+ return adapt_format_write_link(l);
+ })
+
+ var topo_diff = new model_diff.new_topo_diff({
+ node_set_add : new_n_set,
+ link_set_add : new_l_set
+ });
+ return topo_diff;
+ }
+
+ return {
+ adapt_format_read_node : adapt_format_read_node,
+ adapt_format_read_link_ptr : adapt_format_read_link_ptr,
+ adapt_format_write_node : adapt_format_write_node,
+ adapt_format_write_link : adapt_format_write_link,
+ adapt_format_write_topo_diff : adapt_format_write_topo_diff,
+ }
+}); \ No newline at end of file
diff --git a/src/client/robot.js b/src/client/robot.js
new file mode 100644
index 00000000..700e41b6
--- /dev/null
+++ b/src/client/robot.js
@@ -0,0 +1,70 @@
+define(['jquery'], function($) {
+
+var sentence="";
+/*sentence+=" #Rhizibot is showing you a #tutorial|";
+sentence+="#Rhizi visualizes data with #Graphs and #relationships|";
+sentence+="#links and #nodes have #context and #meaning|";
+
+sentence+="#entities and #concepts are #nodes|";
+sentence+="We built this entire graph in 30 seconds, try it out yourself!";*/
+
+sentence+='##Rhizi is a tool for creating interactive #Networks|';
+sentence+='Create #Networks by writing #Sentences|';
+sentence+='like a #Tweet you can put a #Hashtag in your #Sentences|';
+sentence+='#Hello|';
+sentence+='#John|';
+sentence+='Put your text between #Commas to use #"Multiple words"|';
+sentence+='#"John Smith"|';
+sentence+='#"Beauty and the beast"|';
+sentence+='Use the word #And to #"Connect multiple things together"|';
+sentence+='#John likes #Apples and #Oranges and #Pistachio|';
+//sentence+='Choose the #"node type" by using the #"TAB key"|';
+sentence+='#Click on any #Node to change and modify it|';
+sentence+='#Play around and have #Fun!|';
+
+var robot = function (element, sentence) {
+ var r = {
+ speed: 1,
+ counter: 0,
+ sentence: sentence,
+ sentencecounter: 0,
+ element: element || $('#textanalyser')};
+ r.next_event = function () {
+ if(r.counter <= r.sentence.length) {
+ var text = r.element.val();
+ r.counter++;
+ if (r.sentence.charAt(r.counter) !== "|") {
+ r.element.val(text + r.sentence.charAt(r.counter));
+ if ('oninput' in document.documentElement) {
+ r.element.trigger('input', {});
+ }
+ //if(Math.random()>0.9)graph.editType("x","temp",nodetypes[Math.round(Math.random()*4)]);
+ if (r.sentence.charAt(r.counter)==="#") {
+ r.sentencecounter++;
+ r.timeout_id = window.setTimeout( r.next_event, 30/r.speed+Math.random()*160/r.speed );
+ } else {
+ r.timeout_id = window.setTimeout( r.next_event, 50/r.speed+Math.round(Math.random()*100/r.speed) );
+ }
+ } else {
+ var e = jQuery.Event("keypress");
+ e.which = 13;
+ e.keyCode = 13;
+ $("#textanalyser").trigger(e);
+ window.setTimeout( r.next_event, 650/r.speed );
+ }
+ } else {
+ window.clearInterval(r.timeout_id);
+ }
+ }
+ return r;
+}
+
+$('.logo').click(function(){
+ setTimeout( robot(undefined, sentence).next_event, 1000 );
+});
+
+/*var answer = confirm ("Would you like a tutorial?")
+if (answer)
+setTimeout( Robot, 100 );*/
+
+}); // define
diff --git a/src/client/rz_api_backend.js b/src/client/rz_api_backend.js
new file mode 100644
index 00000000..da073f9f
--- /dev/null
+++ b/src/client/rz_api_backend.js
@@ -0,0 +1,222 @@
+"use strict";
+
+/**
+ * API calls designed to execute against a local backend service
+ */
+define(['rz_config'], function(rz_config) {
+
+ function RZ_API_Backend() {
+
+ var rz_server_url = 'http://' + rz_config.rz_server_host + ':' + rz_config.rz_server_port;
+
+ /**
+ * issue rhizi server ajax call
+ */
+ var ajax_rs = function(path, req_opts, on_success, on_error) {
+
+ function on_error_wrapper(xhr, err_text, err_thrown) {
+ // log wrap callback
+ console.error('error: \'' + err_text + '\'');
+ if (on_error && typeof (on_error) === "function") {
+ on_error(err_type, err_text);
+ }
+ }
+
+ function on_success_wrapper(xhr, text) {
+ // log wrap callback
+ var ret_data = xhr.data;
+ console.log('success: ' + JSON.stringify(ret_data));
+
+ if (on_success) {
+ on_success(ret_data);
+ }
+ }
+
+ /*
+ * add common request options
+ */
+ req_opts.dataType = "json";
+ req_opts.contentType = "application/json; charset=utf-8";
+ req_opts.error = on_error_wrapper;
+ req_opts.success = on_success_wrapper;
+ req_opts.headers = {};
+ req_opts.timeout = 8000; // ms
+ req_opts.crossDomain = true;
+
+ $.ajax(rz_server_url + path, req_opts);
+ }
+
+ /**
+ * common attr_diff
+ */
+ this.commit_diff__attr = function(attr_diff, on_success, on_error) {
+
+ var post_dict = {
+ 'attr_diff' : attr_diff
+ }
+
+ var req_opts = {
+ type : 'POST',
+ data : JSON.stringify(post_dict),
+ };
+
+ return ajax_rs('/graph/diff-commit-attr', req_opts, on_success,
+ on_error);
+ }
+
+ /**
+ * commit topo_diff
+ */
+ this.commit_diff__topo = function(topo_diff, on_success, on_error) {
+
+ var post_dict = {
+ 'topo_diff' : topo_diff
+ }
+
+ var req_opts = {
+ type : 'POST',
+ data : JSON.stringify(post_dict),
+ };
+
+ return ajax_rs('/graph/diff-commit-topo', req_opts, on_success,
+ on_error);
+ }
+
+ /**
+ * commit vis_diff
+ */
+ this.commit_diff__vis = function(vis_diff, on_success, on_error) {
+ // TODO impl
+ }
+
+ /**
+ * commit a diff_set
+ */
+ this.commit_diff__set = function(diff_set, on_success, on_error) {
+
+ var post_dict = {
+ 'diff_set' : diff_set
+ }
+
+ var req_opts = {
+ type : 'POST',
+ data : JSON.stringify(post_dict),
+ };
+
+ return ajax_rs('/graph/diff-commit-set', req_opts, on_success,
+ on_error);
+ }
+
+ /**
+ * clone rhizi repo
+ */
+ this.clone = function(depth, on_success, on_error) {
+
+ // prep request
+ var req_opts = {
+ type : 'POST',
+ };
+
+ ajax_rs('/graph/clone', req_opts, on_success, on_error);
+ }
+
+ /**
+ * load node-set by id attribute
+ *
+ * @param on_complete_cb
+ * will be called with the returned json data on successful
+ * invocation
+ * @param on_error
+ * error callback
+ */
+ this.load_node_set = function(id_set, on_success, on_error) {
+
+ // prep request data
+ var post_dict = {
+ 'id_set' : id_set
+ }
+
+ // prep request
+ var req_opts = {
+ type : 'POST',
+ data : JSON.stringify(post_dict),
+ };
+
+ return ajax_rs('/load/node-set-by-id', req_opts, on_success,
+ on_error);
+ }
+
+ /**
+ * load link set by src / dst id
+ */
+ this.load_link_set = function(link_ptr_set, on_success, on_error) {
+
+ // prep request data
+ var post_dict = {
+ 'link_ptr_set' : link_ptr_set
+ }
+
+ // prep request
+ var req_opts = {
+ type : 'POST',
+ data : JSON.stringify(post_dict),
+ };
+
+ return ajax_rs('/load/link-set/by_link_ptr_set', req_opts,
+ on_success, on_error);
+ }
+
+ /**
+ * add a node set
+ */
+ this.add_node_set = function(n_set, on_success, on_error) {
+ var topo_diff = new Topo_Diff();
+ topo_diff.node_set_add = n_set;
+
+ return this.topo_diff_commit(topo_diff, on_success, on_error);
+ }
+
+ /**
+ * add a link set
+ */
+ this.add_link_set = function(l_set, on_success, on_error) {
+ var topo_diff = new Topo_Diff();
+ topo_diff.link_set_add = l_set;
+
+ return this.topo_diff_commit(topo_diff);
+ }
+
+ /**
+ * remove node set
+ */
+ this.remove_node_set = function() {
+ var topo_diff = null;
+ return this.topo_diff_commit(topo_diff);
+ }
+
+ /**
+ * remove link set
+ */
+ this.remove_link_set = function() {
+ var topo_diff = null;
+ return this.topo_diff_commit(topo_diff);
+ }
+
+ /**
+ * update node set
+ */
+ this.update_node_set = function(attr_diff, on_success, on_error) {
+ return this.attr_diff_commit(attr_diff, on_success, on_error);
+ }
+
+ /**
+ * update link set
+ */
+ this.update_link_set = function() {
+ var attr_diff = null;
+ return this.attr_diff_commit(null);
+ }
+ }
+
+ return new RZ_API_Backend();
+});
diff --git a/src/client/rz_api_mesh.js b/src/client/rz_api_mesh.js
new file mode 100644
index 00000000..9014dbac
--- /dev/null
+++ b/src/client/rz_api_mesh.js
@@ -0,0 +1,22 @@
+/**
+ * API calls designed to execute in decentralized fashion
+ */
+define(['rz_api_backend'],
+function(rz_api_backend) {
+ function RZ_API_Mesh() {
+
+ /**
+ * suggest diff block and await commit/reject consensus
+ */
+ // @ajax-trans
+ this.broadcast_possible_next_diff_block = function (diff_set) {
+
+ function on_success(data){
+ // TODO impl
+ }
+
+ rz_api_backend.commit_diff_set(diff_set);
+ }
+ }
+ return new RZ_API_Mesh();
+});
diff --git a/src/client/rz_bus.js b/src/client/rz_bus.js
new file mode 100644
index 00000000..38033bc4
--- /dev/null
+++ b/src/client/rz_bus.js
@@ -0,0 +1,15 @@
+"use strict"
+
+define(['consts', 'Bacon'],
+function(consts, Bacon)
+{
+ var ui_key_bus = new Bacon.Bus(),
+ ui_input_bus = new Bacon.Bus(),
+ names_bus = new Bacon.Bus();
+
+ return {
+ ui_key: ui_key_bus,
+ ui_input: ui_input_bus,
+ names: names_bus,
+ };
+});
diff --git a/src/client/rz_config.js b/src/client/rz_config.js
new file mode 100644
index 00000000..e55e74d0
--- /dev/null
+++ b/src/client/rz_config.js
@@ -0,0 +1,9 @@
+define(function() {
+
+ return {
+ 'rand_id_generator' : 'hash',
+ 'rz_server_host': 'rhizi.local',
+ 'rz_server_port': '8080',
+ 'backend_enabled': false,
+ };
+});
diff --git a/src/client/rz_core.js b/src/client/rz_core.js
new file mode 100644
index 00000000..7ec19692
--- /dev/null
+++ b/src/client/rz_core.js
@@ -0,0 +1,756 @@
+"use strict"
+
+define(['jquery', 'd3', 'consts', 'rz_bus', 'util', 'model/graph', 'model/core', 'view/helpers', 'view/view', 'rz_observer', 'view/selection', 'rz_config'],
+function($, d3, consts, rz_bus, util, model_graph, model_core, view_helpers, view, rz_observer, selection, rz_config) {
+
+var addednodes = [],
+ vis,
+ graphinterval = 0,
+ timeline_timer = 0,
+ deliverables = [],
+ circle, // <-- should not be module globals.
+ scrollValue = 0,
+ graph,
+ drag,
+ force;
+
+// "CSS" for SVG elements. Reused for editing elements.
+var node_text_dx = 15,
+ node_text_dy = '.30em',
+ svg_input_fo_node_x = node_text_dx,
+ svg_input_fo_node_y = '-.70em',
+ svg_input_fo_height = '30px';
+
+/**
+ * svgInput - creates an embedded input element under a given
+ *
+ * edit_node(@sibling, @node)
+ * edit_link(@sibling, @link)
+ */
+var svgInput = (function() {
+ var measure_node = $('#measure-node')[0],
+ measure_link = $('#measure-link')[0],
+ original_element,
+ is_link;
+
+ function appendForeignElementInputWithID(base, elemid, width, height)
+ {
+ var input = document.createElement('input'),
+ body = document.createElement('body'),
+ fo = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+
+ body.appendChild(input);
+
+ fo.setAttribute('height', height || svg_input_fo_height);
+ fo.style.pointerEvents = 'none';
+ input.style.pointerEvents = 'all';
+ fo.appendChild(body);
+ base.appendChild(fo);
+ input.setAttribute('id', elemid);
+ return input;
+ }
+
+ function measure(text)
+ {
+ var span;
+
+ span = is_link ? measure_link : measure_node;
+ span.innerHTML = text;
+ return span.getBoundingClientRect().width; // $().width() works too
+ }
+
+ function onkeydown(e) {
+ var ret = undefined,
+ jelement = createOrGetSvgInput(),
+ element = jelement[0],
+ newname = jelement.val(),
+ fo = createOrGetSvgInputFO(),
+ d;
+
+ if (element != this) {
+ console.log('unexpected editname_on_keypress this should be the svg-input element');
+ }
+
+ if (e.which == 13 || e.which == 27) {
+ ret = false;
+ d = jelement.data().d;
+ if (e.which == 13 && newname != d.name) {
+ if (d.hasOwnProperty('__src')) {
+ graph.editLink(d.__src.id, d.__dst.id, newname);
+ } else {
+ graph.editName(d.id, newname);
+ }
+ rz_bus.names.push([newname]);
+ update_view__graph(true);
+ }
+ hide();
+ }
+ rz_bus.ui_key.push({where: consts.KEYSTROKE_WHERE_EDIT_NODE, keys: [e.which]});
+ return ret;
+ };
+
+ function resize_measure(e) {
+ resize(measure($(e.target).val()) + 30);
+ }
+
+ function resize(new_width) {
+ var svg_input = createOrGetSvgInput(),
+ fo = createOrGetSvgInputFO();
+
+ svg_input.css('width', new_width);
+ fo.attr('width', new_width);
+ }
+
+ // FIXME: element being deleted. Some delete is legit - removal of related element. Some isn't (a click).
+ // Instead of investigating (time constraint) reparenting as sibling, and introducing
+ // this function. Cost of creation of element is negligble, it's just ugly..
+ function createOrGetSvgInput()
+ {
+ var svg_input_name = 'svg-input',
+ svg_input_selector = '#' + svg_input_name,
+ svg_input = $(svg_input_selector);
+
+ if (svg_input.length == 0) {
+ console.log('creating new svg-input');
+ svg_input = $(appendForeignElementInputWithID(vis[0][0], svg_input_name));
+ svg_input.on('keydown', onkeydown);
+ svg_input.bind('change keypress', resize_measure);
+ }
+ return svg_input;
+ }
+
+ function createOrGetSvgInputFO()
+ {
+ return createOrGetSvgInput().parent().parent();
+ }
+
+ /*
+ * @param e visual node element
+ * @param n node model object
+ */
+ function enable(e, n) {
+ var oldname = n.name,
+ svg_input = createOrGetSvgInput(),
+ fo = createOrGetSvgInputFO();
+
+ is_link = n.hasOwnProperty('__src');
+
+ e.parentNode.appendChild(fo[0]); // This will unparent from the old parent
+ if (is_link) {
+ fo.attr('transform', e.getAttribute('transform'));
+ // XXX links set the text-anchor middle attribute. no idea how to do that
+ fo.attr('x', -$(e).width() / 2);
+ fo.attr('y', -$(e).height() / 2 - 3); // XXX This minus 3 is only kinda ok.
+ fo.attr('class', 'svg-input-fo-link');
+ } else {
+ fo.attr('x', svg_input_fo_node_x);
+ fo.attr('y', svg_input_fo_node_y);
+ fo.attr('transform', null);
+ fo.attr('class', 'svg-input-fo-node');
+ }
+ // Set width correctly
+ resize(measure(oldname) + 30);
+ fo.show();
+ svg_input.val(oldname);
+ svg_input.data().d = n;
+ svg_input.focus();
+ if (original_element) {
+ original_element.show();
+ }
+ original_element = $(e);
+ original_element.hide();
+ // TODO: set cursor to correct location in text
+ }
+
+ function hide() {
+ createOrGetSvgInputFO().hide();
+ if (original_element && original_element.show) {
+ original_element.show();
+ }
+ }
+
+ return {
+ enable: enable,
+ hide: hide,
+ };
+}());
+
+
+
+function recenterZoom() {
+ vis.attr("transform", "translate(0,0)scale(1)");
+}
+
+// zoom or drag
+var zoomInProgress = false;
+
+var initDrawingArea = function () {
+
+ function zoom() {
+ zoomInProgress = true;
+ vis.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
+ d3.event.sourceEvent.stopPropagation();
+ }
+
+ function dragstarted(d) {
+ d3.event.sourceEvent.stopPropagation();
+ d3.select(this).classed("dragging", true);
+ d.dragstart = {clientX:d3.event.sourceEvent.clientX, clientY:d3.event.sourceEvent.clientY};
+ force.stop();
+ }
+
+ function dragged(d) {
+ d.x = d3.event.x;
+ d.y = d3.event.y;
+ tick();
+ }
+
+ function dragended(d) {
+ d3.select(this).classed("dragging", false);
+ d3.select(this).classed("fixed", true); // TODO: this is broken since we override all the classes. Need to switch to class addition/removal (i.e. use classed for everything) or set class in one location (so here just set a value on the node, not the element)
+ if (d.dragstart.clientX - d3.event.sourceEvent.clientX != 0 ||
+ d.dragstart.clientY - d3.event.sourceEvent.clientY != 0) {
+ tick();
+ force.resume();
+ }
+ }
+
+ graph = new model_graph.Graph();
+
+ var user_id = $('#user_id'),
+ user = user_id.text();
+
+ if (user_id.length > 0) {
+ console.log('found user ID: \'' + user + '\'');
+ graph.set_user(user);
+ }
+
+ var el = document.body;
+ vis = d3.select(el).append("svg:svg")
+ .attr('id', 'canvas_d3')
+ .attr("width", '100%')
+ .attr("height", '100%')
+ .attr("pointer-events", "all")
+ .append("g")
+ .attr("class", "zoom");
+
+ d3.select(el).select("svg").append("svg:defs")
+ .data(["end"]) // Different link/path types can be defined here
+ .append("svg:marker") // This section adds in the arrows
+ .attr("id", String)
+ .attr("viewBox", "0 -5 10 10")
+ .attr("refX", 22)
+ .attr("refY", -1.5)
+ .attr("markerWidth", 4)
+ .attr("markerHeight", 4)
+ .attr("orient", "auto")
+ .append("svg:path")
+ .attr("d", "M0,-5L10,0L0,5");
+
+ /*
+ * init zoom behavior
+ */
+ var zoom_obj = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
+ zoom_obj(d3.select('#canvas_d3'))
+ d3.select("svg").on("dblclick.zoom", null); // disable zoom on double click
+
+ $('svg').click(svg_click_handler);
+
+ // SVG rendering order is last rendered on top, so to make sure
+ // all links are below the nodes we group them under a single g
+ vis.append("g").attr("id", "link-group");
+ vis.append("g").attr("id", "selected-link-group");
+
+ drag = d3.behavior.drag()
+ .origin(function(d) { return d; })
+ .on("dragstart", dragstarted)
+ .on("drag", dragged)
+ .on("dragend", dragended);
+
+ // $('#canvas_d3').dblclick(canvas_handler_dblclick); - see #138
+ if (rz_config.backend_enabled){
+ graph.load_from_backend( function(){
+ update_view__graph(false);
+ });
+ }
+}
+
+function init_force_layout(){
+ var el = document.body;
+ var w = $(el).innerWidth(),
+ h = $(el).innerHeight();
+
+ force = d3.layout.force()
+ .distance(120)
+ .gravity(0.12)
+ .charge(-1800)
+ .size([w, h])
+ .on("tick", tick)
+ .start();
+}
+
+initDrawingArea();
+init_force_layout();
+
+/**
+ * find the visual element counterpart of a given model object. This relies on
+ * the visual element having an id attribute equal to the object's id.
+ *
+ * @return null if visual element is not found
+ */
+function locate_visual_element(model_obj){
+ var id_sel = $('#' + model_obj.id);
+ if (0 == id_sel.length){
+ console.warn('unable to find visual element for model object: object id: ' + model_obj.id.toString())
+ return null;
+ }
+ return id_sel[0];
+}
+
+/**
+ * add node on canvas double click
+ */
+function canvas_handler_dblclick(){
+ var n = model_core.create_node__set_random_id();
+ n.name = ''; // will be set by user
+
+ graph.addNode(n);
+ update_view__graph();
+
+ var n_ve = locate_visual_element(n); // locate visual element
+
+ var on_slowdown_cb = function(){
+ svgInput.enable($(n_ve).find('.nodetext'), n);
+ observer.disconnect();
+ }
+ var mutation_handler = rz_observer.new_Mutation_Handler__on_dxy_slowdown(on_slowdown_cb);
+ var observer = rz_observer.new_MutationObserver(mutation_handler);
+ mutation_handler.on_slowdown_threshold_reached;
+
+ observer.observe(n_ve, {
+ subtree: false,
+ childList : false,
+ attributes: true,
+ attributeOldValue : true,
+ });
+}
+
+/**
+ * update view: graph
+ */
+function update_view__graph(no_relayout) {
+ var node,
+ link,
+ link_g,
+ linktext,
+ nodetext,
+ unselected_link_group = document.querySelector('#link-group'),
+ selected_link_group = document.querySelector('#selected-link-group');
+
+ link = vis.selectAll("g.link")
+ .data(graph.links(), function(d) { return d.id; });
+
+ link_g = link.enter().append('g')
+ .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping
+ .attr('class', 'link graph')
+
+ link_g.append("path")
+ .attr("class", function(d) {
+ return d.state + ' link graph';
+ })
+ .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping
+ .attr("marker-end", "url(#end)");
+
+ // second path for larger click area
+ link_g.append("path")
+ .attr("class", "ghostlink")
+ .on("click", function(d, i) {
+ if (zoomInProgress) {
+ // don't disable zoomInProgress, it will be disabled by the svg_click_handler
+ // after this events bubbles to the svg element
+ return;
+ }
+ var that = this,
+ src = this.link.__src,
+ dst = this.link.__dst;
+
+ view.edge_info.on_delete(function () {
+ graph.removeLink(that.link);
+ update_view__graph(true);
+ view.edge_info.hide();
+ });
+ view.edge_info.show(d);
+ selection.update([src, dst]);
+ update_view__graph(true);
+ });
+
+ link.attr("class", function(d, i){
+ var temp_and = (d.name && d.name.replace(/ /g,"")=="and" && d.state==="temp") ? "temp_and" : "";
+
+ return ["graph link", temp_and, selection.selected_class(d)].join(' ');
+ });
+
+ link.selectAll('path.link')
+ .attr('class', function(d) {
+ return [d.state, selection.selected_class(d), "link graph"].join(' ');
+ });
+
+ link.exit().remove();
+
+ vis.selectAll('.ghostlink')
+ .data(graph.links())
+ .each(function (d) {
+ this.link = d;
+ });
+
+ linktext = vis.selectAll(".linklabel")
+ .data(graph.links(), function(d) { return d.id; });
+ linktext.enter()
+ .append("text")
+ .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping
+ .attr("class", function(d) {
+ return ["linklabel graph", selection.selected_class(d)].join(' ');
+ })
+ .attr("text-anchor", "middle")
+ .on("click", function(d, i) {
+ if (d.state !== "temp") {
+ svgInput.enable(this, d);
+ }
+ });
+
+ linktext
+ .text(function(d) {
+ var name = d.name || "";
+ if (!(d.__dst.state === "temp" ||
+ d.__src.state === "chosen" || d.__dst.state === "chosen")) {
+ return "";
+ }
+ if (name.length < 25 || d.__src.state === "chosen" ||
+ d.__dst.state === "chosen" || d.state==="temp") {
+ return name;
+ } else {
+ return name.substring(0, 14) + "...";
+ }
+ });
+
+ linktext.exit().remove();
+
+ node = vis.selectAll(".node")
+ .data(graph.nodes(), function(d) {
+ return d.id;
+ });
+
+ var nodeEnter = node.enter()
+ .append("g")
+ .attr('id', function(d){ return d.id; }) // append node id to enable data->visual mapping
+ .attr('visibility', 'hidden') // made visible on first tick
+ .call(drag);
+
+ // reorder nodes so selected are last, and so rendered last, and so on top.
+ (function () {
+ var ontop = [],
+ bubble;
+
+ node.each(function (d) {
+ this.node = d;
+ })
+ .attr('class', function(d) {
+ if (selection.node_selected(d)) {
+ if (d.type == 'bubble') {
+ bubble = this;
+ } else {
+ ontop.push(this);
+ }
+ }
+ return ['node', selection.selected_class(d)].join(' ');
+ });
+ if (bubble === undefined) {
+ // nothing to do if there is no bubble
+ return;
+ }
+ function reparent(new_parent, element) {
+ if (element.parentNode == new_parent) {
+ return;
+ }
+ new_parent.appendChild(element);
+ }
+ // move link to correct group
+ // O(|links|*|ontop|)
+ link.each(function (d) {
+ if (ontop.some(function (node) {
+ var d_node = node.node;
+ return d.__src == d_node || d.__dst == d_node;
+ }))
+ {
+ reparent(selected_link_group, this);
+ } else {
+ reparent(unselected_link_group, this);
+ }
+ });
+ linktext.each(function (d) {
+ if (selection.node_selected(d)) {
+ ontop.push(this);
+ }
+ });
+ function moveToEnd(e) {
+ e.parentNode.appendChild(e);
+ }
+ moveToEnd(bubble);
+ ontop.reverse().forEach(function (e) {
+ moveToEnd(e);
+ });
+ var count_links = function() {
+ return selected_link_group.childElementCount + unselected_link_group.childElementCount;
+ };
+ // put back on top link group on top
+ selected_link_group.parentNode.insertBefore(selected_link_group, bubble.nextSibling);
+ })();
+
+ nodetext = nodeEnter.insert("text")
+ .attr("class", "nodetext graph")
+ .attr("dx", node_text_dx)
+ .attr("dy", node_text_dy)
+ .on("click", function(d, i) {
+ if (d3.event.defaultPrevented) {
+ // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on
+ return;
+ }
+ if (d.state !== "temp") {
+ svgInput.enable(this, d);
+ selection.update([d]);
+ showNodeInfo(this.parentNode.node, i);
+ }
+ d3.event.stopPropagation();
+ });
+
+ node.select('g.node text')
+ .text(function(d) {
+ if (!d.name) {
+ return d.type == 'bubble' ? "" : "_";
+ }
+ if (d.state === "temp" || d.state === 'chosen'
+ || d.state === "enter" || d.state === "exit") {
+ return d.name;
+ } else {
+ if (d.name.length < 28) {
+ return d.name;
+ } else {
+ return d.name.substring(0, 25) + "...";
+ }
+ }
+ });
+
+ circle = nodeEnter.insert("circle");
+ node.select('g.node circle')
+ .attr("class", function(d) {
+ return d.type + " " + d.state + " circle graph";
+ })
+ .attr("r", function(d) {
+ return view_helpers.customSize(d.type) - 2;
+ })
+ .on("click", function(d, i) {
+ if (d3.event.defaultPrevented) {
+ // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on
+ return;
+ }
+ d3.event.stopPropagation();
+ selection.update([d]);
+ if(d.state !== "temp") {
+ showNodeInfo(d, i);
+ }
+ });
+ circle.append("svg:image")
+ .attr("class", "status graph")
+ .attr('x', -7)
+ .attr('y', -8)
+ .attr('width', 15)
+ .attr('height', 15)
+ .attr("xlink:href", function(d) {
+ switch (d.status) {
+ case "done":
+ return "res/img/check.png";
+ break;
+ case "current":
+ return "res/img/wait.png";
+ break;
+ case "waiting":
+ return "res/img/cross.png";
+ break;
+ }
+ });
+
+ node.exit().remove();
+
+ //update deliverables
+ deliverables = [];
+ var nodes = graph.nodes();
+ for (var i = 0; i < nodes.length; i++) {
+ var current = nodes[i];
+ if (current.type === "third-internship-proposal") {
+ deliverables.push({
+ "id": nodes[i].id,
+ "startdate": nodes[i].start,
+ "enddate": nodes[i].end
+ });
+ }
+ //Do something
+ }
+
+ force.nodes(graph.nodes())
+ .links(graph.links())
+
+ if (no_relayout) {
+ // XXX If we are stopped we need to update the text of the links at least,
+ // and this is the simplest way
+ tick();
+ } else {
+ force.alpha(0.1).start();
+ }
+}
+
+
+
+var debug_print = function(message) {
+ var element = $(".debug");
+ if (element.length == 1) {
+ element.html(message);
+ } else {
+ console.log(message);
+ }
+}
+
+function check_for_nan(x) {
+ if (Number.isNaN(x)) {
+ console.log('nan problem');
+ force.stop();
+ }
+ return Number.isNaN(x);
+}
+
+var newnodes=1;
+function tick(e) {
+ //console.log(e);
+ //$(".debug").html(force.alpha());
+ var node = vis.selectAll(".node")
+ .data(force.nodes(), function(d) {
+ return d.id;
+ });
+ var link = vis.selectAll("path.link")
+ .data(graph.links(), function(d) {
+ return d.id;
+ });
+ var linktext = vis.selectAll(".linklabel").data(graph.links());
+
+ function transform(d) {
+ if (check_for_nan(d.x) || check_for_nan(d.y)) {
+ return;
+ }
+ return "translate(" + d.x + "," + d.y + ")";
+ }
+
+ //circles animation
+ var tempcounter = 0,
+ temptotal = graph.nodes().filter(function(d){
+ return d.state === "temp" && d.type !== "chainlink" && d.type !== "bubble";
+ }).length;
+ if (temptotal !== newnodes) {
+ newnodes += temptotal / 15 / (newnodes * newnodes);
+ }
+ newnodes = Math.max(1, Math.min(newnodes, temptotal));
+ graph.nodes().forEach(function(d, i) {
+ var r, a;
+ if (d.state === "temp") {
+ tempcounter++;
+ if (d.type==="chainlink" || d.type==="bubble") {
+ d.x = window.innerWidth / 2;
+ d.y = window.innerHeight / 2;
+ } else {
+ r = 60 + newnodes * 20;
+ a = -Math.PI + Math.PI * 2 * (tempcounter-1) / newnodes + 0.3;
+ d.x = window.innerWidth / 2 + r * Math.cos(a);
+ d.y = window.innerHeight / 2 + r * Math.sin(a);
+ }
+ check_for_nan(d.x);
+ check_for_nan(d.y);
+ }
+ });
+
+ link.attr("d", function(d, i) {
+ var d_val,
+ ghost;
+
+ var dx = d.__dst.x - d.__src.x,
+ dy = d.__dst.y - d.__src.y,
+ dr = Math.sqrt(dx * dx + dy * dy);
+ d_val = "M" + d.__src.x + "," + d.__src.y + "L" + d.__dst.x + "," + d.__dst.y;
+ // update ghostlink position
+ ghost = $(this.nextElementSibling);
+ ghost.attr("d", d_val);
+ return d_val;
+ });
+
+ linktext.attr("transform", function(d) {
+ return "translate(" + (d.__src.x + d.__dst.x) / 2 + "," + (d.__src.y + d.__dst.y) / 2 + ")";
+ });
+
+ node.attr("transform", transform);
+
+ // After initial placement we can make the nodes visible.
+ //links.attr('visibility', 'visible');
+ node.attr('visibility', 'visible');
+}
+
+function showNodeInfo(d, i) {
+ view.node_info.on_save(function(e, form_data) {
+
+ graph.update_node(d, form_data, function(){
+ var old_type = d.type,
+ new_type = form_data.type;
+
+ if (new_type != old_type) {
+ view.node_info.show(d);
+ }
+
+ view.node_info.hide();
+ update_view__graph(true);
+ });
+
+ return false;
+ });
+
+ view.node_info.on_delete(function() {
+ if (confirm('This node and all its connections will be deleted, are you sure?')) {
+ graph.removeNode(d.id);
+ update_view__graph(false);
+ view.node_info.hide();
+ }
+ });
+
+ view.node_info.show(d);
+}
+
+function svg_click_handler(e) {
+ if (zoomInProgress) {
+ zoomInProgress = false;
+ return;
+ }
+ if (e.originalEvent.target.nodeName != 'svg') {
+ return;
+ }
+ svgInput.hide();
+ selection.clear();
+ view.hide();
+ update_view__graph(true);
+}
+
+return {
+ graph: graph,
+ force: force,
+ load_from_json: function(result) {
+ graph.load_from_json(result);
+ recenterZoom();
+ update_view__graph(false);
+ },
+ update_view__graph : update_view__graph,
+}
+}); /* close define call */
diff --git a/src/client/rz_observer.js b/src/client/rz_observer.js
new file mode 100644
index 00000000..718fd0a2
--- /dev/null
+++ b/src/client/rz_observer.js
@@ -0,0 +1,114 @@
+"use strict"
+
+/**
+ * DOM object observers
+ */
+define(
+[],
+function() {
+
+var MutationObserver = window.MutationObserver;
+// more portable version: var MutationObserver =
+// window.MutationObserver ||
+// window.WebKitMutationObserver;
+
+/**
+ * observe SVG object's translate attribute (x,y values), measure
+ * change rate & invoke on_slowdown_threshold_reached() upon
+ * reaching change slowdown threshold.
+ *
+ * Caller is responsible to disconnect observer.
+ */
+function Mutation_Handler__on_dxy_slowdown(
+ on_slowdown_threshold_reached) {
+
+ var x_cur, y_cur, d, dx, dy, avg_d = 0;
+
+ this.on_slowdown_threshold_reached = on_slowdown_threshold_reached;
+
+ /**
+ * handle observer mutation
+ */
+ this.handle_mutation = function(m) {
+ if ('transform' != m.attributeName || null == m.oldValue) {
+ return;
+ }
+
+ /*
+ * parsed txt example:
+ * "translate(173.6007550157428,275.43228723527193)"
+ */
+ var rgx_m = m.oldValue.match(/\((\d+.\d+),(\d+.\d+)\)/);
+ x_cur = rgx_m[1];
+ y_cur = rgx_m[2];
+ if (undefined == this.x_prv) {
+ this.x_prv = x_cur;
+ this.y_prv = y_cur;
+ return;
+ }
+ dx = x_cur - this.x_prv;
+ dy = y_cur - this.y_prv;
+
+ var d = Math.sqrt(dx * dx + dy * dy);
+ // average across last samples
+ avg_d = (1.0 - this.most_recent_sample_weight) * avg_d
+ + this.most_recent_sample_weight * d;
+
+ // debug
+ // console.log({
+ // 'dx' : dx,
+ // 'dy' : dy,
+ // 'd' : d,
+ // 'avg_d' : avg_d,
+ // });
+
+ if (avg_d < this.slowdown_threshold) {
+ // slowdown threshold reached
+ if (this.on_slowdown_threshold_reached) {
+ this.on_slowdown_threshold_reached();
+ }
+ }
+
+ this.x_prv = x_cur;
+ this.y_prv = y_cur;
+ }
+}
+
+function new_Mutation_Handler__on_dxy_slowdown(
+ on_slowdown_threshold_reached, slowdown_threshold,
+ most_recent_sample_weight) {
+ slowdown_threshold = slowdown_threshold || 0.07;
+ most_recent_sample_weight = most_recent_sample_weight || 0.3;
+ var ret = new Mutation_Handler__on_dxy_slowdown(
+ on_slowdown_threshold_reached);
+ ret.slowdown_threshold = slowdown_threshold;
+ ret.most_recent_sample_weight = most_recent_sample_weight;
+ return ret;
+}
+
+function new_MutationObserver(handler) {
+ /*
+ * FIXME implemente with inheritence - currently triggers
+ * 'illegal invocation', possibly due to some interaction with
+ * requirejs
+ */
+ // var o = new
+ // window.MutationObserver(Mutation_Observer.handler);
+ // var ret = Object.create(o);
+ // ret. = undefined;
+ // return ret;
+ var for_each_mutaion = function(m_set) {
+ m_set.forEach(function(m) {
+ handler.handle_mutation(m);
+ })
+ };
+
+ return new MutationObserver(for_each_mutaion);
+}
+
+return {
+ new_MutationObserver : new_MutationObserver,
+ new_Mutation_Handler__on_dxy_slowdown : new_Mutation_Handler__on_dxy_slowdown,
+};
+
+});
diff --git a/src/client/textanalysis.js b/src/client/textanalysis.js
new file mode 100644
index 00000000..830c16c1
--- /dev/null
+++ b/src/client/textanalysis.js
@@ -0,0 +1,501 @@
+"use strict";
+
+define(['rz_core', 'model/core', 'model/util', 'model/diff', 'rz_bus', 'consts'],
+function(rz_core, model_core, model_util, model_diff, rz_bus, consts) {
+
+var typeindex = 0;
+var nodetypes = consts.nodetypes;
+var typeStack = [];
+
+var lastnode;
+
+var sugg = {}, // suggestions for autocompletion of node names
+ suggestions_options = new Bacon.Bus(); // TODO: Property: same as bus, but with initial value
+
+var ANALYSIS_NODE_START = 'ANALYSIS_NODE_START';
+var ANALYSIS_NODE = 'ANALYSIS_NODE'
+var ANALYSIS_LINK = 'ANALYSIS_LINK';
+
+function selectedType()
+{
+ return nodetypes[typeindex];
+}
+
+function autoSuggestAddName(name)
+{
+ /* note that name can contain spaces - this is ok. We might want to limit this though? */
+ sugg[name] = 1;
+ suggestions_options.push(sugg);
+}
+
+/* up_to_two_renames:
+ *
+ * allow one letter or 'new node' to anything changes */
+function up_to_two_renames(graph, old_name, new_name)
+{
+ var not_one_letter = false;
+ var k;
+ /* Allowed renames:
+ * no change
+ * s1 is substring of s2
+ * older (s1) node being 'new node'
+ */
+ function allowed_rename(s1, s2)
+ {
+ return (s1 == s2 ||
+ s1 == 'new node' ||
+ s1.substr(0, s2.length) == s2 ||
+ s2.substr(0, s1.length) == s1);
+ }
+
+ if (old_name.length != new_name.length) {
+ console.log('bug: up_to_two_renames: not equal inputs');
+ return;
+ }
+ if (old_name.length > 2) {
+ console.log('bug: up_to_two_renames: input length 2 < ' + old_name.length);
+ return;
+ }
+ if (old_name.length == 2) {
+ if (allowed_rename(old_name[0], new_name[1]) &&
+ allowed_rename(old_name[1], new_name[0])) {
+ old_name = [old_name[1], old_name[0]];
+ } else {
+ if (!allowed_rename(old_name[0], new_name[0]) ||
+ !allowed_rename(old_name[1], new_name[1])) {
+ not_one_letter = true;
+ }
+ }
+ }
+ if (not_one_letter) {
+ console.log('bug: up_to_two_renames: not one letter changes');
+ console.log(old_name);
+ console.log(new_name);
+ return;
+ }
+ for (k = 0 ; k < old_name.length ; ++k) {
+ graph.editNameByName(old_name[k], new_name[k]);
+ }
+}
+
+// TODO: add escape char, i.e. r"bla\"bla" -> ['bla"bla']
+function tokenize(text, node_token, quote)
+{
+ var segment = [],
+ subsegment = [],
+ sentence = [],
+ quoteword;
+ var j;
+
+ segment = text.split(node_token);
+ for (j = 0; j < segment.length; j++) {
+ if (j !== 0) sentence.push(node_token);
+ subsegment = segment[j].split(" ");
+ if (subsegment.length === 0) {
+ sentence.push(" ");
+ }
+ for (var k = 0; k < subsegment.length; k++) {
+ if (subsegment[k] !== " " && subsegment[k] !== "") {
+ if (subsegment[k].charAt(0) === quote) {
+ quoteword = "";
+ do {
+ quoteword += subsegment[k] + ' ';
+ if(subsegment[k].charAt(subsegment[k].length-1) !== quote)k++;
+ } while (k < subsegment.length && subsegment[k].charAt(subsegment[k].length - 1) !== quote);
+ if (subsegment[k] && subsegment[k]!==quoteword.replace(/ /g, "")) {
+ quoteword += subsegment[k];
+ }
+ sentence.push(quoteword.replace(new RegExp(quote, 'g'), ""));
+ } else {
+ sentence.push(subsegment[k]);
+ }
+ }
+ }
+ }
+ return sentence;
+}
+
+/*
+ * textAnalyser
+ *
+ * Input:
+ * @newtext - new sentence
+ * @finalize - is this an intermediate editing state or are we editing the graph
+ *
+ * Output:
+ * none
+ *
+ * Side effect:
+ * updating graph (global)
+ *
+ * Implementation notes:
+ * There is no well defined grammer. The translation goes from obvious to not
+ * so much for more complex sentences involving more than two nodes (two '#'
+ * marks).
+ *
+ */
+var textAnalyser = function (newtext, finalize) {
+ var sentence,
+ token_set_new_node_names = [], // token set representing new node names
+ token_set_new_link_names = [], // token set representing new link names
+ linkindex = 0,
+ nodeindex = 0,
+ orderStack = [],
+ and_count = 0,
+ prefix = "",
+ m,
+ word,
+ completeSentence,
+ typesetter, starGraph,
+ n,
+ link_hash = {},
+ yell_bug = false, // TODO: fix both issues
+ NODE = "NODE",
+ LINK = "LINK",
+ START = "START",
+ ret = model_diff.new_topo_diff();
+
+ function addNode(name, type, state) {
+ if (type === undefined) {
+ console.log('bug: textanalyser.addNode of type undefined');
+ }
+ var node = model_core.create_node_from_spec(
+ {'name':name,
+ 'type':type,
+ 'state':state});
+
+ ret.node_set_add.push(node);
+ }
+
+ function __addLink(src, dst, name, state) {
+ if (!src || !dst) {
+ if (yell_bug) {
+ console.log('bug - adding link (' + src + ', ' + dst + ')');
+ }
+ return;
+ }
+ if (link_hash[src] && link_hash[src][dst]) {
+ if (yell_bug) {
+ console.log('bug - adding link twice (' + src + ', ' + dst + ')');
+ }
+ return;
+ }
+ if (!link_hash[src]) {
+ link_hash[src] = {};
+ }
+ link_hash[src][dst] = 1;
+
+ var link = {'__src': src, '__dst':dst, 'name':name, 'state':state}; // can't use model_core.create_link_from_spec as src,dst are only names
+ ret.link_set_add.push(link);
+ }
+
+ function apply_conjugator_link_logic(link, drop_conjugator_links){
+ if (drop_conjugator_links && link.name && (link.name.replace(/ /g,"") === "and")) {
+ link.state = "temp";
+ }
+ }
+
+ if (newtext.indexOf('#') == -1 || finalize) {
+ lastnode = null;
+ }
+
+ //Sentence Sequencing
+ //Build the words and cuts the main elements
+ sentence = tokenize(newtext, '#', '"');
+
+ // build new node,link arrays in order of appearance
+ for (m = 0; m < sentence.length; m++) {
+ switch (sentence[m]) {
+ case "#":
+ orderStack.push(START);
+ break;
+ case "and":
+ case "+":
+ case ",":
+ case "&":
+ sentence[m] = "and";
+ and_count++;
+ //orderStack.push("AND");
+ default:
+ if (orderStack[orderStack.length - 1] === START) {
+ orderStack.push(NODE);
+ token_set_new_node_names.push(sentence[m]);
+ linkindex++;
+ } else if (orderStack[orderStack.length - 1] === NODE) {
+ orderStack.push(LINK);
+ if (!token_set_new_link_names[linkindex]) {
+ token_set_new_link_names[linkindex] = sentence[m] + " ";
+ } else {
+ token_set_new_link_names[linkindex] += sentence[m] + " ";
+ }
+ } else {
+ if (!token_set_new_link_names[linkindex]) {
+ token_set_new_link_names[linkindex] = sentence[m] + " ";
+ } else {
+ token_set_new_link_names[linkindex] += sentence[m] + " ";
+ }
+ }
+ if (token_set_new_node_names.length === 0) {
+ prefix += (prefix.length > 0 ? ' ' : '') + sentence[m];
+ }
+ break;
+ }
+ }
+
+ starGraph = (token_set_new_link_names.length - and_count) >= 3 ||
+ ((token_set_new_link_names.length - and_count >= 1) &&
+ token_set_new_link_names.length > 2 &&
+ orderStack.length > 1 &&
+ orderStack[orderStack.length - 1] != NODE);
+
+ //PREFIX not null case - put complete sentence in first link.
+ if (prefix && !starGraph) {
+ token_set_new_link_names[1] = prefix + " " + token_set_new_node_names[0] +
+ (token_set_new_link_names[1] !== undefined || token_set_new_node_names[1] !== undefined ?
+ " " : "")
+ + (token_set_new_link_names[1] !== undefined ? token_set_new_link_names[1] : "")
+ + (token_set_new_node_names[1] !== undefined ? token_set_new_node_names[1] : "");
+ }
+
+ //WRITE COMPLETE SENTENCE
+ linkindex = 0;
+ nodeindex = 0;
+ word = "";
+ completeSentence = prefix.length > 0 ? String(prefix) + " " : "";
+ for (m = 0; m < orderStack.length; m++) {
+ if (orderStack[m] === NODE) {
+ word += " (" + token_set_new_node_names[nodeindex] + ") ";
+ completeSentence += token_set_new_node_names[nodeindex] + " ";
+ nodeindex++;
+ } else if (orderStack[m] === LINK) {
+ word += " -->" + token_set_new_link_names[nodeindex] + " --> ";
+ completeSentence += token_set_new_link_names[nodeindex];
+ }
+ }
+ completeSentence = completeSentence.trim();
+
+ //REBUILD GRAPH
+ linkindex = 0;
+ nodeindex = 0;
+
+ //CHANGE TO PERMANENT STATE AND UPDATE SUGGESTIONLIST
+ typesetter = "";
+ if (finalize === true) {
+ typesetter = "perm";
+ for (n = 0; n < token_set_new_node_names.length; n++) {
+ autoSuggestAddName(token_set_new_node_names[n]);
+ }
+ } else {
+ typesetter = "temp";
+ }
+
+ //ADD SURROUNDING BUBBLE
+ if (orderStack.length > 0) {
+ addNode("", "bubble","temp");
+ }
+
+ //0-N ORDER STACK
+ for (m = 0; m < orderStack.length - 1; m++) {
+ switch (orderStack[m]) {
+ case START:
+ if (!typeStack[nodeindex]) {
+ typeStack[nodeindex] = selectedType();
+ }
+ break;
+ case NODE:
+ addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex], typesetter);
+ if (!starGraph && nodeindex > 0) {
+ __addLink(token_set_new_node_names[nodeindex - 1],
+ token_set_new_node_names[nodeindex],
+ token_set_new_link_names[linkindex], typesetter);
+ }
+ nodeindex++;
+ break;
+ case LINK:
+ linkindex++;
+ break;
+ }
+ }
+
+ //FINAL N ORDER
+ switch (orderStack[orderStack.length - 1]) {
+ case START:
+ typeStack[nodeindex] = selectedType();
+ addNode("new node", typeStack[nodeindex], "temp");
+ if (!starGraph && nodeindex > 0) {
+ __addLink(token_set_new_node_names[nodeindex - 1], "new node",
+ token_set_new_link_names[linkindex], "temp");
+ and_connect("new node");
+ }
+ ret.state = ANALYSIS_NODE_START;
+ break;
+ case NODE:
+ typeStack[nodeindex] = selectedType();
+ addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex], typesetter);
+ if (!starGraph && nodeindex > 0) {
+ __addLink(token_set_new_node_names[nodeindex - 1],
+ token_set_new_node_names[nodeindex],
+ token_set_new_link_names[linkindex], typesetter);
+ and_connect(token_set_new_node_names[nodeindex]);
+ }
+ ret.state = ANALYSIS_NODE_START;
+ break;
+ case LINK:
+ linkindex++;
+ addNode("new node", selectedType(), "temp");
+ if (!starGraph) {
+ __addLink(token_set_new_node_names[nodeindex - 1], "new node", token_set_new_link_names[linkindex], "temp");
+ and_connect("new node");
+ }
+ ret.state = ANALYSIS_LINK;
+ break;
+ }
+
+ //EXTERNAL AND CONNECTION CHECKING
+ function and_connect(node) {
+ var verb;
+ for(var x=0;x<token_set_new_link_names.length;x++){
+ if(token_set_new_link_names[x])if(token_set_new_link_names[x].replace(/ /g,"")!=="and"){
+ verb = token_set_new_link_names[x];
+ for(var y=0; y<x ;y++){
+ __addLink(token_set_new_node_names[y], node, verb, typesetter);
+ for(var z=x; z<token_set_new_node_names.length ;z++){
+ __addLink(token_set_new_node_names[y], token_set_new_node_names[z], verb, typesetter);
+ }
+ }
+ }
+ }
+ }
+
+ /*console.log(sentence);
+ console.log(completeSentence);
+ console.log(orderStack);*/
+
+ //STAR CASE
+ if (starGraph) {
+ addNode(completeSentence, "chainlink", typesetter);
+ for (n = 0; n < token_set_new_node_names.length; n++) {
+ __addLink(token_set_new_node_names[n], completeSentence, "", typesetter);
+ }
+ }
+
+ ret.drop_conjugator_links = and_count < linkindex;
+
+ ret.applyToGraph = function(graph, backend_commit) {
+ window.ret = ret;
+
+ /*
+ * generate fitered node set who:
+ * - are not name-present in graph
+ * - are not of type 'bubble'
+ */
+ var n_set = ret.node_set_add.filter(function(node) {
+ return false == graph.hasNodeByNameAndNotState(node.name, "temp")
+ && node.type !== 'bubble';
+ }),
+ link_set = ret.link_set_add.map(
+ function (link) {
+ return [link.__src, link.__dst];
+ }),
+ comp = graph.compareSubset('temp', n_set, link_set);
+
+ if (false == finalize && comp.graph_same) {
+ if (comp.old_name && comp.new_name) {
+ up_to_two_renames(graph, comp.old_name, comp.new_name);
+ }
+ } else {
+ // REINITIALISE GRAPH (DUMB BUT IT WORKS)
+ graph.removeNodes(function(n){ return "temp" == n.state; });
+ graph.removeLinks("temp");
+
+ ret.for_each_node_add(function (node) {
+ if (true == finalize && node.state == 'temp') {
+ console.log('bug: temp node creation on finalize');
+ } else {
+ if (!finalize) {
+ lastnode = graph.addNode(node);
+ } else {
+ graph.addNode(node);
+ }
+ }
+ });
+ }
+ ret.for_each_link_add(function (link) {
+ if (false == finalize || link.name !== 'and') {
+ apply_conjugator_link_logic(link, ret.drop_conjugator_links);
+ graph.addLinkByName(link.__src,
+ link.__dst,
+ link.name,
+ link.state);
+ }
+ });
+
+ if (!finalize) {
+ graph.markRelated(token_set_new_node_names);
+ } else {
+ graph.removeRelated();
+ }
+
+ if (finalize && backend_commit) {
+ // broadcast diff:
+ // - finalize?
+ // - broadcast_diff requested by caller
+ var topo_diff = model_util.adapt_format_write_topo_diff(ret.nodes, ret.links);
+ var diff_set = model_diff.new_diff_set();
+ diff_set.add_diff_obj(topo_diff);
+ graph.commit_diff_set(diff_set);
+ }
+
+ // UPDATE GRAPH ONCE
+ rz_core.update_view__graph(!finalize && comp.graph_same);
+ };
+
+ if (finalize) {
+ typeStack = [];
+ }
+
+ return ret;
+};
+
+function init(graph)
+{
+ function onNodeAdded(diff) {
+ if (!diff || !diff.nodes || !diff.nodes.added) {
+ return;
+ }
+ for (var k in diff.nodes.added) {
+ var node = diff.nodes[k];
+ autoSuggestAddName(node.name.toLowerCase());
+ }
+ }
+ function toLowerCase(n) {
+ return n.toLowerCase();
+ }
+ function onSuggestedNameAdd(names) {
+ names.map(toLowerCase).forEach(autoSuggestAddName);
+ }
+ graph.diffBus.onValue(onNodeAdded);
+ rz_bus.names.onValue(onSuggestedNameAdd);
+}
+
+return {
+ init:init,
+ textAnalyser:textAnalyser,
+ suggestions_options: suggestions_options,
+ ANALYSIS_NODE_START:ANALYSIS_NODE_START,
+ ANALYSIS_NODE: ANALYSIS_NODE,
+ ANALYSIS_LINK:ANALYSIS_LINK,
+
+ //for the external arrow-type changer
+ lastnode: function() { return lastnode; },
+
+ selected_type_next: function() {
+ typeindex = (typeindex + 1) % 5;
+ return selectedType();
+ },
+ selected_type_prev: function() {
+ typeindex = (typeindex + 4) % 5;
+ return selectedType();
+ }
+};
+});
diff --git a/src/client/textanalysis.ui.js b/src/client/textanalysis.ui.js
new file mode 100644
index 00000000..5f5718bd
--- /dev/null
+++ b/src/client/textanalysis.ui.js
@@ -0,0 +1,195 @@
+"use strict"
+
+define(['jquery', 'Bacon', 'consts', 'rz_bus', 'rz_core', 'textanalysis', 'view/completer'],
+function($, Bacon, consts, rz_bus, rz_core, textanalysis, completer) {
+
+var text = "", // Last text of sentence
+ element_name = '#textanalyser',
+ element = $(element_name),
+ element_raw = element[0],
+ plus_button = $('.add-button'),
+ description = consts.description;
+
+var typeselection = function TypeSelectionDialog() {
+ var e = $('.typeselection'),
+ e_intro = e.find('#intro'),
+ e_label = e.find('#chosentypelabel'),
+ e_name = e.find('#chosentypename'),
+ e_desc = e.find('#chosentypedesc'),
+ typeselection = {};
+
+ typeselection.analysisNodeStart = function() {
+ typeselection.show();
+ }
+ typeselection.show = function() {
+ e.css({
+ top: window.innerHeight / 2 - 115,
+ left: window.innerWidth / 2 - 325
+ });
+ e_label.hide();
+ e_desc.hide();
+ e_intro.show();
+ e.show();
+ }
+ typeselection.hide = function() {
+ e.hide();
+ }
+ typeselection.showChosenType = function(nodetype) {
+ var desc = description[nodetype];
+
+ e_intro.hide();
+ e_label.show();
+ e_name.html(nodetype);
+ if (desc) {
+ e_desc.html(description[nodetype]);
+ e_desc.show();
+ } else {
+ e_desc.hide();
+ }
+ }
+ return typeselection;
+}();
+
+var analysisCompleter = completer(element, $('#input-suggestion'), {hideOnTab: false});
+
+function analyzeSentence(sentence, finalize)
+{
+ var ret = textanalysis.textAnalyser(sentence, finalize);
+
+ switch (ret.state) {
+ case textanalysis.ANALYSIS_NODE_START:
+ typeselection.analysisNodeStart();
+ break;
+ case textanalysis.ANALYSIS_LINK:
+ typeselection.hide();
+ break;
+ }
+
+ var backend_commit = false;
+ ret.applyToGraph(rz_core.graph, backend_commit);
+
+ if (finalize || sentence.length == 0) {
+ typeselection.hide();
+ }
+}
+
+function textSelect(inp, s, e) {
+ e = e || s;
+ if (inp.createTextRange) {
+ var r = inp.createTextRange();
+ r.collapse(true);
+ r.moveEnd('character', e);
+ r.moveStart('character', s);
+ r.select();
+ }else if(inp.setSelectionRange) {
+ inp.focus();
+ inp.setSelectionRange(s, e);
+ }
+}
+
+function changeType(arg) {
+ var lastnode = textanalysis.lastnode(),
+ nodetype,
+ id;
+
+ if(!lastnode) {
+ id = "new node";
+ } else {
+ id = lastnode.id;
+ }
+ nodetype = (arg === 'up'? textanalysis.selected_type_next() : textanalysis.selected_type_prev());
+
+ if (arg === 'up') {
+ rz_core.graph.editType(id, null, nodetype);
+ typeselection.showChosenType(nodetype);
+ rz_core.graph.findCoordinates(id);
+ } else {
+ rz_core.graph.editType(id, null, nodetype);
+ typeselection.showChosenType(nodetype);
+ rz_core.graph.findCoordinates(id);
+ }
+ rz_core.update_view__graph(true);
+}
+
+return {
+ analyzeSentence: analyzeSentence,
+ main:function () {
+ if (element.length != 1) {
+ return;
+ }
+
+ analysisCompleter.options.plug(textanalysis.suggestions_options);
+
+ var document_keydown = new Bacon.Bus();
+ rz_bus.ui_key.plug(document_keydown);
+
+ element.keydown(function(e) {
+ var ret = undefined;
+
+ switch (e.keyCode) {
+ case 13:
+ if (!analysisCompleter.handleEnter()) {
+ submitNewSentence();
+ } else {
+ analyzeSentence(element.val(), false);
+ }
+ ret = false;
+ break;
+ case 9: //TAB
+ if (textanalysis.lastnode()) {
+ e.preventDefault();
+ changeType(e.shiftKey ? "up" : "down", textanalysis.lastnode());
+ ret = false;
+ }
+ break;
+ }
+ document_keydown.push({where: consts.KEYSTROKE_WHERE_TEXTANALYSIS, keys: [e.keyCode]});
+ return ret;
+ });
+ element.bind('input selectionchange click', function() {
+ analysisCompleter.oninput(element_raw.value, element_raw.selectionStart);
+ });
+
+ function submitNewSentence() {
+ text = element.val();
+ element.val("");
+ analyzeSentence(text, true);
+ text = "";
+ }
+
+ // Click is required to prevent the default action - this is a form so that's a post,
+ // and away we go.
+ // The mousedown is required because CSS3 transitions eat some events sometimes. This is
+ // the closest I've come to an explanation:
+ // http://stackoverflow.com/questions/15786891/browser-sometimes-ignores-a-jquery-click-event-during-a-css3-transform
+ plus_button.bind("click mousedown", function(e) {
+ console.dir(e);
+ submitNewSentence();
+ e.preventDefault();
+ });
+
+ var input = new Bacon.Bus();
+ rz_bus.ui_input.plug(input);
+ if ('oninput' in document.documentElement) {
+ element.on('input', function(e) {
+ text = element.val();
+ analyzeSentence(text, false);
+ input.push({where: consts.INPUT_WHERE_TEXTANALYSIS, input: text});
+ });
+ } else {
+ console.log('textanalysis.ui: fallback to polling');
+ window.setInterval(function() {
+ if (element.val() != text) {
+ if (text.length * 8 > 500) {
+ element.css('width', text.length * 8 + 20);
+ }
+ // text changed
+ text = element.val();
+ analyzeSentence(text, false);
+ suggestionChange = false;
+ }
+ }, 50);
+ }
+ }
+};
+}); // define
diff --git a/src/client/util.js b/src/client/util.js
new file mode 100644
index 00000000..3fd4aecb
--- /dev/null
+++ b/src/client/util.js
@@ -0,0 +1,72 @@
+"use strict"
+
+define(function() {
+
+ function assert(condition, message) {
+ if (false == condition) {
+ message = message || "Assertion failed";
+ if (typeof Error !== "undefined") {
+ throw new Error(message);
+ }
+ throw message; // Fallback
+ }
+ }
+
+ function set_from_array(a) {
+ var ret = {};
+ for (var k = 0; k < a.length; ++k) {
+ ret[a[k]] = 1;
+ }
+ return ret;
+ }
+
+ function set_from_object(o) {
+ var ret = {}
+ for ( var k in o) {
+ ret[k] = 1;
+ }
+ return ret;
+ }
+
+ function set_diff(sa, sb) {
+ var ret = {
+ a_b : [],
+ b_a : []
+ };
+ var i;
+ for (i in sa) {
+ if (!(i in sb)) {
+ ret.a_b.push(i);
+ }
+ }
+ for (i in sb) {
+ if (!(i in sa)) {
+ ret.b_a.push(i);
+ }
+ }
+ return ret;
+ }
+
+ function array_diff(aa, ab) {
+ var sa = set_from_array(aa);
+ var sb = set_from_array(ab);
+ return set_diff(sa, sb);
+ }
+
+ // TODO: jquery BBQ: $.deparam.querystring().json;
+ function getParameterByName(name) {
+ name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
+ var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
+ results = regex.exec(location.search);
+ return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
+ }
+
+ return {
+ assert: assert,
+ set_from_array: set_from_array,
+ set_from_object: set_from_object,
+ set_diff: set_diff,
+ array_diff: array_diff,
+ getParameterByName: getParameterByName,
+ };
+});
diff --git a/src/client/view/completer.js b/src/client/view/completer.js
new file mode 100644
index 00000000..d2593321
--- /dev/null
+++ b/src/client/view/completer.js
@@ -0,0 +1,229 @@
+define(
+['jquery', 'Bacon'],
+function($, Bacon) {
+
+function unquoted(name)
+{
+ var start = 0,
+ end = name.length;
+
+ if (name.length >= 1) {
+ if (name.charAt(0) == '"') {
+ start = 1;
+ if (name.length > 1 && name.charAt(name.length - 1) == '"') {
+ end = name.length - 1;
+ }
+ }
+ return name.substring(start, end);
+ }
+ return name;
+}
+
+function setCaret(e, num)
+{
+ e.selectionStart = e.selectionEnd = num;
+}
+
+var completer = (function (input_element, dropdown, base_config) {
+ var config = get_config(base_config),
+ dropdown_raw = dropdown[0],
+ options_bus = new Bacon.Bus(),
+ options = [],
+ selected_index = -1,
+ input_element_raw = input_element[0],
+ completion_start = 0,
+ completion_end = 0,
+ minimum_length = 1;
+
+ // turn off the browser's autocomplete
+ input_element.attr('autocomplete', 'off');
+
+ //$('.ui-autocomplete').css('width', '10px');
+ options_bus.onValue(function update_options(new_options) {
+ options = new_options;
+ });
+
+ input_element.keyup(function(e) {
+ var ret = undefined;
+ switch (e.keyCode) {
+ case 38: //UP
+ prev_option();
+ ret = false;
+ break;
+ case 40: //DOWN
+ next_option();
+ ret = false;
+ break;
+ case 27: // Escape
+ hide();
+ ret = false;
+ break;
+ default:
+ // This catches cursor move due to keyboard events. no event for cursor movement itself
+ // below we catch cursor moves due to mouse click
+ oninput(input_element_raw.value, input_element_raw.selectionStart);
+ }
+ return ret;
+ });
+ input_element.keydown(function(e) {
+ switch (e.keyCode) {
+ case 38:
+ case 40:
+ return false;
+ case 9: // Tab
+ if (config.hideOnTab) {
+ hide();
+ }
+ break;
+ }
+ });
+
+ function get_config(base) {
+ return {
+ triggerStart: base && base.triggerStart || '#',
+ triggerEnd: base && base.triggerEnd || ' ',
+ hideOnTab: base && base.hasOwnProperty('hideOnTab') ? base.hideOnTab : true,
+ };
+ }
+
+ function completions(text)
+ {
+ var ret = [],
+ noquotes = unquoted(text.toLowerCase());
+
+ for (var name in options) {
+ if (name.toLowerCase().indexOf(noquotes) === 0) {
+ ret.push(name);
+ }
+ }
+ return ret;
+ }
+
+ function show() {
+ if (dropdown.children().length > 0) {
+ dropdown.show();
+ }
+ }
+ function hide()
+ {
+ dropdown.hide();
+ }
+
+ /***
+ * #this is a #
+ * ^
+ *
+ * #this is a #t
+ * ^
+ *
+ * #this and #that then #he
+ * ^
+ */
+ function oninput(text, cursor) {
+ var hash = text.slice(0, cursor).lastIndexOf(config.triggerStart);
+ // TODO check if current completion has been invalidated
+ _invalidateSelection();
+ hide();
+ dropdown_raw.innerHTML = ""; // remove all elements
+ if (hash == -1 && config.triggerStart != ' ') { // space matches start of string too
+ return;
+ }
+ var space = text.slice(hash + 1).indexOf(config.triggerEnd);
+ space = space == -1 ? text.length : space;
+ if (space < cursor) {
+ return;
+ }
+ completion_start = hash + 1;
+ completion_end = space;
+ var string = text.slice(completion_start, completion_end);
+ if (string.length < minimum_length) {
+ return;
+ }
+ completions(string).forEach(function(name) {
+ var suggestion = $('<div class="suggestion-item">' + name + '</div>');
+ suggestion.on('click', function(e) {
+ _applySuggestion(name);
+ input_element.focus();
+ });
+ dropdown.append(suggestion);
+ });
+ show();
+ }
+
+ function _invalidateSelection() {
+ update_highlighting(-1);
+ }
+
+ function _move_option(change, default_value) {
+ var next,
+ n = dropdown.children().length;
+
+ if (n == 0) {
+ return;
+ }
+ show();
+ if (selected_index == -1) {
+ next = default_value;
+ } else {
+ next = (selected_index + change) % n;
+ }
+ update_highlighting(next);
+ }
+ function next_option() {
+ _move_option(1, 0);
+ }
+ function prev_option() {
+ _move_option(dropdown.children().length - 1, dropdown.children().length - 1);
+ }
+ function _get_option(index) {
+ if (dropdown.children().length <= index) {
+ console.log('error: dropdown does not contain index ' + index +
+ ', it has ' + dropdown.children().length + ' elements');
+ return '';
+ }
+ var e = dropdown.children()[index],
+ s = e.innerText || e.textContent;
+ if (s.indexOf(' ') != -1) {
+ return '"' + s + '"';
+ }
+ return s;
+ }
+ function _choice(i) {
+ return dropdown.children().eq(i);
+ }
+ function update_highlighting(new_index) {
+ if (selected_index != -1) {
+ _choice(selected_index).removeClass('selected');
+ }
+ if (new_index != -1) {
+ _choice(new_index).addClass('selected');
+ }
+ selected_index = new_index;
+ }
+ function _applySuggestion(str) {
+ var cur = input_element.val(),
+ start = cur.slice(0, completion_start) + str + ' ';
+ input_element.val(start + cur.slice(completion_end));
+ setCaret(input_element, start.length);
+ oninput('', 0);
+ }
+ function handleEnter() {
+ if (selected_index == -1) {
+ return false;
+ }
+ _applySuggestion(_get_option(selected_index));
+ return true;
+ }
+
+ return {
+ options: options_bus,
+ oninput: oninput,
+ next_option: next_option,
+ prev_option: prev_option,
+ handleEnter: handleEnter,
+ };
+});
+
+return completer;
+
+});
diff --git a/src/client/view/edge_info.js b/src/client/view/edge_info.js
new file mode 100644
index 00000000..d711e200
--- /dev/null
+++ b/src/client/view/edge_info.js
@@ -0,0 +1,36 @@
+"use strict"
+
+define(['view/internal'],
+function(internal) {
+
+var delete_button = internal.edit_tab.get('edge', '#deleteedge'),
+ delete_callback = undefined;
+
+delete_button.on('click', function() {
+ if (delete_callback) {
+ delete_callback();
+ }
+});
+
+function show(link)
+{
+ internal.edit_tab.show('edge');
+ internal.edit_tab.get('edge', '#edgetitle').html(link.name);
+}
+
+function hide()
+{
+ internal.edit_tab.hide();
+}
+
+function on_delete(f)
+{
+ delete_callback = f;
+}
+
+return {
+ show: show,
+ hide: hide,
+ on_delete: on_delete,
+};
+});
diff --git a/src/client/view/helpers.js b/src/client/view/helpers.js
new file mode 100644
index 00000000..ad11763a
--- /dev/null
+++ b/src/client/view/helpers.js
@@ -0,0 +1,87 @@
+"use strict"
+
+define(function() {
+function customColor(type) {
+ var color;
+ switch (type) {
+ case "person":
+ color = '#FCB924'; //blue
+ break;
+ case "club":
+ color = '#ee3654'; //magenta
+ break;
+ case "skill":
+ color = '#fad900'; //yellow
+ break;
+ case "third-internship-proposal":
+ color = '#33c2e0'; //cyan
+ break;
+ case "internship":
+ color = '#ff8b11'; //orange
+ break;
+ case "interest":
+ color = '#8b3ab0'; //purple
+ break;
+ case "project":
+ color = "#40C200"; //green
+ break;
+ case "empty":
+ color = "#919095"; //mid-grey
+ break;
+ case "chainlink":
+ color = "#363636"; //dark-grey
+ break;
+ case "bubble":
+ color = "rgba(255,255,255,0.2)"; // white, 0.2 opaque
+ break;
+ default:
+ console.log('bug: unknown type ' + type);
+ color = '#d4d4d9'; //mid-light grey
+ break;
+ }
+ return color;
+}
+
+function customSize(type) {
+ var size;
+ switch (type) {
+ case "person":
+ size = 12;
+ break;
+ case "club":
+ size = 12;
+ break;
+ case "skill":
+ size = 12;
+ break;
+ case "third-internship-proposal":
+ size = 12;
+ break;
+ case "internship":
+ size = 12;
+ break;
+ case "interest":
+ size = 12;
+ break;
+ case "empty":
+ size = 9;
+ break;
+ case "chainlink":
+ size = 8;
+ break;
+ case "bubble":
+ size = 180;
+ break;
+ default:
+ size = 9;
+ break;
+ }
+ return size;
+}
+
+
+return {
+ customSize: customSize,
+ customColor: customColor,
+};
+});
diff --git a/src/client/view/internal.js b/src/client/view/internal.js
new file mode 100644
index 00000000..bc18bd75
--- /dev/null
+++ b/src/client/view/internal.js
@@ -0,0 +1,14 @@
+define(['view/tab'],
+function(tab) {
+
+var EDGE_INFO_SELECTOR = '.edge_info',
+ NODE_INFO_SELECTOR = '.info',
+ edit_tab = new tab.Tab({edge: EDGE_INFO_SELECTOR, node: NODE_INFO_SELECTOR});
+
+return {
+ EDGE_INFO_SELECTOR: EDGE_INFO_SELECTOR,
+ NODE_INFO_SELECTOR: NODE_INFO_SELECTOR,
+ edit_tab: edit_tab,
+};
+
+});
diff --git a/src/client/view/node_info.js b/src/client/view/node_info.js
new file mode 100644
index 00000000..44f2fab8
--- /dev/null
+++ b/src/client/view/node_info.js
@@ -0,0 +1,117 @@
+define(['jquery', 'jquery-ui', 'view/helpers', 'view/internal'],
+function($, _unused_jquery_ui, view_helpers, internal) {
+
+var d = null,
+ submit_callback = null,
+ delete_callback = null;
+
+function _get_form_data() {
+ return {
+ name: $('.info #editformname').val(),
+ type: $('.info #edittype').val(),
+ url: $('.info #editurl').val(),
+ status: $('.info #editstatus').val(),
+ startdate: $("#editstartdate").val(),
+ enddate: $("#editenddate").val(),
+ };
+}
+
+//internal.edit_tab.get('node', "#editbox").submit(function(e) {
+// if (submit_callback) {
+// return submit_callback(e, _get_form_data());
+// }
+// console.log('bug: edit tab submit called with no callback set');
+// e.preventDefault();
+//})
+
+//internal.edit_tab.get('node', "#deletenode").click(function(e) {
+// if (delete_callback) {
+// return delete_callback(e, _get_form_data());
+// }
+// console.log('bug: edit tab delete called with no callback set');
+// e.preventDefault();
+//});
+
+function show(d) {
+ var info = $('.info'),
+ f = false,
+ t = true,
+ visible = {
+ "third-internship-proposal": [t, t, t, f, f],
+ "chainlink": [f, f, f, f, f],
+ "skill": [f, f, f, f, t],
+ "interest": [f, f, f, f, t],
+ "_defaults": [f, f, f, f, t],
+ },
+ fields = ["#status", "#startdate", "#enddate", "#desc", "#url"],
+ flags = visible.hasOwnProperty(d.type) ? visible[d.type] : visible._defaults,
+ i;
+
+ internal.edit_tab.show('node');
+
+ for (i = 0 ; i < flags.length; ++i) {
+ var elem = info.find(fields[i]);
+ elem[flags[i] ? 'show' : 'hide']();
+ }
+
+ $('.info').attr('class', 'info');
+ $('.info').addClass('type-' + d.type); // Add a class to distinguish types for css
+
+ $('.info').find('#editformname').val(d.name);
+ $("#editenddate").datepicker({
+ inline: true,
+ showOtherMonths: true,
+ dayNamesMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ });
+ $("#editstartdate").datepicker({
+ inline: true,
+ showOtherMonths: true,
+ dayNamesMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ });
+
+ $('#editdescription').val(d.type);
+
+ $('#edittype').val(d.type);
+
+ $('#editurl').val(d.url);
+
+ $('#editstatus').val(d.status);
+
+ if (d.type === "third-internship-proposal") {
+ $('#editstartdate').val(d.start);
+ $('#editenddate').val(d.end);
+ }
+}
+
+function hide()
+{
+ internal.edit_tab.hide();
+}
+
+function on_save(f) {
+ $('#edit-node-dialog__save').click(function(e) {
+ return f(e, _get_form_data());
+ });
+}
+
+function on_delete(f) {
+ $('#edit-node-dialog__delete').click(function(e) {
+ return f(e, _get_form_data());
+ });
+}
+
+function on_keyup(f) {
+ $('.info').keyup(function(e) {
+ return f(e, _get_form_data());
+ });
+}
+
+return {
+ show: show,
+ hide: hide,
+ on_save: on_save,
+ on_delete: on_delete,
+ on_keyup: on_keyup,
+};
+
+});
diff --git a/src/client/view/selection.js b/src/client/view/selection.js
new file mode 100644
index 00000000..d51d1a4b
--- /dev/null
+++ b/src/client/view/selection.js
@@ -0,0 +1,102 @@
+define(['rz_core'],
+function(rz_core) {
+
+function get_rz_core()
+{
+ // circular dependency on rz_core, so require.js cannot solve it.
+ if (rz_core === undefined) {
+ rz_core = require('rz_core');
+ }
+ return rz_core;
+}
+
+var selected_nodes = [];
+
+function byVisitors(node_selector, link_selector) {
+ var new_selected_nodes = get_rz_core().graph.findByVisitors(node_selector, link_selector);
+
+ clear();
+ connectedComponent(new_selected_nodes);
+}
+
+function connectedComponent(nodes) {
+ var connected = get_rz_core().graph.getConnectedNodesAndLinks(nodes, 1),
+ i,
+ node,
+ link,
+ data;
+
+ selected_nodes = nodes.map(function(x) { return x; });
+
+ for (i = 0 ; i < connected.nodes.length ; ++i) {
+ data = connected.nodes[i];
+ node = data.node;
+ switch (data.type) {
+ case 'exit':
+ node.state = 'exit';
+ break;
+ case 'enter':
+ node.state = 'enter';
+ break;
+ };
+ }
+ for (i = 0 ; i < connected.links.length ; ++i) {
+ data = connected.links[i];
+ link = data.link;
+ switch (data.type) {
+ case 'exit':
+ link.state = 'exit';
+ break;
+ case 'enter':
+ link.state = 'enter';
+ break;
+ };
+ }
+ nodes.forEach(function (n) { n.state = 'chosen'; });
+}
+
+var node_selected = function(node) {
+ return node.state == 'chosen' || node.state == 'enter' || node.state == 'exit' || node.state == 'selected'
+ || node.state == 'temp' || node.state == 'related';
+}
+
+var selected_class = function(node) {
+ return selected_nodes.length > 0 ? (node_selected(node) ? "selected" : "notselected") : "";
+}
+
+var clear = function() {
+ selected_nodes.length = 0;
+ get_rz_core().graph.setRegularState();
+}
+
+function arr_compare(a1, a2)
+{
+ if (a1.length != a2.length) {
+ return false;
+ }
+ for (var i = 0 ; i < a1.length ; ++i) {
+ if (a1[i] != a2[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+var update = function(nodes) {
+ var set = !arr_compare(nodes, selected_nodes);
+ clear();
+ if (set) {
+ connectedComponent(nodes);
+ }
+}
+
+return {
+ byVisitors: byVisitors,
+ connectedComponent: connectedComponent,
+ clear: clear,
+ update: update,
+ selected_class: selected_class,
+ node_selected: node_selected,
+};
+
+});
diff --git a/src/client/view/tab.js b/src/client/view/tab.js
new file mode 100644
index 00000000..97ba1351
--- /dev/null
+++ b/src/client/view/tab.js
@@ -0,0 +1,57 @@
+"use strict"
+
+define(['jquery'],
+function($) {
+
+function Tab(dict) {
+ var k,
+ selector = {},
+ name = [];
+
+ for (k in dict) {
+ if (dict.hasOwnProperty(k) == false) {
+ continue;
+ }
+ selector[k] = dict[k];
+ name.push(k);
+ }
+ this._selector = selector;
+ this._name = name;
+}
+
+Tab.prototype.show = function(shown_name) {
+ var i,
+ name,
+ element;
+
+ for (i = 0 ; i < this._name.length ; ++i) {
+ name = this._name[i];
+ element = $(this._selector[name]);
+ if (name === shown_name) {
+ element.fadeIn(300);
+ } else {
+ element.hide();
+ }
+ }
+}
+
+Tab.prototype.hide = function() {
+ var i;
+
+ for (i = 0 ; i < this._name.length ; ++i) {
+ $(this._selector[this._name[i]]).fadeOut(300);
+ }
+}
+
+Tab.prototype.get = function(name, sel) {
+ // selector concatenation
+ var e = $(this._selector[name] + ' ' + sel);
+
+ return e;
+}
+
+return {
+ Tab: Tab
+};
+
+});
diff --git a/src/client/view/timeline.js b/src/client/view/timeline.js
new file mode 100644
index 00000000..0a26c0a7
--- /dev/null
+++ b/src/client/view/timeline.js
@@ -0,0 +1,180 @@
+"use strict"
+
+define(['jquery', 'rz_core'],
+function ($, rz_core) {
+
+function checkSwitch(checkswitch) {
+
+ if (checkswitch.checked) {
+ vis.selectAll(".timeline").remove();
+ $('.missingdates').fadeOut(300);
+ scrollValue = $('body').scrollLeft();
+
+ $('body').scrollLeft(0);
+ graphstate = "GRAPH";
+ rz_core.update_view__graph();
+
+ $('.status').fadeOut(600);
+
+ //boxedin=false;
+
+ } else {
+ timelineTimer=0;
+ $('.missingdates').fadeIn(300);
+
+ graph.recenterZoom();
+
+ $('body').scrollLeft(scrollValue);
+
+ graphstate = "TIMELINE";
+
+ rz_core.update_view__graph();
+
+ $('.status').fadeIn(600);
+
+
+ initAxis();
+
+ //boxedin=true;
+ }
+
+}
+
+///For some reason JQuery's $('body').scroll never worked, so I found something else.
+$('body').bind('DOMMouseScroll', function(e){
+ if(graphstate==="TIMELINE"){
+ if(e.originalEvent.detail !== 0) {
+ $('.overlay').hide();
+ }else{
+ $('.overlay').show();
+ }
+ }else{
+ return false;
+ }
+ });
+
+ //IE, Opera, Safari
+ $('body').bind('mousewheel', function(e){
+ if(graphstate==="TIMELINE"){
+ if(e.originalEvent.wheelDelta !== 0) {
+ $('.overlay').hide();
+ }else{
+ $('.overlay').show();
+ var left = $('body').offset().left;
+ $('.overlay').scrollLeft(left);
+ }
+ }else{
+ return false;
+ }
+ });
+
+function initAxis() {
+ var bar_height = 20;
+ var row_height = bar_height + 10;
+ var vertical_padding = 150
+ var bar_start_offset = 40;
+ var h = 15 * row_height + vertical_padding;
+
+
+ var min = deliverables[0].startdate;
+ var max = deliverables[0].enddate;
+
+ for (var i = 0; i < deliverables.length; i++) {
+ var deliv = deliverables[i];
+ if (min > deliv.startdate) min = deliv.startdate;
+ if (end < deliv.enddate) end = deliv.enddate;
+ }
+ min = new Date(min); ///with min
+ max = new Date(max);
+ console.log("min "+min+" max "+max);
+
+ ///update interval
+ var timeDiff = Math.abs(max.getTime() - min.getTime());
+ var diffDays = timeDiff / (1000 * 3600 * 24);
+ var w = Math.round(diffDays * 15 - 100);
+ graphinterval = w / diffDays;
+
+ var svg=vis;
+ vis.attr("width",w);
+
+ var paddingLeft = 150;
+ var paddingTop = 120;
+
+ var xScale = d3.time.scale()
+ .domain([min, max]).nice()
+ .range([paddingLeft, w]);
+
+ var xAxis = d3.svg.axis()
+ .scale(xScale)
+ .orient("bottom");
+
+ // Lines
+ var line = svg.append("g")
+ .selectAll("line")
+ .data(xScale.ticks(40))
+ .enter().append("line")
+ .attr("x1", xScale)
+ .attr("x2", xScale)
+ .attr("y1", paddingTop + 30)
+ .attr("y2", h - 50)
+ .attr("class", "timeline")
+ .style("stroke", "#ccc");
+
+ var y = function (i) {
+ return i * row_height + paddingTop + bar_start_offset;
+ }
+
+ var labelY = function (i) {
+ return i * row_height + paddingTop + bar_start_offset + 13;
+ }
+
+ // Company bars
+ var bar = svg.selectAll("rect")
+ .data(function (d) {
+ return Math.random() * 5;
+ });
+
+ bar.enter().append("rect")
+ .attr("y", -100)
+ .attr("x", -1000)
+ .attr("width", 100)
+ .attr("height", bar_height)
+ .attr("class", "company-bar timeline")
+ .on("mouseover", function (d) {
+ d3.select(this).style("fill", "#F5AF00");
+ getCompanyData(String(d.uid))
+ })
+ .on("mouseout", function () {
+ d3.select(this).style("fill", "#fc0");
+ });
+
+ var label = svg.selectAll("text")
+ .data(deliverables, function (key) {
+ return key.id
+ });;
+
+ label.enter().append("text")
+ .attr("class", "bar-label timeline")
+ // .attr("text-anchor","end")
+ .attr("x", paddingLeft - 10)
+ .attr("y", function (d, i) {
+ return labelY(i);
+ })
+ .text(function (d) {
+ });
+
+ // Bottom Axis
+ var btmAxis = svg.append("g")
+ .attr("transform", "translate(0," + (h - 25) + ")")
+ .attr("class", "axis timeline")
+ .call(xAxis);
+
+ // Top Axis
+ var topAxis = svg.append("g")
+ .attr("transform", "translate(0," + paddingTop + ")")
+ .attr("class", "axis timeline")
+ .call(xAxis);
+}
+
+return {};
+});
diff --git a/src/client/view/view.js b/src/client/view/view.js
new file mode 100644
index 00000000..95bfc09b
--- /dev/null
+++ b/src/client/view/view.js
@@ -0,0 +1,12 @@
+"use strict"
+
+define(['view/node_info', 'view/edge_info', 'view/internal'],
+function(view_node_info, view_edge_info, view_internal) {
+return {
+ 'node_info': view_node_info,
+ 'edge_info': view_edge_info,
+ 'hide': function() {
+ view_internal.edit_tab.hide();
+ },
+};
+});