summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ScrollTo.js17
-rw-r--r--src/app.js30
-rw-r--r--src/buttons.js84
-rw-r--r--src/consts.js10
-rw-r--r--src/drag_n_drop.js31
-rw-r--r--src/history.js135
-rw-r--r--src/main.js24
-rw-r--r--src/model/core.js80
-rw-r--r--src/model/diff.js144
-rw-r--r--src/model/graph.js649
-rw-r--r--src/model/util.js117
-rw-r--r--src/robot.js70
-rw-r--r--src/rz_api_backend.js220
-rw-r--r--src/rz_api_mesh.js22
-rw-r--r--src/rz_core.js744
-rw-r--r--src/rz_observer.js118
-rw-r--r--src/signal.js22
-rw-r--r--src/textanalysis.js459
-rw-r--r--src/textanalysis.ui.js164
-rw-r--r--src/util.js52
-rw-r--r--src/view/edge_info.js36
-rw-r--r--src/view/helpers.js78
-rw-r--r--src/view/internal.js14
-rw-r--r--src/view/node_info.js63
-rw-r--r--src/view/tab.js57
-rw-r--r--src/view/timeline.js183
-rw-r--r--src/view/view.js12
27 files changed, 3635 insertions, 0 deletions
diff --git a/src/ScrollTo.js b/src/ScrollTo.js
new file mode 100644
index 00000000..a015f08c
--- /dev/null
+++ b/src/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/app.js b/src/app.js
new file mode 100644
index 00000000..db2b87ab
--- /dev/null
+++ b/src/app.js
@@ -0,0 +1,30 @@
+(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',
+ }
+ }
+
+ 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/buttons.js b/src/buttons.js
new file mode 100644
index 00000000..500d56fc
--- /dev/null
+++ b/src/buttons.js
@@ -0,0 +1,84 @@
+"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 a').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);
+});
+
+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);
+});
+
+$('a.set-user').click(function() {
+ $('.set-user').hide();
+ $('.set-user-form').show();
+ $('.set-user-form').submit(function() {
+ var user = $('.set-user-input').val();
+ rz_core.graph.set_user(user);
+ $('.set-user').html('user: ' + user);
+ $('.set-user').show();
+ $('.set-user-form').hide();
+ $('.save-history').show();
+ return false;
+ })
+});
+
+$('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/consts.js b/src/consts.js
new file mode 100644
index 00000000..0c2af53b
--- /dev/null
+++ b/src/consts.js
@@ -0,0 +1,10 @@
+define(function() {
+ // TODO: enums, sometime
+ return {
+ APPLIED_GRAPH_DIFF: 'graph_diff',
+ KEYSTROKES: 'keystrokes',
+ KEYSTROKE_WHERE_EDIT_NODE: 'keystroke_where_edit_node',
+ KEYSTROKE_WHERE_DOCUMENT: 'keystroke_where_document',
+ KEYSTROKE_WHERE_TEXTANALYSIS: 'keystroke_where_textanalysis',
+ };
+});
diff --git a/src/drag_n_drop.js b/src/drag_n_drop.js
new file mode 100644
index 00000000..da166ad4
--- /dev/null
+++ b/src/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/history.js b/src/history.js
new file mode 100644
index 00000000..b46dd260
--- /dev/null
+++ b/src/history.js
@@ -0,0 +1,135 @@
+// 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', 'signal'],
+ function($, saveAs, consts, signal) {
+
+/* user - username (string)
+ * svg - svg element for catching zoom events (jquery DOMNode wrapper)
+ */
+function History(user, transform_element) {
+ var that = this;
+ this.records = [];
+ this.user = user;
+ this.transform_element = transform_element;
+ signal.slot(consts.APPLIED_GRAPH_DIFF, function(obj) {
+ return that.record_graph_diff(obj)
+ });
+ signal.slot(consts.KEYSTROKES, function(obj) {
+ return that.record_keystrokes(obj);
+ });
+ // 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_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_history = 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) {
+ throw "Invalid arguments";
+ }
+ keys = keys.filter(function(k) { return k !== undefined; });
+ if (keys.length == 0) {
+ return;
+ }
+ this.record(ACTION_KEYSTROKES, {
+ 'keys': keys,
+ 'where': where
+ });
+}
+
+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/main.js b/src/main.js
new file mode 100644
index 00000000..67213056
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,24 @@
+define(['textanalysis.ui', 'buttons', 'history', 'drag_n_drop', 'robot'],
+function(textanalysis_ui, buttons, history, drag_n_drop, robot) {
+
+ function expand(obj){
+ if (!obj.savesize) {
+ obj.savesize = obj.size;
+ }
+ obj.size = Math.max(obj.savesize, obj.value.length);
+ }
+
+ this.main = function() {
+ console.log('Rhizi main started');
+ drag_n_drop.init();
+ $('#editname').onkeyup = function() { expand(this); };
+ $('#editlinkname').onkeyup = function() { expand(this); };
+ $('#textanalyser').onkeyup = function() { expand(this); };
+
+ textanalysis_ui.main();
+ }
+
+ return {
+ main: main };
+ }
+);
diff --git a/src/model/core.js b/src/model/core.js
new file mode 100644
index 00000000..43a87b5d
--- /dev/null
+++ b/src/model/core.js
@@ -0,0 +1,80 @@
+"use strict"
+
+/**
+ * core model module - currently unused
+ */
+define([], function() {
+
+ /**
+ * return a random id
+ */
+ var random_id = function() {
+ return Math.random().toString(36).substring(2);
+ }
+
+ function random_node_name() {
+ return Math.random().toString(36).substring(2, 10);
+ }
+
+ function Node() {
+ }
+
+ function create_node__set_random_id(node_spec) {
+ if (undefined == node_spec) {
+ node_spec = {};
+ }
+
+ var ret = new Node();
+ ret.id = random_id();
+
+ ret.name = node_spec.name;
+ ret.type = node_spec.type;
+ ret.state = node_spec.state;
+ ret.status = node_spec.status;
+ ret.url = node_spec.url;
+
+ ret.start = node_spec.start;
+ ret.end = node_spec.end;
+
+ return ret;
+ }
+
+ function create_link__set_random_id(link_spec) {
+ var ret = new Link();
+ ret.id = random_id();
+ }
+
+ /**
+ * determine if nodes are equal by ID
+ *
+ * @param other_node
+ * @returns {Boolean}
+ */
+ Node.prototype.equlas_by_id = function(other) {
+ ret = this.id.toLowerCase() == other.id.toLowerCase();
+ if (false == ret) {
+ console.debug(this.id + ' != ' + other.id);
+ }
+ return ret;
+ }
+
+ function Link() {
+
+ }
+
+ /**
+ * determine if links are equal by ID
+ *
+ * @param other_node
+ * @returns {Boolean}
+ */
+ Link.prototype.equlas_by_id = function(other) {
+ return this.id.toLowerCase() == other.id.toLowerCase();
+ }
+
+ return {
+ random_node_name : random_node_name,
+ create_node__set_random_id : create_node__set_random_id,
+ create_link__set_random_id : create_link__set_random_id,
+ };
+});
diff --git a/src/model/diff.js b/src/model/diff.js
new file mode 100644
index 00000000..1bcfc5a2
--- /dev/null
+++ b/src/model/diff.js
@@ -0,0 +1,144 @@
+"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;
+ }
+
+ /**
+ * 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, id) {
+
+ if ('node' != type && 'link' != type) {
+ console
+ .error('attempt to init attribute diff for unsupported type: '
+ + type);
+ return;
+ }
+
+ var type_field = '__type_' + type;
+ 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_attr_write_node = 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_attr_rm_node = 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;
+ }
+
+ /**
+ * Visual diff object expressing any visual change to the state of a
+ * particular visualization type
+ */
+ function Vis_Diff(obj_spec) {
+ }
+
+ function new_topo_diff(obj_spec) {
+ /*
+ * validate obj_spec
+ */
+ // TODO
+ var 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/model/graph.js b/src/model/graph.js
new file mode 100644
index 00000000..820daef0
--- /dev/null
+++ b/src/model/graph.js
@@ -0,0 +1,649 @@
+"use strict"
+
+define(['signal', 'consts', 'util', 'textanalysis', 'model/core', 'model/util', 'rz_api_backend', 'rz_api_mesh'],
+function (signal, consts, util, textanalysis, model_core, model_util, rz_api_backend, rz_api_mesh) {
+
+function Graph(el) {
+
+ var nodes = [],
+ links = [];
+
+ function id_generator_generator() {
+ var id = 0;
+ function get_next() {
+ var next = id;
+ id += 1;
+ return next;
+ }
+ return get_next;
+ }
+ var id_generator = id_generator_generator();
+
+ ///FUNCTIONS
+ this.addNode = function(name, type, state) {
+ if (type === undefined) {
+ console.log('bug: adding undefined type');
+ }
+ var new_node = this._addNodeNoHistory(
+ {name:name,
+ type:type,
+ state:state,
+ start:0,
+ end:0,
+ status:"unknown"});
+ if (new_node) {
+ signal.signal(consts.APPLIED_GRAPH_DIFF, [{
+ nodes: {add: [new_node]}}]);
+ return new_node;
+ }
+ }
+
+ this._addNodeNoHistory = function(spec) {
+ // No history recorded - this is a helper for loading from files / constant graphs
+ var node;
+ if (spec.id === undefined) {
+ node = findNodeByName(spec.name, null);
+ } else {
+ if (spec.id !== undefined) {
+ node = findNode(spec.id, null);
+ }
+ }
+ if (node === undefined) {
+ node = {
+ "id": spec.id || id_generator(),
+ "name": spec.name,
+ "type": spec.type,
+ "state": spec.state,
+ "start": spec.start,
+ "end": spec.end,
+ "status": spec.status,
+ 'url': spec.url,
+ 'x': spec.x,
+ 'y': spec.y,
+ };
+ nodes.push(node);
+ }
+ return node;
+ }
+
+ this.removeNode = function(id, state) {
+ var i = 0;
+ var n = findNode(id, state);
+ while (i < links.length) {
+ if ((links[i]['source'] === n) || (links[i]['target'] == n)) links.splice(i, 1);
+ else i++;
+ }
+ var index = findNodeIndex(id, state);
+ if (index !== undefined) {
+ nodes.splice(index, 1);
+ }
+ signal.signal(consts.APPLIED_GRAPH_DIFF, [{nodes: {removed: [id]}}]);
+ }
+
+ this.removeNodes = function(state) {
+ var id = null;
+ var ns = findNodes(null, state);
+ for (var j = 0; j < ns.length; j++) {
+ var n = ns[j];
+ var i = 0;
+ while (i < links.length) {
+ if ((links[i]['source'] === n) || (links[i]['target'] == n)) links.splice(i, 1);
+ else i++;
+ }
+ var index = findNodeIndex(id, state);
+ if (index !== undefined) {
+ nodes.splice(index, 1);
+ }
+ }
+ if (ns.length > 0) {
+ signal.signal(consts.APPLIED_GRAPH_DIFF, [{nodes: {removed: ns.map(function(n) { return n.id; })}}]);
+ }
+ }
+
+ /**
+ *
+ * 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(n, d) {
+ var i = 0,
+ j = 0,
+ adjacentnode,
+ link,
+ link2,
+ ret = {'nodes':[], 'links':[]};
+
+ $(".debug").html(n.state);
+
+ if (n === 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;
+
+ while (i < links.length) {
+ link = links[i];
+ // XXX: using name comparison because n might be stale
+ if (compareNames(link.source.name, n.name)) {
+ adjacentnode = findNode(link.target.id, null);
+ if (adjacentnode.state !== "temp") {
+ ret.nodes.push({type: 'exit', node: adjacentnode});
+ }
+ ret.links.push({type: 'exit', link: link});
+
+ if (link.target.type === "chainlink") {
+ while (j < links.length) {
+ link2 = links[j];
+ if (link.target.id === link2.target.id &&
+ link2.target.type === "chainlink" &&
+ link2.target.state !== "temp") {
+ adjacentnode = findNode(link2.source.id, null);
+ if (adjacentnode.state !== "temp") {
+ ret.nodes.push({type: 'enter', node: adjacentnode});
+ }
+ ret.links.push({type: 'enter', link: link2});
+ }
+ j++;
+ }
+ }
+ j=0;
+ }
+ if (compareNames(links[i].target.name, n.name)) {
+ adjacentnode = findNode(links[i].source.id, null);
+ if (adjacentnode.state !== "temp") adjacentnode.state = "enter";
+ links[i].state = "enter";
+ }
+ i++;
+ }
+ 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.source.name, link.target.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(sourceName, targetName, name, state, drop_conjugator_links) {
+ var source = findNodeByName(sourceName, null),
+ target = findNodeByName(targetName, null),
+ sourceId = source ? source.id : null,
+ targetId = target ? target.id : null;
+
+ if (sourceId === null || targetId === null) {
+ console.log('error: link of missing nodes: ' + sourceName + ' (' + sourceId + ') -> '
+ + targetName + ' (' + targetId + ')');
+ return;
+ }
+ this.addLink(sourceId, targetId, name, state, drop_conjugator_links);
+ }
+
+ this.addLink = function(sourceId, targetId, name, state, drop_conjugator_links) {
+ var sourceNode = findNode(sourceId, null);
+ var targetNode = findNode(targetId, null);
+ var found = findLink(sourceId,targetId,name);
+
+ if (drop_conjugator_links && name && (name.replace(/ /g,"") === "and")) {
+ state = "temp";
+ }
+ if (sourceNode === undefined || targetNode === undefined) {
+ return;
+ }
+ if (!found) {
+ var id = id_generator(),
+ link = {
+ source: sourceNode,
+ target: targetNode,
+ name: name,
+ state: state,
+ id: id,
+ };
+ links.push(link);
+ signal.signal(consts.APPLIED_GRAPH_DIFF, [{links: {add: [link]}}]);
+ } else {
+ found.name = name;
+ found.state = state;
+ }
+ }
+
+ this.editLink = function(sourceId, targetId, newname, newstate) {
+ var link = findLink(sourceId, targetId, newname);
+ if (link !== undefined) {
+ link.name = newname;
+ if (newstate !== undefined) {
+ link.state = newstate;
+ }
+ }
+ }
+
+ this.editLinkTarget = function(sourceId, targetId, newTarget) {
+ var link = findLink(sourceId, targetId, null);
+ if (link !== undefined) {
+ link.target = findNode(newTarget, null);
+
+ } else {
+
+ }
+ }
+
+ this.editNameByName = function(old_name, new_name) {
+ var node = findNodeByName(old_name, null);
+
+ 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 index2 = findNodeByName(new_name, null);
+ var index = findNode(id, null);
+ var acceptReplace=true;
+
+ if ((index !== undefined)) {
+ if (index2 !== undefined) {
+ acceptReplace = confirm('"' + index2.name + '" will replace "' + index.name + '", are you sure?');
+ if (acceptReplace){
+ for (var i = 0; i < links.length; i++) {
+ if (links[i].source === index) {
+ links[i].source = index2;
+ }
+ if (links[i].target === index) {
+ links[i].target = index2;
+ }
+ }
+ this.removeNode(index.id,null);
+ }
+ } else {
+ index.name = new_name;
+ }
+ }
+ }
+
+ this.editDates = function(id, type, start, end) {
+ var index = findNode(id, type);
+ if ((index !== undefined)) {
+ index.start = start;
+ index.end = end;
+ }
+ }
+
+ /**
+ * editType:
+ *
+ * @return true if type changed
+ */
+ this.editType = function(id, state, newtype) {
+ var index = findNode(id, state);
+ if ((index !== undefined)) {
+ index.type = newtype;
+ }
+ }
+
+ this.editURL = function(id, state, url) {
+ var index = findNode(id, state);
+ if ((index === undefined)) return;
+ index.url = url;
+ }
+
+ this.editState = function(id, state, newstate) {
+ var index = findNode(id, state);
+
+ if ((index !== undefined)) {
+ index.state = newstate;
+ }
+ }
+
+ this.findCoordinates = function(id, type) {
+ var index = findNode(id, type);
+ if ((index !== undefined)) {
+ $('.typeselection').css('top', index.y - 90);
+ $('.typeselection').css('left', index.x - 230);
+ }
+ }
+
+ this.removeLink = function(link) {
+ var link,
+ 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.sourceId === links[i].sourceId && link.targetId === links[i].targetId) {
+ 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(sourceId, targetId, name) {
+ for (var i = 0; i < links.length; i++) {
+ if (links[i].source.id === sourceId && links[i].target.id === targetId) {
+ 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) {
+ var i;
+
+ for (i = 0 ; i < nodes.length; ++i) {
+ if (compareNames(nodes[i].name, name) && nodes[i].state === state) {
+ return true;
+ }
+ }
+ return false;
+ }
+ this.hasNodeByName = hasNodeByName;
+
+ 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;
+
+ var findNode = function(id, state) {
+ for (var i = 0; i < nodes.length; i++) {
+ if (nodes[i].id === id || nodes[i].state === state)
+ return nodes[i];
+ };
+ }
+
+ var findNodeByName = function(name, state) {
+ for (var i = 0 ; i < nodes.length ; ++i) {
+ if (compareNames(nodes[i].name, name) || nodes[i].state === state) {
+ 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){
+ // TODO impl
+ }
+
+ rz_api_mesh.broadcast_possible_next_diff_block(diff_set);
+ }
+
+ /**
+ * perform initial DB load from backend
+ */
+ // @ajax-trans
+ function load_from_backend() {
+
+ function on_success(data){
+ var n_set = [];
+ var l_set = [];
+ var len;
+
+ len = data['node_set'].length;
+ for (var i = 0; i < len; i++) {
+ var n_raw = data['node_set'][i];
+ var n = model_util.adapt_format_read_node(n_raw);
+ n_set.push(n);
+ }
+
+ len = data['link_set'].length;
+ for (var i = 0; i < len; i++) {
+ var l_raw = data['link_set'][i];
+ var l = model_util.adapt_format_read_link(l_raw);
+ l_set.push(l);
+ }
+
+ len = n_set.length
+ for (var i = 0; i < len; i++) {
+ var n = n_set[i];
+ graph.addNode(n.id, n.type, n.state)
+ }
+
+ len = l_set.length
+ for (var i = 0; i < len; i++) {
+ var l = l_set[i];
+ graph.addLink_byIds(l.sourceId, l.targetId, l.name, "perm")
+ }
+ }
+
+ rz_api_backend.clone(0, on_success);
+ }
+
+ this.load_from_json = function(json) {
+ var data = JSON.parse(json);
+ var i, node, link;
+
+ clear();
+ if (data == null) {
+ console.log('load callback: no data to load');
+ return;
+ }
+ for(i = 0; i < data["nodes"].length; i++){
+ node = data.nodes[i];
+ this._addNodeNoHistory({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,
+ });
+ textanalysis.autoSuggestAddName(node.name.toLowerCase());
+ }
+ for(i = 0; i < data["links"].length; i++){
+ link = data.links[i];
+ this.addLink(link.source, link.target, link.name, "perm");
+ }
+ this.clear_history();
+ }
+
+ 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({
+ "source":link.source.id,
+ "target":link.target.id,
+ "name":link.name
+ });
+ }
+ return JSON.stringify(d);
+ }
+
+ this.set_user = function(user) {
+ this.user = user;
+ this.history = new History(this.user, $('svg g.zoom')[0]);
+ console.log('new user: ' + user);
+ }
+
+ 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;
+
+}
+
+return {
+ Graph: Graph,
+};
+
+});
diff --git a/src/model/util.js b/src/model/util.js
new file mode 100644
index 00000000..43fa66db
--- /dev/null
+++ b/src/model/util.js
@@ -0,0 +1,117 @@
+"use strict"
+
+/**
+ * model utility functions:
+ * - convert from/to client/backend data representations
+ */
+define(['jquery', 'model/diff'],
+function($, model_diff) {
+
+ /**
+ * read by adapting from backend format
+ */
+ function adapt_format_read_node(n_raw) {
+ var ret;
+
+ ret = $.extend({
+ 'type' : n_raw['__type'].toLowerCase(), // discard all
+ // labels except
+ 'state' : 'temp',
+ }, n_raw);
+
+ delete ret.__label_set;
+
+ return ret;
+ }
+
+ /**
+ * write by adapting to backend format
+ */
+ function adapt_format_write_node(n_raw) {
+ var ret = $.extend({
+ '__type' : n_raw.type,
+ }, n_raw);
+
+ delete ret.type;
+ delete ret.state;
+
+ return ret
+ }
+
+ /**
+ * read by adapting from backend format
+ */
+ function adapt_format_read_link(l_raw) {
+ var ret;
+
+ ret = $.extend({
+ 'sourceId' : l_raw['__src'],
+ 'targetId' : l_raw['__dst'],
+ 'type' : l_raw['__type'].toLowerCase(), // discard all
+ // labels except
+ 'state' : 'temp',
+ }, l_raw);
+
+ ret['name'] = ret['type'];
+
+ delete ret.__src;
+ delete ret.__dst;
+ delete ret.__type;
+
+ return ret;
+ }
+
+ /**
+ * write by adapting to backend format
+ */
+ function adapt_format_write_link(l_raw) {
+ var ret = $.extend({
+ '__src' : l_raw.sourceId,
+ '__dst' : l_raw.targetId,
+ '__type' : 'textual_link',
+ }, l_raw);
+
+ delete ret.source;
+ delete ret.target;
+ delete ret.state;
+
+ 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 : adapt_format_read_link,
+ 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/robot.js b/src/robot.js
new file mode 100644
index 00000000..700e41b6
--- /dev/null
+++ b/src/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/rz_api_backend.js b/src/rz_api_backend.js
new file mode 100644
index 00000000..df162225
--- /dev/null
+++ b/src/rz_api_backend.js
@@ -0,0 +1,220 @@
+"use strict";
+
+/**
+ * API calls designed to execute against a local backend service
+ */
+define([], function() {
+
+ function RZ_API_Backend() {
+
+ /**
+ * 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('http://127.0.0.1:3000' + 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/attr-diff-commit', 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/rz_api_mesh.js b/src/rz_api_mesh.js
new file mode 100644
index 00000000..9014dbac
--- /dev/null
+++ b/src/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/rz_core.js b/src/rz_core.js
new file mode 100644
index 00000000..7b56d41b
--- /dev/null
+++ b/src/rz_core.js
@@ -0,0 +1,744 @@
+"use strict"
+
+define(['jquery', 'd3', 'consts', 'signal', 'util', 'model/graph', 'model/core', 'view/helpers', 'view/view', 'rz_observer'],
+function($, d3, consts, signal, util, model_graph, model_core, view_helpers, view, rz_observer) {
+
+var addednodes = [];
+
+var vis;
+
+var graphstate = "GRAPH";
+var graphinterval = 0;
+
+var ganttTimer = 0;
+
+var deliverables = [];
+
+var circle; // <-- should not be module globals.
+
+var scrollValue = 0,
+ zoomObject;
+
+var graph;
+
+var drag;
+
+var force;
+
+var state_to_link_class = {
+ enter:'enterlink graph',
+ exit:'exitlink graph',
+};
+
+function recenterZoom() {
+ vis.attr("transform", "translate(0,0)scale(1)");
+}
+
+var initDrawingArea = function () {
+
+ function zoom() {
+ if (graphstate === "GRAPH") {
+ vis.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
+ }
+ if (graphstate === "GANTT") {
+ vis.attr("transform", "translate(0,0)scale(1)");
+ }
+ }
+
+ 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) {
+ d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
+ tick();
+ }
+
+ function dragended(d) {
+ d3.select(this).classed("dragging", false);
+ d3.select(this).classed("fixed", true);
+ d3.select(this).attr("dx", d3.event.x).attr("dy", d3.event.y);
+ if (d.dragstart.clientX - d3.event.sourceEvent.clientX != 0 ||
+ d.dragstart.clientY - d3.event.sourceEvent.clientY != 0) {
+ tick();
+ force.resume();
+ }
+ }
+
+ var el = document.body;
+
+ graph = new model_graph.Graph(el);
+
+ //Zoom scale behavior in zoom.js
+ zoomObject = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
+
+ vis = d3.select(el).append("svg:svg")
+ .attr('id', 'canvas_d3')
+ .attr("width", '100%')
+ .attr("height", '100%')
+ .attr("pointer-events", "all")
+ .call(zoomObject)
+ .append("g")
+ .attr("class", "zoom");
+
+ // TODO: why do we need this huge overlay (hugeness also not constant)
+ vis.append("rect")
+ .attr("class", "overlay graph")
+ .attr("width", $(el).innerWidth() * 12)
+ .attr("height", $(el).innerHeight() * 12)
+ .attr("x", -$(el).innerWidth() * 5)
+ .attr("y", -$(el).innerHeight() * 5);
+ $('.overlay').click(mousedown);
+
+ // 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");
+
+ drag = d3.behavior.drag()
+ .origin(function(d) { return d; })
+ .on("dragstart", dragstarted)
+ .on("drag", dragged)
+ .on("dragend", dragended);
+
+ 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();
+
+ graph.update = update;
+ $('#canvas_d3').dblclick(canvas_handler_dblclick);
+}
+
+initDrawingArea();
+
+/**
+ * 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._addNodeNoHistory(n); // FIXME: clean once node creation functions consolidate
+ graph.update();
+
+ var n_ve = locate_visual_element(n); // locate visual element
+
+ var on_slowdown_cb = function(){
+ var set_focus = true;
+ editNode(n_ve, n, set_focus);
+ 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,
+ });
+}
+
+function update(no_relayout) {
+ var node,
+ link,
+ link_g,
+ linktext,
+ nodetext,
+ link_group;
+
+ link_group = vis.select('#link-group');
+ link = link_group.selectAll("g.link")
+ .data(graph.links());
+
+ link_g = link.enter().append('g')
+ .attr('class', 'link graph');
+
+ link_g.append("svg:defs").selectAll("marker")
+ .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")
+ .attr("class", "graph")
+ .style("fill", function(d){
+ if (d.state==="enter" || d.state==="exit") {
+ return "EDE275";
+ } else {
+ return "#aaa";
+ }
+ })
+ .append("svg:path")
+ .attr("d", "M0,-5L10,0L0,5");
+
+ link_g.append("path")
+ .attr("class", function(d) {
+ return state_to_link_class[d.state] || 'link graph';
+ })
+ .attr("marker-end", "url(#end)");
+
+ // second path for larger click area
+ link_g.append("path")
+ .attr("class", "ghostlink")
+ .on("click", function(d, i) {
+ var that = this,
+ source = this.link.source,
+ target = this.link.target;
+
+ view.edge_info.on_delete(function () {
+ graph.removeLink(that.link);
+ graph.update(true);
+ view.edge_info.hide();
+ });
+ view.edge_info.show(d);
+ highlight(source);
+ highlight(target);
+ source.state = 'chosen';
+ target.state = 'chosen';
+ graph.update(true);
+ });
+
+ link.style("stroke-dasharray", function(d,i){
+ if(d.name && d.name.replace(/ /g,"")=="and" && d.state==="temp")
+ return "3,3";
+ else
+ return "0,0";
+ });
+
+ link.selectAll('path.link')
+ .attr('stroke-width', function(d) {
+ if (d.state === 'exit' || d.state === 'enter') {
+ return "4px";
+ }
+ return "2.0px";
+ });
+
+ link.exit().remove();
+
+ link_group.selectAll('.ghostlink')
+ .data(graph.links())
+ .each(function (d) {
+ this.link = d;
+ });
+
+ linktext = vis.selectAll(".linklabel").data(graph.links());
+ linktext.enter()
+ .append("text")
+ .attr("class", "linklabel graph")
+ .attr("text-anchor", "middle")
+ .on("click", function(d, i) {
+ if (d.state !== "temp") {
+ editLink(this, d, i);
+ }
+ });
+
+ linktext
+ .text(function(d) {
+ var name = d.name || "";
+ if (!(d.target.state === "temp" ||
+ d.source.state === "chosen" || d.target.state === "chosen")) {
+ return "";
+ }
+ if (name.length < 25 || d.source.state === "chosen" ||
+ d.target.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('class', 'node')
+ .attr('id', function(d){ return d.id; }) // append node id to enable data->visual mapping
+ .attr('visibility', 'hidden') // made visible on first tick
+ .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"){
+ editNode(this, d, i);
+ showInfo(this.node, i);
+ }
+ })
+ .call(drag);
+
+ node.each(function (d) {
+ this.node = d;
+ });
+
+ nodetext = nodeEnter.insert("text")
+ .attr("class", "nodetext graph")
+ .attr("dx", 15)
+ .attr("dy", ".30em");
+
+ node.select('g.node text')
+ .text(function(d) {
+ if (!d.name) {
+ return "";
+ }
+ 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", "circle graph")
+ .attr("r", function(d) {
+ return view_helpers.customSize(d.type) - 2;
+ })
+ .style("fill", function(d) {
+ return view_helpers.customColor(d.type);
+ })
+ .style("stroke", function(d) {
+ if (d.state === "chosen") return "#EDE275";
+ if (d.state === "enter") return "#EDE275";
+ if (d.type === "bubble") return "#101010";
+ if (d.state === "exit") return "#EDE275";
+ if (d.type === "chainlink") return "#AAA";
+
+ return "#fff";
+ })
+ .style("stroke-width", function(d) {
+ if (d.state === "temp" && d.type !== "empty" || d.state === "chosen") return "3px";
+ else return "1.5px";
+ })
+ .style("box-shadow", function(d) {
+ if (d.state === "temp") return "0 0 40px #FFFF8F";
+ else return "0 0 0px #FFFF8F";
+ })
+ .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();
+ if(d.state!=="temp") {
+ showInfo(d, i);
+ } else {
+ removeHighlight();
+ }
+ update(true);
+ });
+
+ //if(graphstate==="GANTT"){
+ nodeEnter.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;
+ }
+ })
+ .on("click", function(d, i) {
+ if(d.state!=="temp")showInfo(d, i);
+ });
+ //}
+
+ 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 === "deliverable") {
+ 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.select("#link-group").selectAll("path.link")
+ .data(graph.links());
+ var linktext = vis.selectAll(".linklabel").data(graph.links());
+
+ function transform(d) {
+ if (graphstate === "GRAPH" || d.type === "deliverable") {
+ if (check_for_nan(d.x) || check_for_nan(d.y)) {
+ return;
+ }
+ if (d.state === "temp") {
+ return "translate(" + d.x + "," + d.y + ")";
+ } else {
+ return "translate(" + d.x + "," + d.y + ")";
+ }
+ } else {
+ return "translate(0,0)";
+ }
+ return "translate(" + d.x + "," + d.y + ")";
+ }
+
+ if (graphstate === "GANTT") {
+ var k = 20 * e.alpha;
+ var today = new Date();
+ var missingcounter = 0;
+
+ graph.nodes().forEach(function(d, i) {
+ if ((d.start === 0 || d.end === 0)) {
+ d.x = 450 + missingcounter * 100;
+ d.y = window.innerWidth / 2;
+ if (missingcounter >= 6) {
+ d.x = 450 + (missingcounter - 6) * 100;
+ d.y = window.innerWidth / 2 + 50;
+ }
+ missingcounter++;
+ } else {
+ //var min= 150+graphinterval*Math.ceil(Math.abs(d.start.getTime() - today.getTime()) / (1000 * 3600 * 24)) - $('.gantbox').scrollLeft();
+ //var max= 150+graphinterval*Math.ceil(Math.abs(d.end.getTime() - d.start.getTime()) / (1000 * 3600 * 24)) - $('.gantbox').scrollLeft();
+ //d.x = min+Math.sin(today.getTime()/1000*Math.PI*2/10)*max;
+ ganttTimer++;
+ if (ganttTimer < 3000) {
+ d.x = 150 + graphinterval * Math.ceil(Math.abs(d.start.getTime() - today.getTime()) / (1000 * 3600 * 24)) * ganttTimer / 3000;
+ d.y = 150 + d.start.getHours() * 17;
+ } else {
+ d.x = 150 + graphinterval * Math.ceil(Math.abs(d.start.getTime() - today.getTime()) / (1000 * 3600 * 24));
+ d.y = 150 + d.start.getHours() * 17;
+ }
+ }
+ if (d.state === "chosen") {
+ scrollValue = d.x;
+ }
+ });
+ } else {
+ //circles animation
+ var tempcounter = 0;
+ var temptotal = 0;
+ graph.nodes().forEach(function(d, i) {
+ if (d.state === "temp" && d.type!=="chainlink" && d.type!=="bubble") {
+ temptotal++;
+ }
+ });
+ if(temptotal!==newnodes){
+ newnodes+=temptotal/15/(newnodes*newnodes);
+ }
+ if(newnodes>=temptotal){
+ newnodes=temptotal;
+ }
+ if(newnodes<1)newnodes=1;
+ graph.nodes().forEach(function(d, i) {
+ if (d.state === "temp") {
+ tempcounter++;
+ if(d.type==="chainlink" || d.type==="bubble"){
+ d.x = window.innerWidth / 2;
+ d.y = window.innerHeight / 2;
+ } else {
+ d.x = window.innerWidth / 2 + (60+newnodes*20) * Math.cos(-Math.PI+Math.PI * 2 * (tempcounter-1) / newnodes+0.3);
+ d.y = window.innerHeight / 2 + (60+newnodes*20) * Math.sin(-Math.PI+Math.PI * 2 * (tempcounter-1) / newnodes+0.3);
+ }
+ check_for_nan(d.x);
+ check_for_nan(d.y);
+ }
+ });
+ }
+
+ link.attr("d", function(d, i) {
+ var d_val,
+ ghost;
+
+ if (graphstate === "GRAPH") {
+ var dx = d.target.x - d.source.x,
+ dy = d.target.y - d.source.y,
+ dr = Math.sqrt(dx * dx + dy * dy);
+ d_val = "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
+ } else if (graphstate === "GANTT") {
+ if (d.state === "enter" || d.state === "exit") {
+ var dx = d.target.x - d.source.x,
+ dy = d.target.y - d.source.y,
+ dr = Math.sqrt(dx * dx + dy * dy) * 5;
+ d_val = "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
+ } else {
+ var dx = d.target.x - d.source.x,
+ dy = d.target.y - d.source.y,
+ dr = Math.sqrt(dx * dx + dy * dy) * 5;
+
+ d_val = "M" + 0 + "," + 0 + "A" + dr + "," + dr + " 0 0,1 " + 0 + "," + 0;
+ }
+ }
+ // update ghostlink position
+ ghost = $(this.nextElementSibling);
+ ghost.attr("d", d_val);
+ return d_val;
+ });
+
+
+ linktext.attr("transform", function(d) {
+ if (graphstate === "GRAPH") {
+ return "translate(" + (d.source.x + d.target.x) / 2 + "," + (d.source.y + d.target.y) / 2 + ")";
+ } else {
+ return "translate(0,0)";
+ }
+ });
+
+ node.attr("transform", transform);
+
+ // After initial placement we can make the nodes visible.
+ //links.attr('visibility', 'visible');
+ node.attr('visibility', 'visible');
+}
+
+function removeHighlight() {
+ // TODO: stop manipulating state
+ var nodes = graph.nodes(),
+ links = graph.links(),
+ k = 0, j = 0;
+
+ while (k < nodes.length) {
+ if (nodes[k]['state'] === "enter" || nodes[k]['state'] === "exit" || nodes[k]['state'] === "chosen") {
+ nodes[k]['state'] = "perm";
+ }
+ k++;
+ }
+ while (j < links.length) {
+ links[j]['state'] = "perm";
+ j++;
+ }
+}
+
+function highlight(n)
+{
+ var n,
+ connected = graph.getConnectedNodesAndLinks(n, 1),
+ i,
+ node,
+ link,
+ data;
+
+ n.state = 'chosen';
+
+ 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;
+ };
+ }
+}
+
+function showInfo(d, i) {
+ if (d.state !== "chosen" && d.state !== 'temp') {
+ highlight(d);
+ view.node_info.show(d);
+ view.node_info.on_submit(function() {
+ if (d.type === "deliverable") {
+ graph.editDates(d.id, null, new Date($("#editstartdate").val()), new Date($("#editenddate").val()));
+ }
+ graph.editType(d.id,d.type,$('#edittype').val());
+ graph.editURL(d.id, d.type, $('#editurl').val());
+ graph.update(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, null);
+ graph.update(false);
+ view.node_info.hide();
+ }
+ });
+ } else {
+ removeHighlight();
+ view.node_info.hide();
+ }
+ graph.update(true);
+}
+
+function mousedown() {
+ $('.editinfo').css('top', -100);
+ $('.editinfo').css('left', 0);
+ $('.editlinkinfo').css('top', -100);
+ $('.editlinkinfo').css('left', 0);
+ removeHighlight();
+ view.hide();
+ graph.update(true);
+}
+
+function AddedUnique(newnode) {
+ truth = true;
+ for (var p = 0; p < addednodes.length; p++) {
+ if (addednodes[p] === newnode) {
+ truth = false;
+ }
+ }
+ return truth;
+}
+
+
+$('#editform').keypress(function(e) {
+ signal.signal(consts.KEYSTROKES, [{where: consts.KEYSTROKE_WHERE_EDIT_NODE, keys: [e.which]}]);
+ if (e.which == 13) {
+ $('.editinfo').css('top', -100);
+ $('.editinfo').css('left', 0);
+ var element = $('#editname');
+ var newname = element.val();
+ var d = element.data().d;
+ graph.editName(d.id, newname);
+ graph.update(true);
+ return false;
+ }
+});
+
+
+/**
+ * @param e visual node element
+ * @param n node model object
+ */
+function editNode(e, n, set_focus) {
+ var oldname = n.name;
+ var en_element = $('#editname');
+ var offset = $(e).find('.nodetext').offset();
+
+ $('.editinfo').css('top', offset.top);
+ $('.editinfo').css('left', offset.left);
+ en_element.val(oldname);
+ en_element.data().d = n;
+
+ if (set_focus){
+ en_element.focus();
+ }
+}
+
+function editLink(link, d, i) {
+ var offset = $(link).offset(),
+ oldname = d.name;
+
+ $('.editlinkinfo').css('top', offset.top);
+ $('.editlinkinfo').css('left', offset.left);
+ $('#editlinkname').val(oldname);
+
+ // TODO: handle escape as well to quit without changes (enter does submit)
+ $('#editlinkform').submit(function() {
+ graph.editLink(d.source.id, d.target.id, $('#editlinkname').val());
+ $('.editlinkinfo').css('top', -100);
+ $('.editlinkinfo').css('left', 0);
+ graph.update(true);
+
+ return false;
+ });
+
+ graph.update(true);
+}
+
+return {
+ graph: graph,
+ force: force,
+ load_from_json: function(result) {
+ graph.load_from_json(result);
+ recenterZoom();
+ update(false);
+ }
+}
+}); /* close define call */
diff --git a/src/rz_observer.js b/src/rz_observer.js
new file mode 100644
index 00000000..4fe6385f
--- /dev/null
+++ b/src/rz_observer.js
@@ -0,0 +1,118 @@
+"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) {
+ if (undefined == slowdown_threshold) {
+ slowdown_threshold = 0.07;
+ }
+ if (undefined == 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/signal.js b/src/signal.js
new file mode 100644
index 00000000..e5888b4c
--- /dev/null
+++ b/src/signal.js
@@ -0,0 +1,22 @@
+/*
+ * Signal/slot (i.e. blackboard pattern) for rhizi.
+ *
+ * This is a thin wrapper over jquery right now.
+ * But just keeping it here to make any future change of implementation slightly
+ * easier.
+ */
+
+define(['jquery'], function($) {
+ function slot(name, handler) {
+ $(window).on(name, function(e, args) {
+ handler(args);
+ });
+ };
+ function signal(name, obj) {
+ $(window).trigger(name, obj);
+ }
+ return {
+ 'slot': slot,
+ 'signal': signal
+ };
+})
diff --git a/src/textanalysis.js b/src/textanalysis.js
new file mode 100644
index 00000000..4cff49bc
--- /dev/null
+++ b/src/textanalysis.js
@@ -0,0 +1,459 @@
+"use strict";
+
+define(['rz_core', 'model/util', 'model/diff'],
+function(rz_core, model_util, model_diff) {
+
+var typeindex = 0;
+var nodetypes = ["person", "project", "skill", "deliverable", "objective"];
+var typeStack = [];
+
+var lastnode;
+
+var sugg = {}; // suggestions for autocompletion of node names
+
+var ANALYSIS_NODE_START = 'ANALYSIS_NODE_START';
+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? */
+ if(name.split(" ").length > 1) {
+ sugg['"' + name + '"'] = 1;
+ } else {
+ sugg[name] = 1;
+ }
+}
+
+function autocompleteCallback(request, response_callback)
+{
+ var ret = [];
+ if (request.term === "" || request.term) {
+ for (var name in sugg) {
+ if (name.toLowerCase().indexOf(request.term.toLowerCase()) === 0) {
+ ret.push(name);
+ }
+ }
+ }
+ response_callback(ret);
+}
+
+/* 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 then two nodes (two '#'
+ * marks).
+ *
+ */
+var textAnalyser = function (newtext, finalize) {
+ var sentence,
+ newlinks = [],
+ newnodes = [],
+ linkindex = 0,
+ nodeindex = 0,
+ orderStack = [],
+ and_count = 0,
+ prefix = "",
+ ret = {'nodes': [], 'links': []},
+ m,
+ word,
+ completeSentence,
+ typesetter, starGraph,
+ n,
+ link_hash = {},
+ yell_bug = false, // TODO: fix both issues
+ NODE = "NODE",
+ LINK = "LINK",
+ START = "START";
+
+ function addNode(name, type, state) {
+ if (type === undefined) {
+ console.log('bug: textanalyser.addNode of type undefined');
+ }
+ ret.nodes.push({'name':name, 'type':type, 'state':state});
+ }
+ 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;
+ ret.links.push({'sourceName':src, 'targetName':dst, 'name':name ? name.trim() : "", 'state':state});
+ }
+
+ //Sentence Sequencing
+ //Build the words and cuts the main elements
+ sentence = tokenize(newtext, '#', '"');
+
+ //BUILD NEW NODE AND LINK ARRAYS WITH ORDER OF APPEARENCE
+ 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);
+ newnodes.push(sentence[m]);
+ linkindex++;
+ } else if (orderStack[orderStack.length - 1] === NODE) {
+ orderStack.push(LINK);
+ if (!newlinks[linkindex]) {
+ newlinks[linkindex] = sentence[m] + " ";
+ } else {
+ newlinks[linkindex] += sentence[m] + " ";
+ }
+ } else {
+ if (!newlinks[linkindex]) {
+ newlinks[linkindex] = sentence[m] + " ";
+ } else {
+ newlinks[linkindex] += sentence[m] + " ";
+ }
+ }
+ if (newnodes.length === 0) {
+ prefix += (prefix.length > 0 ? ' ' : '') + sentence[m];
+ }
+ break;
+ }
+ }
+
+ starGraph = (newlinks.length - and_count) >= 3 ||
+ ((newlinks.length - and_count >= 1) &&
+ newlinks.length > 2 &&
+ orderStack.length > 1 &&
+ orderStack[orderStack.length - 1] != NODE);
+
+ //PREFIX not null case - put complete sentence in first link.
+ if (prefix && !starGraph) {
+ newlinks[1] = prefix + " " + newnodes[0] +
+ (newlinks[1] !== undefined || newnodes[1] !== undefined ?
+ " " : "")
+ + (newlinks[1] !== undefined ? newlinks[1] : "")
+ + (newnodes[1] !== undefined ? newnodes[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 += " (" + newnodes[nodeindex] + ") ";
+ completeSentence += newnodes[nodeindex] + " ";
+ nodeindex++;
+ } else if (orderStack[m] === LINK) {
+ word += " -->" + newlinks[nodeindex] + " --> ";
+ completeSentence += newlinks[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 < newnodes.length; n++) {
+ autoSuggestAddName(newnodes[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(newnodes[nodeindex], typeStack[nodeindex], typesetter);
+ if (!starGraph) {
+ addLink(newnodes[nodeindex - 1], newnodes[nodeindex],
+ newlinks[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) {
+ addLink(newnodes[nodeindex - 1], "new node", newlinks[linkindex], "temp");
+ and_connect("new node");
+ }
+ ret.state = ANALYSIS_NODE_START;
+ break;
+ case NODE:
+ typeStack[nodeindex] = selectedType();
+ addNode(newnodes[nodeindex], typeStack[nodeindex], typesetter);
+ if (!starGraph) {
+ addLink(newnodes[nodeindex - 1], newnodes[nodeindex], newlinks[linkindex], typesetter);
+ and_connect(newnodes[nodeindex]);
+ }
+ break;
+ case LINK:
+ linkindex++;
+ addNode("new node", selectedType(), "temp");
+ if (!starGraph) {
+ addLink(newnodes[nodeindex - 1], "new node", newlinks[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<newlinks.length;x++){
+ if(newlinks[x])if(newlinks[x].replace(/ /g,"")!=="and"){
+ verb = newlinks[x];
+ for(var y=0; y<x ;y++){
+ addLink(newnodes[y], node, verb, typesetter);
+ for(var z=x; z<newnodes.length ;z++){
+ addLink(newnodes[y], newnodes[z], verb, typesetter);
+ }
+ }
+ }
+ }
+ }
+
+ /*console.log(sentence);
+ console.log(completeSentence);
+ console.log(orderStack);*/
+
+ //STAR CASE
+ if (starGraph) {
+ addNode(completeSentence, "chainlink", typesetter);
+ for (n = 0; n < newnodes.length; n++) {
+ addLink(newnodes[n], completeSentence, "", typesetter);
+ }
+ }
+
+ ret.drop_conjugator_links = and_count < linkindex;
+
+ ret.applyToGraph = function(graph, backend_commit) {
+ window.ret = ret;
+ var comp = graph.compareSubset('temp',
+ ret.nodes.filter(
+ function(node) {
+ return !graph.hasNodeByName(node.name, "perm")
+ && node.type !== 'bubble';
+ }).map(function (node) {
+ return {name: node.name};
+ }),
+ ret.links.map(
+ function (link) {
+ return [link.sourceName, link.targetName];
+ }));
+
+ var k, n, l;
+ if (comp.graph_same && !finalize) {
+ if (comp.old_name && comp.new_name) {
+ up_to_two_renames(graph, comp.old_name, comp.new_name);
+ }
+ for (k in ret.links) {
+ l = ret.links[k];
+ graph.addLinkByName(l.sourceName, l.targetName, l.name, l.state, ret.drop_conjugator_links);
+ }
+ } else {
+ // REINITIALISE GRAPH (DUMB BUT IT WORKS)
+ graph.removeNodes("temp");
+ graph.removeLinks("temp");
+ for (k in ret.nodes) {
+ n = ret.nodes[k];
+ if (n.state == 'temp' && finalize) {
+ console.log('bug: temp node creation on finalize');
+ } else {
+ lastnode = graph.addNode(n.name, n.type, n.state);
+ }
+ }
+ for (k in ret.links) {
+ l = ret.links[k];
+ if (!finalize || l.name !== 'and') {
+ graph.addLinkByName(l.sourceName, l.targetName, l.name, l.state, ret.drop_conjugator_links);
+ }
+ }
+ }
+
+ 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
+ graph.update(!finalize && comp.graph_same);
+ };
+
+ if (finalize) {
+ typeStack = [];
+ }
+
+ return ret;
+};
+
+return {
+ autocompleteCallback:autocompleteCallback,
+ textAnalyser:textAnalyser,
+ autoSuggestAddName:autoSuggestAddName,
+ ANALYSIS_NODE_START:ANALYSIS_NODE_START,
+ 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/textanalysis.ui.js b/src/textanalysis.ui.js
new file mode 100644
index 00000000..0be87f64
--- /dev/null
+++ b/src/textanalysis.ui.js
@@ -0,0 +1,164 @@
+"use strict"
+
+define(['autocomplete', 'rz_core', 'textanalysis', 'signal', 'consts'],
+function(autocomplete, rz_core, textanalysis, signal, consts) {
+var text = ""; // Last text of sentence
+var element_name = '#textanalyser';
+var element = $(element_name);
+var suggestionChange;
+
+function analyzeSentence(sentence, finalize)
+{
+ var ret = textanalysis.textAnalyser(sentence, finalize);
+
+ switch (ret.state) {
+ case textanalysis.ANALYSIS_NODE_START:
+ $('.typeselection').css({top:window.innerHeight/2-115,left:window.innerWidth/2-325});
+ $('.typeselection').html('<table><tr><td style="height:28px"></td></tr><tr><td>Use [TAB] key to pick a type</td></tr></table>');
+ break;
+ case textanalysis.ANALYSIS_LINK:
+ $('.typeselection').css('top', -300);
+ $('.typeselection').css('left', 0);
+ break;
+ }
+
+ var backend_commit = false;
+ ret.applyToGraph(rz_core.graph, backend_commit);
+
+ if (finalize || sentence.length == 0) {
+ $('.typeselection').css('top', -300);
+ $('.typeselection').css('left', 0);
+ $('span.ui-helper-hidden-accessible').hide();
+ } else {
+ $('span.ui-helper-hidden-accessible').show();
+ }
+}
+
+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').html('<table><tr><td style="height:28px"></td></tr><tr><td>' + "Chosen Type: " + nodetype + '</td></tr></table>');
+ rz_core.graph.findCoordinates(id, null);
+ } else {
+ rz_core.graph.editType(id, null, nodetype);
+ $('.typeselection').html('<table><tr><td style="height:28px"></td></tr><tr><td>' + "Chosen Type: " + nodetype + '</td></tr></table>');
+ rz_core.graph.findCoordinates(id, null);
+ }
+ rz_core.graph.update(true);
+}
+
+return {
+ analyzeSentence: analyzeSentence,
+ main:function () {
+ if (element.length != 1) {
+ return;
+ }
+
+ element.autocompleteTrigger({
+ triggerStart: '#',
+ triggerEnd: '',
+ source: textanalysis.autocompleteCallback,
+ open: function() {
+ $('.ui-autocomplete').css('width', '10px');
+ },
+ });
+
+ $(document).keydown(function(e) {
+ signal.signal(consts.KEYSTROKES, [{where: consts.KEYSTROKE_WHERE_DOCUMENT, keys: [e.keyCode]}]);
+ if (e.keyCode == 9) {//TAB
+ e.preventDefault();
+ changeType(e.shiftKey ? "up" : "down", textanalysis.lastnode());
+ return false;
+ }
+
+ if (e.keyCode == 37) {//UP
+ $('html, body').scrollLeft(0);
+ }
+ if (e.keyCode == 39) {//DOWN
+ $('html, body').scrollLeft(0);
+ }
+
+ if (e.keyCode == 38) {//UP
+ suggestionChange = true;
+ }
+ if (e.keyCode == 40) {//DOWN
+ suggestionChange = true;
+ }
+
+ if (e.keyCode == 9) {//TAB
+ return false;
+ }
+ });
+
+ element.keypress(function(e) {
+ signal.signal(consts.KEYSTROKES, [{where: consts.KEYSTROKE_WHERE_TEXTANALYSIS, keys:[e.which]}]);
+ if (e.which == 13) {
+ if(!suggestionChange) {
+ text = element.val();
+ element.val("");
+ analyzeSentence(text, true);
+ } else {
+ suggestionChange = false;
+ }
+ return false;
+ }
+
+ if (e.which == 37) {//RIGHT
+ $('body').scrollLeft(0);
+ e.stopPropagation();
+ return false;
+ }
+ if (e.which == 39) { //LEFT
+ $('body').scrollLeft(0);
+ e.stopPropagation();
+ return false;
+ }
+ });
+
+ if ('oninput' in document.documentElement) {
+ element.on('input', function(e) {
+ text = element.val();
+ analyzeSentence(text, false);
+ });
+ } 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/util.js b/src/util.js
new file mode 100644
index 00000000..3113474b
--- /dev/null
+++ b/src/util.js
@@ -0,0 +1,52 @@
+"use strict"
+
+define(function() {
+
+ 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);
+ }
+
+ return {
+ set_from_array : set_from_array,
+ set_from_object : set_from_object,
+ set_diff : set_diff,
+ array_diff : array_diff
+ };
+});
diff --git a/src/view/edge_info.js b/src/view/edge_info.js
new file mode 100644
index 00000000..9652839c
--- /dev/null
+++ b/src/view/edge_info.js
@@ -0,0 +1,36 @@
+
+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,
+};
+});
+"use strict"
diff --git a/src/view/helpers.js b/src/view/helpers.js
new file mode 100644
index 00000000..be0e289e
--- /dev/null
+++ b/src/view/helpers.js
@@ -0,0 +1,78 @@
+"use strict"
+
+define(function() {
+function customColor(type) {
+ var color;
+ switch (type) {
+ case "person":
+ color = '#FCB924';
+ break;
+ case "project":
+ color = '#009DDC';
+ break;
+ case "skill":
+ color = '#62BB47';
+ break;
+ case "deliverable":
+ color = '#202020';
+ break;
+ case "objective":
+ color = '#933E99';
+ break;
+ case "empty":
+ color = "#080808";
+ break;
+ case "chainlink":
+ color = "#fff";
+ break;
+ case "bubble":
+ color = "rgba(0,0,0,0.2)";
+ break;
+ default:
+ console.log('bug: unknown type ' + type);
+ color = '#080808';
+ break;
+ }
+ return color;
+}
+
+function customSize(type) {
+ var size;
+ switch (type) {
+ case "person":
+ size = 12;
+ break;
+ case "project":
+ size = 12;
+ break;
+ case "skill":
+ size = 12;
+ break;
+ case "deliverable":
+ size = 12;
+ break;
+ case "objective":
+ 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/view/internal.js b/src/view/internal.js
new file mode 100644
index 00000000..bc18bd75
--- /dev/null
+++ b/src/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/view/node_info.js b/src/view/node_info.js
new file mode 100644
index 00000000..d455b366
--- /dev/null
+++ b/src/view/node_info.js
@@ -0,0 +1,63 @@
+define(['jquery', 'view/helpers', 'view/internal'],
+function($, view_helpers, internal) {
+
+function show(d) {
+ internal.edit_tab.show('node');
+
+ if (d.type === "deliverable") {
+ $('.info').html('Name: ' + d.id + '<br/><form id="editbox"><label>Type:</label><select id="edittype"><option value="person">Person</option><option value="project">Project</option><option value="skill">Skill</option><option value="deliverable">Deliverable</option><option value="objective">Objective</option></select><br/><label>Status</label><select id="editstatus"><option value="waiting">Waiting</option><option value="current">Current</option><option value="done">Done</option></select><br/><label>Start date:</label><input id="editstartdate"/></br><label>End date:</label><input id="editenddate"/></br><button>Save</button><button id="deletenode">Delete</button></form>');
+ } else if(d.type=== "chainlink"){
+ $('.info').html('Name: ' + d.id + '<br/><form id="editbox"><button>Save</button><button id="deletenode">Delete</button></form>');
+ }else{
+ $('.info').html('Name: ' + d.id + '<br/><form id="editbox"><label>Type:</label><select id="edittype"><option value="person">Person</option><option value="project">Project</option><option value="skill">Skill</option><option value="deliverable">Deliverable</option><option value="objective">Objective</option></select><br/><label>URL:</label><input id="editurl"/><br/><button>Save</button><button id="deletenode">Delete</button></form>');
+ }
+
+ $('.info').css("border-color", view_helpers.customColor(d.type));
+
+ $("#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);
+
+ if (d.type === "deliverable") {
+ $('#editstartdate').val(d.start);
+ $('#editenddate').val(d.end);
+ }
+}
+
+function hide()
+{
+ internal.edit_tab.hide();
+}
+
+function on_submit(f)
+{
+ internal.edit_tab.get('node', "#editbox").submit(f);
+}
+
+function on_delete(f)
+{
+ internal.edit_tab.get('node', "#deletenode").click(f);
+}
+
+return {
+ show: show,
+ hide: hide,
+ on_submit: on_submit,
+ on_delete: on_delete,
+};
+
+});
diff --git a/src/view/tab.js b/src/view/tab.js
new file mode 100644
index 00000000..97ba1351
--- /dev/null
+++ b/src/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/view/timeline.js b/src/view/timeline.js
new file mode 100644
index 00000000..b7443551
--- /dev/null
+++ b/src/view/timeline.js
@@ -0,0 +1,183 @@
+"use strict"
+
+function checkSwitch(checkswitch) {
+
+ if (checkswitch.checked) {
+ vis.selectAll(".gantt").remove();
+ $('.missingdates').fadeOut(300);
+ scrollValue = $('body').scrollLeft();
+
+ $('body').scrollLeft(0);
+ graphstate = "GRAPH";
+ graph.update();
+
+ $('.status').fadeOut(600);
+
+ //boxedin=false;
+
+ } else {
+ ganttTimer=0;
+ $('.missingdates').fadeIn(300);
+
+ graph.recenterZoom();
+
+ $('body').scrollLeft(scrollValue);
+
+ graphstate = "GANTT";
+
+ graph.update();
+
+ $('.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==="GANTT"){
+ if(e.originalEvent.detail !== 0) {
+ $('.overlay').hide();
+ }else{
+ $('.overlay').show();
+ }
+ }else{
+ return false;
+ }
+ });
+
+ //IE, Opera, Safari
+ $('body').bind('mousewheel', function(e){
+ if(graphstate==="GANTT"){
+ 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", "gantt")
+ .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 gantt")
+ .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 gantt")
+ // .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 gantt")
+ .call(xAxis);
+
+ // Top Axis
+ var topAxis = svg.append("g")
+ .attr("transform", "translate(0," + paddingTop + ")")
+ .attr("class", "axis gantt")
+ .call(xAxis);
+}
diff --git a/src/view/view.js b/src/view/view.js
new file mode 100644
index 00000000..95bfc09b
--- /dev/null
+++ b/src/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();
+ },
+};
+});